mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
PUI Template editor (#6541)
* Added first POC for label editor * Added preview item selection * Split code * Fix import * Use liquid lang and added custom tooltips * Auto load first item for preview and add BOM part assembly filter * Make the save&reload action more obvious * Make save optional and use server stored template * Fix icons and inherit model url * Add label/report extra fields to serializer and default templates * Bump api version to v176 * Remove generic and pass template to editor * Added error overlay * Moved default tempaltes in default folder * Only show detail drawer back button if necessary * Rename action dropdown disabled to hidden and add loading disabled to template editor * Fix types * Add icons to editor/preview tabs * Add draggable split pane and make editors use full height * Add SplitButton component * add code editor tag description * fix related model field if empty string * remove debug console.log * move code editor/pdf preview into their own folder * Update api_version.py * add support for multiple editors * fix template editor error handleing while loading/saving code * add documentation for the template editor
This commit is contained in:
parent
950bda4ef6
commit
e4d2e2f96b
@ -1,11 +1,15 @@
|
||||
"""InvenTree API version information."""
|
||||
|
||||
# InvenTree API version
|
||||
INVENTREE_API_VERSION = 180
|
||||
INVENTREE_API_VERSION = 181
|
||||
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
||||
|
||||
INVENTREE_API_TEXT = """
|
||||
|
||||
v181 - 2024-02-21 : https://github.com/inventree/InvenTree/pull/6541
|
||||
- Adds "width" and "height" fields to the LabelTemplate API endpoint
|
||||
- Adds "page_size" and "landscape" fields to the ReportTemplate API endpoint
|
||||
|
||||
v180 - 2024-3-02 : https://github.com/inventree/InvenTree/pull/6463
|
||||
- Tweaks to API documentation to allow automatic documentation generation
|
||||
|
||||
|
@ -15,7 +15,16 @@ class LabelSerializerBase(InvenTreeModelSerializer):
|
||||
@staticmethod
|
||||
def label_fields():
|
||||
"""Generic serializer fields for a label template."""
|
||||
return ['pk', 'name', 'description', 'label', 'filters', 'enabled']
|
||||
return [
|
||||
'pk',
|
||||
'name',
|
||||
'description',
|
||||
'label',
|
||||
'filters',
|
||||
'width',
|
||||
'height',
|
||||
'enabled',
|
||||
]
|
||||
|
||||
|
||||
class StockItemLabelSerializer(LabelSerializerBase):
|
||||
|
@ -24,7 +24,16 @@ class ReportSerializerBase(InvenTreeModelSerializer):
|
||||
@staticmethod
|
||||
def report_fields():
|
||||
"""Generic serializer fields for a report template."""
|
||||
return ['pk', 'name', 'description', 'template', 'filters', 'enabled']
|
||||
return [
|
||||
'pk',
|
||||
'name',
|
||||
'description',
|
||||
'template',
|
||||
'filters',
|
||||
'page_size',
|
||||
'landscape',
|
||||
'enabled',
|
||||
]
|
||||
|
||||
|
||||
class TestReportSerializer(ReportSerializerBase):
|
||||
|
BIN
docs/docs/assets/images/report/template-editor.png
Normal file
BIN
docs/docs/assets/images/report/template-editor.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 327 KiB |
BIN
docs/docs/assets/images/report/template-table.png
Normal file
BIN
docs/docs/assets/images/report/template-table.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 422 KiB |
33
docs/docs/report/template_editor.md
Normal file
33
docs/docs/report/template_editor.md
Normal file
@ -0,0 +1,33 @@
|
||||
---
|
||||
title: Template editor
|
||||
---
|
||||
|
||||
## Template editor
|
||||
|
||||
The Template Editor is integrated into the Admin center of Platform UI (PUI). It allows users to create and edit label and report templates directly with a side by side preview for a more productive workflow.
|
||||
|
||||
![Template Table](../assets/images/report/template-table.png)
|
||||
|
||||
On the left side (1) are all possible possible template types for labels and reports listed. With the "+" button (2), above the template table (3), new templates for the selected type can be created. To switch to the template editor click on a template.
|
||||
|
||||
### Editing Templates
|
||||
|
||||
The Template Editor provides a split pane interface, with a [code editor (1)](#code-editor-1) on the left and a [preview area (2)](#previewing-templates-2) on the right. The split view can be resized as required with the drag handler in-between.
|
||||
|
||||
![Template Editor](../assets/images/report/template-editor.png)
|
||||
|
||||
#### Code editor (1)
|
||||
|
||||
The code editor supports syntax highlighting and making it easier to write and edit templates.
|
||||
|
||||
#### Previewing Templates (2)
|
||||
|
||||
One of the key features of the Template Editor is the ability to preview the rendered output of the templates. Users can select an InvenTree item (3) to render the template for, allowing them to see how the final output will look in production.
|
||||
|
||||
To render the preview currently **overriding the production template is required** due to API limitations. To do so, first select an item (3) to use for the preview and then press the "Save & Reload preview" button at the top right of the code editor. The first time a confirm dialog opens that you need to confirm that the production template now will be overridden.
|
||||
|
||||
If you don't want to override the template, but just render a preview for a template how it is currently stored in InvenTree, click on the down arrow on the right of that button and select "Reload preview". That will just render the selected item (4) with the InvenTree stored template.
|
||||
|
||||
#### Edit template metadata
|
||||
|
||||
Editing metadata such as name, description, filters and even width/height for labels and orientation/page size for reports can be done from the edit modal accessible when clicking on the three dots (4) and select "Edit" in the dropdown menu.
|
@ -135,6 +135,7 @@ nav:
|
||||
- Project Codes: order/project_codes.md
|
||||
- Report:
|
||||
- Templates: report/report.md
|
||||
- Template Editor: report/template_editor.md
|
||||
- Report Types:
|
||||
- Test Reports: report/test.md
|
||||
- Build Order: report/build.md
|
||||
|
@ -11,6 +11,7 @@
|
||||
"compile": "lingui compile --typescript"
|
||||
},
|
||||
"dependencies": {
|
||||
"@codemirror/lang-liquid": "^6.2.1",
|
||||
"@emotion/react": "^11.11.1",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.4.2",
|
||||
"@fortawesome/free-regular-svg-icons": "^6.4.2",
|
||||
@ -30,6 +31,9 @@
|
||||
"@sentry/react": "^7.74.1",
|
||||
"@tabler/icons-react": "^2.39.0",
|
||||
"@tanstack/react-query": "^5.0.0",
|
||||
"@uiw/codemirror-theme-vscode": "^4.21.22",
|
||||
"@uiw/react-codemirror": "^4.21.22",
|
||||
"@uiw/react-split": "^5.9.3",
|
||||
"axios": "^1.6.0",
|
||||
"dayjs": "^1.11.10",
|
||||
"easymde": "^2.18.0",
|
||||
|
118
src/frontend/src/components/buttons/SplitButton.tsx
Normal file
118
src/frontend/src/components/buttons/SplitButton.tsx
Normal file
@ -0,0 +1,118 @@
|
||||
import {
|
||||
ActionIcon,
|
||||
Button,
|
||||
Group,
|
||||
Menu,
|
||||
Text,
|
||||
Tooltip,
|
||||
createStyles,
|
||||
useMantineTheme
|
||||
} from '@mantine/core';
|
||||
import { IconChevronDown, TablerIconsProps } from '@tabler/icons-react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
interface SplitButtonOption {
|
||||
key: string;
|
||||
name: string;
|
||||
onClick: () => void;
|
||||
icon: (props: TablerIconsProps) => JSX.Element;
|
||||
disabled?: boolean;
|
||||
tooltip?: string;
|
||||
}
|
||||
|
||||
interface SplitButtonProps {
|
||||
options: SplitButtonOption[];
|
||||
defaultSelected: string;
|
||||
selected?: string;
|
||||
setSelected?: (value: string) => void;
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
const useStyles = createStyles((theme) => ({
|
||||
button: {
|
||||
borderTopRightRadius: 0,
|
||||
borderBottomRightRadius: 0,
|
||||
'&::before': {
|
||||
borderRadius: '0 !important'
|
||||
}
|
||||
},
|
||||
icon: {
|
||||
borderTopLeftRadius: 0,
|
||||
borderBottomLeftRadius: 0,
|
||||
border: 0,
|
||||
borderLeft: `1px solid ${theme.primaryShade}`
|
||||
}
|
||||
}));
|
||||
|
||||
export function SplitButton({
|
||||
options,
|
||||
defaultSelected,
|
||||
selected,
|
||||
setSelected,
|
||||
loading
|
||||
}: SplitButtonProps) {
|
||||
const [current, setCurrent] = useState<string>(defaultSelected);
|
||||
const { classes } = useStyles();
|
||||
|
||||
useEffect(() => {
|
||||
setSelected?.(current);
|
||||
}, [current]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selected) return;
|
||||
setCurrent(selected);
|
||||
}, [selected]);
|
||||
|
||||
const currentOption = useMemo(() => {
|
||||
return options.find((option) => option.key === current);
|
||||
}, [current, options]);
|
||||
|
||||
const theme = useMantineTheme();
|
||||
|
||||
return (
|
||||
<Group noWrap style={{ gap: 0 }}>
|
||||
<Button
|
||||
onClick={currentOption?.onClick}
|
||||
disabled={loading ? false : currentOption?.disabled}
|
||||
className={classes.button}
|
||||
loading={loading}
|
||||
>
|
||||
{currentOption?.name}
|
||||
</Button>
|
||||
<Menu
|
||||
transitionProps={{ transition: 'pop' }}
|
||||
position="bottom-end"
|
||||
withinPortal
|
||||
>
|
||||
<Menu.Target>
|
||||
<ActionIcon
|
||||
variant="filled"
|
||||
color={theme.primaryColor}
|
||||
size={36}
|
||||
className={classes.icon}
|
||||
>
|
||||
<IconChevronDown size={16} />
|
||||
</ActionIcon>
|
||||
</Menu.Target>
|
||||
|
||||
<Menu.Dropdown>
|
||||
{options.map((option) => (
|
||||
<Menu.Item
|
||||
key={option.key}
|
||||
onClick={() => {
|
||||
setCurrent(option.key);
|
||||
option.onClick();
|
||||
}}
|
||||
disabled={option.disabled}
|
||||
icon={<option.icon />}
|
||||
>
|
||||
<Tooltip label={option.tooltip} position="right">
|
||||
<Text>{option.name}</Text>
|
||||
</Tooltip>
|
||||
</Menu.Item>
|
||||
))}
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
</Group>
|
||||
);
|
||||
}
|
@ -17,7 +17,7 @@ import { Suspense, useMemo } from 'react';
|
||||
import { api } from '../../App';
|
||||
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
||||
import { ModelType } from '../../enums/ModelType';
|
||||
import { InvenTreeIcon } from '../../functions/icons';
|
||||
import { InvenTreeIcon, InvenTreeIconType } from '../../functions/icons';
|
||||
import { getDetailUrl } from '../../functions/urls';
|
||||
import { apiUrl } from '../../states/ApiState';
|
||||
import { useGlobalSettingsState } from '../../states/SettingsState';
|
||||
@ -368,7 +368,7 @@ export function DetailsTableField({
|
||||
justifyContent: 'flex-start'
|
||||
}}
|
||||
>
|
||||
<InvenTreeIcon icon={field.icon ?? field.name} />
|
||||
<InvenTreeIcon icon={(field.icon ?? field.name) as InvenTreeIconType} />
|
||||
</td>
|
||||
<td>
|
||||
<Text>{field.label}</Text>
|
||||
|
@ -1,14 +1,14 @@
|
||||
import { Trans, t } from '@lingui/macro';
|
||||
import { Badge, Tooltip } from '@mantine/core';
|
||||
|
||||
import { InvenTreeIcon } from '../../functions/icons';
|
||||
import { InvenTreeIcon, InvenTreeIconType } from '../../functions/icons';
|
||||
|
||||
/**
|
||||
* Fetches and wraps an InvenTreeIcon in a flex div
|
||||
* @param icon name of icon
|
||||
*
|
||||
*/
|
||||
function PartIcon(icon: string) {
|
||||
function PartIcon(icon: InvenTreeIconType) {
|
||||
return (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '5px' }}>
|
||||
<InvenTreeIcon icon={icon} />
|
||||
|
@ -0,0 +1,152 @@
|
||||
import { liquid } from '@codemirror/lang-liquid';
|
||||
import { vscodeDark } from '@uiw/codemirror-theme-vscode';
|
||||
import { EditorView, hoverTooltip, useCodeMirror } from '@uiw/react-codemirror';
|
||||
import {
|
||||
forwardRef,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useRef,
|
||||
useState
|
||||
} from 'react';
|
||||
|
||||
import { EditorComponent } from '../TemplateEditor';
|
||||
|
||||
type Tag = {
|
||||
label: string;
|
||||
description: string;
|
||||
args: string[];
|
||||
kwargs: { [name: string]: string };
|
||||
returns: string;
|
||||
};
|
||||
|
||||
const tags: Tag[] = [
|
||||
{
|
||||
label: 'qrcode',
|
||||
description: 'Generate a QR code image',
|
||||
args: ['data'],
|
||||
kwargs: {
|
||||
fill_color: 'Fill color (default = black)',
|
||||
back_color: 'Background color (default = white)',
|
||||
version: 'Version (default = 1)',
|
||||
box_size: 'Box size (default = 20)',
|
||||
border: 'Border width (default = 1)',
|
||||
format: 'Format (default = PNG)'
|
||||
},
|
||||
returns: 'base64 encoded qr code image data'
|
||||
},
|
||||
{
|
||||
label: 'barcode',
|
||||
description: 'Generate a barcode image',
|
||||
args: ['data'],
|
||||
kwargs: {
|
||||
barcode_class: 'Barcode code',
|
||||
type: 'Barcode type (default = code128)',
|
||||
format: 'Format (default = PNG)'
|
||||
},
|
||||
returns: 'base64 encoded barcode image data'
|
||||
}
|
||||
];
|
||||
const tagsMap = Object.fromEntries(tags.map((tag) => [tag.label, tag]));
|
||||
|
||||
const renderHelp = (tag: Tag) => {
|
||||
const dom = document.createElement('div');
|
||||
dom.style.whiteSpace = 'pre-line';
|
||||
dom.style.width = '400px';
|
||||
dom.style.padding = '5px';
|
||||
dom.style.height = '200px';
|
||||
dom.style.overflowY = 'scroll';
|
||||
dom.style.border = '1px solid #000';
|
||||
|
||||
const argsStr = tag.args
|
||||
.map((arg) => ` - <code style="color: #9cdcfe;">${arg}</code>`)
|
||||
.join('\n');
|
||||
|
||||
const kwargsStr = Object.entries(tag.kwargs)
|
||||
.map(
|
||||
([name, description]) =>
|
||||
` - <code style="color: #9cdcfe;">${name}</code>: <small>${description}</small>`
|
||||
)
|
||||
.join('\n');
|
||||
|
||||
dom.innerHTML = `Name: <code style="color: #4ec9b0;">${tag.label}</code>
|
||||
<small>${tag.description}</small>
|
||||
Arguments:
|
||||
${argsStr}
|
||||
Keyword arguments:
|
||||
${kwargsStr}
|
||||
Returns: <small>${tag.returns}</small>`;
|
||||
|
||||
return dom;
|
||||
};
|
||||
|
||||
const tooltips = hoverTooltip((view, pos, side) => {
|
||||
// extract the word at the current hover position into the variable text
|
||||
let { from, to, text } = view.state.doc.lineAt(pos);
|
||||
let start = pos,
|
||||
end = pos;
|
||||
while (start > from && /\w/.test(text[start - from - 1])) start--;
|
||||
while (end < to && /\w/.test(text[end - from])) end++;
|
||||
if ((start == pos && side < 0) || (end == pos && side > 0)) return null;
|
||||
text = text.slice(start - from, end - from);
|
||||
|
||||
if (!(text in tagsMap)) return null;
|
||||
return {
|
||||
pos: start,
|
||||
end,
|
||||
above: true,
|
||||
create(view) {
|
||||
return { dom: renderHelp(tagsMap[text]) };
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const extensions = [
|
||||
liquid({
|
||||
tags: Object.values(tagsMap).map((tag) => {
|
||||
return {
|
||||
label: tag.label,
|
||||
type: 'function',
|
||||
info: () => renderHelp(tag),
|
||||
boost: 99
|
||||
};
|
||||
})
|
||||
}),
|
||||
tooltips,
|
||||
EditorView.theme({
|
||||
'&.cm-editor': {
|
||||
height: '100%'
|
||||
}
|
||||
})
|
||||
];
|
||||
|
||||
export const CodeEditorComponent: EditorComponent = forwardRef((props, ref) => {
|
||||
const editor = useRef<HTMLDivElement | null>(null);
|
||||
const [code, setCode] = useState('');
|
||||
const { setContainer } = useCodeMirror({
|
||||
container: editor.current,
|
||||
extensions,
|
||||
value: code,
|
||||
onChange: (value) => setCode(value),
|
||||
theme: vscodeDark
|
||||
});
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
setCode: (code) => setCode(code),
|
||||
getCode: () => code
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
if (editor.current) {
|
||||
setContainer(editor.current);
|
||||
}
|
||||
}, [editor.current]);
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flex: '1', position: 'relative' }}>
|
||||
<div
|
||||
style={{ position: 'absolute', top: 0, bottom: 0, left: 0, right: 0 }}
|
||||
ref={editor}
|
||||
></div>
|
||||
</div>
|
||||
);
|
||||
});
|
@ -0,0 +1,12 @@
|
||||
import { t } from '@lingui/macro';
|
||||
import { IconCode } from '@tabler/icons-react';
|
||||
|
||||
import { Editor } from '../TemplateEditor';
|
||||
import { CodeEditorComponent } from './CodeEditor';
|
||||
|
||||
export const CodeEditor: Editor = {
|
||||
key: 'code',
|
||||
name: t`Code`,
|
||||
icon: IconCode,
|
||||
component: CodeEditorComponent
|
||||
};
|
@ -0,0 +1,80 @@
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { forwardRef, useImperativeHandle, useState } from 'react';
|
||||
|
||||
import { api } from '../../../../App';
|
||||
import { PreviewAreaComponent } from '../TemplateEditor';
|
||||
|
||||
export const PdfPreviewComponent: PreviewAreaComponent = forwardRef(
|
||||
(props, ref) => {
|
||||
const [pdfUrl, setPdfUrl] = useState('');
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
updatePreview: async (
|
||||
code,
|
||||
previewItem,
|
||||
saveTemplate,
|
||||
{ uploadKey, uploadUrl, preview: { itemKey }, templateType }
|
||||
) => {
|
||||
if (saveTemplate) {
|
||||
const formData = new FormData();
|
||||
formData.append(uploadKey, new File([code], 'template.html'));
|
||||
|
||||
const res = await api.patch(uploadUrl, formData);
|
||||
if (res.status !== 200) {
|
||||
throw new Error(res.data);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- TODO: Fix this when implementing the new API ----
|
||||
let preview = await api.get(
|
||||
uploadUrl + `print/?plugin=inventreelabel&${itemKey}=${previewItem}`,
|
||||
{
|
||||
responseType: templateType === 'label' ? 'json' : 'blob',
|
||||
timeout: 30000,
|
||||
validateStatus: () => true
|
||||
}
|
||||
);
|
||||
|
||||
if (preview.status !== 200) {
|
||||
if (preview.data?.non_field_errors) {
|
||||
throw new Error(preview.data?.non_field_errors.join(', '));
|
||||
}
|
||||
|
||||
throw new Error(preview.data);
|
||||
}
|
||||
|
||||
if (templateType === 'label') {
|
||||
preview = await api.get(preview.data.file, {
|
||||
responseType: 'blob'
|
||||
});
|
||||
}
|
||||
// ----
|
||||
let pdf = new Blob([preview.data], {
|
||||
type: preview.headers['content-type']
|
||||
});
|
||||
let srcUrl = URL.createObjectURL(pdf);
|
||||
|
||||
setPdfUrl(srcUrl + '#view=fitH');
|
||||
}
|
||||
}));
|
||||
|
||||
return (
|
||||
<>
|
||||
{!pdfUrl && (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
height: '100%',
|
||||
width: '100%'
|
||||
}}
|
||||
>
|
||||
<Trans>Preview not available, click "Reload Preview".</Trans>
|
||||
</div>
|
||||
)}
|
||||
{pdfUrl && <iframe src={pdfUrl} width="100%" height="100%" />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
@ -0,0 +1,12 @@
|
||||
import { t } from '@lingui/macro';
|
||||
import { IconFileTypePdf } from '@tabler/icons-react';
|
||||
|
||||
import { PreviewArea } from '../TemplateEditor';
|
||||
import { PdfPreviewComponent } from './PdfPreview';
|
||||
|
||||
export const PdfPreview: PreviewArea = {
|
||||
key: 'pdf-preview',
|
||||
name: t`PDF Preview`,
|
||||
icon: IconFileTypePdf,
|
||||
component: PdfPreviewComponent
|
||||
};
|
@ -0,0 +1,380 @@
|
||||
import { t } from '@lingui/macro';
|
||||
import {
|
||||
Alert,
|
||||
CloseButton,
|
||||
Code,
|
||||
Group,
|
||||
Overlay,
|
||||
Stack,
|
||||
Tabs
|
||||
} from '@mantine/core';
|
||||
import { openConfirmModal } from '@mantine/modals';
|
||||
import { showNotification } from '@mantine/notifications';
|
||||
import {
|
||||
IconAlertTriangle,
|
||||
IconDeviceFloppy,
|
||||
IconExclamationCircle,
|
||||
IconRefresh,
|
||||
TablerIconsProps
|
||||
} from '@tabler/icons-react';
|
||||
import Split from '@uiw/react-split';
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState
|
||||
} from 'react';
|
||||
|
||||
import { api } from '../../../App';
|
||||
import { ModelType } from '../../../enums/ModelType';
|
||||
import { apiUrl } from '../../../states/ApiState';
|
||||
import { TemplateI } from '../../../tables/settings/TemplateTable';
|
||||
import { SplitButton } from '../../buttons/SplitButton';
|
||||
import { StandaloneField } from '../../forms/StandaloneField';
|
||||
import { ModelInformationDict } from '../../render/ModelType';
|
||||
|
||||
type EditorProps = {
|
||||
template: TemplateI;
|
||||
};
|
||||
type EditorRef = {
|
||||
setCode: (code: string) => void | Promise<void>;
|
||||
getCode: () => (string | undefined) | Promise<string | undefined>;
|
||||
};
|
||||
export type EditorComponent = React.ForwardRefExoticComponent<
|
||||
EditorProps & React.RefAttributes<EditorRef>
|
||||
>;
|
||||
export type Editor = {
|
||||
key: string;
|
||||
name: string;
|
||||
icon: (props: TablerIconsProps) => React.JSX.Element;
|
||||
component: EditorComponent;
|
||||
};
|
||||
|
||||
type PreviewAreaProps = {};
|
||||
type PreviewAreaRef = {
|
||||
updatePreview: (
|
||||
code: string,
|
||||
previewItem: string,
|
||||
saveTemplate: boolean,
|
||||
templateEditorProps: TemplateEditorProps
|
||||
) => void | Promise<void>;
|
||||
};
|
||||
export type PreviewAreaComponent = React.ForwardRefExoticComponent<
|
||||
PreviewAreaProps & React.RefAttributes<PreviewAreaRef>
|
||||
>;
|
||||
export type PreviewArea = {
|
||||
key: string;
|
||||
name: string;
|
||||
icon: (props: TablerIconsProps) => React.JSX.Element;
|
||||
component: PreviewAreaComponent;
|
||||
};
|
||||
|
||||
export type TemplatePreviewProps = {
|
||||
itemKey: string;
|
||||
model: ModelType;
|
||||
filters?: Record<string, any>;
|
||||
};
|
||||
|
||||
type TemplateEditorProps = {
|
||||
downloadUrl: string;
|
||||
uploadUrl: string;
|
||||
uploadKey: string;
|
||||
preview: TemplatePreviewProps;
|
||||
templateType: 'label' | 'report';
|
||||
editors: Editor[];
|
||||
previewAreas: PreviewArea[];
|
||||
template: TemplateI;
|
||||
};
|
||||
|
||||
export function TemplateEditor(props: TemplateEditorProps) {
|
||||
const { downloadUrl, editors, previewAreas, preview } = props;
|
||||
const editorRef = useRef<EditorRef>();
|
||||
const previewRef = useRef<PreviewAreaRef>();
|
||||
|
||||
const [hasSaveConfirmed, setHasSaveConfirmed] = useState(false);
|
||||
|
||||
const [previewItem, setPreviewItem] = useState<string>('');
|
||||
const [errorOverlay, setErrorOverlay] = useState(null);
|
||||
const [isPreviewLoading, setIsPreviewLoading] = useState(false);
|
||||
|
||||
const [editorValue, setEditorValue] = useState<null | string>(editors[0].key);
|
||||
const [previewValue, setPreviewValue] = useState<null | string>(
|
||||
previewAreas[0].key
|
||||
);
|
||||
|
||||
const codeRef = useRef<string | undefined>();
|
||||
|
||||
const loadCodeToEditor = useCallback(async (code: string) => {
|
||||
try {
|
||||
return await Promise.resolve(editorRef.current?.setCode(code));
|
||||
} catch (e: any) {
|
||||
showNotification({
|
||||
title: t`Error loading template`,
|
||||
message: e?.message ?? e,
|
||||
color: 'red'
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
const getCodeFromEditor = useCallback(async () => {
|
||||
try {
|
||||
return await Promise.resolve(editorRef.current?.getCode());
|
||||
} catch (e: any) {
|
||||
showNotification({
|
||||
title: t`Error saving template`,
|
||||
message: e?.message ?? e,
|
||||
color: 'red'
|
||||
});
|
||||
return undefined;
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!downloadUrl) return;
|
||||
|
||||
api.get(downloadUrl).then((res) => {
|
||||
codeRef.current = res.data;
|
||||
loadCodeToEditor(res.data);
|
||||
});
|
||||
}, [downloadUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
if (codeRef.current === undefined) return;
|
||||
loadCodeToEditor(codeRef.current);
|
||||
}, [editorValue]);
|
||||
|
||||
const updatePreview = useCallback(
|
||||
async (confirmed: boolean, saveTemplate: boolean = true) => {
|
||||
if (!confirmed) {
|
||||
openConfirmModal({
|
||||
title: t`Save & Reload preview?`,
|
||||
children: (
|
||||
<Alert
|
||||
color="yellow"
|
||||
icon={<IconAlertTriangle />}
|
||||
title={t`Are you sure you want to Save & Reload the preview?`}
|
||||
>
|
||||
{t`To render the preview the current template needs to be replaced on the server with your modifications which may break the label if it is under active use. Do you want to proceed?`}
|
||||
</Alert>
|
||||
),
|
||||
labels: {
|
||||
confirm: t`Save & Reload`,
|
||||
cancel: t`Cancel`
|
||||
},
|
||||
confirmProps: {
|
||||
color: 'yellow'
|
||||
},
|
||||
onConfirm: () => {
|
||||
setHasSaveConfirmed(true);
|
||||
updatePreview(true);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const code = await getCodeFromEditor();
|
||||
if (code === undefined || !previewItem) return;
|
||||
|
||||
setIsPreviewLoading(true);
|
||||
Promise.resolve(
|
||||
previewRef.current?.updatePreview(
|
||||
code,
|
||||
previewItem,
|
||||
saveTemplate,
|
||||
props
|
||||
)
|
||||
)
|
||||
.then(() => {
|
||||
setErrorOverlay(null);
|
||||
showNotification({
|
||||
title: t`Preview updated`,
|
||||
message: t`The preview has been updated successfully.`,
|
||||
color: 'green'
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
setErrorOverlay(error.message);
|
||||
})
|
||||
.finally(() => {
|
||||
setIsPreviewLoading(false);
|
||||
});
|
||||
},
|
||||
[previewItem]
|
||||
);
|
||||
|
||||
const previewApiUrl = useMemo(
|
||||
() => ModelInformationDict[preview.model].api_endpoint,
|
||||
[preview.model]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
api
|
||||
.get(apiUrl(previewApiUrl), { params: { limit: 1, ...preview.filters } })
|
||||
.then((res) => {
|
||||
if (res.data.results.length === 0) return;
|
||||
setPreviewItem(res.data.results[0].pk);
|
||||
});
|
||||
}, [previewApiUrl, preview.filters]);
|
||||
|
||||
return (
|
||||
<Stack style={{ height: '100%', flex: '1' }}>
|
||||
<Split style={{ gap: '10px' }}>
|
||||
<Tabs
|
||||
value={editorValue}
|
||||
onTabChange={async (v) => {
|
||||
codeRef.current = await getCodeFromEditor();
|
||||
setEditorValue(v);
|
||||
}}
|
||||
keepMounted={false}
|
||||
style={{
|
||||
minWidth: '300px',
|
||||
flex: '1',
|
||||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
}}
|
||||
>
|
||||
<Tabs.List>
|
||||
{editors.map((Editor) => (
|
||||
<Tabs.Tab
|
||||
key={Editor.key}
|
||||
value={Editor.key}
|
||||
icon={<Editor.icon size="0.8rem" />}
|
||||
>
|
||||
{Editor.name}
|
||||
</Tabs.Tab>
|
||||
))}
|
||||
|
||||
<Group position="right" style={{ flex: '1' }} noWrap>
|
||||
<SplitButton
|
||||
loading={isPreviewLoading}
|
||||
defaultSelected="preview_save"
|
||||
options={[
|
||||
{
|
||||
key: 'preview',
|
||||
name: t`Reload preview`,
|
||||
tooltip: t`Use the currently stored template from the server`,
|
||||
icon: IconRefresh,
|
||||
onClick: () => updatePreview(true, false),
|
||||
disabled: !previewItem || isPreviewLoading
|
||||
},
|
||||
{
|
||||
key: 'preview_save',
|
||||
name: t`Save & Reload preview`,
|
||||
tooltip: t`Save the current template and reload the preview`,
|
||||
icon: IconDeviceFloppy,
|
||||
onClick: () => updatePreview(hasSaveConfirmed),
|
||||
disabled: !previewItem || isPreviewLoading
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</Group>
|
||||
</Tabs.List>
|
||||
|
||||
{editors.map((Editor) => (
|
||||
<Tabs.Panel
|
||||
key={Editor.key}
|
||||
value={Editor.key}
|
||||
style={{
|
||||
display: 'flex',
|
||||
flex: editorValue === Editor.key ? 1 : 0
|
||||
}}
|
||||
>
|
||||
{/* @ts-ignore-next-line */}
|
||||
<Editor.component ref={editorRef} template={props.template} />
|
||||
</Tabs.Panel>
|
||||
))}
|
||||
</Tabs>
|
||||
|
||||
<Tabs
|
||||
value={previewValue}
|
||||
onTabChange={setPreviewValue}
|
||||
style={{
|
||||
minWidth: '200px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
}}
|
||||
>
|
||||
<Tabs.List>
|
||||
{previewAreas.map((PreviewArea) => (
|
||||
<Tabs.Tab
|
||||
key={PreviewArea.key}
|
||||
value={PreviewArea.key}
|
||||
icon={<PreviewArea.icon size="0.8rem" />}
|
||||
>
|
||||
{PreviewArea.name}
|
||||
</Tabs.Tab>
|
||||
))}
|
||||
</Tabs.List>
|
||||
|
||||
<div
|
||||
style={{
|
||||
minWidth: '100%',
|
||||
paddingBottom: '10px',
|
||||
paddingTop: '10px'
|
||||
}}
|
||||
>
|
||||
<StandaloneField
|
||||
fieldDefinition={{
|
||||
field_type: 'related field',
|
||||
api_url: apiUrl(previewApiUrl),
|
||||
description: '',
|
||||
label: t`Select` + ' ' + preview.model + ' ' + t`to preview`,
|
||||
model: preview.model,
|
||||
value: previewItem,
|
||||
filters: preview.filters,
|
||||
onValueChange: (value) => setPreviewItem(value)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{previewAreas.map((PreviewArea) => (
|
||||
<Tabs.Panel
|
||||
key={PreviewArea.key}
|
||||
value={PreviewArea.key}
|
||||
style={{
|
||||
display: 'flex',
|
||||
flex: previewValue === PreviewArea.key ? 1 : 0
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
height: '100%',
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
flex: '1'
|
||||
}}
|
||||
>
|
||||
{/* @ts-ignore-next-line */}
|
||||
<PreviewArea.component ref={previewRef} />
|
||||
|
||||
{errorOverlay && (
|
||||
<Overlay color="red" center blur={0.2}>
|
||||
<CloseButton
|
||||
onClick={() => setErrorOverlay(null)}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '10px',
|
||||
right: '10px',
|
||||
color: '#fff'
|
||||
}}
|
||||
variant="filled"
|
||||
/>
|
||||
<Alert
|
||||
color="red"
|
||||
icon={<IconExclamationCircle />}
|
||||
title={t`Error rendering template`}
|
||||
mx="10px"
|
||||
>
|
||||
<Code>{errorOverlay}</Code>
|
||||
</Alert>
|
||||
</Overlay>
|
||||
)}
|
||||
</div>
|
||||
</Tabs.Panel>
|
||||
))}
|
||||
</Tabs>
|
||||
</Split>
|
||||
</Stack>
|
||||
);
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
export { TemplateEditor } from './TemplateEditor';
|
||||
export { CodeEditor } from './CodeEditor';
|
||||
export { PdfPreview } from './PdfPreview';
|
35
src/frontend/src/components/forms/StandaloneField.tsx
Normal file
35
src/frontend/src/components/forms/StandaloneField.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
import { useMemo } from 'react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
|
||||
import { ApiFormField, ApiFormFieldType } from './fields/ApiFormField';
|
||||
|
||||
export function StandaloneField({
|
||||
fieldDefinition,
|
||||
defaultValue
|
||||
}: {
|
||||
fieldDefinition: ApiFormFieldType;
|
||||
defaultValue?: any;
|
||||
}) {
|
||||
const defaultValues = useMemo(() => {
|
||||
if (defaultValue)
|
||||
return {
|
||||
field: defaultValue
|
||||
};
|
||||
return {};
|
||||
}, [defaultValue]);
|
||||
|
||||
const form = useForm<{}>({
|
||||
criteriaMode: 'all',
|
||||
defaultValues
|
||||
});
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<ApiFormField
|
||||
fieldName="field"
|
||||
definition={fieldDefinition}
|
||||
control={form.control}
|
||||
/>
|
||||
</FormProvider>
|
||||
);
|
||||
}
|
@ -53,7 +53,11 @@ export function RelatedModelField({
|
||||
// If the value is unchanged, do nothing
|
||||
if (field.value === pk) return;
|
||||
|
||||
if (field.value !== null && field.value !== undefined) {
|
||||
if (
|
||||
field.value !== null &&
|
||||
field.value !== undefined &&
|
||||
field.value !== ''
|
||||
) {
|
||||
const url = `${definition.api_url}${field.value}/`;
|
||||
|
||||
api.get(url).then((response) => {
|
||||
|
@ -23,6 +23,7 @@ export type ActionDropdownItem = {
|
||||
name: string;
|
||||
tooltip?: string;
|
||||
disabled?: boolean;
|
||||
hidden?: boolean;
|
||||
onClick?: () => void;
|
||||
indicator?: Omit<IndicatorProps, 'children'>;
|
||||
};
|
||||
@ -42,7 +43,7 @@ export function ActionDropdown({
|
||||
actions: ActionDropdownItem[];
|
||||
}) {
|
||||
const hasActions = useMemo(() => {
|
||||
return actions.some((action) => !action.disabled);
|
||||
return actions.some((action) => !action.hidden);
|
||||
}, [actions]);
|
||||
const indicatorProps = useMemo(() => {
|
||||
return actions.find((action) => action.indicator);
|
||||
@ -61,7 +62,7 @@ export function ActionDropdown({
|
||||
</Indicator>
|
||||
<Menu.Dropdown>
|
||||
{actions.map((action) =>
|
||||
action.disabled ? null : (
|
||||
action.hidden ? null : (
|
||||
<Indicator
|
||||
disabled={!action.indicator}
|
||||
{...action.indicator}
|
||||
@ -108,10 +109,10 @@ export function BarcodeActionDropdown({
|
||||
|
||||
// Common action button for viewing a barcode
|
||||
export function ViewBarcodeAction({
|
||||
disabled = false,
|
||||
hidden = false,
|
||||
onClick
|
||||
}: {
|
||||
disabled?: boolean;
|
||||
hidden?: boolean;
|
||||
onClick?: () => void;
|
||||
}): ActionDropdownItem {
|
||||
return {
|
||||
@ -119,16 +120,16 @@ export function ViewBarcodeAction({
|
||||
name: t`View`,
|
||||
tooltip: t`View barcode`,
|
||||
onClick: onClick,
|
||||
disabled: disabled
|
||||
hidden: hidden
|
||||
};
|
||||
}
|
||||
|
||||
// Common action button for linking a custom barcode
|
||||
export function LinkBarcodeAction({
|
||||
disabled = false,
|
||||
hidden = false,
|
||||
onClick
|
||||
}: {
|
||||
disabled?: boolean;
|
||||
hidden?: boolean;
|
||||
onClick?: () => void;
|
||||
}): ActionDropdownItem {
|
||||
return {
|
||||
@ -136,16 +137,16 @@ export function LinkBarcodeAction({
|
||||
name: t`Link Barcode`,
|
||||
tooltip: t`Link custom barcode`,
|
||||
onClick: onClick,
|
||||
disabled: disabled
|
||||
hidden: hidden
|
||||
};
|
||||
}
|
||||
|
||||
// Common action button for un-linking a custom barcode
|
||||
export function UnlinkBarcodeAction({
|
||||
disabled = false,
|
||||
hidden = false,
|
||||
onClick
|
||||
}: {
|
||||
disabled?: boolean;
|
||||
hidden?: boolean;
|
||||
onClick?: () => void;
|
||||
}): ActionDropdownItem {
|
||||
return {
|
||||
@ -153,17 +154,17 @@ export function UnlinkBarcodeAction({
|
||||
name: t`Unlink Barcode`,
|
||||
tooltip: t`Unlink custom barcode`,
|
||||
onClick: onClick,
|
||||
disabled: disabled
|
||||
hidden: hidden
|
||||
};
|
||||
}
|
||||
|
||||
// Common action button for editing an item
|
||||
export function EditItemAction({
|
||||
disabled = false,
|
||||
hidden = false,
|
||||
tooltip,
|
||||
onClick
|
||||
}: {
|
||||
disabled?: boolean;
|
||||
hidden?: boolean;
|
||||
tooltip?: string;
|
||||
onClick?: () => void;
|
||||
}): ActionDropdownItem {
|
||||
@ -172,17 +173,17 @@ export function EditItemAction({
|
||||
name: t`Edit`,
|
||||
tooltip: tooltip ?? `Edit item`,
|
||||
onClick: onClick,
|
||||
disabled: disabled
|
||||
hidden: hidden
|
||||
};
|
||||
}
|
||||
|
||||
// Common action button for deleting an item
|
||||
export function DeleteItemAction({
|
||||
disabled = false,
|
||||
hidden = false,
|
||||
tooltip,
|
||||
onClick
|
||||
}: {
|
||||
disabled?: boolean;
|
||||
hidden?: boolean;
|
||||
tooltip?: string;
|
||||
onClick?: () => void;
|
||||
}): ActionDropdownItem {
|
||||
@ -191,17 +192,17 @@ export function DeleteItemAction({
|
||||
name: t`Delete`,
|
||||
tooltip: tooltip ?? t`Delete item`,
|
||||
onClick: onClick,
|
||||
disabled: disabled
|
||||
hidden: hidden
|
||||
};
|
||||
}
|
||||
|
||||
// Common action button for duplicating an item
|
||||
export function DuplicateItemAction({
|
||||
disabled = false,
|
||||
hidden = false,
|
||||
tooltip,
|
||||
onClick
|
||||
}: {
|
||||
disabled?: boolean;
|
||||
hidden?: boolean;
|
||||
tooltip?: string;
|
||||
onClick?: () => void;
|
||||
}): ActionDropdownItem {
|
||||
@ -210,6 +211,6 @@ export function DuplicateItemAction({
|
||||
name: t`Duplicate`,
|
||||
tooltip: tooltip ?? t`Duplicate item`,
|
||||
onClick: onClick,
|
||||
disabled: disabled
|
||||
hidden: hidden
|
||||
};
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ import { Trans } from '@lingui/macro';
|
||||
import { Code, Flex, Group, Text } from '@mantine/core';
|
||||
import { Link, To } from 'react-router-dom';
|
||||
|
||||
import { DetailDrawerLink } from '../nav/DetailDrawer';
|
||||
import { YesNoButton } from './YesNoButton';
|
||||
|
||||
export function InfoItem({
|
||||
@ -9,13 +10,15 @@ export function InfoItem({
|
||||
children,
|
||||
type,
|
||||
value,
|
||||
link
|
||||
link,
|
||||
detailDrawerLink
|
||||
}: {
|
||||
name: string;
|
||||
children?: React.ReactNode;
|
||||
type?: 'text' | 'boolean' | 'code';
|
||||
value?: any;
|
||||
link?: To;
|
||||
detailDrawerLink?: boolean;
|
||||
}) {
|
||||
function renderComponent() {
|
||||
if (value === undefined) return null;
|
||||
@ -46,7 +49,15 @@ export function InfoItem({
|
||||
</Text>
|
||||
<Flex>
|
||||
{children}
|
||||
{link ? <Link to={link}>{renderComponent()}</Link> : renderComponent()}
|
||||
{link ? (
|
||||
detailDrawerLink ? (
|
||||
<DetailDrawerLink to={link} text={value} />
|
||||
) : (
|
||||
<Link to={link}>{renderComponent()}</Link>
|
||||
)
|
||||
) : (
|
||||
renderComponent()
|
||||
)}
|
||||
</Flex>
|
||||
</Group>
|
||||
);
|
||||
|
@ -5,11 +5,15 @@ import {
|
||||
Group,
|
||||
MantineNumberSize,
|
||||
Stack,
|
||||
Text
|
||||
Text,
|
||||
createStyles
|
||||
} from '@mantine/core';
|
||||
import { IconChevronLeft } from '@tabler/icons-react';
|
||||
import { useMemo } from 'react';
|
||||
import { Route, Routes, useNavigate, useParams } from 'react-router-dom';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { Link, Route, Routes, useNavigate, useParams } from 'react-router-dom';
|
||||
import type { To } from 'react-router-dom';
|
||||
|
||||
import { useLocalState } from '../../states/LocalState';
|
||||
|
||||
/**
|
||||
* @param title - drawer title
|
||||
@ -25,6 +29,13 @@ export interface DrawerProps {
|
||||
size?: MantineNumberSize;
|
||||
}
|
||||
|
||||
const useStyles = createStyles(() => ({
|
||||
flex: {
|
||||
display: 'flex',
|
||||
flex: 1
|
||||
}
|
||||
}));
|
||||
|
||||
function DetailDrawerComponent({
|
||||
title,
|
||||
position = 'right',
|
||||
@ -33,28 +44,47 @@ function DetailDrawerComponent({
|
||||
}: DrawerProps) {
|
||||
const navigate = useNavigate();
|
||||
const { id } = useParams();
|
||||
const { classes } = useStyles();
|
||||
|
||||
const content = renderContent(id);
|
||||
const opened = useMemo(() => !!id && !!content, [id, content]);
|
||||
|
||||
const [detailDrawerStack, addDetailDrawer] = useLocalState((state) => [
|
||||
state.detailDrawerStack,
|
||||
state.addDetailDrawer
|
||||
]);
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
opened={opened}
|
||||
onClose={() => navigate('../')}
|
||||
onClose={() => {
|
||||
navigate('../');
|
||||
addDetailDrawer(false);
|
||||
}}
|
||||
position={position}
|
||||
size={size}
|
||||
classNames={{ root: classes.flex, body: classes.flex }}
|
||||
scrollAreaComponent={Stack}
|
||||
title={
|
||||
<Group>
|
||||
<ActionIcon variant="outline" onClick={() => navigate(-1)}>
|
||||
<IconChevronLeft />
|
||||
</ActionIcon>
|
||||
{detailDrawerStack > 0 && (
|
||||
<ActionIcon
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
navigate(-1);
|
||||
addDetailDrawer(-1);
|
||||
}}
|
||||
>
|
||||
<IconChevronLeft />
|
||||
</ActionIcon>
|
||||
)}
|
||||
<Text size="xl" fw={600} variant="gradient">
|
||||
{title}
|
||||
</Text>
|
||||
</Group>
|
||||
}
|
||||
>
|
||||
<Stack spacing={'xs'}>
|
||||
<Stack spacing={'xs'} className={classes.flex}>
|
||||
<Divider />
|
||||
{content}
|
||||
</Stack>
|
||||
@ -69,3 +99,17 @@ export function DetailDrawer(props: DrawerProps) {
|
||||
</Routes>
|
||||
);
|
||||
}
|
||||
|
||||
export function DetailDrawerLink({ to, text }: { to: To; text: string }) {
|
||||
const addDetailDrawer = useLocalState((state) => state.addDetailDrawer);
|
||||
|
||||
const onNavigate = useCallback(() => {
|
||||
addDetailDrawer(1);
|
||||
}, [addDetailDrawer]);
|
||||
|
||||
return (
|
||||
<Link to={to} onClick={onNavigate}>
|
||||
<Text>{text}</Text>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
@ -34,6 +34,7 @@ export type PanelType = {
|
||||
content?: ReactNode;
|
||||
hidden?: boolean;
|
||||
disabled?: boolean;
|
||||
showHeadline?: boolean;
|
||||
};
|
||||
|
||||
export type PanelProps = {
|
||||
@ -125,6 +126,8 @@ function BasePanelGroup({
|
||||
// icon={(<InvenTreeIcon icon={panel.name}/>)} // Enable when implementing Icon manager everywhere
|
||||
icon={panel.icon}
|
||||
hidden={panel.hidden}
|
||||
disabled={panel.disabled}
|
||||
style={{ cursor: panel.disabled ? 'unset' : 'pointer' }}
|
||||
>
|
||||
{expanded && panel.label}
|
||||
</Tabs.Tab>
|
||||
@ -159,8 +162,12 @@ function BasePanelGroup({
|
||||
}}
|
||||
>
|
||||
<Stack spacing="md">
|
||||
<StylishText size="xl">{panel.label}</StylishText>
|
||||
<Divider />
|
||||
{panel.showHeadline !== false && (
|
||||
<>
|
||||
<StylishText size="xl">{panel.label}</StylishText>
|
||||
<Divider />
|
||||
</>
|
||||
)}
|
||||
{panel.content ?? <PlaceholderPanel />}
|
||||
</Stack>
|
||||
</Tabs.Panel>
|
||||
@ -176,12 +183,11 @@ function IndexPanelComponent({ pageKey, selectedPanel, panels }: PanelProps) {
|
||||
const panelName =
|
||||
selectedPanel || state.lastUsedPanels[pageKey] || panels[0]?.name;
|
||||
|
||||
if (
|
||||
panels.findIndex(
|
||||
(p) => p.name === panelName && !p.disabled && !p.hidden
|
||||
) === -1
|
||||
) {
|
||||
return panels[0]?.name;
|
||||
const panel = panels.findIndex(
|
||||
(p) => p.name === panelName && !p.disabled && !p.hidden
|
||||
);
|
||||
if (panel === -1) {
|
||||
return panels.find((p) => !p.disabled && !p.hidden)?.name || '';
|
||||
}
|
||||
|
||||
return panelName;
|
||||
|
@ -8,7 +8,7 @@ export interface ModelInformationInterface {
|
||||
label_multiple: string;
|
||||
url_overview?: string;
|
||||
url_detail?: string;
|
||||
api_endpoint?: ApiEndpoints;
|
||||
api_endpoint: ApiEndpoints;
|
||||
cui_detail?: string;
|
||||
}
|
||||
|
||||
|
53
src/frontend/src/defaults/templates.tsx
Normal file
53
src/frontend/src/defaults/templates.tsx
Normal file
@ -0,0 +1,53 @@
|
||||
export const defaultLabelTemplate = `{% extends "label/label_base.html" %}
|
||||
|
||||
{% load l10n i18n barcode %}
|
||||
|
||||
{% block style %}
|
||||
|
||||
.qr {
|
||||
position: absolute;
|
||||
left: 0mm;
|
||||
top: 0mm;
|
||||
{% localize off %}
|
||||
height: {{ height }}mm;
|
||||
width: {{ height }}mm;
|
||||
{% endlocalize %}
|
||||
}
|
||||
|
||||
{% endblock style %}
|
||||
|
||||
{% block content %}
|
||||
<img class='qr' alt="{% trans 'QR Code' %}" src='{% qrcode qr_data %}'>
|
||||
|
||||
{% endblock content %}
|
||||
`;
|
||||
|
||||
export const defaultReportTemplate = `{% extends "report/inventree_report_base.html" %}
|
||||
|
||||
{% load i18n report barcode inventree_extras %}
|
||||
|
||||
{% block page_margin %}
|
||||
margin: 2cm;
|
||||
margin-top: 4cm;
|
||||
{% endblock page_margin %}
|
||||
|
||||
{% block bottom_left %}
|
||||
content: "v{{ report_revision }} - {{ date.isoformat }}";
|
||||
{% endblock bottom_left %}
|
||||
|
||||
{% block bottom_center %}
|
||||
content: "{% inventree_version shortstring=True %}";
|
||||
{% endblock bottom_center %}
|
||||
|
||||
{% block style %}
|
||||
<!-- Custom style -->
|
||||
{% endblock style %}
|
||||
|
||||
{% block header_content %}
|
||||
<!-- Custom header content -->
|
||||
{% endblock header_content %}
|
||||
|
||||
{% block page_content %}
|
||||
<!-- Custom page content -->
|
||||
{% endblock page_content %}
|
||||
`;
|
@ -95,6 +95,10 @@ export enum ApiEndpoints {
|
||||
return_order_list = 'order/ro/',
|
||||
return_order_attachment_list = 'order/ro/attachment/',
|
||||
|
||||
// Template API endpoints
|
||||
label_list = 'label/:variant/',
|
||||
report_list = 'report/:variant/',
|
||||
|
||||
// Plugin API endpoints
|
||||
plugin_list = 'plugins/',
|
||||
plugin_setting_list = 'plugins/:plugin/settings/',
|
||||
|
@ -8,6 +8,7 @@ import {
|
||||
IconBuildingStore,
|
||||
IconCalendar,
|
||||
IconCalendarStats,
|
||||
IconCategory,
|
||||
IconCheck,
|
||||
IconClipboardList,
|
||||
IconCopy,
|
||||
@ -58,102 +59,105 @@ import {
|
||||
IconX
|
||||
} from '@tabler/icons-react';
|
||||
import { IconFlag } from '@tabler/icons-react';
|
||||
import { IconTruckReturn } from '@tabler/icons-react';
|
||||
import { IconInfoCircle } from '@tabler/icons-react';
|
||||
import { IconCalendarTime } from '@tabler/icons-react';
|
||||
import { TablerIconsProps } from '@tabler/icons-react';
|
||||
import React from 'react';
|
||||
|
||||
const icons: { [key: string]: (props: TablerIconsProps) => React.JSX.Element } =
|
||||
{
|
||||
description: IconInfoCircle,
|
||||
variant_of: IconStatusChange,
|
||||
unallocated_stock: IconPackage,
|
||||
total_in_stock: IconPackages,
|
||||
minimum_stock: IconFlag,
|
||||
allocated_to_build_orders: IconTool,
|
||||
allocated_to_sales_orders: IconTruck,
|
||||
can_build: IconTools,
|
||||
ordering: IconShoppingCart,
|
||||
building: IconTool,
|
||||
category: IconBinaryTree2,
|
||||
IPN: Icon123,
|
||||
revision: IconGitBranch,
|
||||
units: IconRulerMeasure,
|
||||
keywords: IconTag,
|
||||
status: IconInfoCircle,
|
||||
info: IconInfoCircle,
|
||||
details: IconInfoCircle,
|
||||
parameters: IconList,
|
||||
stock: IconPackages,
|
||||
variants: IconVersions,
|
||||
allocations: IconBookmarks,
|
||||
bom: IconListTree,
|
||||
builds: IconTools,
|
||||
used_in: IconStack2,
|
||||
manufacturers: IconBuildingFactory2,
|
||||
suppliers: IconBuilding,
|
||||
customers: IconBuildingStore,
|
||||
purchase_orders: IconShoppingCart,
|
||||
sales_orders: IconTruckDelivery,
|
||||
shipment: IconTruckDelivery,
|
||||
scheduling: IconCalendarStats,
|
||||
test_templates: IconTestPipe,
|
||||
related_parts: IconLayersLinked,
|
||||
attachments: IconPaperclip,
|
||||
notes: IconNotes,
|
||||
photo: IconPhoto,
|
||||
upload: IconFileUpload,
|
||||
reject: IconX,
|
||||
select_image: IconGridDots,
|
||||
delete: IconTrash,
|
||||
const icons = {
|
||||
description: IconInfoCircle,
|
||||
variant_of: IconStatusChange,
|
||||
unallocated_stock: IconPackage,
|
||||
total_in_stock: IconPackages,
|
||||
minimum_stock: IconFlag,
|
||||
allocated_to_build_orders: IconTool,
|
||||
allocated_to_sales_orders: IconTruck,
|
||||
can_build: IconTools,
|
||||
ordering: IconShoppingCart,
|
||||
building: IconTool,
|
||||
category: IconBinaryTree2,
|
||||
IPN: Icon123,
|
||||
revision: IconGitBranch,
|
||||
units: IconRulerMeasure,
|
||||
keywords: IconTag,
|
||||
status: IconInfoCircle,
|
||||
info: IconInfoCircle,
|
||||
details: IconInfoCircle,
|
||||
parameters: IconList,
|
||||
stock: IconPackages,
|
||||
variants: IconVersions,
|
||||
allocations: IconBookmarks,
|
||||
bom: IconListTree,
|
||||
builds: IconTools,
|
||||
used_in: IconStack2,
|
||||
manufacturers: IconBuildingFactory2,
|
||||
suppliers: IconBuilding,
|
||||
customers: IconBuildingStore,
|
||||
purchase_orders: IconShoppingCart,
|
||||
sales_orders: IconTruckDelivery,
|
||||
return_orders: IconTruckReturn,
|
||||
shipment: IconTruckDelivery,
|
||||
scheduling: IconCalendarStats,
|
||||
test_templates: IconTestPipe,
|
||||
related_parts: IconLayersLinked,
|
||||
attachments: IconPaperclip,
|
||||
notes: IconNotes,
|
||||
photo: IconPhoto,
|
||||
upload: IconFileUpload,
|
||||
reject: IconX,
|
||||
select_image: IconGridDots,
|
||||
delete: IconTrash,
|
||||
|
||||
// Part Icons
|
||||
active: IconCheck,
|
||||
template: IconCopy,
|
||||
assembly: IconTool,
|
||||
component: IconGridDots,
|
||||
trackable: IconCornerUpRightDouble,
|
||||
purchaseable: IconShoppingCart,
|
||||
saleable: IconCurrencyDollar,
|
||||
virtual: IconWorldCode,
|
||||
inactive: IconX,
|
||||
part: IconBox,
|
||||
supplier_part: IconPackageImport,
|
||||
// Part Icons
|
||||
active: IconCheck,
|
||||
template: IconCopy,
|
||||
assembly: IconTool,
|
||||
component: IconGridDots,
|
||||
trackable: IconCornerUpRightDouble,
|
||||
purchaseable: IconShoppingCart,
|
||||
saleable: IconCurrencyDollar,
|
||||
virtual: IconWorldCode,
|
||||
inactive: IconX,
|
||||
part: IconBox,
|
||||
supplier_part: IconPackageImport,
|
||||
|
||||
calendar: IconCalendar,
|
||||
external: IconExternalLink,
|
||||
creation_date: IconCalendarTime,
|
||||
location: IconMapPin,
|
||||
default_location: IconMapPinHeart,
|
||||
default_supplier: IconShoppingCartHeart,
|
||||
link: IconLink,
|
||||
responsible: IconUserStar,
|
||||
pricing: IconCurrencyDollar,
|
||||
currency: IconCurrencyDollar,
|
||||
stocktake: IconClipboardList,
|
||||
user: IconUser,
|
||||
group: IconUsersGroup,
|
||||
check: IconCheck,
|
||||
copy: IconCopy,
|
||||
quantity: IconNumbers,
|
||||
progress: IconProgressCheck,
|
||||
reference: IconHash,
|
||||
website: IconWorld,
|
||||
email: IconMail,
|
||||
phone: IconPhone,
|
||||
sitemap: IconSitemap
|
||||
};
|
||||
calendar: IconCalendar,
|
||||
external: IconExternalLink,
|
||||
creation_date: IconCalendarTime,
|
||||
location: IconMapPin,
|
||||
default_location: IconMapPinHeart,
|
||||
default_supplier: IconShoppingCartHeart,
|
||||
link: IconLink,
|
||||
responsible: IconUserStar,
|
||||
pricing: IconCurrencyDollar,
|
||||
currency: IconCurrencyDollar,
|
||||
stocktake: IconClipboardList,
|
||||
user: IconUser,
|
||||
group: IconUsersGroup,
|
||||
check: IconCheck,
|
||||
copy: IconCopy,
|
||||
quantity: IconNumbers,
|
||||
progress: IconProgressCheck,
|
||||
reference: IconHash,
|
||||
website: IconWorld,
|
||||
email: IconMail,
|
||||
phone: IconPhone,
|
||||
sitemap: IconSitemap
|
||||
};
|
||||
|
||||
export type InvenTreeIconType = keyof typeof icons;
|
||||
|
||||
/**
|
||||
* Returns a Tabler Icon for the model field name supplied
|
||||
* @param field string defining field name
|
||||
*/
|
||||
export function GetIcon(field: keyof typeof icons) {
|
||||
export function GetIcon(field: InvenTreeIconType) {
|
||||
return icons[field];
|
||||
}
|
||||
|
||||
type IconProps = {
|
||||
icon: string;
|
||||
icon: InvenTreeIconType;
|
||||
iconProps?: TablerIconsProps;
|
||||
};
|
||||
|
||||
|
@ -9,6 +9,7 @@ import {
|
||||
IconListDetails,
|
||||
IconPlugConnected,
|
||||
IconScale,
|
||||
IconTemplate,
|
||||
IconUsersGroup
|
||||
} from '@tabler/icons-react';
|
||||
import { lazy, useMemo } from 'react';
|
||||
@ -55,6 +56,10 @@ const CurrencyTable = Loadable(
|
||||
lazy(() => import('../../../../tables/settings/CurrencyTable'))
|
||||
);
|
||||
|
||||
const TemplateManagementPanel = Loadable(
|
||||
lazy(() => import('./TemplateManagementPanel'))
|
||||
);
|
||||
|
||||
export default function AdminCenter() {
|
||||
const adminCenterPanels: PanelType[] = useMemo(() => {
|
||||
return [
|
||||
@ -106,6 +111,12 @@ export default function AdminCenter() {
|
||||
icon: <IconList />,
|
||||
content: <PartParameterTemplateTable />
|
||||
},
|
||||
{
|
||||
name: 'templates',
|
||||
label: t`Templates`,
|
||||
icon: <IconTemplate />,
|
||||
content: <TemplateManagementPanel />
|
||||
},
|
||||
{
|
||||
name: 'plugin',
|
||||
label: t`Plugins`,
|
||||
|
@ -0,0 +1,211 @@
|
||||
import { t } from '@lingui/macro';
|
||||
import { Stack } from '@mantine/core';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { TemplatePreviewProps } from '../../../../components/editors/TemplateEditor/TemplateEditor';
|
||||
import { ApiFormFieldSet } from '../../../../components/forms/fields/ApiFormField';
|
||||
import { PanelGroup } from '../../../../components/nav/PanelGroup';
|
||||
import {
|
||||
defaultLabelTemplate,
|
||||
defaultReportTemplate
|
||||
} from '../../../../defaults/templates';
|
||||
import { ApiEndpoints } from '../../../../enums/ApiEndpoints';
|
||||
import { ModelType } from '../../../../enums/ModelType';
|
||||
import { InvenTreeIcon, InvenTreeIconType } from '../../../../functions/icons';
|
||||
import { TemplateTable } from '../../../../tables/settings/TemplateTable';
|
||||
|
||||
type TemplateType = {
|
||||
type: 'label' | 'report';
|
||||
name: string;
|
||||
singularName: string;
|
||||
apiEndpoints: ApiEndpoints;
|
||||
templateKey: string;
|
||||
additionalFormFields?: ApiFormFieldSet;
|
||||
defaultTemplate: string;
|
||||
variants: {
|
||||
name: string;
|
||||
key: string;
|
||||
icon: InvenTreeIconType;
|
||||
preview: TemplatePreviewProps;
|
||||
}[];
|
||||
};
|
||||
|
||||
export default function TemplateManagementPanel() {
|
||||
const templateTypes = useMemo(() => {
|
||||
const templateTypes: TemplateType[] = [
|
||||
{
|
||||
type: 'label',
|
||||
name: t`Labels`,
|
||||
singularName: t`Label`,
|
||||
apiEndpoints: ApiEndpoints.label_list,
|
||||
templateKey: 'label',
|
||||
additionalFormFields: {
|
||||
width: {},
|
||||
height: {}
|
||||
},
|
||||
defaultTemplate: defaultLabelTemplate,
|
||||
variants: [
|
||||
{
|
||||
name: t`Part`,
|
||||
key: 'part',
|
||||
icon: 'part',
|
||||
preview: {
|
||||
itemKey: 'part',
|
||||
model: ModelType.part
|
||||
}
|
||||
},
|
||||
{
|
||||
name: t`Location`,
|
||||
key: 'location',
|
||||
icon: 'location',
|
||||
preview: {
|
||||
itemKey: 'location',
|
||||
model: ModelType.stocklocation
|
||||
}
|
||||
},
|
||||
{
|
||||
name: t`Stock item`,
|
||||
key: 'stock',
|
||||
icon: 'stock',
|
||||
preview: {
|
||||
itemKey: 'item',
|
||||
model: ModelType.stockitem
|
||||
}
|
||||
},
|
||||
{
|
||||
name: t`Build line`,
|
||||
key: 'buildline',
|
||||
icon: 'builds',
|
||||
preview: {
|
||||
itemKey: 'line',
|
||||
model: ModelType.build
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'report',
|
||||
name: t`Reports`,
|
||||
singularName: t`Report`,
|
||||
apiEndpoints: ApiEndpoints.report_list,
|
||||
templateKey: 'template',
|
||||
additionalFormFields: {
|
||||
page_size: {},
|
||||
landscape: {}
|
||||
},
|
||||
defaultTemplate: defaultReportTemplate,
|
||||
variants: [
|
||||
{
|
||||
name: t`Purchase order`,
|
||||
key: 'po',
|
||||
icon: 'purchase_orders',
|
||||
preview: {
|
||||
itemKey: 'order',
|
||||
model: ModelType.purchaseorder
|
||||
}
|
||||
},
|
||||
{
|
||||
name: t`Sales order`,
|
||||
key: 'so',
|
||||
icon: 'sales_orders',
|
||||
preview: {
|
||||
itemKey: 'order',
|
||||
model: ModelType.salesorder
|
||||
}
|
||||
},
|
||||
{
|
||||
name: t`Return order`,
|
||||
key: 'ro',
|
||||
icon: 'return_orders',
|
||||
preview: {
|
||||
itemKey: 'order',
|
||||
model: ModelType.returnorder
|
||||
}
|
||||
},
|
||||
{
|
||||
name: t`Build`,
|
||||
key: 'build',
|
||||
icon: 'builds',
|
||||
preview: {
|
||||
itemKey: 'build',
|
||||
model: ModelType.build
|
||||
}
|
||||
},
|
||||
{
|
||||
name: t`Bill of Materials`,
|
||||
key: 'bom',
|
||||
icon: 'bom',
|
||||
preview: {
|
||||
itemKey: 'part',
|
||||
model: ModelType.part,
|
||||
filters: { assembly: true }
|
||||
}
|
||||
},
|
||||
{
|
||||
name: t`Tests`,
|
||||
key: 'test',
|
||||
icon: 'test_templates',
|
||||
preview: {
|
||||
itemKey: 'item',
|
||||
model: ModelType.stockitem
|
||||
}
|
||||
},
|
||||
{
|
||||
name: t`Stock location`,
|
||||
key: 'slr',
|
||||
icon: 'default_location',
|
||||
preview: {
|
||||
itemKey: 'location',
|
||||
model: ModelType.stocklocation
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
return templateTypes;
|
||||
}, []);
|
||||
|
||||
const panels = useMemo(() => {
|
||||
return templateTypes.flatMap((templateType) => {
|
||||
return [
|
||||
// Add panel headline
|
||||
{ name: templateType.type, label: templateType.name, disabled: true },
|
||||
|
||||
// Add panel for each variant
|
||||
...templateType.variants.map((variant) => {
|
||||
return {
|
||||
name: variant.key,
|
||||
label: variant.name,
|
||||
content: (
|
||||
<TemplateTable
|
||||
templateProps={{
|
||||
apiEndpoint: templateType.apiEndpoints,
|
||||
templateType: templateType.type as 'label' | 'report',
|
||||
templateTypeTranslation: templateType.singularName,
|
||||
variant: variant.key,
|
||||
templateKey: templateType.templateKey,
|
||||
preview: variant.preview,
|
||||
additionalFormFields: templateType.additionalFormFields,
|
||||
defaultTemplate: templateType.defaultTemplate
|
||||
}}
|
||||
/>
|
||||
),
|
||||
icon: <InvenTreeIcon icon={variant.icon} />,
|
||||
showHeadline: false
|
||||
};
|
||||
})
|
||||
];
|
||||
});
|
||||
}, [templateTypes]);
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<PanelGroup
|
||||
pageKey="admin-center-templates"
|
||||
panels={panels}
|
||||
collapsible={false}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
}
|
@ -300,10 +300,10 @@ export default function BuildDetail() {
|
||||
actions={[
|
||||
ViewBarcodeAction({}),
|
||||
LinkBarcodeAction({
|
||||
disabled: build?.barcode_hash
|
||||
hidden: build?.barcode_hash
|
||||
}),
|
||||
UnlinkBarcodeAction({
|
||||
disabled: !build?.barcode_hash
|
||||
hidden: !build?.barcode_hash
|
||||
})
|
||||
]}
|
||||
/>,
|
||||
@ -326,7 +326,7 @@ export default function BuildDetail() {
|
||||
actions={[
|
||||
EditItemAction({
|
||||
onClick: () => editBuild.open(),
|
||||
disabled: !user.hasChangeRole(UserRoles.build)
|
||||
hidden: !user.hasChangeRole(UserRoles.build)
|
||||
}),
|
||||
DuplicateItemAction({})
|
||||
]}
|
||||
|
@ -282,11 +282,11 @@ export default function CompanyDetail(props: CompanyDetailProps) {
|
||||
icon={<IconDots />}
|
||||
actions={[
|
||||
EditItemAction({
|
||||
disabled: !user.hasChangeRole(UserRoles.purchase_order),
|
||||
hidden: !user.hasChangeRole(UserRoles.purchase_order),
|
||||
onClick: () => editCompany.open()
|
||||
}),
|
||||
DeleteItemAction({
|
||||
disabled: !user.hasDeleteRole(UserRoles.purchase_order)
|
||||
hidden: !user.hasDeleteRole(UserRoles.purchase_order)
|
||||
})
|
||||
]}
|
||||
/>
|
||||
|
@ -638,10 +638,10 @@ export default function PartDetail() {
|
||||
actions={[
|
||||
ViewBarcodeAction({}),
|
||||
LinkBarcodeAction({
|
||||
disabled: part?.barcode_hash
|
||||
hidden: part?.barcode_hash
|
||||
}),
|
||||
UnlinkBarcodeAction({
|
||||
disabled: !part?.barcode_hash
|
||||
hidden: !part?.barcode_hash
|
||||
})
|
||||
]}
|
||||
/>,
|
||||
@ -669,11 +669,11 @@ export default function PartDetail() {
|
||||
actions={[
|
||||
DuplicateItemAction({}),
|
||||
EditItemAction({
|
||||
disabled: !user.hasChangeRole(UserRoles.part),
|
||||
hidden: !user.hasChangeRole(UserRoles.part),
|
||||
onClick: () => editPart.open()
|
||||
}),
|
||||
DeleteItemAction({
|
||||
disabled: part?.active
|
||||
hidden: part?.active
|
||||
})
|
||||
]}
|
||||
/>
|
||||
|
@ -273,10 +273,10 @@ export default function PurchaseOrderDetail() {
|
||||
actions={[
|
||||
ViewBarcodeAction({}),
|
||||
LinkBarcodeAction({
|
||||
disabled: order?.barcode_hash
|
||||
hidden: order?.barcode_hash
|
||||
}),
|
||||
UnlinkBarcodeAction({
|
||||
disabled: !order?.barcode_hash
|
||||
hidden: !order?.barcode_hash
|
||||
})
|
||||
]}
|
||||
/>,
|
||||
|
@ -317,10 +317,10 @@ export default function StockDetail() {
|
||||
actions={[
|
||||
ViewBarcodeAction({}),
|
||||
LinkBarcodeAction({
|
||||
disabled: stockitem?.barcode_hash
|
||||
hidden: stockitem?.barcode_hash
|
||||
}),
|
||||
UnlinkBarcodeAction({
|
||||
disabled: !stockitem?.barcode_hash
|
||||
hidden: !stockitem?.barcode_hash
|
||||
})
|
||||
]}
|
||||
/>,
|
||||
|
@ -29,6 +29,8 @@ interface LocalStateProps {
|
||||
tableKey: string
|
||||
) => (names: Record<string, string>) => void;
|
||||
clearTableColumnNames: () => void;
|
||||
detailDrawerStack: number;
|
||||
addDetailDrawer: (value: number | false) => void;
|
||||
}
|
||||
|
||||
export const useLocalState = create<LocalStateProps>()(
|
||||
@ -61,6 +63,7 @@ export const useLocalState = create<LocalStateProps>()(
|
||||
});
|
||||
}
|
||||
},
|
||||
// tables
|
||||
tableColumnNames: {},
|
||||
getTableColumnNames: (tableKey) => {
|
||||
return get().tableColumnNames[tableKey] || {};
|
||||
@ -76,6 +79,14 @@ export const useLocalState = create<LocalStateProps>()(
|
||||
},
|
||||
clearTableColumnNames: () => {
|
||||
set({ tableColumnNames: {} });
|
||||
},
|
||||
// detail drawers
|
||||
detailDrawerStack: 0,
|
||||
addDetailDrawer: (value) => {
|
||||
set({
|
||||
detailDrawerStack:
|
||||
value === false ? 0 : get().detailDrawerStack + value
|
||||
});
|
||||
}
|
||||
}),
|
||||
{
|
||||
|
@ -20,7 +20,6 @@ import { IconCheck, IconDots, IconRefresh } from '@tabler/icons-react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { api } from '../../App';
|
||||
import { AddItemButton } from '../../components/buttons/AddItemButton';
|
||||
@ -32,7 +31,10 @@ import {
|
||||
import { InfoItem } from '../../components/items/InfoItem';
|
||||
import { UnavailableIndicator } from '../../components/items/UnavailableIndicator';
|
||||
import { YesNoButton } from '../../components/items/YesNoButton';
|
||||
import { DetailDrawer } from '../../components/nav/DetailDrawer';
|
||||
import {
|
||||
DetailDrawer,
|
||||
DetailDrawerLink
|
||||
} from '../../components/nav/DetailDrawer';
|
||||
import {
|
||||
StatusRenderer,
|
||||
TableStatusRenderer
|
||||
@ -289,9 +291,10 @@ function MachineDrawer({
|
||||
<InfoItem name={t`Machine Type`}>
|
||||
<Group spacing="xs">
|
||||
{machineType ? (
|
||||
<Link to={`../type-${machine?.machine_type}`}>
|
||||
<Text>{machineType.name}</Text>
|
||||
</Link>
|
||||
<DetailDrawerLink
|
||||
to={`../type-${machine?.machine_type}`}
|
||||
text={machineType.name}
|
||||
/>
|
||||
) : (
|
||||
<Text>{machine?.machine_type}</Text>
|
||||
)}
|
||||
@ -301,9 +304,10 @@ function MachineDrawer({
|
||||
<InfoItem name={t`Machine Driver`}>
|
||||
<Group spacing="xs">
|
||||
{machineDriver ? (
|
||||
<Link to={`../driver-${machine?.driver}`}>
|
||||
<Text>{machineDriver.name}</Text>
|
||||
</Link>
|
||||
<DetailDrawerLink
|
||||
to={`../driver-${machine?.driver}`}
|
||||
text={machineDriver.name}
|
||||
/>
|
||||
) : (
|
||||
<Text>{machine?.driver}</Text>
|
||||
)}
|
||||
|
@ -120,6 +120,7 @@ function MachineTypeDrawer({ machineTypeSlug }: { machineTypeSlug: string }) {
|
||||
? `../../plugin/${machineType?.provider_plugin?.pk}/`
|
||||
: undefined
|
||||
}
|
||||
detailDrawerLink
|
||||
/>
|
||||
)}
|
||||
<InfoItem
|
||||
@ -224,6 +225,7 @@ function MachineDriverDrawer({
|
||||
? `../type-${machineDriver?.machine_type}`
|
||||
: undefined
|
||||
}
|
||||
detailDrawerLink
|
||||
/>
|
||||
{!machineDriver?.is_builtin && (
|
||||
<InfoItem
|
||||
@ -235,6 +237,7 @@ function MachineDriverDrawer({
|
||||
? `../../plugin/${machineDriver?.provider_plugin?.pk}/`
|
||||
: undefined
|
||||
}
|
||||
detailDrawerLink
|
||||
/>
|
||||
)}
|
||||
<InfoItem
|
||||
|
303
src/frontend/src/tables/settings/TemplateTable.tsx
Normal file
303
src/frontend/src/tables/settings/TemplateTable.tsx
Normal file
@ -0,0 +1,303 @@
|
||||
import { Trans, t } from '@lingui/macro';
|
||||
import { Box, Group, LoadingOverlay, Stack, Text, Title } from '@mantine/core';
|
||||
import { IconDots } from '@tabler/icons-react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { AddItemButton } from '../../components/buttons/AddItemButton';
|
||||
import {
|
||||
CodeEditor,
|
||||
PdfPreview,
|
||||
TemplateEditor
|
||||
} from '../../components/editors/TemplateEditor';
|
||||
import { TemplatePreviewProps } from '../../components/editors/TemplateEditor/TemplateEditor';
|
||||
import { ApiFormFieldSet } from '../../components/forms/fields/ApiFormField';
|
||||
import {
|
||||
ActionDropdown,
|
||||
DeleteItemAction,
|
||||
EditItemAction
|
||||
} from '../../components/items/ActionDropdown';
|
||||
import { DetailDrawer } from '../../components/nav/DetailDrawer';
|
||||
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
||||
import {
|
||||
useCreateApiFormModal,
|
||||
useDeleteApiFormModal,
|
||||
useEditApiFormModal
|
||||
} from '../../hooks/UseForm';
|
||||
import { useInstance } from '../../hooks/UseInstance';
|
||||
import { useTable } from '../../hooks/UseTable';
|
||||
import { apiUrl } from '../../states/ApiState';
|
||||
import { TableColumn } from '../Column';
|
||||
import { BooleanColumn } from '../ColumnRenderers';
|
||||
import { InvenTreeTable } from '../InvenTreeTable';
|
||||
import { RowAction, RowDeleteAction, RowEditAction } from '../RowActions';
|
||||
|
||||
export type TemplateI = {
|
||||
pk: number;
|
||||
name: string;
|
||||
description: string;
|
||||
filters: string;
|
||||
enabled: boolean;
|
||||
};
|
||||
|
||||
export interface TemplateProps {
|
||||
apiEndpoint: ApiEndpoints;
|
||||
templateType: 'label' | 'report';
|
||||
templateTypeTranslation: string;
|
||||
variant: string;
|
||||
templateKey: string;
|
||||
additionalFormFields?: ApiFormFieldSet;
|
||||
preview: TemplatePreviewProps;
|
||||
defaultTemplate: string;
|
||||
}
|
||||
|
||||
export function TemplateDrawer({
|
||||
id,
|
||||
refreshTable,
|
||||
templateProps
|
||||
}: {
|
||||
id: string;
|
||||
refreshTable: () => void;
|
||||
templateProps: TemplateProps;
|
||||
}) {
|
||||
const {
|
||||
apiEndpoint,
|
||||
templateType,
|
||||
templateTypeTranslation,
|
||||
variant,
|
||||
additionalFormFields
|
||||
} = templateProps;
|
||||
const navigate = useNavigate();
|
||||
const {
|
||||
instance: template,
|
||||
refreshInstance,
|
||||
instanceQuery: { isFetching, error }
|
||||
} = useInstance<TemplateI>({
|
||||
endpoint: apiEndpoint,
|
||||
pathParams: { variant },
|
||||
pk: id,
|
||||
throwError: true
|
||||
});
|
||||
|
||||
const editTemplate = useEditApiFormModal({
|
||||
url: apiEndpoint,
|
||||
pathParams: { variant },
|
||||
pk: id,
|
||||
title: t`Edit` + ' ' + templateTypeTranslation,
|
||||
fields: {
|
||||
name: {},
|
||||
description: {},
|
||||
filters: {},
|
||||
enabled: {},
|
||||
...additionalFormFields
|
||||
},
|
||||
onFormSuccess: (data) => {
|
||||
refreshInstance();
|
||||
refreshTable();
|
||||
}
|
||||
});
|
||||
|
||||
const deleteTemplate = useDeleteApiFormModal({
|
||||
url: apiEndpoint,
|
||||
pathParams: { variant },
|
||||
pk: id,
|
||||
title: t`Delete` + ' ' + templateTypeTranslation,
|
||||
onFormSuccess: () => {
|
||||
refreshTable();
|
||||
navigate('../');
|
||||
}
|
||||
});
|
||||
|
||||
if (isFetching) {
|
||||
return <LoadingOverlay visible={true} />;
|
||||
}
|
||||
|
||||
if (error || !template) {
|
||||
return (
|
||||
<Text>
|
||||
{(error as any)?.response?.status === 404 ? (
|
||||
<Trans>
|
||||
{templateTypeTranslation} with id {id} not found
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans>
|
||||
An error occurred while fetching {templateTypeTranslation} details
|
||||
</Trans>
|
||||
)}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack spacing="xs" style={{ display: 'flex', flex: '1' }}>
|
||||
{editTemplate.modal}
|
||||
{deleteTemplate.modal}
|
||||
|
||||
<Group position="apart">
|
||||
<Box></Box>
|
||||
|
||||
<Group>
|
||||
<Title order={4}>{template?.name}</Title>
|
||||
</Group>
|
||||
|
||||
<Group>
|
||||
<ActionDropdown
|
||||
tooltip={templateTypeTranslation + ' ' + t`actions`}
|
||||
icon={<IconDots />}
|
||||
actions={[
|
||||
EditItemAction({
|
||||
tooltip: t`Edit` + ' ' + templateTypeTranslation,
|
||||
onClick: editTemplate.open
|
||||
}),
|
||||
DeleteItemAction({
|
||||
tooltip: t`Delete` + ' ' + templateTypeTranslation,
|
||||
onClick: deleteTemplate.open
|
||||
})
|
||||
]}
|
||||
/>
|
||||
</Group>
|
||||
</Group>
|
||||
|
||||
<TemplateEditor
|
||||
downloadUrl={(template as any)[templateProps.templateKey]}
|
||||
uploadUrl={apiUrl(apiEndpoint, id, { variant })}
|
||||
uploadKey={templateProps.templateKey}
|
||||
preview={templateProps.preview}
|
||||
templateType={templateType}
|
||||
template={template}
|
||||
editors={[CodeEditor]}
|
||||
previewAreas={[PdfPreview]}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
export function TemplateTable({
|
||||
templateProps
|
||||
}: {
|
||||
templateProps: TemplateProps;
|
||||
}) {
|
||||
const {
|
||||
apiEndpoint,
|
||||
templateType,
|
||||
templateTypeTranslation,
|
||||
variant,
|
||||
templateKey,
|
||||
additionalFormFields,
|
||||
defaultTemplate
|
||||
} = templateProps;
|
||||
const table = useTable(`${templateType}-${variant}`);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const openDetailDrawer = useCallback((pk: number) => navigate(`${pk}/`), []);
|
||||
|
||||
const columns: TableColumn<TemplateI>[] = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
accessor: 'name',
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
accessor: 'description',
|
||||
sortable: false
|
||||
},
|
||||
{
|
||||
accessor: 'filters',
|
||||
sortable: false
|
||||
},
|
||||
...Object.entries(additionalFormFields || {})?.map(([key, field]) => ({
|
||||
accessor: key,
|
||||
sortable: false
|
||||
})),
|
||||
BooleanColumn({ accessor: 'enabled', title: t`Enabled` })
|
||||
];
|
||||
}, []);
|
||||
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<number>(-1);
|
||||
|
||||
const rowActions = useCallback((record: TemplateI): RowAction[] => {
|
||||
return [
|
||||
RowEditAction({
|
||||
onClick: () => openDetailDrawer(record.pk)
|
||||
}),
|
||||
RowDeleteAction({
|
||||
onClick: () => {
|
||||
setSelectedTemplate(record.pk), deleteTemplate.open();
|
||||
}
|
||||
})
|
||||
];
|
||||
}, []);
|
||||
|
||||
const deleteTemplate = useDeleteApiFormModal({
|
||||
url: apiEndpoint,
|
||||
pathParams: { variant },
|
||||
pk: selectedTemplate,
|
||||
title: t`Delete` + ' ' + templateTypeTranslation,
|
||||
onFormSuccess: table.refreshTable
|
||||
});
|
||||
|
||||
const newTemplate = useCreateApiFormModal({
|
||||
url: apiEndpoint,
|
||||
pathParams: { variant },
|
||||
title: t`Create new` + ' ' + templateTypeTranslation,
|
||||
fields: {
|
||||
name: {},
|
||||
description: {},
|
||||
filters: {},
|
||||
enabled: {},
|
||||
[templateKey]: {
|
||||
hidden: true,
|
||||
value: new File([defaultTemplate], 'template.html')
|
||||
},
|
||||
...additionalFormFields
|
||||
},
|
||||
onFormSuccess: (data) => {
|
||||
table.refreshTable();
|
||||
openDetailDrawer(data.pk);
|
||||
}
|
||||
});
|
||||
|
||||
const tableActions = useMemo(() => {
|
||||
let actions = [];
|
||||
|
||||
actions.push(
|
||||
<AddItemButton
|
||||
key={`add-${templateType}`}
|
||||
onClick={() => newTemplate.open()}
|
||||
tooltip={t`Add` + ' ' + templateTypeTranslation}
|
||||
/>
|
||||
);
|
||||
|
||||
return actions;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
{newTemplate.modal}
|
||||
{deleteTemplate.modal}
|
||||
<DetailDrawer
|
||||
title={t`Edit` + ' ' + templateTypeTranslation}
|
||||
size={'90%'}
|
||||
renderContent={(id) => {
|
||||
return (
|
||||
<TemplateDrawer
|
||||
id={id ?? ''}
|
||||
refreshTable={table.refreshTable}
|
||||
templateProps={templateProps}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<InvenTreeTable
|
||||
url={apiUrl(apiEndpoint, undefined, { variant })}
|
||||
tableState={table}
|
||||
columns={columns}
|
||||
props={{
|
||||
rowActions: rowActions,
|
||||
tableActions: tableActions,
|
||||
onRowClick: (record) => openDetailDrawer(record.pk)
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
@ -7,7 +7,10 @@ import { Link } from 'react-router-dom';
|
||||
|
||||
import { AddItemButton } from '../../components/buttons/AddItemButton';
|
||||
import { EditApiForm } from '../../components/forms/ApiForm';
|
||||
import { DetailDrawer } from '../../components/nav/DetailDrawer';
|
||||
import {
|
||||
DetailDrawer,
|
||||
DetailDrawerLink
|
||||
} from '../../components/nav/DetailDrawer';
|
||||
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
||||
import { openCreateApiForm, openDeleteApiForm } from '../../functions/forms';
|
||||
import { useInstance } from '../../hooks/UseInstance';
|
||||
@ -125,7 +128,10 @@ export function UserDrawer({
|
||||
<List>
|
||||
{userDetail?.groups?.map((group) => (
|
||||
<List.Item key={group.pk}>
|
||||
<Link to={`../group-${group.pk}`}>{group.name}</Link>
|
||||
<DetailDrawerLink
|
||||
to={`../group-${group.pk}`}
|
||||
text={group.name}
|
||||
/>
|
||||
</List.Item>
|
||||
))}
|
||||
</List>
|
||||
|
@ -322,6 +322,13 @@
|
||||
dependencies:
|
||||
regenerator-runtime "^0.14.0"
|
||||
|
||||
"@babel/runtime@^7.18.6":
|
||||
version "7.23.9"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.9.tgz#47791a15e4603bb5f905bc0753801cf21d6345f7"
|
||||
integrity sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==
|
||||
dependencies:
|
||||
regenerator-runtime "^0.14.0"
|
||||
|
||||
"@babel/template@^7.22.15":
|
||||
version "7.22.15"
|
||||
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.15.tgz#09576efc3830f0430f4548ef971dde1350ef2f38"
|
||||
@ -356,6 +363,133 @@
|
||||
"@babel/helper-validator-identifier" "^7.22.20"
|
||||
to-fast-properties "^2.0.0"
|
||||
|
||||
"@codemirror/autocomplete@^6.0.0":
|
||||
version "6.12.0"
|
||||
resolved "https://registry.yarnpkg.com/@codemirror/autocomplete/-/autocomplete-6.12.0.tgz#3fa620a8a3f42ded7751749916e8375f6bbbb333"
|
||||
integrity sha512-r4IjdYFthwbCQyvqnSlx0WBHRHi8nBvU+WjJxFUij81qsBfhNudf/XKKmmC2j3m0LaOYUQTf3qiEK1J8lO1sdg==
|
||||
dependencies:
|
||||
"@codemirror/language" "^6.0.0"
|
||||
"@codemirror/state" "^6.0.0"
|
||||
"@codemirror/view" "^6.17.0"
|
||||
"@lezer/common" "^1.0.0"
|
||||
|
||||
"@codemirror/commands@^6.0.0", "@codemirror/commands@^6.1.0":
|
||||
version "6.3.3"
|
||||
resolved "https://registry.yarnpkg.com/@codemirror/commands/-/commands-6.3.3.tgz#03face5bf5f3de0fc4e09b177b3c91eda2ceb7e9"
|
||||
integrity sha512-dO4hcF0fGT9tu1Pj1D2PvGvxjeGkbC6RGcZw6Qs74TH+Ed1gw98jmUgd2axWvIZEqTeTuFrg1lEB1KV6cK9h1A==
|
||||
dependencies:
|
||||
"@codemirror/language" "^6.0.0"
|
||||
"@codemirror/state" "^6.4.0"
|
||||
"@codemirror/view" "^6.0.0"
|
||||
"@lezer/common" "^1.1.0"
|
||||
|
||||
"@codemirror/lang-css@^6.0.0":
|
||||
version "6.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@codemirror/lang-css/-/lang-css-6.2.1.tgz#5dc0a43b8e3c31f6af7aabd55ff07fe9aef2a227"
|
||||
integrity sha512-/UNWDNV5Viwi/1lpr/dIXJNWiwDxpw13I4pTUAsNxZdg6E0mI2kTQb0P2iHczg1Tu+H4EBgJR+hYhKiHKko7qg==
|
||||
dependencies:
|
||||
"@codemirror/autocomplete" "^6.0.0"
|
||||
"@codemirror/language" "^6.0.0"
|
||||
"@codemirror/state" "^6.0.0"
|
||||
"@lezer/common" "^1.0.2"
|
||||
"@lezer/css" "^1.0.0"
|
||||
|
||||
"@codemirror/lang-html@^6.0.0":
|
||||
version "6.4.8"
|
||||
resolved "https://registry.yarnpkg.com/@codemirror/lang-html/-/lang-html-6.4.8.tgz#961db9b1037efcb1d0f50ae6082e5a367fa1470c"
|
||||
integrity sha512-tE2YK7wDlb9ZpAH6mpTPiYm6rhfdQKVDa5r9IwIFlwwgvVaKsCfuKKZoJGWsmMZIf3FQAuJ5CHMPLymOtg1hXw==
|
||||
dependencies:
|
||||
"@codemirror/autocomplete" "^6.0.0"
|
||||
"@codemirror/lang-css" "^6.0.0"
|
||||
"@codemirror/lang-javascript" "^6.0.0"
|
||||
"@codemirror/language" "^6.4.0"
|
||||
"@codemirror/state" "^6.0.0"
|
||||
"@codemirror/view" "^6.17.0"
|
||||
"@lezer/common" "^1.0.0"
|
||||
"@lezer/css" "^1.1.0"
|
||||
"@lezer/html" "^1.3.0"
|
||||
|
||||
"@codemirror/lang-javascript@^6.0.0":
|
||||
version "6.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@codemirror/lang-javascript/-/lang-javascript-6.2.2.tgz#7141090b22994bef85bcc5608a3bc1257f2db2ad"
|
||||
integrity sha512-VGQfY+FCc285AhWuwjYxQyUQcYurWlxdKYT4bqwr3Twnd5wP5WSeu52t4tvvuWmljT4EmgEgZCqSieokhtY8hg==
|
||||
dependencies:
|
||||
"@codemirror/autocomplete" "^6.0.0"
|
||||
"@codemirror/language" "^6.6.0"
|
||||
"@codemirror/lint" "^6.0.0"
|
||||
"@codemirror/state" "^6.0.0"
|
||||
"@codemirror/view" "^6.17.0"
|
||||
"@lezer/common" "^1.0.0"
|
||||
"@lezer/javascript" "^1.0.0"
|
||||
|
||||
"@codemirror/lang-liquid@^6.2.1":
|
||||
version "6.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@codemirror/lang-liquid/-/lang-liquid-6.2.1.tgz#78ded5e5b2aabbdf4687787ba9a29fce0da7e2ad"
|
||||
integrity sha512-J1Mratcm6JLNEiX+U2OlCDTysGuwbHD76XwuL5o5bo9soJtSbz2g6RU3vGHFyS5DC8rgVmFSzi7i6oBftm7tnA==
|
||||
dependencies:
|
||||
"@codemirror/autocomplete" "^6.0.0"
|
||||
"@codemirror/lang-html" "^6.0.0"
|
||||
"@codemirror/language" "^6.0.0"
|
||||
"@codemirror/state" "^6.0.0"
|
||||
"@codemirror/view" "^6.0.0"
|
||||
"@lezer/common" "^1.0.0"
|
||||
"@lezer/highlight" "^1.0.0"
|
||||
"@lezer/lr" "^1.3.1"
|
||||
|
||||
"@codemirror/language@^6.0.0", "@codemirror/language@^6.4.0", "@codemirror/language@^6.6.0":
|
||||
version "6.10.1"
|
||||
resolved "https://registry.yarnpkg.com/@codemirror/language/-/language-6.10.1.tgz#428c932a158cb75942387acfe513c1ece1090b05"
|
||||
integrity sha512-5GrXzrhq6k+gL5fjkAwt90nYDmjlzTIJV8THnxNFtNKWotMIlzzN+CpqxqwXOECnUdOndmSeWntVrVcv5axWRQ==
|
||||
dependencies:
|
||||
"@codemirror/state" "^6.0.0"
|
||||
"@codemirror/view" "^6.23.0"
|
||||
"@lezer/common" "^1.1.0"
|
||||
"@lezer/highlight" "^1.0.0"
|
||||
"@lezer/lr" "^1.0.0"
|
||||
style-mod "^4.0.0"
|
||||
|
||||
"@codemirror/lint@^6.0.0":
|
||||
version "6.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@codemirror/lint/-/lint-6.5.0.tgz#ea43b6e653dcc5bcd93456b55e9fe62e63f326d9"
|
||||
integrity sha512-+5YyicIaaAZKU8K43IQi8TBy6mF6giGeWAH7N96Z5LC30Wm5JMjqxOYIE9mxwMG1NbhT2mA3l9hA4uuKUM3E5g==
|
||||
dependencies:
|
||||
"@codemirror/state" "^6.0.0"
|
||||
"@codemirror/view" "^6.0.0"
|
||||
crelt "^1.0.5"
|
||||
|
||||
"@codemirror/search@^6.0.0":
|
||||
version "6.5.6"
|
||||
resolved "https://registry.yarnpkg.com/@codemirror/search/-/search-6.5.6.tgz#8f858b9e678d675869112e475f082d1e8488db93"
|
||||
integrity sha512-rpMgcsh7o0GuCDUXKPvww+muLA1pDJaFrpq/CCHtpQJYz8xopu4D1hPcKRoDD0YlF8gZaqTNIRa4VRBWyhyy7Q==
|
||||
dependencies:
|
||||
"@codemirror/state" "^6.0.0"
|
||||
"@codemirror/view" "^6.0.0"
|
||||
crelt "^1.0.5"
|
||||
|
||||
"@codemirror/state@^6.0.0", "@codemirror/state@^6.1.1", "@codemirror/state@^6.4.0":
|
||||
version "6.4.1"
|
||||
resolved "https://registry.yarnpkg.com/@codemirror/state/-/state-6.4.1.tgz#da57143695c056d9a3c38705ed34136e2b68171b"
|
||||
integrity sha512-QkEyUiLhsJoZkbumGZlswmAhA7CBU02Wrz7zvH4SrcifbsqwlXShVXg65f3v/ts57W3dqyamEriMhij1Z3Zz4A==
|
||||
|
||||
"@codemirror/theme-one-dark@^6.0.0":
|
||||
version "6.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@codemirror/theme-one-dark/-/theme-one-dark-6.1.2.tgz#fcef9f9cfc17a07836cb7da17c9f6d7231064df8"
|
||||
integrity sha512-F+sH0X16j/qFLMAfbciKTxVOwkdAS336b7AXTKOZhy8BR3eH/RelsnLgLFINrpST63mmN2OuwUt0W2ndUgYwUA==
|
||||
dependencies:
|
||||
"@codemirror/language" "^6.0.0"
|
||||
"@codemirror/state" "^6.0.0"
|
||||
"@codemirror/view" "^6.0.0"
|
||||
"@lezer/highlight" "^1.0.0"
|
||||
|
||||
"@codemirror/view@^6.0.0", "@codemirror/view@^6.17.0", "@codemirror/view@^6.23.0":
|
||||
version "6.24.1"
|
||||
resolved "https://registry.yarnpkg.com/@codemirror/view/-/view-6.24.1.tgz#c151d589dc27f9197c68d395811b93c21c801767"
|
||||
integrity sha512-sBfP4rniPBRQzNakwuQEqjEuiJDWJyF2kqLLqij4WXRoVwPPJfjx966Eq3F7+OPQxDtMt/Q9MWLoZLWjeveBlg==
|
||||
dependencies:
|
||||
"@codemirror/state" "^6.4.0"
|
||||
style-mod "^4.1.0"
|
||||
w3c-keyname "^2.2.4"
|
||||
|
||||
"@emotion/babel-plugin@^11.11.0":
|
||||
version "11.11.0"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz#c2d872b6a7767a9d176d007f5b31f7d504bb5d6c"
|
||||
@ -791,6 +925,52 @@
|
||||
"@jridgewell/resolve-uri" "^3.1.0"
|
||||
"@jridgewell/sourcemap-codec" "^1.4.14"
|
||||
|
||||
"@lezer/common@^1.0.0", "@lezer/common@^1.0.2", "@lezer/common@^1.1.0", "@lezer/common@^1.2.0":
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@lezer/common/-/common-1.2.1.tgz#198b278b7869668e1bebbe687586e12a42731049"
|
||||
integrity sha512-yemX0ZD2xS/73llMZIK6KplkjIjf2EvAHcinDi/TfJ9hS25G0388+ClHt6/3but0oOxinTcQHJLDXh6w1crzFQ==
|
||||
|
||||
"@lezer/css@^1.0.0", "@lezer/css@^1.1.0":
|
||||
version "1.1.8"
|
||||
resolved "https://registry.yarnpkg.com/@lezer/css/-/css-1.1.8.tgz#11fd456dac53bc899b266778794ed4ca9576a5a4"
|
||||
integrity sha512-7JhxupKuMBaWQKjQoLtzhGj83DdnZY9MckEOG5+/iLKNK2ZJqKc6hf6uc0HjwCX7Qlok44jBNqZhHKDhEhZYLA==
|
||||
dependencies:
|
||||
"@lezer/common" "^1.2.0"
|
||||
"@lezer/highlight" "^1.0.0"
|
||||
"@lezer/lr" "^1.0.0"
|
||||
|
||||
"@lezer/highlight@^1.0.0", "@lezer/highlight@^1.1.3":
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@lezer/highlight/-/highlight-1.2.0.tgz#e5898c3644208b4b589084089dceeea2966f7780"
|
||||
integrity sha512-WrS5Mw51sGrpqjlh3d4/fOwpEV2Hd3YOkp9DBt4k8XZQcoTHZFB7sx030A6OcahF4J1nDQAa3jXlTVVYH50IFA==
|
||||
dependencies:
|
||||
"@lezer/common" "^1.0.0"
|
||||
|
||||
"@lezer/html@^1.3.0":
|
||||
version "1.3.9"
|
||||
resolved "https://registry.yarnpkg.com/@lezer/html/-/html-1.3.9.tgz#097150f0fb0d14e274515d3b3e50e7bd4a1d7ebc"
|
||||
integrity sha512-MXxeCMPyrcemSLGaTQEZx0dBUH0i+RPl8RN5GwMAzo53nTsd/Unc/t5ZxACeQoyPUM5/GkPLRUs2WliOImzkRA==
|
||||
dependencies:
|
||||
"@lezer/common" "^1.2.0"
|
||||
"@lezer/highlight" "^1.0.0"
|
||||
"@lezer/lr" "^1.0.0"
|
||||
|
||||
"@lezer/javascript@^1.0.0":
|
||||
version "1.4.13"
|
||||
resolved "https://registry.yarnpkg.com/@lezer/javascript/-/javascript-1.4.13.tgz#e6459a000e1d7369db3e97b1764da63eeb5afe1b"
|
||||
integrity sha512-5IBr8LIO3xJdJH1e9aj/ZNLE4LSbdsx25wFmGRAZsj2zSmwAYjx26JyU/BYOCpRQlu1jcv1z3vy4NB9+UkfRow==
|
||||
dependencies:
|
||||
"@lezer/common" "^1.2.0"
|
||||
"@lezer/highlight" "^1.1.3"
|
||||
"@lezer/lr" "^1.3.0"
|
||||
|
||||
"@lezer/lr@^1.0.0", "@lezer/lr@^1.3.0", "@lezer/lr@^1.3.1":
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@lezer/lr/-/lr-1.4.0.tgz#ed52a75dbbfbb0d1eb63710ea84c35ee647cb67e"
|
||||
integrity sha512-Wst46p51km8gH0ZUmeNrtpRYmdlRHUpN1DQd3GFAyKANi8WVz8c2jHYTf1CVScFaCjQw1iO3ZZdqGDxQPRErTg==
|
||||
dependencies:
|
||||
"@lezer/common" "^1.0.0"
|
||||
|
||||
"@lingui/babel-plugin-extract-messages@4.5.0":
|
||||
version "4.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@lingui/babel-plugin-extract-messages/-/babel-plugin-extract-messages-4.5.0.tgz#71e56cc2eae73890caeea15a00ae4965413430ec"
|
||||
@ -1342,6 +1522,52 @@
|
||||
dependencies:
|
||||
"@types/yargs-parser" "*"
|
||||
|
||||
"@uiw/codemirror-extensions-basic-setup@4.21.22":
|
||||
version "4.21.22"
|
||||
resolved "https://registry.yarnpkg.com/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.21.22.tgz#7e68e5f9fb305d7a35948190351f219a1443d775"
|
||||
integrity sha512-Lxq2EitQb/MwbNrMHBmVdSIR96WmaICnYBYeZbLUxmr4kQcbrA6HXqNSNZJ0V4ZihPfKnNs9+g87QK0HsadE6A==
|
||||
dependencies:
|
||||
"@codemirror/autocomplete" "^6.0.0"
|
||||
"@codemirror/commands" "^6.0.0"
|
||||
"@codemirror/language" "^6.0.0"
|
||||
"@codemirror/lint" "^6.0.0"
|
||||
"@codemirror/search" "^6.0.0"
|
||||
"@codemirror/state" "^6.0.0"
|
||||
"@codemirror/view" "^6.0.0"
|
||||
|
||||
"@uiw/codemirror-theme-vscode@^4.21.22":
|
||||
version "4.21.22"
|
||||
resolved "https://registry.yarnpkg.com/@uiw/codemirror-theme-vscode/-/codemirror-theme-vscode-4.21.22.tgz#76cdcacbcda93de4a409c673bb5e52901c4588f1"
|
||||
integrity sha512-8E7txA1IFCB+a38tovL7ZoKLC1mDfA3X83XYU+KQoaxyPzImm8aJLVDghHH8EUF0gOdJWPSW13OPVAGCayBKCA==
|
||||
dependencies:
|
||||
"@uiw/codemirror-themes" "4.21.22"
|
||||
|
||||
"@uiw/codemirror-themes@4.21.22":
|
||||
version "4.21.22"
|
||||
resolved "https://registry.yarnpkg.com/@uiw/codemirror-themes/-/codemirror-themes-4.21.22.tgz#3a1b813a1b7b9bfd48fc2935df24d277a78ee294"
|
||||
integrity sha512-oRMNtDmD6ER0EH2/NKGbrUzeRJbZ/4+GE3/9OItaAGhdsd2V33WGqVX7QwXsjLNhpNfscbVKB3PYLyRooBdlfg==
|
||||
dependencies:
|
||||
"@codemirror/language" "^6.0.0"
|
||||
"@codemirror/state" "^6.0.0"
|
||||
"@codemirror/view" "^6.0.0"
|
||||
|
||||
"@uiw/react-codemirror@^4.21.22":
|
||||
version "4.21.22"
|
||||
resolved "https://registry.yarnpkg.com/@uiw/react-codemirror/-/react-codemirror-4.21.22.tgz#38482e1b94e45eb4f33e7f8d5337762aa5891987"
|
||||
integrity sha512-VmxU9oRXwcleG2u5Ui2xVXaLVPL8cBuRN3vA41hlu4OQ/ftJb+4p+dBd6bZ+NJKSXm3LufbPGzu8oKwNO4tG4A==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.18.6"
|
||||
"@codemirror/commands" "^6.1.0"
|
||||
"@codemirror/state" "^6.1.1"
|
||||
"@codemirror/theme-one-dark" "^6.0.0"
|
||||
"@uiw/codemirror-extensions-basic-setup" "4.21.22"
|
||||
codemirror "^6.0.0"
|
||||
|
||||
"@uiw/react-split@^5.9.3":
|
||||
version "5.9.3"
|
||||
resolved "https://registry.yarnpkg.com/@uiw/react-split/-/react-split-5.9.3.tgz#3d99f2b62288d4a6eb716b0fd6b37a2c5dc7f82e"
|
||||
integrity sha512-HgwETU+kRhzZAp+YiE4Yu8bNJm3jxxnGgGPfkadUl8jA1wsMD3aMMskuhQF5akiUUUadiLUvAc8e1RH9Y/SKDw==
|
||||
|
||||
"@vitejs/plugin-react@^4.1.0":
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@vitejs/plugin-react/-/plugin-react-4.1.0.tgz#e4f56f46fd737c5d386bb1f1ade86ba275fe09bd"
|
||||
@ -1602,6 +1828,19 @@ codemirror@^5.63.1:
|
||||
resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.65.15.tgz#66899278f44a7acde0eb641388cd563fe6dfbe19"
|
||||
integrity sha512-YC4EHbbwQeubZzxLl5G4nlbLc1T21QTrKGaOal/Pkm9dVDMZXMH7+ieSPEOZCtO9I68i8/oteJKOxzHC2zR+0g==
|
||||
|
||||
codemirror@^6.0.0:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-6.0.1.tgz#62b91142d45904547ee3e0e0e4c1a79158035a29"
|
||||
integrity sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==
|
||||
dependencies:
|
||||
"@codemirror/autocomplete" "^6.0.0"
|
||||
"@codemirror/commands" "^6.0.0"
|
||||
"@codemirror/language" "^6.0.0"
|
||||
"@codemirror/lint" "^6.0.0"
|
||||
"@codemirror/search" "^6.0.0"
|
||||
"@codemirror/state" "^6.0.0"
|
||||
"@codemirror/view" "^6.0.0"
|
||||
|
||||
color-convert@^1.9.0:
|
||||
version "1.9.3"
|
||||
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
|
||||
@ -1679,6 +1918,11 @@ cosmiconfig@^8.0.0:
|
||||
parse-json "^5.2.0"
|
||||
path-type "^4.0.0"
|
||||
|
||||
crelt@^1.0.5:
|
||||
version "1.0.6"
|
||||
resolved "https://registry.yarnpkg.com/crelt/-/crelt-1.0.6.tgz#7cc898ea74e190fb6ef9dae57f8f81cf7302df72"
|
||||
integrity sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==
|
||||
|
||||
css-color-keywords@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/css-color-keywords/-/css-color-keywords-1.0.0.tgz#fea2616dc676b2962686b3af8dbdbe180b244e05"
|
||||
@ -2796,6 +3040,11 @@ strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
dependencies:
|
||||
ansi-regex "^5.0.1"
|
||||
|
||||
style-mod@^4.0.0, style-mod@^4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/style-mod/-/style-mod-4.1.0.tgz#a313a14f4ae8bb4d52878c0053c4327fb787ec09"
|
||||
integrity sha512-Ca5ib8HrFn+f+0n4N4ScTIA9iTOQ7MaGS1ylHcoVqW9J7w2w8PzN6g9gKmTYgGEBH8e120+RCmhpje6jC5uGWA==
|
||||
|
||||
styled-components@^6.1.0:
|
||||
version "6.1.0"
|
||||
resolved "https://registry.yarnpkg.com/styled-components/-/styled-components-6.1.0.tgz#228e3ab9c1ee1daa4b0a06aae30df0ed14fda274"
|
||||
@ -2983,6 +3232,11 @@ vite@^4.5.2:
|
||||
optionalDependencies:
|
||||
fsevents "~2.3.2"
|
||||
|
||||
w3c-keyname@^2.2.4:
|
||||
version "2.2.8"
|
||||
resolved "https://registry.yarnpkg.com/w3c-keyname/-/w3c-keyname-2.2.8.tgz#7b17c8c6883d4e8b86ac8aba79d39e880f8869c5"
|
||||
integrity sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==
|
||||
|
||||
wcwidth@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8"
|
||||
|
Loading…
Reference in New Issue
Block a user