2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-06-15 03:25:42 +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

View File

@ -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

View File

@ -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 []

View File

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

View File

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

View File

@ -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<TemplateEditorProps>) {
<Tabs
value={previewValue}
onChange={setPreviewValue}
keepMounted={false}
style={{
minWidth: '200px',
display: 'flex',

View File

@ -10,10 +10,15 @@ import {
} from 'react';
import { TemplateI } from '../../tables/settings/TemplateTable';
import { EditorComponent } from '../editors/TemplateEditor/TemplateEditor';
import {
EditorComponent,
PreviewAreaComponent,
PreviewAreaRef
} from '../editors/TemplateEditor/TemplateEditor';
import {
PluginUIFuncWithoutInvenTreeContextType,
TemplateEditorUIFeature
TemplateEditorUIFeature,
TemplatePreviewUIFeature
} from './PluginUIFeatureTypes';
export const getPluginTemplateEditor = (
@ -43,7 +48,8 @@ export const getPluginTemplateEditor = (
useEffect(() => {
(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 = (
</Stack>
);
}) 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;

View File

@ -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<string, any>;
responseOptions: Record<string, any>;
renderContext: Record<string, any>;
renderReturnType: any;
};
export type PluginUIGetFeatureType<T extends BaseUIFeature> = (
ref: HTMLDivElement,
params: {
renderContext: T['renderContext'];
inventreeContext: InvenTreeContext;
}
) => void;
export type PluginUIGetFeatureType<T extends BaseUIFeature> = (params: {
renderContext: T['renderContext'];
inventreeContext: InvenTreeContext;
}) => T['renderReturnType'];
export type PluginUIFuncWithoutInvenTreeContextType<T extends BaseUIFeature> = (
ref: HTMLDivElement,
renderContext: T['renderContext']
) => void;
) => T['renderReturnType'];
export type PluginUIFeatureAPIResponse<T extends BaseUIFeature> = {
feature_type: T['featureType'];
@ -33,8 +32,8 @@ export type PluginUIFeatureAPIResponse<T extends BaseUIFeature> = {
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>;
}) => void;
};
renderReturnType: void;
};

View File

@ -14,9 +14,11 @@ import { apiUrl } from '../states/ApiState';
import { useGlobalSettingsState } from '../states/SettingsState';
export function usePluginUIFeature<UIFeatureT extends BaseUIFeature>({
enabled = true,
featureType,
context
}: {
enabled?: boolean;
featureType: UIFeatureT['featureType'];
context: UIFeatureT['requestContext'];
}) {
@ -31,7 +33,7 @@ export function usePluginUIFeature<UIFeatureT extends BaseUIFeature>({
const { data: pluginData } = useQuery<
PluginUIFeatureAPIResponse<UIFeatureT>[]
>({
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<UIFeatureT extends BaseUIFeature>({
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
});

View File

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

View File

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

View File

@ -10,12 +10,21 @@ import {
PdfPreview,
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 { AttachmentLink } from '../../components/items/AttachmentLink';
import { DetailDrawer } from '../../components/nav/DetailDrawer';
import { getPluginTemplateEditor } from '../../components/plugins/PluginUIFeature';
import { TemplateEditorUIFeature } from '../../components/plugins/PluginUIFeatureTypes';
import {
getPluginTemplateEditor,
getPluginTemplatePreview
} from '../../components/plugins/PluginUIFeature';
import {
TemplateEditorUIFeature,
TemplatePreviewUIFeature
} from '../../components/plugins/PluginUIFeatureTypes';
import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType';
import { GetIcon } from '../../functions/icons';
@ -54,8 +63,7 @@ export type TemplateI = {
};
export interface TemplateProps {
templateType: 'label' | 'report';
modelType: ModelType;
modelType: ModelType.labeltemplate | ModelType.reporttemplate;
templateEndpoint: ApiEndpoints;
printingEndpoint: ApiEndpoints;
additionalFormFields?: ApiFormFieldSet;
@ -68,8 +76,7 @@ export function TemplateDrawer({
id: string | number;
templateProps: TemplateProps;
}>) {
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<TemplateEditorUIFeature>({
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<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) {
return <LoadingOverlay visible={true} />;
}
@ -134,7 +171,7 @@ export function TemplateDrawer({
printingUrl={apiUrl(printingEndpoint)}
template={template}
editors={editors}
previewAreas={[PdfPreview]}
previewAreas={previews}
/>
</Stack>
);