mirror of
https://github.com/inventree/InvenTree.git
synced 2025-06-27 09:10:51 +00:00
refactor ref into renderContext to make it more generic and support template preview area ui plugins
This commit is contained in:
@ -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>
|
||||||
);
|
);
|
||||||
|
Reference in New Issue
Block a user