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:
@ -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
|
||||
|
@ -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 []
|
||||
|
@ -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 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',
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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
|
||||
});
|
||||
|
@ -6,7 +6,6 @@ export default function LabelTemplatePanel() {
|
||||
return (
|
||||
<TemplateTable
|
||||
templateProps={{
|
||||
templateType: 'label',
|
||||
modelType: ModelType.labeltemplate,
|
||||
templateEndpoint: ApiEndpoints.label_list,
|
||||
printingEndpoint: ApiEndpoints.label_print,
|
||||
|
@ -9,7 +9,6 @@ export default function ReportTemplateTable() {
|
||||
return (
|
||||
<TemplateTable
|
||||
templateProps={{
|
||||
templateType: 'report',
|
||||
modelType: ModelType.reporttemplate,
|
||||
templateEndpoint: ApiEndpoints.report_list,
|
||||
printingEndpoint: ApiEndpoints.report_print,
|
||||
|
@ -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>
|
||||
);
|
||||
|
Reference in New Issue
Block a user