From 9a386428ba6af7b9728861a6536f453b8005a95a Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 13 Jun 2026 13:11:17 +0000 Subject: [PATCH] Frontend updates --- .../src/components/editors/NotesEditor.tsx | 62 +++++++++------- src/frontend/src/forms/CommonForms.tsx | 72 +++++++++++++++++-- .../Index/Settings/AdminCenter/Index.tsx | 11 +++ .../AdminCenter/NoteTemplatePanel.tsx | 15 ++++ 4 files changed, 131 insertions(+), 29 deletions(-) create mode 100644 src/frontend/src/pages/Index/Settings/AdminCenter/NoteTemplatePanel.tsx diff --git a/src/frontend/src/components/editors/NotesEditor.tsx b/src/frontend/src/components/editors/NotesEditor.tsx index da0366f99f..ffd9fff902 100644 --- a/src/frontend/src/components/editors/NotesEditor.tsx +++ b/src/frontend/src/components/editors/NotesEditor.tsx @@ -57,7 +57,7 @@ import { } from '@tabler/icons-react'; import { useShallow } from 'zustand/react/shallow'; import { formatDate } from '../../defaults/formatters'; -import { useNoteFields } from '../../forms/CommonForms'; +import { useNoteFields, useNoteTemplateFields } from '../../forms/CommonForms'; import { useCreateApiFormModal, useDeleteApiFormModal, @@ -111,10 +111,12 @@ function NoteInfoHover({ note }: { note: any }) { export default function NotesEditor({ modelType, modelId, + templateMode = false, setDirtyCallback }: Readonly<{ - modelType: ModelType; - modelId: number; + modelType?: ModelType; + modelId?: number; + templateMode?: boolean; setDirtyCallback?: (dirty: boolean) => void; }>) { const api = useApi(); @@ -173,23 +175,22 @@ export default function NotesEditor({ onUpdate: () => setIsDirty(true) }); - // Fetch the available notes for the given model type and ID + // Fetch the available notes for the given model type and ID (or all templates) const notesQuery = useQuery({ - queryKey: ['notes', modelType, modelId], + queryKey: ['notes', modelType, modelId, templateMode], queryFn: async () => { + const params: Record = templateMode + ? { template: true } + : { model_id: modelId, model_type: modelType }; + return api - .get(apiUrl(ApiEndpoints.note_list), { - params: { - model_id: modelId, - model_type: modelType - } - }) + .get(apiUrl(ApiEndpoints.note_list), { params }) .then((response) => response.data ?? []); }, staleTime: 0, refetchOnWindowFocus: false, refetchOnMount: true, - enabled: !!modelId && !!modelType + enabled: templateMode ? true : !!modelId && !!modelType }); const [selectedNote, setSelectedNote] = useState(undefined); @@ -244,11 +245,15 @@ export default function NotesEditor({ const canEdit: boolean = useMemo( () => - user.hasChangePermission(modelType) && + (templateMode + ? user.isStaff() + : modelType + ? user.hasChangePermission(modelType) + : false) && notesQuery.isFetched && notesQuery.isSuccess && !!notesQuery.data, - [user, modelType, notesQuery] + [user, modelType, templateMode, notesQuery] ); const isInTable = useEditorState({ @@ -271,11 +276,16 @@ export default function NotesEditor({ return notesQuery.data && notesQuery.data.length > 0; }, [notesQuery.data]); - const noteFields = useNoteFields({ modelType: modelType, modelId: modelId }); + const noteFields = useNoteFields({ + modelType: modelType!, + modelId: modelId! + }); + const noteTemplateFields = useNoteTemplateFields(); + const activeFields = templateMode ? noteTemplateFields : noteFields; const createNote = useCreateApiFormModal({ - title: t`Add Note`, - fields: noteFields, + title: templateMode ? t`Add Note Template` : t`Add Note`, + fields: activeFields, url: apiUrl(ApiEndpoints.note_list), method: 'POST', successMessage: null, @@ -287,7 +297,7 @@ export default function NotesEditor({ }); const deleteNote = useDeleteApiFormModal({ - title: t`Delete Note`, + title: templateMode ? t`Delete Note Template` : t`Delete Note`, url: apiUrl(ApiEndpoints.note_list), pk: selectedNoteId, onFormSuccess: () => { @@ -297,8 +307,8 @@ export default function NotesEditor({ }); const editNote = useEditApiFormModal({ - title: t`Edit Note`, - fields: noteFields, + title: templateMode ? t`Edit Note Template` : t`Edit Note`, + fields: activeFields, url: apiUrl(ApiEndpoints.note_list), pk: selectedNoteId, onFormSuccess: (response: any) => { @@ -442,9 +452,7 @@ export default function NotesEditor({ tooltip={t`Note Actions`} actions={[ EditItemAction({ - hidden: - !selectedNote || - !user.hasChangePermission(modelType), + hidden: !selectedNote || !canEdit, onClick: () => { editNote.open(); } @@ -452,7 +460,11 @@ export default function NotesEditor({ DeleteItemAction({ hidden: !selectedNote || - !user.hasDeletePermission(modelType), + !(templateMode + ? user.isStaff() + : modelType + ? user.hasDeletePermission(modelType) + : false), onClick: () => { deleteNote.open(); } @@ -626,7 +638,7 @@ export default function NotesEditor({ ) : ( }> - {t`There are no notes yet for this item.`} + {t`There are no notes here yet.`} )} diff --git a/src/frontend/src/forms/CommonForms.tsx b/src/frontend/src/forms/CommonForms.tsx index 0058e79389..0378bdb0d4 100644 --- a/src/frontend/src/forms/CommonForms.tsx +++ b/src/frontend/src/forms/CommonForms.tsx @@ -285,6 +285,24 @@ export function useParameterFields({ ]); } +export function useNoteTemplateFields(): ApiFormFieldSet { + return useMemo(() => { + return { + template: { + hidden: true, + value: true + }, + model_type: { + label: t`Model Type`, + description: t`Limit this template to a specific model type, or leave blank for all models`, + required: false + }, + title: {}, + description: {} + }; + }, []); +} + export function useNoteFields({ modelType, modelId @@ -292,6 +310,27 @@ export function useNoteFields({ modelType: ModelType; modelId: number; }): ApiFormFieldSet { + const api = useApi(); + + const [title, setTitle] = useState(''); + const [description, setDescription] = useState(''); + const [content, setContent] = useState(''); + + const fetchTemplate = useCallback( + (pk: number | null) => { + if (!pk) return; + api + .get(apiUrl(ApiEndpoints.note_list, pk)) + .then((response) => { + setTitle(response.data.title ?? ''); + setDescription(response.data.description ?? ''); + setContent(response.data.content ?? ''); + }) + .catch(() => {}); + }, + [api] + ); + return useMemo(() => { return { model_type: { @@ -302,9 +341,34 @@ export function useNoteFields({ hidden: true, value: modelId }, - title: {}, - description: {}, - primary: {} + template_source: { + field_type: 'related field', + label: t`From Template`, + description: t`Optionally pre-fill this note from an existing template`, + api_url: apiUrl(ApiEndpoints.note_list), + filters: { + template: true, + model_type: modelType + }, + pk_field: 'pk', + render_description_field: 'description', + required: false, + onValueChange: (value: any) => fetchTemplate(value), + value: null + }, + title: { + value: title, + onValueChange: (value: any) => setTitle(value) + }, + description: { + value: description, + onValueChange: (value: any) => setDescription(value) + }, + primary: {}, + content: { + hidden: true, + value: content + } }; - }, [modelType, modelId]); + }, [modelType, modelId, title, description, content, fetchTemplate]); } diff --git a/src/frontend/src/pages/Index/Settings/AdminCenter/Index.tsx b/src/frontend/src/pages/Index/Settings/AdminCenter/Index.tsx index 214f6c49d3..013f6fdff3 100644 --- a/src/frontend/src/pages/Index/Settings/AdminCenter/Index.tsx +++ b/src/frontend/src/pages/Index/Settings/AdminCenter/Index.tsx @@ -11,6 +11,7 @@ import { IconList, IconListDetails, IconMail, + IconNotes, IconPackages, IconPlugConnected, IconQrcode, @@ -71,6 +72,8 @@ const MachineManagementPanel = Loadable( const ParameterPanel = Loadable(lazy(() => import('./ParameterPanel'))); +const NoteTemplatePanel = Loadable(lazy(() => import('./NoteTemplatePanel'))); + const ErrorReportTable = Loadable( lazy(() => import('../../../../tables/settings/ErrorTable')) ); @@ -195,6 +198,13 @@ export default function AdminCenter() { content: , hidden: !user.hasViewRole(UserRoles.part) }, + { + name: 'notes', + label: t`Note Templates`, + icon: , + content: , + hidden: !user.isStaff() + }, { name: 'category-parameters', label: t`Category Parameters`, @@ -274,6 +284,7 @@ export default function AdminCenter() { panelIDs: [ 'parameters', 'category-parameters', + 'notes', 'location-types', 'stocktake' ] diff --git a/src/frontend/src/pages/Index/Settings/AdminCenter/NoteTemplatePanel.tsx b/src/frontend/src/pages/Index/Settings/AdminCenter/NoteTemplatePanel.tsx new file mode 100644 index 0000000000..f3640f47a9 --- /dev/null +++ b/src/frontend/src/pages/Index/Settings/AdminCenter/NoteTemplatePanel.tsx @@ -0,0 +1,15 @@ +import { t } from '@lingui/core/macro'; +import { Alert, Stack } from '@mantine/core'; +import { IconInfoCircle } from '@tabler/icons-react'; +import NotesEditor from '../../../../components/editors/NotesEditor'; + +export default function NoteTemplatePanel() { + return ( + + } title={t`Note Templates`}> + {t`Note templates can be used to create pre-defined notes which can be easily added to any model instance.`} + + + + ); +}