Skip to content

Commit ae16823

Browse files
[ui] Settings fix (inventree#10239)
* Enhance playwright test * Update zustand * Fix machine settings * Fix for PluginSettingList * Fix user plugin settings * Fix issue in RelatedModelField * Enforce values when rebuilding a form * Update react-hook-form * Enhanced playwright testing
1 parent ccdd6ea commit ae16823

File tree

8 files changed

+183
-131
lines changed

8 files changed

+183
-131
lines changed

src/frontend/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,15 +93,15 @@
9393
"react": "^19.1.1",
9494
"react-dom": "^19.1.1",
9595
"react-grid-layout": "1.4.4",
96-
"react-hook-form": "^7.54.2",
96+
"react-hook-form": "^7.62.0",
9797
"react-is": "^19.1.1",
9898
"react-router-dom": "^6.26.2",
9999
"react-select": "^5.9.0",
100100
"react-simplemde-editor": "^5.2.0",
101101
"react-window": "1.8.11",
102102
"recharts": "^2.15.0",
103103
"styled-components": "^6.1.14",
104-
"zustand": "^5.0.3"
104+
"zustand": "^5.0.8"
105105
},
106106
"devDependencies": {
107107
"@babel/core": "^7.26.10",

src/frontend/src/components/buttons/PrintingActions.tsx

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,12 @@ export function PrintingActions({
3434

3535
const enabled = useMemo(() => items.length > 0, [items]);
3636

37-
const [pluginKey, setPluginKey] = useState<string>('');
37+
const defaultLabelPlugin = useMemo(
38+
() => userSettings.getSetting('LABEL_DEFAULT_PRINTER'),
39+
[userSettings]
40+
);
41+
42+
const [pluginKey, setPluginKey] = useState<string | null>(null);
3843

3944
const labelPrintingEnabled = useMemo(() => {
4045
return enableLabels && globalSettings.isSet('LABEL_ENABLE');
@@ -96,7 +101,8 @@ export function PrintingActions({
96101

97102
fields['plugin'] = {
98103
...fields['plugin'],
99-
value: userSettings.getSetting('LABEL_DEFAULT_PRINTER'),
104+
default: defaultLabelPlugin,
105+
value: pluginKey,
100106
filters: {
101107
active: true,
102108
mixin: 'labels'
@@ -109,11 +115,12 @@ export function PrintingActions({
109115
};
110116

111117
return fields;
112-
}, [printingFields.data, items]);
118+
}, [defaultLabelPlugin, pluginKey, printingFields.data, items]);
113119

114120
const labelModal = useCreateApiFormModal({
115121
url: apiUrl(ApiEndpoints.label_print),
116122
title: t`Print Label`,
123+
modalId: 'print-labels',
117124
fields: labelFields,
118125
timeout: 5000,
119126
onClose: () => {
@@ -128,8 +135,9 @@ export function PrintingActions({
128135
});
129136

130137
const reportModal = useCreateApiFormModal({
131-
title: t`Print Report`,
132138
url: apiUrl(ApiEndpoints.report_print),
139+
title: t`Print Report`,
140+
modalId: 'print-reports',
133141
timeout: 5000,
134142
fields: {
135143
template: {

src/frontend/src/components/forms/fields/RelatedModelField.tsx

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,14 @@ export function RelatedModelField({
4646
// Keep track of the primary key value for this field
4747
const [pk, setPk] = useState<number | null>(null);
4848

49+
// Handle condition where the form is rebuilt dynamically
50+
useEffect(() => {
51+
const value = field.value || pk;
52+
if (value && value != form.getValues()[fieldName]) {
53+
form.setValue(fieldName, value);
54+
}
55+
}, [pk, field.value]);
56+
4957
const [offset, setOffset] = useState<number>(0);
5058

5159
const [initialData, setInitialData] = useState<{}>({});
@@ -118,12 +126,10 @@ export function RelatedModelField({
118126
// If the value is unchanged, do nothing
119127
if (field.value === pk) return;
120128

121-
if (
122-
field?.value !== null &&
123-
field?.value !== undefined &&
124-
field?.value !== ''
125-
) {
126-
const url = `${definition.api_url}${field.value}/`;
129+
const id = pk || field.value;
130+
131+
if (id !== null && id !== undefined && id !== '') {
132+
const url = `${definition.api_url}${id}/`;
127133

128134
if (!url) {
129135
setPk(null);

src/frontend/src/components/settings/SettingList.tsx

Lines changed: 52 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,14 @@ import { t } from '@lingui/core/macro';
22
import { Trans } from '@lingui/react/macro';
33
import { Alert, Skeleton, Stack, Text } from '@mantine/core';
44
import { notifications } from '@mantine/notifications';
5-
import React, {
6-
useCallback,
7-
useEffect,
8-
useMemo,
9-
useRef,
10-
useState
11-
} from 'react';
5+
import React, { useCallback, useEffect, useMemo, useState } from 'react';
126
import { useStore } from 'zustand';
137

148
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
159
import type { ModelType } from '@lib/enums/ModelType';
1610
import { apiUrl } from '@lib/functions/Api';
1711
import type { Setting, SettingsStateProps } from '@lib/types/Settings';
18-
import { IconExclamationCircle } from '@tabler/icons-react';
12+
import { IconExclamationCircle, IconInfoCircle } from '@tabler/icons-react';
1913
import { useApi } from '../../contexts/ApiContext';
2014
import { useEditApiFormModal } from '../../hooks/UseForm';
2115
import {
@@ -152,6 +146,14 @@ export function SettingList({
152146
return <Skeleton animate />;
153147
}
154148

149+
if ((keys || allKeys).length === 0) {
150+
return (
151+
<Alert color='blue' icon={<IconInfoCircle />} title={t`No Settings`}>
152+
<Text>{t`There are no configurable settings available`}</Text>
153+
</Alert>
154+
);
155+
}
156+
155157
return (
156158
<>
157159
{editSettingModal.modal}
@@ -211,13 +213,20 @@ export function PluginSettingList({
211213
pluginKey: string;
212214
onLoaded?: (settings: SettingsStateProps) => void;
213215
}>) {
214-
const pluginSettingsStore = useRef(
215-
createPluginSettingsState({
216-
plugin: pluginKey,
217-
endpoint: ApiEndpoints.plugin_setting_list
218-
})
219-
).current;
220-
const pluginSettings = useStore(pluginSettingsStore);
216+
const store = useMemo(
217+
() =>
218+
createPluginSettingsState({
219+
plugin: pluginKey,
220+
endpoint: ApiEndpoints.plugin_setting_list
221+
}),
222+
[pluginKey]
223+
);
224+
225+
const pluginSettings = useStore(store);
226+
227+
useEffect(() => {
228+
pluginSettings.fetchSettings();
229+
}, [pluginSettings.fetchSettings]);
221230

222231
return <SettingList settingsState={pluginSettings} onLoaded={onLoaded} />;
223232
}
@@ -229,13 +238,20 @@ export function PluginUserSettingList({
229238
pluginKey: string;
230239
onLoaded?: (settings: SettingsStateProps) => void;
231240
}>) {
232-
const pluginUserSettingsState = useRef(
233-
createPluginSettingsState({
234-
plugin: pluginKey,
235-
endpoint: ApiEndpoints.plugin_user_setting_list
236-
})
237-
).current;
238-
const pluginUserSettings = useStore(pluginUserSettingsState);
241+
const store = useMemo(
242+
() =>
243+
createPluginSettingsState({
244+
plugin: pluginKey,
245+
endpoint: ApiEndpoints.plugin_user_setting_list
246+
}),
247+
[pluginKey]
248+
);
249+
250+
const pluginUserSettings = useStore(store);
251+
252+
useEffect(() => {
253+
pluginUserSettings.fetchSettings();
254+
}, [pluginUserSettings.fetchSettings]);
239255

240256
return <SettingList settingsState={pluginUserSettings} onLoaded={onLoaded} />;
241257
}
@@ -249,13 +265,20 @@ export function MachineSettingList({
249265
configType: 'M' | 'D';
250266
onChange?: () => void;
251267
}>) {
252-
const machineSettingsStore = useRef(
253-
createMachineSettingsState({
254-
machine: machinePk,
255-
configType: configType
256-
})
257-
).current;
258-
const machineSettings = useStore(machineSettingsStore);
268+
const store = useMemo(
269+
() =>
270+
createMachineSettingsState({
271+
machine: machinePk,
272+
configType: configType
273+
}),
274+
[machinePk, configType]
275+
);
276+
277+
const machineSettings = useStore(store);
278+
279+
useEffect(() => {
280+
machineSettings.fetchSettings();
281+
}, [machineSettings.fetchSettings]);
259282

260283
return <SettingList settingsState={machineSettings} onChange={onChange} />;
261284
}

src/frontend/src/states/SettingsStates.tsx

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import type {
1212
SettingsLookup,
1313
SettingsStateProps
1414
} from '@lib/types/Settings';
15-
import { useEffect } from 'react';
1615
import { api } from '../App';
1716
import { useUserState } from './UserState';
1817

@@ -132,7 +131,7 @@ export const createPluginSettingsState = ({
132131
}: CreatePluginSettingStateProps) => {
133132
const pathParams: PathParams = { plugin };
134133

135-
const store = createStore<SettingsStateProps>()((set, get) => ({
134+
return createStore<SettingsStateProps>()((set, get) => ({
136135
settings: [],
137136
lookup: {},
138137
loaded: false,
@@ -188,12 +187,6 @@ export const createPluginSettingsState = ({
188187
return isTrue(value);
189188
}
190189
}));
191-
192-
useEffect(() => {
193-
store.getState().fetchSettings();
194-
}, [plugin]);
195-
196-
return store;
197190
};
198191

199192
/**
@@ -210,7 +203,7 @@ export const createMachineSettingsState = ({
210203
}: CreateMachineSettingStateProps) => {
211204
const pathParams: PathParams = { machine, config_type: configType };
212205

213-
const store = createStore<SettingsStateProps>()((set, get) => ({
206+
return createStore<SettingsStateProps>((set, get) => ({
214207
settings: [],
215208
lookup: {},
216209
loaded: false,
@@ -255,12 +248,6 @@ export const createMachineSettingsState = ({
255248
return isTrue(value);
256249
}
257250
}));
258-
259-
useEffect(() => {
260-
store.getState().fetchSettings();
261-
}, [machine, configType]);
262-
263-
return store;
264251
};
265252

266253
/*

src/frontend/src/tables/machine/MachineListTable.tsx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,10 @@ function MachineDrawer({
307307
multiple
308308
defaultValue={['machine-info', 'machine-settings', 'driver-settings']}
309309
>
310-
<Accordion.Item value='machine-info'>
310+
<Accordion.Item
311+
key={`machine-info-${machinePk}`}
312+
value='machine-info'
313+
>
311314
<Accordion.Control>
312315
<StylishText size='lg'>{t`Machine Information`}</StylishText>
313316
</Accordion.Control>
@@ -393,7 +396,10 @@ function MachineDrawer({
393396
</Accordion.Panel>
394397
</Accordion.Item>
395398
{machine?.is_driver_available && (
396-
<Accordion.Item value='machine-settings'>
399+
<Accordion.Item
400+
key={`machine-settings-${machinePk}`}
401+
value='machine-settings'
402+
>
397403
<Accordion.Control>
398404
<StylishText size='lg'>{t`Machine Settings`}</StylishText>
399405
</Accordion.Control>
@@ -409,7 +415,10 @@ function MachineDrawer({
409415
</Accordion.Item>
410416
)}
411417
{machine?.is_driver_available && (
412-
<Accordion.Item value='driver-settings'>
418+
<Accordion.Item
419+
key={`driver-settings-${machinePk}`}
420+
value='driver-settings'
421+
>
413422
<Accordion.Control>
414423
<StylishText size='lg'>{t`Driver Settings`}</StylishText>
415424
</Accordion.Control>

src/frontend/tests/pui_machines.spec.ts

Lines changed: 35 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -33,21 +33,35 @@ test('Machines - Activation', async ({ browser, request }) => {
3333
});
3434

3535
await page.reload();
36-
37-
await page.getByRole('button', { name: 'action-button-add-machine' }).click();
38-
await page
39-
.getByRole('textbox', { name: 'text-field-name' })
40-
.fill('my-dummy-machine');
41-
await page
42-
.getByRole('textbox', { name: 'choice-field-machine_type' })
43-
.fill('label');
44-
await page.getByRole('option', { name: 'Label Printer' }).click();
45-
46-
await page.getByRole('textbox', { name: 'choice-field-driver' }).click();
47-
await page
48-
.getByRole('option', { name: 'Sample Label Printer Driver' })
49-
.click();
50-
await page.getByRole('button', { name: 'Submit' }).click();
36+
await page.waitForLoadState('networkidle');
37+
await page.waitForTimeout(1000);
38+
39+
// Create machine config if it does not already exist
40+
const exists: boolean = await page
41+
.getByRole('cell', { name: 'my-dummy-machine' })
42+
.isVisible({ timeout: 250 });
43+
44+
if (!exists) {
45+
await page
46+
.getByRole('button', { name: 'action-button-add-machine' })
47+
.click();
48+
await page
49+
.getByRole('textbox', { name: 'text-field-name' })
50+
.fill('my-dummy-machine');
51+
await page
52+
.getByRole('textbox', { name: 'choice-field-machine_type' })
53+
.fill('label');
54+
await page.getByRole('option', { name: 'Label Printer' }).click();
55+
56+
await page.getByRole('textbox', { name: 'choice-field-driver' }).click();
57+
await page
58+
.getByRole('option', { name: 'Sample Label Printer Driver' })
59+
.click();
60+
await page.getByRole('button', { name: 'Submit' }).click();
61+
} else {
62+
// Machine already exists - just click on it to open the "machine drawer"
63+
await page.getByRole('cell', { name: 'my-dummy-machine' }).click();
64+
}
5165

5266
// Creating the new machine opens the "machine drawer"
5367

@@ -59,9 +73,14 @@ test('Machines - Activation', async ({ browser, request }) => {
5973

6074
// Edit the available setting
6175
await page.getByRole('button', { name: 'edit-setting-CONNECTION' }).click();
76+
77+
const setting_value = await page
78+
.getByRole('textbox', { name: 'text-field-value' })
79+
.inputValue();
80+
6281
await page
6382
.getByRole('textbox', { name: 'text-field-value' })
64-
.fill('a new value');
83+
.fill(`${setting_value}-2`);
6584
await page.getByRole('button', { name: 'Submit' }).click();
6685
await page.getByText('Setting CONNECTION updated successfully').waitFor();
6786

0 commit comments

Comments
 (0)