mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-11-04 15:15:42 +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:
		@@ -1,11 +1,15 @@
 | 
				
			|||||||
"""InvenTree API version information."""
 | 
					"""InvenTree API version information."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# InvenTree API version
 | 
					# 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."""
 | 
					"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
INVENTREE_API_TEXT = """
 | 
					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
 | 
					v180 - 2024-3-02 : https://github.com/inventree/InvenTree/pull/6463
 | 
				
			||||||
    - Tweaks to API documentation to allow automatic documentation generation
 | 
					    - Tweaks to API documentation to allow automatic documentation generation
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -15,7 +15,16 @@ class LabelSerializerBase(InvenTreeModelSerializer):
 | 
				
			|||||||
    @staticmethod
 | 
					    @staticmethod
 | 
				
			||||||
    def label_fields():
 | 
					    def label_fields():
 | 
				
			||||||
        """Generic serializer fields for a label template."""
 | 
					        """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):
 | 
					class StockItemLabelSerializer(LabelSerializerBase):
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -24,7 +24,16 @@ class ReportSerializerBase(InvenTreeModelSerializer):
 | 
				
			|||||||
    @staticmethod
 | 
					    @staticmethod
 | 
				
			||||||
    def report_fields():
 | 
					    def report_fields():
 | 
				
			||||||
        """Generic serializer fields for a report template."""
 | 
					        """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):
 | 
					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.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#### 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
 | 
					    - Project Codes: order/project_codes.md
 | 
				
			||||||
  - Report:
 | 
					  - Report:
 | 
				
			||||||
    - Templates: report/report.md
 | 
					    - Templates: report/report.md
 | 
				
			||||||
 | 
					    - Template Editor: report/template_editor.md
 | 
				
			||||||
    - Report Types:
 | 
					    - Report Types:
 | 
				
			||||||
      - Test Reports: report/test.md
 | 
					      - Test Reports: report/test.md
 | 
				
			||||||
      - Build Order: report/build.md
 | 
					      - Build Order: report/build.md
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -11,6 +11,7 @@
 | 
				
			|||||||
        "compile": "lingui compile --typescript"
 | 
					        "compile": "lingui compile --typescript"
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "dependencies": {
 | 
					    "dependencies": {
 | 
				
			||||||
 | 
					        "@codemirror/lang-liquid": "^6.2.1",
 | 
				
			||||||
        "@emotion/react": "^11.11.1",
 | 
					        "@emotion/react": "^11.11.1",
 | 
				
			||||||
        "@fortawesome/fontawesome-svg-core": "^6.4.2",
 | 
					        "@fortawesome/fontawesome-svg-core": "^6.4.2",
 | 
				
			||||||
        "@fortawesome/free-regular-svg-icons": "^6.4.2",
 | 
					        "@fortawesome/free-regular-svg-icons": "^6.4.2",
 | 
				
			||||||
@@ -30,6 +31,9 @@
 | 
				
			|||||||
        "@sentry/react": "^7.74.1",
 | 
					        "@sentry/react": "^7.74.1",
 | 
				
			||||||
        "@tabler/icons-react": "^2.39.0",
 | 
					        "@tabler/icons-react": "^2.39.0",
 | 
				
			||||||
        "@tanstack/react-query": "^5.0.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",
 | 
					        "axios": "^1.6.0",
 | 
				
			||||||
        "dayjs": "^1.11.10",
 | 
					        "dayjs": "^1.11.10",
 | 
				
			||||||
        "easymde": "^2.18.0",
 | 
					        "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 { api } from '../../App';
 | 
				
			||||||
import { ApiEndpoints } from '../../enums/ApiEndpoints';
 | 
					import { ApiEndpoints } from '../../enums/ApiEndpoints';
 | 
				
			||||||
import { ModelType } from '../../enums/ModelType';
 | 
					import { ModelType } from '../../enums/ModelType';
 | 
				
			||||||
import { InvenTreeIcon } from '../../functions/icons';
 | 
					import { InvenTreeIcon, InvenTreeIconType } from '../../functions/icons';
 | 
				
			||||||
import { getDetailUrl } from '../../functions/urls';
 | 
					import { getDetailUrl } from '../../functions/urls';
 | 
				
			||||||
import { apiUrl } from '../../states/ApiState';
 | 
					import { apiUrl } from '../../states/ApiState';
 | 
				
			||||||
import { useGlobalSettingsState } from '../../states/SettingsState';
 | 
					import { useGlobalSettingsState } from '../../states/SettingsState';
 | 
				
			||||||
@@ -368,7 +368,7 @@ export function DetailsTableField({
 | 
				
			|||||||
          justifyContent: 'flex-start'
 | 
					          justifyContent: 'flex-start'
 | 
				
			||||||
        }}
 | 
					        }}
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        <InvenTreeIcon icon={field.icon ?? field.name} />
 | 
					        <InvenTreeIcon icon={(field.icon ?? field.name) as InvenTreeIconType} />
 | 
				
			||||||
      </td>
 | 
					      </td>
 | 
				
			||||||
      <td>
 | 
					      <td>
 | 
				
			||||||
        <Text>{field.label}</Text>
 | 
					        <Text>{field.label}</Text>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,14 +1,14 @@
 | 
				
			|||||||
import { Trans, t } from '@lingui/macro';
 | 
					import { Trans, t } from '@lingui/macro';
 | 
				
			||||||
import { Badge, Tooltip } from '@mantine/core';
 | 
					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
 | 
					 * Fetches and wraps an InvenTreeIcon in a flex div
 | 
				
			||||||
 * @param icon name of icon
 | 
					 * @param icon name of icon
 | 
				
			||||||
 *
 | 
					 *
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
function PartIcon(icon: string) {
 | 
					function PartIcon(icon: InvenTreeIconType) {
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <div style={{ display: 'flex', alignItems: 'center', gap: '5px' }}>
 | 
					    <div style={{ display: 'flex', alignItems: 'center', gap: '5px' }}>
 | 
				
			||||||
      <InvenTreeIcon icon={icon} />
 | 
					      <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 the value is unchanged, do nothing
 | 
				
			||||||
    if (field.value === pk) return;
 | 
					    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}/`;
 | 
					      const url = `${definition.api_url}${field.value}/`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      api.get(url).then((response) => {
 | 
					      api.get(url).then((response) => {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -23,6 +23,7 @@ export type ActionDropdownItem = {
 | 
				
			|||||||
  name: string;
 | 
					  name: string;
 | 
				
			||||||
  tooltip?: string;
 | 
					  tooltip?: string;
 | 
				
			||||||
  disabled?: boolean;
 | 
					  disabled?: boolean;
 | 
				
			||||||
 | 
					  hidden?: boolean;
 | 
				
			||||||
  onClick?: () => void;
 | 
					  onClick?: () => void;
 | 
				
			||||||
  indicator?: Omit<IndicatorProps, 'children'>;
 | 
					  indicator?: Omit<IndicatorProps, 'children'>;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
@@ -42,7 +43,7 @@ export function ActionDropdown({
 | 
				
			|||||||
  actions: ActionDropdownItem[];
 | 
					  actions: ActionDropdownItem[];
 | 
				
			||||||
}) {
 | 
					}) {
 | 
				
			||||||
  const hasActions = useMemo(() => {
 | 
					  const hasActions = useMemo(() => {
 | 
				
			||||||
    return actions.some((action) => !action.disabled);
 | 
					    return actions.some((action) => !action.hidden);
 | 
				
			||||||
  }, [actions]);
 | 
					  }, [actions]);
 | 
				
			||||||
  const indicatorProps = useMemo(() => {
 | 
					  const indicatorProps = useMemo(() => {
 | 
				
			||||||
    return actions.find((action) => action.indicator);
 | 
					    return actions.find((action) => action.indicator);
 | 
				
			||||||
@@ -61,7 +62,7 @@ export function ActionDropdown({
 | 
				
			|||||||
      </Indicator>
 | 
					      </Indicator>
 | 
				
			||||||
      <Menu.Dropdown>
 | 
					      <Menu.Dropdown>
 | 
				
			||||||
        {actions.map((action) =>
 | 
					        {actions.map((action) =>
 | 
				
			||||||
          action.disabled ? null : (
 | 
					          action.hidden ? null : (
 | 
				
			||||||
            <Indicator
 | 
					            <Indicator
 | 
				
			||||||
              disabled={!action.indicator}
 | 
					              disabled={!action.indicator}
 | 
				
			||||||
              {...action.indicator}
 | 
					              {...action.indicator}
 | 
				
			||||||
@@ -108,10 +109,10 @@ export function BarcodeActionDropdown({
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// Common action button for viewing a barcode
 | 
					// Common action button for viewing a barcode
 | 
				
			||||||
export function ViewBarcodeAction({
 | 
					export function ViewBarcodeAction({
 | 
				
			||||||
  disabled = false,
 | 
					  hidden = false,
 | 
				
			||||||
  onClick
 | 
					  onClick
 | 
				
			||||||
}: {
 | 
					}: {
 | 
				
			||||||
  disabled?: boolean;
 | 
					  hidden?: boolean;
 | 
				
			||||||
  onClick?: () => void;
 | 
					  onClick?: () => void;
 | 
				
			||||||
}): ActionDropdownItem {
 | 
					}): ActionDropdownItem {
 | 
				
			||||||
  return {
 | 
					  return {
 | 
				
			||||||
@@ -119,16 +120,16 @@ export function ViewBarcodeAction({
 | 
				
			|||||||
    name: t`View`,
 | 
					    name: t`View`,
 | 
				
			||||||
    tooltip: t`View barcode`,
 | 
					    tooltip: t`View barcode`,
 | 
				
			||||||
    onClick: onClick,
 | 
					    onClick: onClick,
 | 
				
			||||||
    disabled: disabled
 | 
					    hidden: hidden
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Common action button for linking a custom barcode
 | 
					// Common action button for linking a custom barcode
 | 
				
			||||||
export function LinkBarcodeAction({
 | 
					export function LinkBarcodeAction({
 | 
				
			||||||
  disabled = false,
 | 
					  hidden = false,
 | 
				
			||||||
  onClick
 | 
					  onClick
 | 
				
			||||||
}: {
 | 
					}: {
 | 
				
			||||||
  disabled?: boolean;
 | 
					  hidden?: boolean;
 | 
				
			||||||
  onClick?: () => void;
 | 
					  onClick?: () => void;
 | 
				
			||||||
}): ActionDropdownItem {
 | 
					}): ActionDropdownItem {
 | 
				
			||||||
  return {
 | 
					  return {
 | 
				
			||||||
@@ -136,16 +137,16 @@ export function LinkBarcodeAction({
 | 
				
			|||||||
    name: t`Link Barcode`,
 | 
					    name: t`Link Barcode`,
 | 
				
			||||||
    tooltip: t`Link custom barcode`,
 | 
					    tooltip: t`Link custom barcode`,
 | 
				
			||||||
    onClick: onClick,
 | 
					    onClick: onClick,
 | 
				
			||||||
    disabled: disabled
 | 
					    hidden: hidden
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Common action button for un-linking a custom barcode
 | 
					// Common action button for un-linking a custom barcode
 | 
				
			||||||
export function UnlinkBarcodeAction({
 | 
					export function UnlinkBarcodeAction({
 | 
				
			||||||
  disabled = false,
 | 
					  hidden = false,
 | 
				
			||||||
  onClick
 | 
					  onClick
 | 
				
			||||||
}: {
 | 
					}: {
 | 
				
			||||||
  disabled?: boolean;
 | 
					  hidden?: boolean;
 | 
				
			||||||
  onClick?: () => void;
 | 
					  onClick?: () => void;
 | 
				
			||||||
}): ActionDropdownItem {
 | 
					}): ActionDropdownItem {
 | 
				
			||||||
  return {
 | 
					  return {
 | 
				
			||||||
@@ -153,17 +154,17 @@ export function UnlinkBarcodeAction({
 | 
				
			|||||||
    name: t`Unlink Barcode`,
 | 
					    name: t`Unlink Barcode`,
 | 
				
			||||||
    tooltip: t`Unlink custom barcode`,
 | 
					    tooltip: t`Unlink custom barcode`,
 | 
				
			||||||
    onClick: onClick,
 | 
					    onClick: onClick,
 | 
				
			||||||
    disabled: disabled
 | 
					    hidden: hidden
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Common action button for editing an item
 | 
					// Common action button for editing an item
 | 
				
			||||||
export function EditItemAction({
 | 
					export function EditItemAction({
 | 
				
			||||||
  disabled = false,
 | 
					  hidden = false,
 | 
				
			||||||
  tooltip,
 | 
					  tooltip,
 | 
				
			||||||
  onClick
 | 
					  onClick
 | 
				
			||||||
}: {
 | 
					}: {
 | 
				
			||||||
  disabled?: boolean;
 | 
					  hidden?: boolean;
 | 
				
			||||||
  tooltip?: string;
 | 
					  tooltip?: string;
 | 
				
			||||||
  onClick?: () => void;
 | 
					  onClick?: () => void;
 | 
				
			||||||
}): ActionDropdownItem {
 | 
					}): ActionDropdownItem {
 | 
				
			||||||
@@ -172,17 +173,17 @@ export function EditItemAction({
 | 
				
			|||||||
    name: t`Edit`,
 | 
					    name: t`Edit`,
 | 
				
			||||||
    tooltip: tooltip ?? `Edit item`,
 | 
					    tooltip: tooltip ?? `Edit item`,
 | 
				
			||||||
    onClick: onClick,
 | 
					    onClick: onClick,
 | 
				
			||||||
    disabled: disabled
 | 
					    hidden: hidden
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Common action button for deleting an item
 | 
					// Common action button for deleting an item
 | 
				
			||||||
export function DeleteItemAction({
 | 
					export function DeleteItemAction({
 | 
				
			||||||
  disabled = false,
 | 
					  hidden = false,
 | 
				
			||||||
  tooltip,
 | 
					  tooltip,
 | 
				
			||||||
  onClick
 | 
					  onClick
 | 
				
			||||||
}: {
 | 
					}: {
 | 
				
			||||||
  disabled?: boolean;
 | 
					  hidden?: boolean;
 | 
				
			||||||
  tooltip?: string;
 | 
					  tooltip?: string;
 | 
				
			||||||
  onClick?: () => void;
 | 
					  onClick?: () => void;
 | 
				
			||||||
}): ActionDropdownItem {
 | 
					}): ActionDropdownItem {
 | 
				
			||||||
@@ -191,17 +192,17 @@ export function DeleteItemAction({
 | 
				
			|||||||
    name: t`Delete`,
 | 
					    name: t`Delete`,
 | 
				
			||||||
    tooltip: tooltip ?? t`Delete item`,
 | 
					    tooltip: tooltip ?? t`Delete item`,
 | 
				
			||||||
    onClick: onClick,
 | 
					    onClick: onClick,
 | 
				
			||||||
    disabled: disabled
 | 
					    hidden: hidden
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Common action button for duplicating an item
 | 
					// Common action button for duplicating an item
 | 
				
			||||||
export function DuplicateItemAction({
 | 
					export function DuplicateItemAction({
 | 
				
			||||||
  disabled = false,
 | 
					  hidden = false,
 | 
				
			||||||
  tooltip,
 | 
					  tooltip,
 | 
				
			||||||
  onClick
 | 
					  onClick
 | 
				
			||||||
}: {
 | 
					}: {
 | 
				
			||||||
  disabled?: boolean;
 | 
					  hidden?: boolean;
 | 
				
			||||||
  tooltip?: string;
 | 
					  tooltip?: string;
 | 
				
			||||||
  onClick?: () => void;
 | 
					  onClick?: () => void;
 | 
				
			||||||
}): ActionDropdownItem {
 | 
					}): ActionDropdownItem {
 | 
				
			||||||
@@ -210,6 +211,6 @@ export function DuplicateItemAction({
 | 
				
			|||||||
    name: t`Duplicate`,
 | 
					    name: t`Duplicate`,
 | 
				
			||||||
    tooltip: tooltip ?? t`Duplicate item`,
 | 
					    tooltip: tooltip ?? t`Duplicate item`,
 | 
				
			||||||
    onClick: onClick,
 | 
					    onClick: onClick,
 | 
				
			||||||
    disabled: disabled
 | 
					    hidden: hidden
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,6 +2,7 @@ import { Trans } from '@lingui/macro';
 | 
				
			|||||||
import { Code, Flex, Group, Text } from '@mantine/core';
 | 
					import { Code, Flex, Group, Text } from '@mantine/core';
 | 
				
			||||||
import { Link, To } from 'react-router-dom';
 | 
					import { Link, To } from 'react-router-dom';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { DetailDrawerLink } from '../nav/DetailDrawer';
 | 
				
			||||||
import { YesNoButton } from './YesNoButton';
 | 
					import { YesNoButton } from './YesNoButton';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function InfoItem({
 | 
					export function InfoItem({
 | 
				
			||||||
@@ -9,13 +10,15 @@ export function InfoItem({
 | 
				
			|||||||
  children,
 | 
					  children,
 | 
				
			||||||
  type,
 | 
					  type,
 | 
				
			||||||
  value,
 | 
					  value,
 | 
				
			||||||
  link
 | 
					  link,
 | 
				
			||||||
 | 
					  detailDrawerLink
 | 
				
			||||||
}: {
 | 
					}: {
 | 
				
			||||||
  name: string;
 | 
					  name: string;
 | 
				
			||||||
  children?: React.ReactNode;
 | 
					  children?: React.ReactNode;
 | 
				
			||||||
  type?: 'text' | 'boolean' | 'code';
 | 
					  type?: 'text' | 'boolean' | 'code';
 | 
				
			||||||
  value?: any;
 | 
					  value?: any;
 | 
				
			||||||
  link?: To;
 | 
					  link?: To;
 | 
				
			||||||
 | 
					  detailDrawerLink?: boolean;
 | 
				
			||||||
}) {
 | 
					}) {
 | 
				
			||||||
  function renderComponent() {
 | 
					  function renderComponent() {
 | 
				
			||||||
    if (value === undefined) return null;
 | 
					    if (value === undefined) return null;
 | 
				
			||||||
@@ -46,7 +49,15 @@ export function InfoItem({
 | 
				
			|||||||
      </Text>
 | 
					      </Text>
 | 
				
			||||||
      <Flex>
 | 
					      <Flex>
 | 
				
			||||||
        {children}
 | 
					        {children}
 | 
				
			||||||
        {link ? <Link to={link}>{renderComponent()}</Link> : renderComponent()}
 | 
					        {link ? (
 | 
				
			||||||
 | 
					          detailDrawerLink ? (
 | 
				
			||||||
 | 
					            <DetailDrawerLink to={link} text={value} />
 | 
				
			||||||
 | 
					          ) : (
 | 
				
			||||||
 | 
					            <Link to={link}>{renderComponent()}</Link>
 | 
				
			||||||
 | 
					          )
 | 
				
			||||||
 | 
					        ) : (
 | 
				
			||||||
 | 
					          renderComponent()
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
      </Flex>
 | 
					      </Flex>
 | 
				
			||||||
    </Group>
 | 
					    </Group>
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,11 +5,15 @@ import {
 | 
				
			|||||||
  Group,
 | 
					  Group,
 | 
				
			||||||
  MantineNumberSize,
 | 
					  MantineNumberSize,
 | 
				
			||||||
  Stack,
 | 
					  Stack,
 | 
				
			||||||
  Text
 | 
					  Text,
 | 
				
			||||||
 | 
					  createStyles
 | 
				
			||||||
} from '@mantine/core';
 | 
					} from '@mantine/core';
 | 
				
			||||||
import { IconChevronLeft } from '@tabler/icons-react';
 | 
					import { IconChevronLeft } from '@tabler/icons-react';
 | 
				
			||||||
import { useMemo } from 'react';
 | 
					import { useCallback, useMemo } from 'react';
 | 
				
			||||||
import { Route, Routes, useNavigate, useParams } from 'react-router-dom';
 | 
					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
 | 
					 * @param title - drawer title
 | 
				
			||||||
@@ -25,6 +29,13 @@ export interface DrawerProps {
 | 
				
			|||||||
  size?: MantineNumberSize;
 | 
					  size?: MantineNumberSize;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const useStyles = createStyles(() => ({
 | 
				
			||||||
 | 
					  flex: {
 | 
				
			||||||
 | 
					    display: 'flex',
 | 
				
			||||||
 | 
					    flex: 1
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function DetailDrawerComponent({
 | 
					function DetailDrawerComponent({
 | 
				
			||||||
  title,
 | 
					  title,
 | 
				
			||||||
  position = 'right',
 | 
					  position = 'right',
 | 
				
			||||||
@@ -33,28 +44,47 @@ function DetailDrawerComponent({
 | 
				
			|||||||
}: DrawerProps) {
 | 
					}: DrawerProps) {
 | 
				
			||||||
  const navigate = useNavigate();
 | 
					  const navigate = useNavigate();
 | 
				
			||||||
  const { id } = useParams();
 | 
					  const { id } = useParams();
 | 
				
			||||||
 | 
					  const { classes } = useStyles();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const content = renderContent(id);
 | 
					  const content = renderContent(id);
 | 
				
			||||||
  const opened = useMemo(() => !!id && !!content, [id, content]);
 | 
					  const opened = useMemo(() => !!id && !!content, [id, content]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const [detailDrawerStack, addDetailDrawer] = useLocalState((state) => [
 | 
				
			||||||
 | 
					    state.detailDrawerStack,
 | 
				
			||||||
 | 
					    state.addDetailDrawer
 | 
				
			||||||
 | 
					  ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <Drawer
 | 
					    <Drawer
 | 
				
			||||||
      opened={opened}
 | 
					      opened={opened}
 | 
				
			||||||
      onClose={() => navigate('../')}
 | 
					      onClose={() => {
 | 
				
			||||||
 | 
					        navigate('../');
 | 
				
			||||||
 | 
					        addDetailDrawer(false);
 | 
				
			||||||
 | 
					      }}
 | 
				
			||||||
      position={position}
 | 
					      position={position}
 | 
				
			||||||
      size={size}
 | 
					      size={size}
 | 
				
			||||||
 | 
					      classNames={{ root: classes.flex, body: classes.flex }}
 | 
				
			||||||
 | 
					      scrollAreaComponent={Stack}
 | 
				
			||||||
      title={
 | 
					      title={
 | 
				
			||||||
        <Group>
 | 
					        <Group>
 | 
				
			||||||
          <ActionIcon variant="outline" onClick={() => navigate(-1)}>
 | 
					          {detailDrawerStack > 0 && (
 | 
				
			||||||
 | 
					            <ActionIcon
 | 
				
			||||||
 | 
					              variant="outline"
 | 
				
			||||||
 | 
					              onClick={() => {
 | 
				
			||||||
 | 
					                navigate(-1);
 | 
				
			||||||
 | 
					                addDetailDrawer(-1);
 | 
				
			||||||
 | 
					              }}
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
              <IconChevronLeft />
 | 
					              <IconChevronLeft />
 | 
				
			||||||
            </ActionIcon>
 | 
					            </ActionIcon>
 | 
				
			||||||
 | 
					          )}
 | 
				
			||||||
          <Text size="xl" fw={600} variant="gradient">
 | 
					          <Text size="xl" fw={600} variant="gradient">
 | 
				
			||||||
            {title}
 | 
					            {title}
 | 
				
			||||||
          </Text>
 | 
					          </Text>
 | 
				
			||||||
        </Group>
 | 
					        </Group>
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    >
 | 
					    >
 | 
				
			||||||
      <Stack spacing={'xs'}>
 | 
					      <Stack spacing={'xs'} className={classes.flex}>
 | 
				
			||||||
        <Divider />
 | 
					        <Divider />
 | 
				
			||||||
        {content}
 | 
					        {content}
 | 
				
			||||||
      </Stack>
 | 
					      </Stack>
 | 
				
			||||||
@@ -69,3 +99,17 @@ export function DetailDrawer(props: DrawerProps) {
 | 
				
			|||||||
    </Routes>
 | 
					    </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;
 | 
					  content?: ReactNode;
 | 
				
			||||||
  hidden?: boolean;
 | 
					  hidden?: boolean;
 | 
				
			||||||
  disabled?: boolean;
 | 
					  disabled?: boolean;
 | 
				
			||||||
 | 
					  showHeadline?: boolean;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type PanelProps = {
 | 
					export type PanelProps = {
 | 
				
			||||||
@@ -125,6 +126,8 @@ function BasePanelGroup({
 | 
				
			|||||||
                    //                    icon={(<InvenTreeIcon icon={panel.name}/>)}  // Enable when implementing Icon manager everywhere
 | 
					                    //                    icon={(<InvenTreeIcon icon={panel.name}/>)}  // Enable when implementing Icon manager everywhere
 | 
				
			||||||
                    icon={panel.icon}
 | 
					                    icon={panel.icon}
 | 
				
			||||||
                    hidden={panel.hidden}
 | 
					                    hidden={panel.hidden}
 | 
				
			||||||
 | 
					                    disabled={panel.disabled}
 | 
				
			||||||
 | 
					                    style={{ cursor: panel.disabled ? 'unset' : 'pointer' }}
 | 
				
			||||||
                  >
 | 
					                  >
 | 
				
			||||||
                    {expanded && panel.label}
 | 
					                    {expanded && panel.label}
 | 
				
			||||||
                  </Tabs.Tab>
 | 
					                  </Tabs.Tab>
 | 
				
			||||||
@@ -159,8 +162,12 @@ function BasePanelGroup({
 | 
				
			|||||||
                }}
 | 
					                }}
 | 
				
			||||||
              >
 | 
					              >
 | 
				
			||||||
                <Stack spacing="md">
 | 
					                <Stack spacing="md">
 | 
				
			||||||
 | 
					                  {panel.showHeadline !== false && (
 | 
				
			||||||
 | 
					                    <>
 | 
				
			||||||
                      <StylishText size="xl">{panel.label}</StylishText>
 | 
					                      <StylishText size="xl">{panel.label}</StylishText>
 | 
				
			||||||
                      <Divider />
 | 
					                      <Divider />
 | 
				
			||||||
 | 
					                    </>
 | 
				
			||||||
 | 
					                  )}
 | 
				
			||||||
                  {panel.content ?? <PlaceholderPanel />}
 | 
					                  {panel.content ?? <PlaceholderPanel />}
 | 
				
			||||||
                </Stack>
 | 
					                </Stack>
 | 
				
			||||||
              </Tabs.Panel>
 | 
					              </Tabs.Panel>
 | 
				
			||||||
@@ -176,12 +183,11 @@ function IndexPanelComponent({ pageKey, selectedPanel, panels }: PanelProps) {
 | 
				
			|||||||
    const panelName =
 | 
					    const panelName =
 | 
				
			||||||
      selectedPanel || state.lastUsedPanels[pageKey] || panels[0]?.name;
 | 
					      selectedPanel || state.lastUsedPanels[pageKey] || panels[0]?.name;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (
 | 
					    const panel = panels.findIndex(
 | 
				
			||||||
      panels.findIndex(
 | 
					 | 
				
			||||||
      (p) => p.name === panelName && !p.disabled && !p.hidden
 | 
					      (p) => p.name === panelName && !p.disabled && !p.hidden
 | 
				
			||||||
      ) === -1
 | 
					    );
 | 
				
			||||||
    ) {
 | 
					    if (panel === -1) {
 | 
				
			||||||
      return panels[0]?.name;
 | 
					      return panels.find((p) => !p.disabled && !p.hidden)?.name || '';
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return panelName;
 | 
					    return panelName;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,7 +8,7 @@ export interface ModelInformationInterface {
 | 
				
			|||||||
  label_multiple: string;
 | 
					  label_multiple: string;
 | 
				
			||||||
  url_overview?: string;
 | 
					  url_overview?: string;
 | 
				
			||||||
  url_detail?: string;
 | 
					  url_detail?: string;
 | 
				
			||||||
  api_endpoint?: ApiEndpoints;
 | 
					  api_endpoint: ApiEndpoints;
 | 
				
			||||||
  cui_detail?: string;
 | 
					  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_list = 'order/ro/',
 | 
				
			||||||
  return_order_attachment_list = 'order/ro/attachment/',
 | 
					  return_order_attachment_list = 'order/ro/attachment/',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Template API endpoints
 | 
				
			||||||
 | 
					  label_list = 'label/:variant/',
 | 
				
			||||||
 | 
					  report_list = 'report/:variant/',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Plugin API endpoints
 | 
					  // Plugin API endpoints
 | 
				
			||||||
  plugin_list = 'plugins/',
 | 
					  plugin_list = 'plugins/',
 | 
				
			||||||
  plugin_setting_list = 'plugins/:plugin/settings/',
 | 
					  plugin_setting_list = 'plugins/:plugin/settings/',
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,6 +8,7 @@ import {
 | 
				
			|||||||
  IconBuildingStore,
 | 
					  IconBuildingStore,
 | 
				
			||||||
  IconCalendar,
 | 
					  IconCalendar,
 | 
				
			||||||
  IconCalendarStats,
 | 
					  IconCalendarStats,
 | 
				
			||||||
 | 
					  IconCategory,
 | 
				
			||||||
  IconCheck,
 | 
					  IconCheck,
 | 
				
			||||||
  IconClipboardList,
 | 
					  IconClipboardList,
 | 
				
			||||||
  IconCopy,
 | 
					  IconCopy,
 | 
				
			||||||
@@ -58,13 +59,13 @@ import {
 | 
				
			|||||||
  IconX
 | 
					  IconX
 | 
				
			||||||
} from '@tabler/icons-react';
 | 
					} from '@tabler/icons-react';
 | 
				
			||||||
import { IconFlag } from '@tabler/icons-react';
 | 
					import { IconFlag } from '@tabler/icons-react';
 | 
				
			||||||
 | 
					import { IconTruckReturn } from '@tabler/icons-react';
 | 
				
			||||||
import { IconInfoCircle } from '@tabler/icons-react';
 | 
					import { IconInfoCircle } from '@tabler/icons-react';
 | 
				
			||||||
import { IconCalendarTime } from '@tabler/icons-react';
 | 
					import { IconCalendarTime } from '@tabler/icons-react';
 | 
				
			||||||
import { TablerIconsProps } from '@tabler/icons-react';
 | 
					import { TablerIconsProps } from '@tabler/icons-react';
 | 
				
			||||||
import React from 'react';
 | 
					import React from 'react';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const icons: { [key: string]: (props: TablerIconsProps) => React.JSX.Element } =
 | 
					const icons = {
 | 
				
			||||||
  {
 | 
					 | 
				
			||||||
  description: IconInfoCircle,
 | 
					  description: IconInfoCircle,
 | 
				
			||||||
  variant_of: IconStatusChange,
 | 
					  variant_of: IconStatusChange,
 | 
				
			||||||
  unallocated_stock: IconPackage,
 | 
					  unallocated_stock: IconPackage,
 | 
				
			||||||
@@ -95,6 +96,7 @@ const icons: { [key: string]: (props: TablerIconsProps) => React.JSX.Element } =
 | 
				
			|||||||
  customers: IconBuildingStore,
 | 
					  customers: IconBuildingStore,
 | 
				
			||||||
  purchase_orders: IconShoppingCart,
 | 
					  purchase_orders: IconShoppingCart,
 | 
				
			||||||
  sales_orders: IconTruckDelivery,
 | 
					  sales_orders: IconTruckDelivery,
 | 
				
			||||||
 | 
					  return_orders: IconTruckReturn,
 | 
				
			||||||
  shipment: IconTruckDelivery,
 | 
					  shipment: IconTruckDelivery,
 | 
				
			||||||
  scheduling: IconCalendarStats,
 | 
					  scheduling: IconCalendarStats,
 | 
				
			||||||
  test_templates: IconTestPipe,
 | 
					  test_templates: IconTestPipe,
 | 
				
			||||||
@@ -144,16 +146,18 @@ const icons: { [key: string]: (props: TablerIconsProps) => React.JSX.Element } =
 | 
				
			|||||||
  sitemap: IconSitemap
 | 
					  sitemap: IconSitemap
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type InvenTreeIconType = keyof typeof icons;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Returns a Tabler Icon for the model field name supplied
 | 
					 * Returns a Tabler Icon for the model field name supplied
 | 
				
			||||||
 * @param field string defining field name
 | 
					 * @param field string defining field name
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
export function GetIcon(field: keyof typeof icons) {
 | 
					export function GetIcon(field: InvenTreeIconType) {
 | 
				
			||||||
  return icons[field];
 | 
					  return icons[field];
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type IconProps = {
 | 
					type IconProps = {
 | 
				
			||||||
  icon: string;
 | 
					  icon: InvenTreeIconType;
 | 
				
			||||||
  iconProps?: TablerIconsProps;
 | 
					  iconProps?: TablerIconsProps;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,6 +9,7 @@ import {
 | 
				
			|||||||
  IconListDetails,
 | 
					  IconListDetails,
 | 
				
			||||||
  IconPlugConnected,
 | 
					  IconPlugConnected,
 | 
				
			||||||
  IconScale,
 | 
					  IconScale,
 | 
				
			||||||
 | 
					  IconTemplate,
 | 
				
			||||||
  IconUsersGroup
 | 
					  IconUsersGroup
 | 
				
			||||||
} from '@tabler/icons-react';
 | 
					} from '@tabler/icons-react';
 | 
				
			||||||
import { lazy, useMemo } from 'react';
 | 
					import { lazy, useMemo } from 'react';
 | 
				
			||||||
@@ -55,6 +56,10 @@ const CurrencyTable = Loadable(
 | 
				
			|||||||
  lazy(() => import('../../../../tables/settings/CurrencyTable'))
 | 
					  lazy(() => import('../../../../tables/settings/CurrencyTable'))
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const TemplateManagementPanel = Loadable(
 | 
				
			||||||
 | 
					  lazy(() => import('./TemplateManagementPanel'))
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default function AdminCenter() {
 | 
					export default function AdminCenter() {
 | 
				
			||||||
  const adminCenterPanels: PanelType[] = useMemo(() => {
 | 
					  const adminCenterPanels: PanelType[] = useMemo(() => {
 | 
				
			||||||
    return [
 | 
					    return [
 | 
				
			||||||
@@ -106,6 +111,12 @@ export default function AdminCenter() {
 | 
				
			|||||||
        icon: <IconList />,
 | 
					        icon: <IconList />,
 | 
				
			||||||
        content: <PartParameterTemplateTable />
 | 
					        content: <PartParameterTemplateTable />
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        name: 'templates',
 | 
				
			||||||
 | 
					        label: t`Templates`,
 | 
				
			||||||
 | 
					        icon: <IconTemplate />,
 | 
				
			||||||
 | 
					        content: <TemplateManagementPanel />
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
      {
 | 
					      {
 | 
				
			||||||
        name: 'plugin',
 | 
					        name: 'plugin',
 | 
				
			||||||
        label: t`Plugins`,
 | 
					        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={[
 | 
					        actions={[
 | 
				
			||||||
          ViewBarcodeAction({}),
 | 
					          ViewBarcodeAction({}),
 | 
				
			||||||
          LinkBarcodeAction({
 | 
					          LinkBarcodeAction({
 | 
				
			||||||
            disabled: build?.barcode_hash
 | 
					            hidden: build?.barcode_hash
 | 
				
			||||||
          }),
 | 
					          }),
 | 
				
			||||||
          UnlinkBarcodeAction({
 | 
					          UnlinkBarcodeAction({
 | 
				
			||||||
            disabled: !build?.barcode_hash
 | 
					            hidden: !build?.barcode_hash
 | 
				
			||||||
          })
 | 
					          })
 | 
				
			||||||
        ]}
 | 
					        ]}
 | 
				
			||||||
      />,
 | 
					      />,
 | 
				
			||||||
@@ -326,7 +326,7 @@ export default function BuildDetail() {
 | 
				
			|||||||
        actions={[
 | 
					        actions={[
 | 
				
			||||||
          EditItemAction({
 | 
					          EditItemAction({
 | 
				
			||||||
            onClick: () => editBuild.open(),
 | 
					            onClick: () => editBuild.open(),
 | 
				
			||||||
            disabled: !user.hasChangeRole(UserRoles.build)
 | 
					            hidden: !user.hasChangeRole(UserRoles.build)
 | 
				
			||||||
          }),
 | 
					          }),
 | 
				
			||||||
          DuplicateItemAction({})
 | 
					          DuplicateItemAction({})
 | 
				
			||||||
        ]}
 | 
					        ]}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -282,11 +282,11 @@ export default function CompanyDetail(props: CompanyDetailProps) {
 | 
				
			|||||||
        icon={<IconDots />}
 | 
					        icon={<IconDots />}
 | 
				
			||||||
        actions={[
 | 
					        actions={[
 | 
				
			||||||
          EditItemAction({
 | 
					          EditItemAction({
 | 
				
			||||||
            disabled: !user.hasChangeRole(UserRoles.purchase_order),
 | 
					            hidden: !user.hasChangeRole(UserRoles.purchase_order),
 | 
				
			||||||
            onClick: () => editCompany.open()
 | 
					            onClick: () => editCompany.open()
 | 
				
			||||||
          }),
 | 
					          }),
 | 
				
			||||||
          DeleteItemAction({
 | 
					          DeleteItemAction({
 | 
				
			||||||
            disabled: !user.hasDeleteRole(UserRoles.purchase_order)
 | 
					            hidden: !user.hasDeleteRole(UserRoles.purchase_order)
 | 
				
			||||||
          })
 | 
					          })
 | 
				
			||||||
        ]}
 | 
					        ]}
 | 
				
			||||||
      />
 | 
					      />
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -638,10 +638,10 @@ export default function PartDetail() {
 | 
				
			|||||||
        actions={[
 | 
					        actions={[
 | 
				
			||||||
          ViewBarcodeAction({}),
 | 
					          ViewBarcodeAction({}),
 | 
				
			||||||
          LinkBarcodeAction({
 | 
					          LinkBarcodeAction({
 | 
				
			||||||
            disabled: part?.barcode_hash
 | 
					            hidden: part?.barcode_hash
 | 
				
			||||||
          }),
 | 
					          }),
 | 
				
			||||||
          UnlinkBarcodeAction({
 | 
					          UnlinkBarcodeAction({
 | 
				
			||||||
            disabled: !part?.barcode_hash
 | 
					            hidden: !part?.barcode_hash
 | 
				
			||||||
          })
 | 
					          })
 | 
				
			||||||
        ]}
 | 
					        ]}
 | 
				
			||||||
      />,
 | 
					      />,
 | 
				
			||||||
@@ -669,11 +669,11 @@ export default function PartDetail() {
 | 
				
			|||||||
        actions={[
 | 
					        actions={[
 | 
				
			||||||
          DuplicateItemAction({}),
 | 
					          DuplicateItemAction({}),
 | 
				
			||||||
          EditItemAction({
 | 
					          EditItemAction({
 | 
				
			||||||
            disabled: !user.hasChangeRole(UserRoles.part),
 | 
					            hidden: !user.hasChangeRole(UserRoles.part),
 | 
				
			||||||
            onClick: () => editPart.open()
 | 
					            onClick: () => editPart.open()
 | 
				
			||||||
          }),
 | 
					          }),
 | 
				
			||||||
          DeleteItemAction({
 | 
					          DeleteItemAction({
 | 
				
			||||||
            disabled: part?.active
 | 
					            hidden: part?.active
 | 
				
			||||||
          })
 | 
					          })
 | 
				
			||||||
        ]}
 | 
					        ]}
 | 
				
			||||||
      />
 | 
					      />
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -273,10 +273,10 @@ export default function PurchaseOrderDetail() {
 | 
				
			|||||||
        actions={[
 | 
					        actions={[
 | 
				
			||||||
          ViewBarcodeAction({}),
 | 
					          ViewBarcodeAction({}),
 | 
				
			||||||
          LinkBarcodeAction({
 | 
					          LinkBarcodeAction({
 | 
				
			||||||
            disabled: order?.barcode_hash
 | 
					            hidden: order?.barcode_hash
 | 
				
			||||||
          }),
 | 
					          }),
 | 
				
			||||||
          UnlinkBarcodeAction({
 | 
					          UnlinkBarcodeAction({
 | 
				
			||||||
            disabled: !order?.barcode_hash
 | 
					            hidden: !order?.barcode_hash
 | 
				
			||||||
          })
 | 
					          })
 | 
				
			||||||
        ]}
 | 
					        ]}
 | 
				
			||||||
      />,
 | 
					      />,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -317,10 +317,10 @@ export default function StockDetail() {
 | 
				
			|||||||
        actions={[
 | 
					        actions={[
 | 
				
			||||||
          ViewBarcodeAction({}),
 | 
					          ViewBarcodeAction({}),
 | 
				
			||||||
          LinkBarcodeAction({
 | 
					          LinkBarcodeAction({
 | 
				
			||||||
            disabled: stockitem?.barcode_hash
 | 
					            hidden: stockitem?.barcode_hash
 | 
				
			||||||
          }),
 | 
					          }),
 | 
				
			||||||
          UnlinkBarcodeAction({
 | 
					          UnlinkBarcodeAction({
 | 
				
			||||||
            disabled: !stockitem?.barcode_hash
 | 
					            hidden: !stockitem?.barcode_hash
 | 
				
			||||||
          })
 | 
					          })
 | 
				
			||||||
        ]}
 | 
					        ]}
 | 
				
			||||||
      />,
 | 
					      />,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -29,6 +29,8 @@ interface LocalStateProps {
 | 
				
			|||||||
    tableKey: string
 | 
					    tableKey: string
 | 
				
			||||||
  ) => (names: Record<string, string>) => void;
 | 
					  ) => (names: Record<string, string>) => void;
 | 
				
			||||||
  clearTableColumnNames: () => void;
 | 
					  clearTableColumnNames: () => void;
 | 
				
			||||||
 | 
					  detailDrawerStack: number;
 | 
				
			||||||
 | 
					  addDetailDrawer: (value: number | false) => void;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const useLocalState = create<LocalStateProps>()(
 | 
					export const useLocalState = create<LocalStateProps>()(
 | 
				
			||||||
@@ -61,6 +63,7 @@ export const useLocalState = create<LocalStateProps>()(
 | 
				
			|||||||
          });
 | 
					          });
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
 | 
					      // tables
 | 
				
			||||||
      tableColumnNames: {},
 | 
					      tableColumnNames: {},
 | 
				
			||||||
      getTableColumnNames: (tableKey) => {
 | 
					      getTableColumnNames: (tableKey) => {
 | 
				
			||||||
        return get().tableColumnNames[tableKey] || {};
 | 
					        return get().tableColumnNames[tableKey] || {};
 | 
				
			||||||
@@ -76,6 +79,14 @@ export const useLocalState = create<LocalStateProps>()(
 | 
				
			|||||||
      },
 | 
					      },
 | 
				
			||||||
      clearTableColumnNames: () => {
 | 
					      clearTableColumnNames: () => {
 | 
				
			||||||
        set({ tableColumnNames: {} });
 | 
					        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 { useQuery } from '@tanstack/react-query';
 | 
				
			||||||
import { useCallback, useMemo, useState } from 'react';
 | 
					import { useCallback, useMemo, useState } from 'react';
 | 
				
			||||||
import { useNavigate } from 'react-router-dom';
 | 
					import { useNavigate } from 'react-router-dom';
 | 
				
			||||||
import { Link } from 'react-router-dom';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { api } from '../../App';
 | 
					import { api } from '../../App';
 | 
				
			||||||
import { AddItemButton } from '../../components/buttons/AddItemButton';
 | 
					import { AddItemButton } from '../../components/buttons/AddItemButton';
 | 
				
			||||||
@@ -32,7 +31,10 @@ import {
 | 
				
			|||||||
import { InfoItem } from '../../components/items/InfoItem';
 | 
					import { InfoItem } from '../../components/items/InfoItem';
 | 
				
			||||||
import { UnavailableIndicator } from '../../components/items/UnavailableIndicator';
 | 
					import { UnavailableIndicator } from '../../components/items/UnavailableIndicator';
 | 
				
			||||||
import { YesNoButton } from '../../components/items/YesNoButton';
 | 
					import { YesNoButton } from '../../components/items/YesNoButton';
 | 
				
			||||||
import { DetailDrawer } from '../../components/nav/DetailDrawer';
 | 
					import {
 | 
				
			||||||
 | 
					  DetailDrawer,
 | 
				
			||||||
 | 
					  DetailDrawerLink
 | 
				
			||||||
 | 
					} from '../../components/nav/DetailDrawer';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  StatusRenderer,
 | 
					  StatusRenderer,
 | 
				
			||||||
  TableStatusRenderer
 | 
					  TableStatusRenderer
 | 
				
			||||||
@@ -289,9 +291,10 @@ function MachineDrawer({
 | 
				
			|||||||
            <InfoItem name={t`Machine Type`}>
 | 
					            <InfoItem name={t`Machine Type`}>
 | 
				
			||||||
              <Group spacing="xs">
 | 
					              <Group spacing="xs">
 | 
				
			||||||
                {machineType ? (
 | 
					                {machineType ? (
 | 
				
			||||||
                  <Link to={`../type-${machine?.machine_type}`}>
 | 
					                  <DetailDrawerLink
 | 
				
			||||||
                    <Text>{machineType.name}</Text>
 | 
					                    to={`../type-${machine?.machine_type}`}
 | 
				
			||||||
                  </Link>
 | 
					                    text={machineType.name}
 | 
				
			||||||
 | 
					                  />
 | 
				
			||||||
                ) : (
 | 
					                ) : (
 | 
				
			||||||
                  <Text>{machine?.machine_type}</Text>
 | 
					                  <Text>{machine?.machine_type}</Text>
 | 
				
			||||||
                )}
 | 
					                )}
 | 
				
			||||||
@@ -301,9 +304,10 @@ function MachineDrawer({
 | 
				
			|||||||
            <InfoItem name={t`Machine Driver`}>
 | 
					            <InfoItem name={t`Machine Driver`}>
 | 
				
			||||||
              <Group spacing="xs">
 | 
					              <Group spacing="xs">
 | 
				
			||||||
                {machineDriver ? (
 | 
					                {machineDriver ? (
 | 
				
			||||||
                  <Link to={`../driver-${machine?.driver}`}>
 | 
					                  <DetailDrawerLink
 | 
				
			||||||
                    <Text>{machineDriver.name}</Text>
 | 
					                    to={`../driver-${machine?.driver}`}
 | 
				
			||||||
                  </Link>
 | 
					                    text={machineDriver.name}
 | 
				
			||||||
 | 
					                  />
 | 
				
			||||||
                ) : (
 | 
					                ) : (
 | 
				
			||||||
                  <Text>{machine?.driver}</Text>
 | 
					                  <Text>{machine?.driver}</Text>
 | 
				
			||||||
                )}
 | 
					                )}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -120,6 +120,7 @@ function MachineTypeDrawer({ machineTypeSlug }: { machineTypeSlug: string }) {
 | 
				
			|||||||
                    ? `../../plugin/${machineType?.provider_plugin?.pk}/`
 | 
					                    ? `../../plugin/${machineType?.provider_plugin?.pk}/`
 | 
				
			||||||
                    : undefined
 | 
					                    : undefined
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					                detailDrawerLink
 | 
				
			||||||
              />
 | 
					              />
 | 
				
			||||||
            )}
 | 
					            )}
 | 
				
			||||||
            <InfoItem
 | 
					            <InfoItem
 | 
				
			||||||
@@ -224,6 +225,7 @@ function MachineDriverDrawer({
 | 
				
			|||||||
                  ? `../type-${machineDriver?.machine_type}`
 | 
					                  ? `../type-${machineDriver?.machine_type}`
 | 
				
			||||||
                  : undefined
 | 
					                  : undefined
 | 
				
			||||||
              }
 | 
					              }
 | 
				
			||||||
 | 
					              detailDrawerLink
 | 
				
			||||||
            />
 | 
					            />
 | 
				
			||||||
            {!machineDriver?.is_builtin && (
 | 
					            {!machineDriver?.is_builtin && (
 | 
				
			||||||
              <InfoItem
 | 
					              <InfoItem
 | 
				
			||||||
@@ -235,6 +237,7 @@ function MachineDriverDrawer({
 | 
				
			|||||||
                    ? `../../plugin/${machineDriver?.provider_plugin?.pk}/`
 | 
					                    ? `../../plugin/${machineDriver?.provider_plugin?.pk}/`
 | 
				
			||||||
                    : undefined
 | 
					                    : undefined
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					                detailDrawerLink
 | 
				
			||||||
              />
 | 
					              />
 | 
				
			||||||
            )}
 | 
					            )}
 | 
				
			||||||
            <InfoItem
 | 
					            <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 { AddItemButton } from '../../components/buttons/AddItemButton';
 | 
				
			||||||
import { EditApiForm } from '../../components/forms/ApiForm';
 | 
					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 { ApiEndpoints } from '../../enums/ApiEndpoints';
 | 
				
			||||||
import { openCreateApiForm, openDeleteApiForm } from '../../functions/forms';
 | 
					import { openCreateApiForm, openDeleteApiForm } from '../../functions/forms';
 | 
				
			||||||
import { useInstance } from '../../hooks/UseInstance';
 | 
					import { useInstance } from '../../hooks/UseInstance';
 | 
				
			||||||
@@ -125,7 +128,10 @@ export function UserDrawer({
 | 
				
			|||||||
          <List>
 | 
					          <List>
 | 
				
			||||||
            {userDetail?.groups?.map((group) => (
 | 
					            {userDetail?.groups?.map((group) => (
 | 
				
			||||||
              <List.Item key={group.pk}>
 | 
					              <List.Item key={group.pk}>
 | 
				
			||||||
                <Link to={`../group-${group.pk}`}>{group.name}</Link>
 | 
					                <DetailDrawerLink
 | 
				
			||||||
 | 
					                  to={`../group-${group.pk}`}
 | 
				
			||||||
 | 
					                  text={group.name}
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
              </List.Item>
 | 
					              </List.Item>
 | 
				
			||||||
            ))}
 | 
					            ))}
 | 
				
			||||||
          </List>
 | 
					          </List>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -322,6 +322,13 @@
 | 
				
			|||||||
  dependencies:
 | 
					  dependencies:
 | 
				
			||||||
    regenerator-runtime "^0.14.0"
 | 
					    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":
 | 
					"@babel/template@^7.22.15":
 | 
				
			||||||
  version "7.22.15"
 | 
					  version "7.22.15"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.15.tgz#09576efc3830f0430f4548ef971dde1350ef2f38"
 | 
					  resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.15.tgz#09576efc3830f0430f4548ef971dde1350ef2f38"
 | 
				
			||||||
@@ -356,6 +363,133 @@
 | 
				
			|||||||
    "@babel/helper-validator-identifier" "^7.22.20"
 | 
					    "@babel/helper-validator-identifier" "^7.22.20"
 | 
				
			||||||
    to-fast-properties "^2.0.0"
 | 
					    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":
 | 
					"@emotion/babel-plugin@^11.11.0":
 | 
				
			||||||
  version "11.11.0"
 | 
					  version "11.11.0"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz#c2d872b6a7767a9d176d007f5b31f7d504bb5d6c"
 | 
					  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/resolve-uri" "^3.1.0"
 | 
				
			||||||
    "@jridgewell/sourcemap-codec" "^1.4.14"
 | 
					    "@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":
 | 
					"@lingui/babel-plugin-extract-messages@4.5.0":
 | 
				
			||||||
  version "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"
 | 
					  resolved "https://registry.yarnpkg.com/@lingui/babel-plugin-extract-messages/-/babel-plugin-extract-messages-4.5.0.tgz#71e56cc2eae73890caeea15a00ae4965413430ec"
 | 
				
			||||||
@@ -1342,6 +1522,52 @@
 | 
				
			|||||||
  dependencies:
 | 
					  dependencies:
 | 
				
			||||||
    "@types/yargs-parser" "*"
 | 
					    "@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":
 | 
					"@vitejs/plugin-react@^4.1.0":
 | 
				
			||||||
  version "4.1.0"
 | 
					  version "4.1.0"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/@vitejs/plugin-react/-/plugin-react-4.1.0.tgz#e4f56f46fd737c5d386bb1f1ade86ba275fe09bd"
 | 
					  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"
 | 
					  resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.65.15.tgz#66899278f44a7acde0eb641388cd563fe6dfbe19"
 | 
				
			||||||
  integrity sha512-YC4EHbbwQeubZzxLl5G4nlbLc1T21QTrKGaOal/Pkm9dVDMZXMH7+ieSPEOZCtO9I68i8/oteJKOxzHC2zR+0g==
 | 
					  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:
 | 
					color-convert@^1.9.0:
 | 
				
			||||||
  version "1.9.3"
 | 
					  version "1.9.3"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
 | 
					  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"
 | 
					    parse-json "^5.2.0"
 | 
				
			||||||
    path-type "^4.0.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:
 | 
					css-color-keywords@^1.0.0:
 | 
				
			||||||
  version "1.0.0"
 | 
					  version "1.0.0"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/css-color-keywords/-/css-color-keywords-1.0.0.tgz#fea2616dc676b2962686b3af8dbdbe180b244e05"
 | 
					  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:
 | 
					  dependencies:
 | 
				
			||||||
    ansi-regex "^5.0.1"
 | 
					    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:
 | 
					styled-components@^6.1.0:
 | 
				
			||||||
  version "6.1.0"
 | 
					  version "6.1.0"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/styled-components/-/styled-components-6.1.0.tgz#228e3ab9c1ee1daa4b0a06aae30df0ed14fda274"
 | 
					  resolved "https://registry.yarnpkg.com/styled-components/-/styled-components-6.1.0.tgz#228e3ab9c1ee1daa4b0a06aae30df0ed14fda274"
 | 
				
			||||||
@@ -2983,6 +3232,11 @@ vite@^4.5.2:
 | 
				
			|||||||
  optionalDependencies:
 | 
					  optionalDependencies:
 | 
				
			||||||
    fsevents "~2.3.2"
 | 
					    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:
 | 
					wcwidth@^1.0.1:
 | 
				
			||||||
  version "1.0.1"
 | 
					  version "1.0.1"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8"
 | 
					  resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8"
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user