Skip to content

Commit

Permalink
#513 - support for custom field + panel view
Browse files Browse the repository at this point in the history
  • Loading branch information
estruyf committed Feb 27, 2023
1 parent cf6133b commit 026bcd0
Show file tree
Hide file tree
Showing 12 changed files with 191 additions and 21 deletions.
6 changes: 3 additions & 3 deletions src/commands/Dashboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -254,10 +254,10 @@ export class Dashboard {
} 'self' 'unsafe-inline' https://*`,
`script-src ${
isProd ? `'nonce-${nonce}'` : `http://${localServerUrl} http://0.0.0.0:${localPort}`
} https://* 'unsafe-eval'`,
`style-src ${webView.cspSource} 'self' 'unsafe-inline'`,
} 'unsafe-eval' https://*`,
`style-src ${webView.cspSource} 'self' 'unsafe-inline' https://*`,
`font-src ${webView.cspSource}`,
`connect-src https://o1022172.ingest.sentry.io ${
`connect-src https://o1022172.ingest.sentry.io https://* ${
isProd
? ``
: `ws://${localServerUrl} ws://0.0.0.0:${localPort} http://${localServerUrl} http://0.0.0.0:${localPort}`
Expand Down
9 changes: 0 additions & 9 deletions src/dashboardWebView/components/Contents/Item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,6 @@ export interface IItemProps extends Page { }

const PREVIEW_IMAGE_FIELD = 'fmPreviewImage';

declare global {
interface Window {
fmExternal: {
getCardImage(filePath: string, data: any): Promise<string | undefined>;
getCardFooter: (filePath: string, data: any) => Promise<string | undefined>;
}
}
}

