mirror of
https://github.com/estruyf/vscode-front-matter.git
synced 2026-07-07 02:11:25 +02:00
#513 - support for custom field + panel view
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface CustomPanelViewResult {
|
||||
title: string;
|
||||
content: string;
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user