mirror of
https://github.com/inventree/InvenTree.git
synced 2026-11-10 02:47:49 +00:00
[UI] Fixes for "tags" field (#13233)
- Fetch appropriate 'tags' detail in the form request - Additional playwright tests
This commit is contained in:
1 parent
061981ac54
commit
3383e865cb
18 files changed
+76
-24
No files matched your search
@@ -75,6 +75,7 @@ export type ApiFormFieldHeader = {
|
||||
* @param addRow : Callback function to add a new row to a table field
|
||||
* @param headers : Optional definitions of table headers (for table fields)
|
||||
* @param singleFetchFunction : Optional function to fetch a single value for this field (used for fetching the initial value when editing an existing object)
|
||||
* @param fetchParams : Optional query parameters required on the initial data request to populate this field (e.g. optional serializer fields such as 'tags')
|
||||
*/
|
||||
export type ApiFormFieldType = {
|
||||
label?: string;
|
||||
@@ -138,6 +139,7 @@ export type ApiFormFieldType = {
|
||||
headers?: ApiFormFieldHeader[];
|
||||
depends_on?: string[];
|
||||
singleFetchFunction?: (value: any) => Promise<any> | null;
|
||||
fetchParams?: Record<string, string>;
|
||||
};
|
||||
|
||||
export type ApiFormFieldSet = Record<string, ApiFormFieldType>;
|
||||
|
||||
@@ -261,7 +261,28 @@ export function ApiForm({
|
||||
props.pathParams,
|
||||
props.queryParams
|
||||
),
|
||||
[props.url, props.pk, props.pathParams]
|
||||
[props.url, props.pk, props.pathParams, props.queryParams]
|
||||
);
|
||||
|
||||
// Collect any query parameters which individual fields require,
|
||||
// in order to populate their values from the initial data request
|
||||
// (e.g. optional serializer fields such as 'tags').
|
||||
// This ensures the form fetches the required data independent of where it is launched.
|
||||
const fetchParams: Record<string, string> = useMemo(() => {
|
||||
const params: Record<string, string> = {};
|
||||
|
||||
mapFields(props.fields ?? {}, (_path, field) => {
|
||||
Object.assign(params, field.fetchParams ?? {});
|
||||
return undefined;
|
||||
});
|
||||
|
||||
return params;
|
||||
}, [props.fields]);
|
||||
|
||||
// Stable key for the fetch params, for use in query keys
|
||||
const fetchParamsKey: string = useMemo(
|
||||
() => new URLSearchParams(fetchParams).toString(),
|
||||
[fetchParams]
|
||||
);
|
||||
|
||||
// Define function to process API response
|
||||
@@ -298,22 +319,26 @@ export function ApiForm({
|
||||
props.method,
|
||||
props.url,
|
||||
props.pk,
|
||||
props.pathParams
|
||||
props.pathParams,
|
||||
fetchParamsKey
|
||||
],
|
||||
queryFn: async () => {
|
||||
return await api.get(url).then((response: any) => {
|
||||
// Process API response
|
||||
const fetchedData: any = processFields(fields, response.data);
|
||||
return await api
|
||||
.get(url, { params: fetchParams })
|
||||
.then((response: any) => {
|
||||
// Process API response
|
||||
const fetchedData: any = processFields(fields, response.data);
|
||||
|
||||
// Update form values, but only for the fields specified for this form
|
||||
form.reset(fetchedData);
|
||||
return fetchedData;
|
||||
});
|
||||
// Update form values, but only for the fields specified for this form
|
||||
form.reset(fetchedData);
|
||||
return fetchedData;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const _fields: any = props.fields || {};
|
||||
// Shallow copy, so that per-key replacements below do not leak into props.fields
|
||||
const _fields: any = { ...(props.fields || {}) };
|
||||
const _initialData: any = props.initialData || {};
|
||||
const _fetchedData: any = initialDataQuery.data || {};
|
||||
|
||||
@@ -332,6 +357,17 @@ export function ApiForm({
|
||||
if (_fetchedData && k in _fetchedData) {
|
||||
_fields[k].value = _fetchedData[k];
|
||||
}
|
||||
|
||||
// If a field requires specific fetch params, but the server did not return a value,
|
||||
// exclude the field from the form. Otherwise, submitting an (incorrectly) empty value
|
||||
// could overwrite existing data on the server (e.g. clearing existing tags)
|
||||
if (
|
||||
_fields[k].fetchParams &&
|
||||
initialDataQuery.data &&
|
||||
_fetchedData[k] === undefined
|
||||
) {
|
||||
_fields[k] = { ..._fields[k], hidden: true, exclude: true };
|
||||
}
|
||||
}
|
||||
|
||||
// Preserve the previous reference for any field whose definition did not
|
||||
@@ -362,7 +398,8 @@ export function ApiForm({
|
||||
props.method,
|
||||
props.url,
|
||||
props.pk,
|
||||
props.pathParams
|
||||
props.pathParams,
|
||||
fetchParamsKey
|
||||
]
|
||||
});
|
||||
initialDataQuery.refetch();
|
||||
|
||||
@@ -93,6 +93,7 @@ export function ApiFormField({
|
||||
placeholderWarning: undefined,
|
||||
placeholderWarningCompare: undefined,
|
||||
singleFetchFunction: undefined,
|
||||
fetchParams: undefined,
|
||||
autoFillFilters: undefined,
|
||||
onValueChange: undefined,
|
||||
adjustFilters: undefined,
|
||||
|
||||
@@ -58,7 +58,8 @@ function TagsField({
|
||||
return {
|
||||
...definition,
|
||||
allow_null: undefined,
|
||||
allow_blank: undefined
|
||||
allow_blank: undefined,
|
||||
fetchParams: undefined
|
||||
};
|
||||
}, [definition]);
|
||||
|
||||
|
||||
@@ -35,7 +35,9 @@ export function TagsField({
|
||||
field_type: 'tags',
|
||||
label: label ?? t`Tags`,
|
||||
description: description ?? t`Tags for this item`,
|
||||
placeholder: placeholder ?? t`Select tags`
|
||||
placeholder: placeholder ?? t`Select tags`,
|
||||
// 'tags' is an optional serializer field, which must be explicitly requested
|
||||
fetchParams: { tags: 'true' }
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -413,7 +413,6 @@ export default function BuildDetail() {
|
||||
title: t`Edit Build Order`,
|
||||
modalId: 'edit-build-order',
|
||||
fields: editBuildOrderFields,
|
||||
queryParams: new URLSearchParams({ tags: 'true' }),
|
||||
onFormSuccess: refreshInstance
|
||||
});
|
||||
|
||||
|
||||
@@ -230,7 +230,6 @@ export default function CompanyDetail(props: Readonly<CompanyDetailProps>) {
|
||||
pk: company?.pk,
|
||||
title: t`Edit Company`,
|
||||
fields: useMemo(() => companyFields({}), []),
|
||||
queryParams: new URLSearchParams({ tags: 'true' }),
|
||||
onFormSuccess: refreshInstance
|
||||
});
|
||||
|
||||
|
||||
@@ -141,7 +141,6 @@ export default function ManufacturerPartDetail() {
|
||||
pk: manufacturerPart?.pk,
|
||||
title: t`Edit Manufacturer Part`,
|
||||
fields: editManufacturerPartFields,
|
||||
queryParams: new URLSearchParams({ tags: 'true' }),
|
||||
onFormSuccess: refreshInstance
|
||||
});
|
||||
|
||||
|
||||
@@ -187,7 +187,6 @@ export default function SupplierPartDetail() {
|
||||
pk: supplierPart?.pk,
|
||||
title: t`Edit Supplier Part`,
|
||||
fields: supplierPartFields,
|
||||
queryParams: new URLSearchParams({ tags: 'true' }),
|
||||
onFormSuccess: refreshInstance
|
||||
});
|
||||
|
||||
|
||||
@@ -673,7 +673,6 @@ export default function PartDetail() {
|
||||
pk: part.pk,
|
||||
title: t`Edit Part`,
|
||||
fields: partFields,
|
||||
queryParams: new URLSearchParams({ tags: 'true' }),
|
||||
onFormSuccess: refreshInstance
|
||||
});
|
||||
|
||||
|
||||
@@ -94,7 +94,6 @@ export default function PurchaseOrderDetail() {
|
||||
pk: id,
|
||||
title: t`Edit Purchase Order`,
|
||||
fields: purchaseOrderFields,
|
||||
queryParams: new URLSearchParams({ tags: 'true' }),
|
||||
onFormSuccess: () => {
|
||||
refreshInstance();
|
||||
}
|
||||
|
||||
@@ -205,7 +205,6 @@ export default function ReturnOrderDetail() {
|
||||
pk: order.pk,
|
||||
title: t`Edit Return Order`,
|
||||
fields: returnOrderFields,
|
||||
queryParams: new URLSearchParams({ tags: 'true' }),
|
||||
onFormSuccess: () => {
|
||||
refreshInstance();
|
||||
}
|
||||
|
||||
@@ -110,7 +110,6 @@ export default function SalesOrderDetail() {
|
||||
pk: order.pk,
|
||||
title: t`Edit Sales Order`,
|
||||
fields: salesOrderFields,
|
||||
queryParams: new URLSearchParams({ tags: 'true' }),
|
||||
onFormSuccess: () => {
|
||||
refreshInstance();
|
||||
}
|
||||
|
||||
@@ -140,7 +140,6 @@ export default function SalesOrderShipmentDetail() {
|
||||
pk: shipment.pk,
|
||||
fields: editShipmentFields,
|
||||
title: t`Edit Shipment`,
|
||||
queryParams: new URLSearchParams({ tags: 'true' }),
|
||||
onFormSuccess: refreshShipment
|
||||
});
|
||||
|
||||
|
||||
@@ -355,7 +355,6 @@ export default function StockDetail() {
|
||||
title: t`Edit Stock Item`,
|
||||
modalId: 'edit-stock-item',
|
||||
fields: editStockItemFields,
|
||||
queryParams: new URLSearchParams({ tags: 'true' }),
|
||||
onFormSuccess: refreshInstance
|
||||
});
|
||||
|
||||
|
||||
@@ -229,7 +229,6 @@ export default function TransferOrderDetail() {
|
||||
pk: order.pk,
|
||||
title: t`Edit Transfer Order`,
|
||||
fields: transferOrderFields,
|
||||
queryParams: new URLSearchParams({ tags: 'true' }),
|
||||
onFormSuccess: () => {
|
||||
refreshInstance();
|
||||
}
|
||||
|
||||
@@ -166,6 +166,12 @@ test('Build Order - Tags', async ({ browser }) => {
|
||||
});
|
||||
|
||||
await expect(tagsField).toBeVisible();
|
||||
|
||||
// Existing tags are pre-filled in the edit form
|
||||
const dialog = page.getByRole('dialog');
|
||||
await expect(dialog.getByText('Electronics', { exact: true })).toBeVisible();
|
||||
await expect(dialog.getByText('PCB Assembly', { exact: true })).toBeVisible();
|
||||
|
||||
await page.getByRole('button', { name: 'Cancel' }).click();
|
||||
});
|
||||
|
||||
|
||||
@@ -306,6 +306,20 @@ test('Sales Orders - Shipments - Tags', async ({ browser }) => {
|
||||
await page.getByRole('button', { name: 'apply-tags-filter' }).click();
|
||||
await page.getByRole('button', { name: 'filter-drawer-close' }).click();
|
||||
|
||||
// Edit a shipment from the table (not the detail page)
|
||||
// Existing tags must be pre-filled in the edit form (ref: #13224)
|
||||
await clickOnRowMenu(page.getByRole('cell', { name: 'SO0008' }));
|
||||
await page.getByRole('menuitem', { name: 'Edit' }).click();
|
||||
|
||||
const dialog = page.getByRole('dialog');
|
||||
await expect(
|
||||
dialog.getByRole('combobox', { name: 'tags-field-tags' })
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
dialog.getByText('Requires Payment', { exact: true })
|
||||
).toBeVisible();
|
||||
await dialog.getByRole('button', { name: 'Cancel' }).click();
|
||||
|
||||
// Click through to one of the selected shipments
|
||||
await page.getByRole('cell', { name: 'SO0007' }).click();
|
||||
await page.getByText('Sales Order: SO0007').first().waitFor();
|
||||
|
||||
Reference in new issue
Block a user