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}
/>
);