export const Item: React.FunctionComponent<IItemProps> = ({
fmFilePath,
date,
Expand Down
15 changes: 15 additions & 0 deletions src/dashboardWebView/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,28 @@ import { MemoryRouter } from 'react-router-dom';
import './styles.css';
import { Preview } from './components/Preview';
import { SettingsProvider } from './providers/SettingsProvider';
import { CustomPanelViewResult } from '../models';

declare const acquireVsCodeApi: <T = unknown>() => {
getState: () => T;
setState: (data: T) => void;
postMessage: (msg: unknown) => void;
};

declare global {
interface Window {
fmExternal: {
getCustomFields: {
name: string,
html: (data: any, change: (value: any) => void) => Promise<CustomPanelViewResult | undefined>
}[];
getPanelView: (data: any) => Promise<CustomPanelViewResult | undefined>;
getCardImage: (filePath: string, data: any) => Promise<string | undefined>;
getCardFooter: (filePath: string, data: any) => Promise<string | undefined>;
}
}
}

export const routePaths: { [name: string]: string } = {
welcome: '/welcome',
contents: '/contents',
Expand Down
34 changes: 29 additions & 5 deletions src/explorerView/ExplorerView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
DataListener,
SettingsListener
} from './../listeners/panel';
import { TelemetryEvent } from '../constants';
import { SETTING_EXPERIMENTAL, SETTING_EXTENSIBILITY_SCRIPTS, TelemetryEvent } from '../constants';
import {
CancellationToken,
Disposable,
Expand All @@ -25,6 +25,7 @@ import { WebviewHelper } from '@estruyf/vscode';
import { Extension } from '../helpers/Extension';
import { Telemetry } from '../helpers/Telemetry';
import { GitListener, ModeListener } from '../listeners/general';
import { Folders } from '../commands';

export class ExplorerView implements WebviewViewProvider, Disposable {
public static readonly viewType = 'frontMatter.explorer';
Expand Down Expand Up @@ -201,17 +202,34 @@ export class ExplorerView implements WebviewViewProvider, Disposable {
scriptUri = `http://${localServerUrl}/${dashboardFile}`;
}

// Get experimental setting
const experimental = Settings.get(SETTING_EXPERIMENTAL);
const extensibilityScripts = Settings.get<string[]>(SETTING_EXTENSIBILITY_SCRIPTS) || [];

const scriptsToLoad: string[] = [];
if (experimental) {
for (const script of extensibilityScripts) {
if (script.startsWith('https://')) {
scriptsToLoad.push(script);
} else {
const absScriptPath = Folders.getAbsFilePath(script);
const scriptUri = webView.asWebviewUri(Uri.file(absScriptPath));
scriptsToLoad.push(scriptUri.toString());
}
}
}

const csp = [
`default-src 'none';`,
`img-src ${`vscode-file://vscode-app`} ${
webView.cspSource
} https://api.visitorbadge.io 'self' 'unsafe-inline' https://*`,
`script-src 'unsafe-eval' ${
`script-src 'unsafe-eval' https://* ${
isProd ? `'nonce-${nonce}'` : `http://${localServerUrl} http://0.0.0.0:${localPort}`
}`,
`style-src ${webView.cspSource} 'self' 'unsafe-inline'`,
`style-src ${webView.cspSource} 'self' 'unsafe-inline' https://*`,
`font-src ${webView.cspSource}`,
`connect-src https://o1022172.ingest.sentry.io ${
`connect-src https://o1022172.ingest.sentry.io https://* ${
isProd
? ``
: `ws://${localServerUrl} ws://0.0.0.0:${localPort} http://${localServerUrl} http://0.0.0.0:${localPort}`
Expand All @@ -235,9 +253,15 @@ export class ExplorerView implements WebviewViewProvider, Disposable {
isBeta ? 'BETA' : 'main'
}" data-version="${version.usedVersion}" ></div>
<img style="display:none" src="https://api.visitorbadge.io/api/combined?user=estruyf&repo=frontmatter-usage&countColor=%23263759&slug=${`panel-${version.installedVersion}`}" alt="Daily usage" />
${(scriptsToLoad || [])
.map((script) => {
return `<script type="module" src="${script}" nonce="${nonce}"></script>`;
})
.join('')}
<script ${isProd ? `nonce="${nonce}"` : ''} src="${scriptUri}"></script>
<img style="display:none" src="https://api.visitorbadge.io/api/combined?user=estruyf&repo=frontmatter-usage&countColor=%23263759&slug=${`panel-${version.installedVersion}`}" alt="Daily usage" />
</body>
</html>
`;
Expand Down
4 changes: 4 additions & 0 deletions src/models/CustomPanelViewResult.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface CustomPanelViewResult {
title: string;
content: string;
}
1 change: 1 addition & 0 deletions src/models/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export * from './BaseFieldProps';
export * from './BlockFieldData';
export * from './Choice';
export * from './ContentFolder';
export * from './CustomPanelViewResult';
export * from './CustomPlaceholder';
export * from './CustomTaxonomyData';
export * from './DashboardData';
Expand Down
5 changes: 4 additions & 1 deletion src/panelWebView/ViewPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,12 @@ import useMessages from './hooks/useMessages';
import { FeatureFlag } from '../components/features/FeatureFlag';
import { FEATURE_FLAG } from '../constants/Features';
import { GitAction } from './components/Git/GitAction';
import { CustomView } from './components/CustomView';

export interface IViewPanelProps { }

export const ViewPanel: React.FunctionComponent<IViewPanelProps> = (
props: React.PropsWithChildren<IViewPanelProps>
{ }: React.PropsWithChildren<IViewPanelProps>
) => {
const {
loading,
Expand Down Expand Up @@ -50,6 +51,8 @@ export const ViewPanel: React.FunctionComponent<IViewPanelProps> = (
<div className={`ext_actions`}>
<GitAction settings={settings} />

<CustomView metadata={metadata} />

<FeatureFlag features={mode?.features || []} flag={FEATURE_FLAG.panel.globalSettings}>
<GlobalSettings settings={settings} />
</FeatureFlag>
Expand Down
43 changes: 43 additions & 0 deletions src/panelWebView/components/CustomView/CustomView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import * as React from 'react';
import { useEffect, useState } from 'react';
import { CustomPanelViewResult } from '../../../models';
import { Collapsible } from '../Collapsible';

export interface ICustomViewProps {
metadata: any;
}

export const CustomView: React.FunctionComponent<ICustomViewProps> = ({ metadata }: React.PropsWithChildren<ICustomViewProps>) => {
const [customViewTitle, setCustomViewTitle] = useState<string | undefined>(undefined);
const [customHtml, setCustomHtml] = useState<string | undefined>(undefined);

useEffect(() => {
console.log(window.fmExternal)
if (window.fmExternal && window.fmExternal.getPanelView) {
window.fmExternal.getPanelView(metadata).then((viewDetails: CustomPanelViewResult | undefined) => {
if (viewDetails && viewDetails.title && viewDetails.content) {
setCustomViewTitle(viewDetails.title);
setCustomHtml(viewDetails.content);
} else {
setCustomViewTitle(undefined);
setCustomHtml(undefined);
}
});
}
}, []);

if (!customHtml || !customViewTitle) {
return null;
}


return (
<Collapsible
id={`custom-view`}
className={`base__actions`}
title={customViewTitle}
>
<div dangerouslySetInnerHTML={{ __html: customHtml }} />
</Collapsible>
);
};
1 change: 1 addition & 0 deletions src/panelWebView/components/CustomView/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './CustomView';
58 changes: 58 additions & 0 deletions src/panelWebView/components/Fields/CustomField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { CodeIcon } from '@heroicons/react/outline';
import * as React from 'react';
import { useEffect, useMemo, useState } from 'react';
import { BaseFieldProps, CustomPanelViewResult } from '../../../models';
import { FieldMessage } from './FieldMessage';
import { FieldTitle } from './FieldTitle';

export interface ICustomFieldProps extends BaseFieldProps<any> {
fieldData: {
name: string,
html: (data: any, onChange: (value: any) => void) => Promise<CustomPanelViewResult | undefined>,
};
onChange: (value: any) => void;
}

export const CustomField: React.FunctionComponent<ICustomFieldProps> = ({ label, value, required, description, fieldData, onChange }: React.PropsWithChildren<ICustomFieldProps>) => {
const [customHtml, setCustomHtml] = useState<any>(null);

const internalChange = (newValue: any) => {
onChange(newValue);
};

const showRequiredState = useMemo(() => {
return required && !value;
}, [required, value]);

useEffect(() => {
if (fieldData.html) {
fieldData.html(value, internalChange).then((data) => {
if (data) {
setCustomHtml(data);
} else {
setCustomHtml(null);
}
});
}
}, [fieldData, value]);

if (!customHtml) {
return null;
}

return (
<div className={`metadata_field`}>
<FieldTitle label={label} icon={<CodeIcon />} required={required} />

<div className="metadata_field">
<div dangerouslySetInnerHTML={{ __html: customHtml }} />
</div>

<FieldMessage
name={label.toLowerCase()}
description={description}
showRequired={showRequiredState}
/>
</div>
);
};
33 changes: 31 additions & 2 deletions src/panelWebView/components/Fields/WrapperField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Messenger } from '@estruyf/vscode/dist/client';
import * as React from 'react';
import { useCallback, useEffect, useState } from 'react';
import { DateHelper } from '../../../helpers/DateHelper';
import { BlockFieldData, Field, PanelSettings, WhenOperator } from '../../../models';
import { BlockFieldData, CustomPanelViewResult, Field, PanelSettings, WhenOperator } from '../../../models';
import { Command } from '../../Command';
import { CommandToCode } from '../../CommandToCode';
import { TagType } from '../../TagType';
Expand All @@ -26,7 +26,8 @@ import {
SlugField,
PreviewImageField,
PreviewImageValue,
NumberField
NumberField,
CustomField
} from '.';
import { fieldWhenClause } from '../../../utils/fieldWhenClause';

Expand Down Expand Up @@ -65,6 +66,10 @@ export const WrapperField: React.FunctionComponent<IWrapperFieldProps> = ({
renderFields
}: React.PropsWithChildren<IWrapperFieldProps>) => {
const [fieldValue, setFieldValue] = useState<any | undefined>(undefined);
const [customFields, setCustomFields] = useState<{
name: string;
html: (data: any, onChange: (value: any) => void) => Promise<CustomPanelViewResult | undefined>;
}[]>([]);

const listener = useCallback(
(event: any) => {
Expand Down Expand Up @@ -132,6 +137,14 @@ export const WrapperField: React.FunctionComponent<IWrapperFieldProps> = ({
};
}, [field, parent]);

useEffect(() => {
if (window.fmExternal && window.fmExternal.getCustomFields) {
setCustomFields(window.fmExternal.getCustomFields || []);
} else {
setCustomFields([]);
}
}, []);

if (field.hidden || fieldValue === undefined) {
return null;
}
Expand Down Expand Up @@ -470,6 +483,22 @@ export const WrapperField: React.FunctionComponent<IWrapperFieldProps> = ({
/>
</FieldBoundary>
);
} else if (customFields.find(f => f.name === field.type)) {
const fieldData = customFields.find(f => f.name === field.type);
if (fieldData) {
return (
<CustomField
key={field.name}
label={field.title || field.name}
description={field.description}
value={fieldValue}
required={!!field.required}
onChange={(value: any) => onSendUpdate(field.name, value, parentFields)}
fieldData={fieldData} />
);
} else {
return null;
}
} else {
console.warn(`Unknown field type: ${field.type}`);
return null;
Expand Down
3 changes: 2 additions & 1 deletion src/panelWebView/components/Fields/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
export * from './ChoiceButton';
export * from './ChoiceField';
export * from './CustomField';
export * from './DataFileField';
export * from './DateTimeField';
export * from './DraftField';
export * from './FieldMessage';
export * from './FieldTitle';
export * from './FileField';
export * from './ImageFallback';
Expand All @@ -11,7 +13,6 @@ export * from './NumberField';
export * from './PreviewImage';
export * from './PreviewImageField';
export * from './RequiredAsterix';
export * from './FieldMessage';
export * from './SlugField';
export * from './TextField';
export * from './Toggle';
Expand Down

0 comments on commit 026bcd0

Please sign in to comment.