From ca17a0732224d82a45e28163394da56606845730 Mon Sep 17 00:00:00 2001 From: wolflu05 <76838159+wolflu05@users.noreply.github.com> Date: Wed, 18 Sep 2024 11:12:52 +0200 Subject: [PATCH] refactor ref into renderContext to make it more generic and support template preview area ui plugins --- src/backend/InvenTree/plugin/base/ui/api.py | 4 +- .../integration/user_interface_sample.py | 33 +++++++++++ .../static/plugin/sample_template_editor.js | 19 ++++++ .../static/plugin/sample_template_preview.js | 12 ++++ .../editors/TemplateEditor/TemplateEditor.tsx | 3 +- .../components/plugins/PluginUIFeature.tsx | 59 ++++++++++++++++++- .../plugins/PluginUIFeatureTypes.ts | 39 +++++++----- src/frontend/src/hooks/UsePluginUIFeature.tsx | 8 ++- .../AdminCenter/LabelTemplatePanel.tsx | 1 - .../AdminCenter/ReportTemplatePanel.tsx | 1 - .../src/tables/settings/TemplateTable.tsx | 55 ++++++++++++++--- 11 files changed, 200 insertions(+), 34 deletions(-) create mode 100644 src/backend/InvenTree/plugin/samples/static/plugin/sample_template_editor.js create mode 100644 src/backend/InvenTree/plugin/samples/static/plugin/sample_template_preview.js diff --git a/src/backend/InvenTree/plugin/base/ui/api.py b/src/backend/InvenTree/plugin/base/ui/api.py index 077b3971ce..02b083bbbe 100644 --- a/src/backend/InvenTree/plugin/base/ui/api.py +++ b/src/backend/InvenTree/plugin/base/ui/api.py @@ -72,8 +72,8 @@ class PluginUIFeatureList(APIView): if not plugin_features or type(plugin_features) is not list: continue - for feature in plugin_features: - features.append(feature) + for _feature in plugin_features: + features.append(_feature) return Response( UIPluginSerializers.PluginUIFeatureSerializer(features, many=True).data diff --git a/src/backend/InvenTree/plugin/samples/integration/user_interface_sample.py b/src/backend/InvenTree/plugin/samples/integration/user_interface_sample.py index 5b0fbf2f77..800698e9a7 100644 --- a/src/backend/InvenTree/plugin/samples/integration/user_interface_sample.py +++ b/src/backend/InvenTree/plugin/samples/integration/user_interface_sample.py @@ -122,3 +122,36 @@ class SampleUserInterfacePlugin(SettingsMixin, UserInterfaceMixin, InvenTreePlug }) return panels + + def get_ui_features(self, feature_type, context, request): + """Return a list of custom features to be injected into the UI.""" + if ( + feature_type == 'template_editor' + and context.get('template_type') == 'labeltemplate' + ): + return [ + { + 'feature_type': 'template_editor', + 'options': { + 'key': 'sample-template-editor', + 'title': 'Sample Template Editor', + 'icon': 'keywords', + }, + 'source': '/static/plugin/sample_template_editor.js', + } + ] + + if feature_type == 'template_preview': + return [ + { + 'feature_type': 'template_preview', + 'options': { + 'key': 'sample-template-preview', + 'title': 'Sample Template Preview', + 'icon': 'category', + }, + 'source': '/static/plugin/sample_template_preview.js', + } + ] + + return [] diff --git a/src/backend/InvenTree/plugin/samples/static/plugin/sample_template_editor.js b/src/backend/InvenTree/plugin/samples/static/plugin/sample_template_editor.js new file mode 100644 index 0000000000..6a43197535 --- /dev/null +++ b/src/backend/InvenTree/plugin/samples/static/plugin/sample_template_editor.js @@ -0,0 +1,19 @@ +export function getFeature({ renderContext, pluginContext }) { + const { ref } = renderContext; + console.log("Template editor feature was called with", renderContext, pluginContext); + const t = document.createElement("textarea"); + t.rows = 25; + t.cols = 60; + + renderContext.registerHandlers({ + setCode: (code) => { + t.value = code; + }, + getCode: () => { + return t.value; + } + }); + + ref.innerHTML = ""; + ref.appendChild(t); +} diff --git a/src/backend/InvenTree/plugin/samples/static/plugin/sample_template_preview.js b/src/backend/InvenTree/plugin/samples/static/plugin/sample_template_preview.js new file mode 100644 index 0000000000..a23b2b28c8 --- /dev/null +++ b/src/backend/InvenTree/plugin/samples/static/plugin/sample_template_preview.js @@ -0,0 +1,12 @@ +export function getFeature({ renderContext, pluginContext }) { + const { ref } = renderContext; + console.log("Template preview feature was called with", renderContext, pluginContext); + + renderContext.registerHandlers({ + updatePreview: (...args) => { + console.log("updatePreview", args); + } + }); + + ref.innerHTML = "

Hello world

"; +} diff --git a/src/frontend/src/components/editors/TemplateEditor/TemplateEditor.tsx b/src/frontend/src/components/editors/TemplateEditor/TemplateEditor.tsx index 1f03f3ce71..40fdc0ee84 100644 --- a/src/frontend/src/components/editors/TemplateEditor/TemplateEditor.tsx +++ b/src/frontend/src/components/editors/TemplateEditor/TemplateEditor.tsx @@ -52,7 +52,7 @@ export type Editor = { }; type PreviewAreaProps = {}; -type PreviewAreaRef = { +export type PreviewAreaRef = { updatePreview: ( code: string, previewItem: string, @@ -300,6 +300,7 @@ export function TemplateEditor(props: Readonly) { { (async () => { try { - await func(elRef.current!, { + await func({ + ref: elRef.current!, registerHandlers: ({ getCode, setCode }) => { setCodeRef.current = setCode; getCodeRef.current = getCode; @@ -76,3 +82,50 @@ export const getPluginTemplateEditor = ( ); }) as EditorComponent; + +export const getPluginTemplatePreview = ( + func: PluginUIFuncWithoutInvenTreeContextType, + template: TemplateI +) => + forwardRef((props, ref) => { + const elRef = useRef(); + const [error, setError] = useState(undefined); + + const updatePreviewRef = useRef(); + + useImperativeHandle(ref, () => ({ + updatePreview: (...args) => updatePreviewRef.current?.(...args) + })); + + useEffect(() => { + (async () => { + try { + await func({ + ref: elRef.current!, + registerHandlers: ({ updatePreview }) => { + updatePreviewRef.current = updatePreview; + }, + template + }); + } catch (error) { + setError(t`Error occurred while rendering the template preview.`); + console.error(error); + } + })(); + }, []); + + return ( + + {error && ( + } + > + {error} + + )} +
+
+ ); + }) as PreviewAreaComponent; diff --git a/src/frontend/src/components/plugins/PluginUIFeatureTypes.ts b/src/frontend/src/components/plugins/PluginUIFeatureTypes.ts index b341bfe2de..a187b690e6 100644 --- a/src/frontend/src/components/plugins/PluginUIFeatureTypes.ts +++ b/src/frontend/src/components/plugins/PluginUIFeatureTypes.ts @@ -1,5 +1,7 @@ +import { ModelType } from '../../enums/ModelType'; import { InvenTreeIconType } from '../../functions/icons'; import { TemplateI } from '../../tables/settings/TemplateTable'; +import { TemplateEditorProps } from '../editors/TemplateEditor/TemplateEditor'; import { InvenTreeContext } from './PluginContext'; // #region Type Helpers @@ -8,20 +10,17 @@ export type BaseUIFeature = { requestContext: Record; responseOptions: Record; renderContext: Record; + renderReturnType: any; }; -export type PluginUIGetFeatureType = ( - ref: HTMLDivElement, - params: { - renderContext: T['renderContext']; - inventreeContext: InvenTreeContext; - } -) => void; +export type PluginUIGetFeatureType = (params: { + renderContext: T['renderContext']; + inventreeContext: InvenTreeContext; +}) => T['renderReturnType']; export type PluginUIFuncWithoutInvenTreeContextType = ( - ref: HTMLDivElement, renderContext: T['renderContext'] -) => void; +) => T['renderReturnType']; export type PluginUIFeatureAPIResponse = { feature_type: T['featureType']; @@ -33,8 +32,8 @@ export type PluginUIFeatureAPIResponse = { export type TemplateEditorUIFeature = { featureType: 'template_editor'; requestContext: { - template_type: string; - template_model: string; + template_type: ModelType.labeltemplate | ModelType.reporttemplate; + template_model: ModelType; }; responseOptions: { key: string; @@ -42,19 +41,21 @@ export type TemplateEditorUIFeature = { icon: InvenTreeIconType; }; renderContext: { - registerHandlers: (params: { + ref: HTMLDivElement; + registerHandlers: (handlers: { setCode: (code: string) => void; getCode: () => string; }) => void; template: TemplateI; }; + renderReturnType: void; }; export type TemplatePreviewUIFeature = { featureType: 'template_preview'; requestContext: { - template_type: string; - template_model: string; + template_type: ModelType.labeltemplate | ModelType.reporttemplate; + template_model: ModelType; }; responseOptions: { key: string; @@ -62,6 +63,16 @@ export type TemplatePreviewUIFeature = { icon: InvenTreeIconType; }; renderContext: { + ref: HTMLDivElement; template: TemplateI; + registerHandlers: (handlers: { + updatePreview: ( + code: string, + previewItem: string, + saveTemplate: boolean, + templateEditorProps: TemplateEditorProps + ) => void | Promise; + }) => void; }; + renderReturnType: void; }; diff --git a/src/frontend/src/hooks/UsePluginUIFeature.tsx b/src/frontend/src/hooks/UsePluginUIFeature.tsx index 51188eab52..8b18568632 100644 --- a/src/frontend/src/hooks/UsePluginUIFeature.tsx +++ b/src/frontend/src/hooks/UsePluginUIFeature.tsx @@ -14,9 +14,11 @@ import { apiUrl } from '../states/ApiState'; import { useGlobalSettingsState } from '../states/SettingsState'; export function usePluginUIFeature({ + enabled = true, featureType, context }: { + enabled?: boolean; featureType: UIFeatureT['featureType']; context: UIFeatureT['requestContext']; }) { @@ -31,7 +33,7 @@ export function usePluginUIFeature({ const { data: pluginData } = useQuery< PluginUIFeatureAPIResponse[] >({ - enabled: pluginUiFeaturesEnabled && !!featureType, + enabled: pluginUiFeaturesEnabled && !!featureType && enabled, queryKey: ['custom-ui-features', featureType, JSON.stringify(context)], queryFn: async () => { if (!pluginUiFeaturesEnabled || !featureType) { @@ -70,14 +72,14 @@ export function usePluginUIFeature({ return ( pluginData?.map((feature) => ({ options: feature.options, - func: (async (ref, renderContext) => { + func: (async (renderContext) => { const func = await findExternalPluginFunction( feature.source, 'getFeature' ); if (!func) return; - return func(ref, { + return func({ renderContext, inventreeContext }); diff --git a/src/frontend/src/pages/Index/Settings/AdminCenter/LabelTemplatePanel.tsx b/src/frontend/src/pages/Index/Settings/AdminCenter/LabelTemplatePanel.tsx index 0ae54d5cbd..cda9c40efa 100644 --- a/src/frontend/src/pages/Index/Settings/AdminCenter/LabelTemplatePanel.tsx +++ b/src/frontend/src/pages/Index/Settings/AdminCenter/LabelTemplatePanel.tsx @@ -6,7 +6,6 @@ export default function LabelTemplatePanel() { return ( ) { - const { modelType, templateType, templateEndpoint, printingEndpoint } = - templateProps; + const { modelType, templateEndpoint, printingEndpoint } = templateProps; const { instance: template, @@ -81,9 +88,11 @@ export function TemplateDrawer({ throwError: true }); + // Editors const extraEditors = usePluginUIFeature({ + enabled: template?.model_type !== undefined, featureType: 'template_editor', - context: { template_type: templateType, template_model: modelType } + context: { template_type: modelType, template_model: template?.model_type! } }); const editors = useMemo(() => { const editors = [CodeEditor]; @@ -107,6 +116,34 @@ export function TemplateDrawer({ return editors; }, [extraEditors, template]); + // Previews + const extraPreviews = usePluginUIFeature({ + enabled: template?.model_type !== undefined, + featureType: 'template_preview', + context: { template_type: modelType, template_model: template?.model_type! } + }); + const previews = useMemo(() => { + const previews = [PdfPreview]; + + if (!template) { + return previews; + } + + previews.push( + ...(extraPreviews?.map( + (preview) => + ({ + key: preview.options.key, + name: preview.options.title, + icon: GetIcon(preview.options.icon), + component: getPluginTemplatePreview(preview.func, template) + } as PreviewArea) + ) || []) + ); + + return previews; + }, [extraPreviews, template]); + if (isFetching) { return ; } @@ -134,7 +171,7 @@ export function TemplateDrawer({ printingUrl={apiUrl(printingEndpoint)} template={template} editors={editors} - previewAreas={[PdfPreview]} + previewAreas={previews} /> );