2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-06-27 01:00:53 +00:00

refactor ref into renderContext to make it more generic and support template preview area ui plugins

This commit is contained in:
wolflu05
2024-09-18 11:12:52 +02:00
parent 15962e2f57
commit ca17a07322
11 changed files with 200 additions and 34 deletions
src
backend
frontend

@ -72,8 +72,8 @@ class PluginUIFeatureList(APIView):
if not plugin_features or type(plugin_features) is not list: if not plugin_features or type(plugin_features) is not list:
continue continue
for feature in plugin_features: for _feature in plugin_features:
features.append(feature) features.append(_feature)
return Response( return Response(
UIPluginSerializers.PluginUIFeatureSerializer(features, many=True).data UIPluginSerializers.PluginUIFeatureSerializer(features, many=True).data

@ -122,3 +122,36 @@ class SampleUserInterfacePlugin(SettingsMixin, UserInterfaceMixin, InvenTreePlug
}) })
return panels 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 []

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

@ -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 = "<h1>Hello world</h1>";
}

@ -52,7 +52,7 @@ export type Editor = {
}; };
type PreviewAreaProps = {}; type PreviewAreaProps = {};
type PreviewAreaRef = { export type PreviewAreaRef = {
updatePreview: ( updatePreview: (
code: string, code: string,
previewItem: string, previewItem: string,
@ -300,6 +300,7 @@ export function TemplateEditor(props: Readonly<TemplateEditorProps>) {
<Tabs <Tabs
value={previewValue} value={previewValue}
onChange={setPreviewValue} onChange={setPreviewValue}
keepMounted={false}
style={{ style={{
minWidth: '200px', minWidth: '200px',
display: 'flex', display: 'flex',

@ -10,10 +10,15 @@ import {
} from 'react'; } from 'react';
import { TemplateI } from '../../tables/settings/TemplateTable'; import { TemplateI } from '../../tables/settings/TemplateTable';
import { EditorComponent } from '../editors/TemplateEditor/TemplateEditor'; import {
EditorComponent,
PreviewAreaComponent,
PreviewAreaRef
} from '../editors/TemplateEditor/TemplateEditor';
import { import {
PluginUIFuncWithoutInvenTreeContextType, PluginUIFuncWithoutInvenTreeContextType,
TemplateEditorUIFeature TemplateEditorUIFeature,
TemplatePreviewUIFeature
} from './PluginUIFeatureTypes'; } from './PluginUIFeatureTypes';
export const getPluginTemplateEditor = ( export const getPluginTemplateEditor = (
@ -43,7 +48,8 @@ export const getPluginTemplateEditor = (
useEffect(() => { useEffect(() => {
(async () => { (async () => {
try { try {
await func(elRef.current!, { await func({
ref: elRef.current!,
registerHandlers: ({ getCode, setCode }) => { registerHandlers: ({ getCode, setCode }) => {
setCodeRef.current = setCode; setCodeRef.current = setCode;
getCodeRef.current = getCode; getCodeRef.current = getCode;
@ -76,3 +82,50 @@ export const getPluginTemplateEditor = (
</Stack> </Stack>
); );
}) as EditorComponent; }) as EditorComponent;
export const getPluginTemplatePreview = (
func: PluginUIFuncWithoutInvenTreeContextType<TemplatePreviewUIFeature>,
template: TemplateI
) =>
forwardRef((props, ref) => {
const elRef = useRef<HTMLDivElement>();
const [error, setError] = useState<string | undefined>(undefined);
const updatePreviewRef = useRef<PreviewAreaRef['updatePreview']>();
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 (
<Stack gap="xs" style={{ display: 'flex', flex: 1 }}>
{error && (
<Alert
color="red"
title={t`Error Loading Plugin`}
icon={<IconExclamationCircle />}
>
<Text>{error}</Text>
</Alert>
)}
<div ref={elRef as any} style={{ display: 'flex', flex: 1 }}></div>
</Stack>
);
}) as PreviewAreaComponent;

@ -1,5 +1,7 @@
import { ModelType } from '../../enums/ModelType';
import { InvenTreeIconType } from '../../functions/icons'; import { InvenTreeIconType } from '../../functions/icons';
import { TemplateI } from '../../tables/settings/TemplateTable'; import { TemplateI } from '../../tables/settings/TemplateTable';
import { TemplateEditorProps } from '../editors/TemplateEditor/TemplateEditor';
import { InvenTreeContext } from './PluginContext'; import { InvenTreeContext } from './PluginContext';
// #region Type Helpers // #region Type Helpers
@ -8,20 +10,17 @@ export type BaseUIFeature = {
requestContext: Record<string, any>; requestContext: Record<string, any>;
responseOptions: Record<string, any>; responseOptions: Record<string, any>;
renderContext: Record<string, any>; renderContext: Record<string, any>;
renderReturnType: any;
}; };
export type PluginUIGetFeatureType<T extends BaseUIFeature> = ( export type PluginUIGetFeatureType<T extends BaseUIFeature> = (params: {
ref: HTMLDivElement,
params: {
renderContext: T['renderContext']; renderContext: T['renderContext'];
inventreeContext: InvenTreeContext; inventreeContext: InvenTreeContext;
} }) => T['renderReturnType'];
) => void;
export type PluginUIFuncWithoutInvenTreeContextType<T extends BaseUIFeature> = ( export type PluginUIFuncWithoutInvenTreeContextType<T extends BaseUIFeature> = (
ref: HTMLDivElement,
renderContext: T['renderContext'] renderContext: T['renderContext']
) => void; ) => T['renderReturnType'];
export type PluginUIFeatureAPIResponse<T extends BaseUIFeature> = { export type PluginUIFeatureAPIResponse<T extends BaseUIFeature> = {
feature_type: T['featureType']; feature_type: T['featureType'];
@ -33,8 +32,8 @@ export type PluginUIFeatureAPIResponse<T extends BaseUIFeature> = {
export type TemplateEditorUIFeature = { export type TemplateEditorUIFeature = {
featureType: 'template_editor'; featureType: 'template_editor';
requestContext: { requestContext: {
template_type: string; template_type: ModelType.labeltemplate | ModelType.reporttemplate;
template_model: string; template_model: ModelType;
}; };
responseOptions: { responseOptions: {
key: string; key: string;
@ -42,19 +41,21 @@ export type TemplateEditorUIFeature = {
icon: InvenTreeIconType; icon: InvenTreeIconType;
}; };
renderContext: { renderContext: {
registerHandlers: (params: { ref: HTMLDivElement;
registerHandlers: (handlers: {
setCode: (code: string) => void; setCode: (code: string) => void;
getCode: () => string; getCode: () => string;
}) => void; }) => void;
template: TemplateI; template: TemplateI;
}; };
renderReturnType: void;
}; };
export type TemplatePreviewUIFeature = { export type TemplatePreviewUIFeature = {
featureType: 'template_preview'; featureType: 'template_preview';
requestContext: { requestContext: {
template_type: string; template_type: ModelType.labeltemplate | ModelType.reporttemplate;
template_model: string; template_model: ModelType;
}; };
responseOptions: { responseOptions: {
key: string; key: string;
@ -62,6 +63,16 @@ export type TemplatePreviewUIFeature = {
icon: InvenTreeIconType; icon: InvenTreeIconType;
}; };
renderContext: { renderContext: {
ref: HTMLDivElement;
template: TemplateI; template: TemplateI;
registerHandlers: (handlers: {
updatePreview: (
code: string,
previewItem: string,
saveTemplate: boolean,
templateEditorProps: TemplateEditorProps
) => void | Promise<void>;
}) => void;
}; };
renderReturnType: void;
}; };

@ -14,9 +14,11 @@ import { apiUrl } from '../states/ApiState';
import { useGlobalSettingsState } from '../states/SettingsState'; import { useGlobalSettingsState } from '../states/SettingsState';
export function usePluginUIFeature<UIFeatureT extends BaseUIFeature>({ export function usePluginUIFeature<UIFeatureT extends BaseUIFeature>({
enabled = true,
featureType, featureType,
context context
}: { }: {
enabled?: boolean;
featureType: UIFeatureT['featureType']; featureType: UIFeatureT['featureType'];
context: UIFeatureT['requestContext']; context: UIFeatureT['requestContext'];
}) { }) {
@ -31,7 +33,7 @@ export function usePluginUIFeature<UIFeatureT extends BaseUIFeature>({
const { data: pluginData } = useQuery< const { data: pluginData } = useQuery<
PluginUIFeatureAPIResponse<UIFeatureT>[] PluginUIFeatureAPIResponse<UIFeatureT>[]
>({ >({
enabled: pluginUiFeaturesEnabled && !!featureType, enabled: pluginUiFeaturesEnabled && !!featureType && enabled,
queryKey: ['custom-ui-features', featureType, JSON.stringify(context)], queryKey: ['custom-ui-features', featureType, JSON.stringify(context)],
queryFn: async () => { queryFn: async () => {
if (!pluginUiFeaturesEnabled || !featureType) { if (!pluginUiFeaturesEnabled || !featureType) {
@ -70,14 +72,14 @@ export function usePluginUIFeature<UIFeatureT extends BaseUIFeature>({
return ( return (
pluginData?.map((feature) => ({ pluginData?.map((feature) => ({
options: feature.options, options: feature.options,
func: (async (ref, renderContext) => { func: (async (renderContext) => {
const func = await findExternalPluginFunction( const func = await findExternalPluginFunction(
feature.source, feature.source,
'getFeature' 'getFeature'
); );
if (!func) return; if (!func) return;
return func(ref, { return func({
renderContext, renderContext,
inventreeContext inventreeContext
}); });

@ -6,7 +6,6 @@ export default function LabelTemplatePanel() {
return ( return (
<TemplateTable <TemplateTable
templateProps={{ templateProps={{
templateType: 'label',
modelType: ModelType.labeltemplate, modelType: ModelType.labeltemplate,
templateEndpoint: ApiEndpoints.label_list, templateEndpoint: ApiEndpoints.label_list,
printingEndpoint: ApiEndpoints.label_print, printingEndpoint: ApiEndpoints.label_print,

@ -9,7 +9,6 @@ export default function ReportTemplateTable() {
return ( return (
<TemplateTable <TemplateTable
templateProps={{ templateProps={{
templateType: 'report',
modelType: ModelType.reporttemplate, modelType: ModelType.reporttemplate,
templateEndpoint: ApiEndpoints.report_list, templateEndpoint: ApiEndpoints.report_list,
printingEndpoint: ApiEndpoints.report_print, printingEndpoint: ApiEndpoints.report_print,

@ -10,12 +10,21 @@ import {
PdfPreview, PdfPreview,
TemplateEditor TemplateEditor
} from '../../components/editors/TemplateEditor'; } from '../../components/editors/TemplateEditor';
import { Editor } from '../../components/editors/TemplateEditor/TemplateEditor'; import {
Editor,
PreviewArea
} from '../../components/editors/TemplateEditor/TemplateEditor';
import { ApiFormFieldSet } from '../../components/forms/fields/ApiFormField'; import { ApiFormFieldSet } from '../../components/forms/fields/ApiFormField';
import { AttachmentLink } from '../../components/items/AttachmentLink'; import { AttachmentLink } from '../../components/items/AttachmentLink';
import { DetailDrawer } from '../../components/nav/DetailDrawer'; import { DetailDrawer } from '../../components/nav/DetailDrawer';
import { getPluginTemplateEditor } from '../../components/plugins/PluginUIFeature'; import {
import { TemplateEditorUIFeature } from '../../components/plugins/PluginUIFeatureTypes'; getPluginTemplateEditor,
getPluginTemplatePreview
} from '../../components/plugins/PluginUIFeature';
import {
TemplateEditorUIFeature,
TemplatePreviewUIFeature
} from '../../components/plugins/PluginUIFeatureTypes';
import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType'; import { ModelType } from '../../enums/ModelType';
import { GetIcon } from '../../functions/icons'; import { GetIcon } from '../../functions/icons';
@ -54,8 +63,7 @@ export type TemplateI = {
}; };
export interface TemplateProps { export interface TemplateProps {
templateType: 'label' | 'report'; modelType: ModelType.labeltemplate | ModelType.reporttemplate;
modelType: ModelType;
templateEndpoint: ApiEndpoints; templateEndpoint: ApiEndpoints;
printingEndpoint: ApiEndpoints; printingEndpoint: ApiEndpoints;
additionalFormFields?: ApiFormFieldSet; additionalFormFields?: ApiFormFieldSet;
@ -68,8 +76,7 @@ export function TemplateDrawer({
id: string | number; id: string | number;
templateProps: TemplateProps; templateProps: TemplateProps;
}>) { }>) {
const { modelType, templateType, templateEndpoint, printingEndpoint } = const { modelType, templateEndpoint, printingEndpoint } = templateProps;
templateProps;
const { const {
instance: template, instance: template,
@ -81,9 +88,11 @@ export function TemplateDrawer({
throwError: true throwError: true
}); });
// Editors
const extraEditors = usePluginUIFeature<TemplateEditorUIFeature>({ const extraEditors = usePluginUIFeature<TemplateEditorUIFeature>({
enabled: template?.model_type !== undefined,
featureType: 'template_editor', featureType: 'template_editor',
context: { template_type: templateType, template_model: modelType } context: { template_type: modelType, template_model: template?.model_type! }
}); });
const editors = useMemo(() => { const editors = useMemo(() => {
const editors = [CodeEditor]; const editors = [CodeEditor];
@ -107,6 +116,34 @@ export function TemplateDrawer({
return editors; return editors;
}, [extraEditors, template]); }, [extraEditors, template]);
// Previews
const extraPreviews = usePluginUIFeature<TemplatePreviewUIFeature>({
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) { if (isFetching) {
return <LoadingOverlay visible={true} />; return <LoadingOverlay visible={true} />;
} }
@ -134,7 +171,7 @@ export function TemplateDrawer({
printingUrl={apiUrl(printingEndpoint)} printingUrl={apiUrl(printingEndpoint)}
template={template} template={template}
editors={editors} editors={editors}
previewAreas={[PdfPreview]} previewAreas={previews}
/> />
</Stack> </Stack>
); );