#513 - support for custom field + panel view

This commit is contained in:
Elio Struyf
2023-02-27 17:37:16 +01:00
parent cf6133b143
commit 026bcd0a84
12 changed files with 191 additions and 21 deletions
+3 -3
View File
@@ -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}`
@@ -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,
+15
View File
@@ -9,6 +9,7 @@ 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;
@@ -16,6 +17,20 @@ declare const acquireVsCodeApi: <T = unknown>() => {
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',
+29 -5
View File
@@ -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,
@@ -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';
@@ -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}`
@@ -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>
`;
+4
View File
@@ -0,0 +1,4 @@
export interface CustomPanelViewResult {
title: string;
content: string;
}
+1
View File
@@ -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';
+4 -1
View File
@@ -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,
@@ -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>
@@ -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>
);
};
@@ -0,0 +1 @@
export * from './CustomView';
@@ -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>
);
};
@@ -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';
@@ -26,7 +26,8 @@ import {
SlugField,
PreviewImageField,
PreviewImageValue,
NumberField
NumberField,
CustomField
} from '.';
import { fieldWhenClause } from '../../../utils/fieldWhenClause';
@@ -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) => {
@@ -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;
}
@@ -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;
+2 -1
View File
@@ -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';
@@ -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';