Merge commit '5c95dfe484b0e01b8a3f95e7413f0f6f71cdc3cd' into block-notes

This commit is contained in:
Oliver Walters
2026-06-16 00:01:49 +00:00
23 changed files with 896 additions and 784 deletions
+50 -49
View File
@@ -49,16 +49,15 @@
"@codemirror/state": "^6.6.0",
"@codemirror/theme-one-dark": "^6.1.3",
"@codemirror/view": "^6.40.0",
"@emotion/react": "^11.13.3",
"@floating-ui/dom": "^1.0.0",
"@fortawesome/fontawesome-svg-core": "^7.0.0",
"@fortawesome/free-regular-svg-icons": "^7.0.0",
"@fortawesome/free-solid-svg-icons": "^7.0.0",
"@fortawesome/react-fontawesome": "^3.0.1",
"@fullcalendar/core": "^6.1.15",
"@fullcalendar/daygrid": "^6.1.15",
"@fullcalendar/interaction": "^6.1.15",
"@fullcalendar/react": "^6.1.15",
"@emotion/react": "^11.14.0",
"@fortawesome/fontawesome-svg-core": "^7.2.0",
"@fortawesome/free-regular-svg-icons": "^7.2.0",
"@fortawesome/free-solid-svg-icons": "^7.2.0",
"@fortawesome/react-fontawesome": "^3.3.1",
"@fullcalendar/core": "^6.1.20",
"@fullcalendar/daygrid": "^6.1.20",
"@fullcalendar/interaction": "^6.1.20",
"@fullcalendar/react": "^6.1.20",
"@github/webauthn-json": "^2.1.1",
"@lingui/core": "^5.9.2",
"@lingui/react": "^5.9.2",
@@ -76,9 +75,9 @@
"@mantine/utils": "^6.0.22",
"@mantine/vanilla-extract": "^9.2.1",
"@messageformat/date-skeleton": "^1.1.0",
"@sentry/react": "^10.43.0",
"@tabler/icons-react": "^3.17.0",
"@tanstack/react-query": "^5.56.2",
"@sentry/react": "^10.57.0",
"@tabler/icons-react": "^3.44.0",
"@tanstack/react-query": "^5.101.0",
"@tiptap/core": "^3.23.6",
"@tiptap/extension-image": "^3.23.6",
"@tiptap/extension-link": "^3.23.6",
@@ -89,65 +88,67 @@
"@uiw/codemirror-theme-vscode": "^4.25.8",
"@uiw/react-codemirror": "^4.25.8",
"@uiw/react-split": "^5.9.4",
"@vanilla-extract/css": "^1.18.0",
"axios": "^1.13.6",
"@vanilla-extract/css": "^1.20.1",
"axios": "^1.17.0",
"clsx": "^2.1.1",
"codemirror": "^6.0.2",
"dayjs": "^1.11.13",
"dompurify": "^3.2.4",
"embla-carousel": "^8.5.2",
"embla-carousel-react": "^8.5.2",
"fuse.js": "^7.0.0",
"dayjs": "^1.11.21",
"dompurify": "^3.4.8",
"easymde": "^2.21.0",
"embla-carousel": "^8.6.0",
"embla-carousel-react": "^8.6.0",
"fuse.js": "^7.4.2",
"html5-qrcode": "^2.3.8",
"mantine-contextmenu": "^9.2.1",
"mantine-datatable": "^9.2.0",
"mantine-datatable": "^9.2.2",
"qrcode": "^1.5.4",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"react": "^19.2.7",
"react-dom": "^19.2.7",
"react-grid-layout": "1.4.4",
"react-hook-form": "^7.62.0",
"react-is": "^19.2.4",
"react-router-dom": "^6.26.2",
"react-select": "^5.9.0",
"react-hook-form": "^7.78.0",
"react-is": "^19.2.7",
"react-router-dom": "^6.30.4",
"react-select": "^5.10.2",
"react-simplemde-editor": "^5.2.0",
"react-window": "1.8.11",
"recharts": "^3.1.2",
"styled-components": "^6.1.14",
"recharts": "^3.8.1",
"styled-components": "^6.4.2",
"tiptap-extension-resizable-image": "^2.1.0",
"undici": "^6.24.0",
"zustand": "^5.0.8"
"undici": "^8.4.1",
"zustand": "^5.0.14"
},
"devDependencies": {
"@babel/core": "^7.29.0",
"@babel/preset-react": "^7.28.5",
"@babel/preset-typescript": "^7.28.5",
"@babel/runtime": "^7.28.6",
"@codecov/vite-plugin": "^1.9.1",
"@babel/core": "^7.29.7",
"@babel/preset-react": "^7.29.7",
"@babel/preset-typescript": "^7.29.7",
"@babel/runtime": "^7.29.7",
"@codecov/vite-plugin": "^2.0.1",
"@flakiness/playwright": "^1.13.0",
"@lingui/babel-plugin-lingui-macro": "^5.9.2",
"@lingui/cli": "^5.9.2",
"@lingui/macro": "^5.9.2",
"@playwright/test": "^1.16.0",
"@types/node": "^25.5.0",
"@types/qrcode": "^1.5.5",
"@types/react": "^19.2.14",
"@lingui/babel-plugin-lingui-macro": "^5.9.5",
"@lingui/cli": "^5.9.5",
"@lingui/macro": "^5.9.5",
"@playwright/test": "^1.60.0",
"@types/node": "^25.9.2",
"@types/qrcode": "^1.5.6",
"@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3",
"@types/react-grid-layout": "^1.3.5",
"@types/react-router-dom": "^5.3.3",
"@types/react-window": "^1.8.8",
"@vanilla-extract/vite-plugin": "^5.1.4",
"@vanilla-extract/vite-plugin": "^5.2.2",
"@vitejs/plugin-react": "^5.2.0",
"babel-plugin-macros": "^3.1.0",
"nyc": "^18.0.0",
"otpauth": "^9.4.1",
"otpauth": "^9.5.1",
"path": "^0.12.7",
"rollup": "^4.59.0",
"rollup-plugin-license": "^3.7.0",
"rollup": "^4.61.1",
"rollup-plugin-license": "^3.7.1",
"typescript": "^5.9.3",
"vite": "^6.4.2",
"vite-plugin-babel-macros": "^1.0.6",
"vite-plugin-dts": "^4.5.4",
"vite-plugin-dts": "^5.0.2",
"vite-plugin-externals": "^0.6.2",
"vite-plugin-istanbul": "^8.0.0"
"vite-plugin-istanbul": "^9.0.1"
},
"overrides": {
},
@@ -68,7 +68,7 @@ export function ApiFormField({
: definition.value
);
}
}, [definition.value]);
}, [definition.value, definition.field_type]);
const fieldDefinition: ApiFormFieldType = useMemo(() => {
return {
+131 -112
View File
@@ -4,7 +4,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
import { ModelType } from '@lib/enums/ModelType';
import { apiUrl } from '@lib/functions/Api';
import type { ApiFormFieldSet } from '@lib/types/Forms';
import type { ApiFormFieldSet, ApiFormFieldType } from '@lib/types/Forms';
import { t } from '@lingui/core/macro';
import type {
StatusCodeInterface,
@@ -117,32 +117,44 @@ export function useParameterTemplateFields(): ApiFormFieldSet {
}, []);
}
export function useParameterFields({
modelType,
modelId
}: {
modelType: ModelType;
modelId: number;
}): ApiFormFieldSet {
/**
* Shared hook for the dynamic "value" field on parameter forms.
*
* When the user selects a parameter template, the field type for the
* corresponding value input (data / default_value) must change to match the
* template's data type (boolean, choice, related-field selection list, or
* plain string). This hook encapsulates that state so it can be reused
* across the "Add Parameter" and "Add Category Parameter" forms.
*
* @param resetDep - When this value changes all internal state is reset to
* defaults. Pass a stringified key derived from the form's context (e.g.
* `${modelType}-${modelId}`) so the field resets when the context switches.
*/
export function useDynamicParameterValueField(resetDep?: any): {
onTemplateValueChange: (value: any, record: any) => void;
valueFieldConfig: ApiFormFieldType;
reset: () => void;
} {
const api = useApi();
const user = useUserState.getState();
const templateCreateFields = useParameterTemplateFields();
const [selectionListId, setSelectionListId] = useState<number | null>(null);
// Valid field choices
const [choices, setChoices] = useState<any[]>([]);
// Field type for "data" input
const [fieldType, setFieldType] = useState<
'string' | 'boolean' | 'choice' | 'related field'
>('string');
// Memoized value for the "data" field
const [data, setData] = useState<string>('');
const reset = useCallback(() => {
setSelectionListId(null);
setFieldType('string');
setChoices([]);
setData('');
}, []);
useEffect(() => {
reset();
}, [resetDep, reset]);
const fetchSelectionEntry = useCallback(
(value: any) => {
if (!value || !selectionListId) {
@@ -151,9 +163,7 @@ export function useParameterFields({
return api
.get(apiUrl(ApiEndpoints.selectionentry_list, selectionListId), {
params: {
value: value
}
params: { value: value }
})
.then((response) => {
if (response.data && response.data.length == 1) {
@@ -166,13 +176,102 @@ export function useParameterFields({
[selectionListId]
);
// Reset the field type and choices when the model changes
useEffect(() => {
setSelectionListId(null);
setFieldType('string');
setChoices([]);
setData('');
}, [modelType, modelId]);
const onTemplateValueChange = useCallback(
(value: any, record: any) => {
setSelectionListId(record?.selectionlist || null);
setData('');
if (record?.checkbox) {
setChoices([]);
setFieldType('boolean');
setData('false');
} else if (record?.choices) {
const _choices: string[] = record.choices.split(',');
if (_choices.length > 0) {
setChoices(
_choices.map((choice) => ({
display_name: choice.trim(),
value: choice.trim()
}))
);
setFieldType('choice');
} else {
setChoices([]);
setFieldType('string');
setData('');
}
} else if (record?.selectionlist) {
setFieldType('related field');
setData('');
} else {
setFieldType('string');
setData('');
}
},
[setFieldType, setData, setChoices]
);
const valueFieldConfig: ApiFormFieldType = useMemo(
() => ({
value: data,
onValueChange: (value: any, record: any) => {
if (fieldType === 'related field' && selectionListId) {
// For related fields, store the primary key value (not the string representation)
setData(record?.value ?? value);
} else {
setData(value);
}
},
field_type: fieldType,
choices: fieldType === 'choice' ? choices : undefined,
default: fieldType === 'boolean' ? false : undefined,
pk_field:
fieldType === 'related field' && selectionListId ? 'value' : undefined,
model:
fieldType === 'related field' && selectionListId
? ModelType.selectionentry
: undefined,
api_url:
fieldType === 'related field' && selectionListId
? apiUrl(ApiEndpoints.selectionentry_list, selectionListId)
: undefined,
filters: fieldType === 'related field' ? { active: true } : undefined,
adjustValue: (value: any) => {
let v: string = value.toString().trim();
if (fieldType === 'boolean') {
if (v.toLowerCase() !== 'true') {
v = 'false';
}
}
return v;
},
singleFetchFunction: fetchSelectionEntry
}),
[data, fieldType, choices, selectionListId, fetchSelectionEntry]
);
return { onTemplateValueChange, valueFieldConfig, reset };
}
export function useParameterFields({
modelType,
modelId
}: {
modelType: ModelType;
modelId: number;
}): ApiFormFieldSet {
const user = useUserState.getState();
const templateCreateFields = useParameterTemplateFields();
const resetKey = useMemo(
() => `${modelType}-${modelId}`,
[modelType, modelId]
);
const { onTemplateValueChange, valueFieldConfig } =
useDynamicParameterValueField(resetKey);
return useMemo(() => {
return {
@@ -189,97 +288,17 @@ export function useParameterFields({
for_model: modelType,
enabled: true
},
onValueChange: (value: any, record: any) => {
setSelectionListId(record?.selectionlist || null);
// Adjust the type of the "data" field based on the selected template
if (record?.checkbox) {
// This is a "checkbox" field
setChoices([]);
setFieldType('boolean');
setData('false');
} else if (record?.choices) {
const _choices: string[] = record.choices.split(',');
if (_choices.length > 0) {
setChoices(
_choices.map((choice) => {
return {
display_name: choice.trim(),
value: choice.trim()
};
})
);
setFieldType('choice');
} else {
setChoices([]);
setFieldType('string');
}
} else if (record?.selectionlist) {
setFieldType('related field');
} else {
// Default to a simple string field
setFieldType('string');
}
},
onValueChange: onTemplateValueChange,
addCreateFields: user.isStaff() ? templateCreateFields : undefined
},
data: {
value: data,
onValueChange: (value: any, record: any) => {
if (fieldType === 'related field' && selectionListId) {
// For related fields, we need to store the selected primary key value (not the string representation)
setData(record?.value ?? value);
} else {
setData(value);
}
},
type: fieldType,
field_type: fieldType,
choices: fieldType === 'choice' ? choices : undefined,
default: fieldType === 'boolean' ? false : undefined,
pk_field:
fieldType === 'related field' && selectionListId
? 'value'
: undefined,
model:
fieldType === 'related field' && selectionListId
? ModelType.selectionentry
: undefined,
api_url:
fieldType === 'related field' && selectionListId
? apiUrl(ApiEndpoints.selectionentry_list, selectionListId)
: undefined,
filters:
fieldType === 'related field'
? {
active: true
}
: undefined,
adjustValue: (value: any) => {
// Coerce boolean value into a string (required by backend)
let v: string = value.toString().trim();
if (fieldType === 'boolean') {
if (v.toLowerCase() !== 'true') {
v = 'false';
}
}
return v;
},
singleFetchFunction: fetchSelectionEntry
},
data: valueFieldConfig,
note: {}
};
}, [
data,
modelType,
fieldType,
choices,
modelId,
selectionListId,
onTemplateValueChange,
valueFieldConfig,
templateCreateFields,
user
]);
@@ -16,6 +16,7 @@ import type { TableFilter } from '@lib/types/Filters';
import type { ApiFormFieldSet } from '@lib/types/Forms';
import type { TableColumn } from '@lib/types/Tables';
import { IconInfoCircle } from '@tabler/icons-react';
import { useDynamicParameterValueField } from '../../forms/CommonForms';
import {
useCreateApiFormModal,
useDeleteApiFormModal,
@@ -32,16 +33,21 @@ export default function PartCategoryTemplateTable({
const table = useTable('part-category-parameter-templates');
const user = useUserState();
const { onTemplateValueChange, valueFieldConfig, reset } =
useDynamicParameterValueField(categoryId);
const formFields: ApiFormFieldSet = useMemo(() => {
return {
category: {
value: categoryId,
disabled: categoryId !== undefined
},
template: {},
default_value: {}
template: {
onValueChange: onTemplateValueChange
},
default_value: valueFieldConfig
};
}, [categoryId]);
}, [categoryId, onTemplateValueChange, valueFieldConfig]);
const [selectedTemplate, setSelectedTemplate] = useState<number>(0);
@@ -49,6 +55,7 @@ export default function PartCategoryTemplateTable({
url: ApiEndpoints.category_parameter_list,
title: t`Add Category Parameter`,
fields: useMemo(() => ({ ...formFields }), [formFields]),
onOpen: reset,
table: table
});
@@ -57,6 +64,7 @@ export default function PartCategoryTemplateTable({
pk: selectedTemplate,
title: t`Edit Category Parameter`,
fields: useMemo(() => ({ ...formFields }), [formFields]),
onOpen: reset,
table: table
});
+347 -557
View File
File diff suppressed because it is too large Load Diff