2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-11-14 20:06:44 +00:00
* Remove debouncing from text field

* Add debounce to data import field

* Only apply for strings values

* Fix unit test

* More unit test tweaks
This commit is contained in:
Oliver
2025-11-14 17:35:59 +11:00
committed by GitHub
parent 8cb808f613
commit ba9b5438b4
4 changed files with 43 additions and 25 deletions

View File

@@ -1,6 +1,5 @@
import { t } from '@lingui/core/macro'; import { t } from '@lingui/core/macro';
import { TextInput, Tooltip } from '@mantine/core'; import { TextInput, Tooltip } from '@mantine/core';
import { useDebouncedValue } from '@mantine/hooks';
import { IconCopyCheck, IconX } from '@tabler/icons-react'; import { IconCopyCheck, IconX } from '@tabler/icons-react';
import { import {
type ReactNode, type ReactNode,
@@ -40,30 +39,26 @@ export default function TextField({
const { value } = useMemo(() => field, [field]); const { value } = useMemo(() => field, [field]);
const [rawText, setRawText] = useState<string>(value || ''); const [textValue, setTextValue] = useState<string>(value || '');
const [debouncedText] = useDebouncedValue(rawText, 100); const onTextChange = useCallback(
(value: any) => {
setTextValue(value);
onChange(value);
},
[onChange]
);
useEffect(() => { useEffect(() => {
setRawText(value || ''); setTextValue(value || '');
}, [value]); }, [value]);
const onTextChange = useCallback((value: any) => {
setRawText(value);
}, []);
useEffect(() => {
if (debouncedText !== value) {
onChange(debouncedText);
}
}, [debouncedText]);
// Construct a "right section" for the text field // Construct a "right section" for the text field
const textFieldRightSection: ReactNode = useMemo(() => { const textFieldRightSection: ReactNode = useMemo(() => {
if (definition.rightSection) { if (definition.rightSection) {
// Use the specified override value // Use the specified override value
return definition.rightSection; return definition.rightSection;
} else if (value) { } else if (textValue) {
if (!definition.required && !definition.disabled) { if (!definition.required && !definition.disabled) {
// Render a button to clear the text field // Render a button to clear the text field
return ( return (
@@ -78,7 +73,7 @@ export default function TextField({
); );
} }
} else if ( } else if (
!value && !textValue &&
definition.placeholder && definition.placeholder &&
placeholderAutofill && placeholderAutofill &&
!definition.disabled !definition.disabled
@@ -94,7 +89,7 @@ export default function TextField({
</Tooltip> </Tooltip>
); );
} }
}, [placeholderAutofill, definition, value]); }, [placeholderAutofill, definition, textValue]);
return ( return (
<TextInput <TextInput
@@ -103,19 +98,19 @@ export default function TextField({
id={fieldId} id={fieldId}
aria-label={`text-field-${field.name}`} aria-label={`text-field-${field.name}`}
type={definition.field_type} type={definition.field_type}
value={rawText || ''} value={textValue || ''}
error={definition.error ?? error?.message} error={definition.error ?? error?.message}
radius='sm' radius='sm'
onChange={(event) => onTextChange(event.currentTarget.value)} onChange={(event) => onTextChange(event.currentTarget.value)}
onBlur={(event) => { onBlur={(event) => {
if (event.currentTarget.value != value) { if (event.currentTarget.value != textValue) {
onChange(event.currentTarget.value); onTextChange(event.currentTarget.value);
} }
}} }}
onKeyDown={(event) => { onKeyDown={(event) => {
if (event.code === 'Enter') { if (event.code === 'Enter') {
// Bypass debounce on enter key // Bypass debounce on enter key
onChange(event.currentTarget.value); onTextChange(event.currentTarget.value);
} }
onKeyDown(event.code); onKeyDown(event.code);
}} }}

View File

@@ -16,6 +16,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
import { ApiEndpoints } from '@lib/enums/ApiEndpoints'; import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
import { apiUrl } from '@lib/functions/Api'; import { apiUrl } from '@lib/functions/Api';
import type { ApiFormFieldType } from '@lib/types/Forms'; import type { ApiFormFieldType } from '@lib/types/Forms';
import { useDebouncedValue } from '@mantine/hooks';
import { useApi } from '../../contexts/ApiContext'; import { useApi } from '../../contexts/ApiContext';
import type { ImportSessionState } from '../../hooks/UseImportSession'; import type { ImportSessionState } from '../../hooks/UseImportSession';
import { StandaloneField } from '../forms/StandaloneField'; import { StandaloneField } from '../forms/StandaloneField';
@@ -83,6 +84,14 @@ function ImporterDefaultField({
}) { }) {
const api = useApi(); const api = useApi();
const [rawValue, setRawValue] = useState<any>('');
const fieldType: string = useMemo(() => {
return session.availableFields[fieldName]?.type;
}, [fieldName, session.availableFields]);
const [value] = useDebouncedValue(rawValue, fieldType == 'string' ? 500 : 10);
const onChange = useCallback( const onChange = useCallback(
(value: any) => { (value: any) => {
// Update the default value for the field // Update the default value for the field
@@ -105,6 +114,11 @@ function ImporterDefaultField({
[fieldName, session, session.fieldDefaults] [fieldName, session, session.fieldDefaults]
); );
// Update the default value after the debounced value changes
useEffect(() => {
onChange(value);
}, [value]);
const fieldDef: ApiFormFieldType = useMemo(() => { const fieldDef: ApiFormFieldType = useMemo(() => {
let def: any = session.availableFields[fieldName]; let def: any = session.availableFields[fieldName];
@@ -114,7 +128,10 @@ function ImporterDefaultField({
value: session.fieldDefaults[fieldName], value: session.fieldDefaults[fieldName],
field_type: def.type, field_type: def.type,
description: def.help_text, description: def.help_text,
onValueChange: onChange required: false,
onValueChange: (value: string) => {
setRawValue(value);
}
}; };
} }

View File

@@ -98,7 +98,7 @@ test('Forms - Supplier Validation', async ({ browser }) => {
// Check for validation errors // Check for validation errors
await page.getByText('Form Error').waitFor(); await page.getByText('Form Error').waitFor();
await page.getByText('Errors exist for one or more').waitFor(); await page.getByText('Errors exist for one or more').waitFor();
await page.getByText('This field may not be blank.').waitFor(); await page.getByText('This field is required').waitFor();
await page.getByText('Enter a valid URL.').waitFor(); await page.getByText('Enter a valid URL.').waitFor();
// Fill out another field, expect that the errors persist // Fill out another field, expect that the errors persist
@@ -106,7 +106,7 @@ test('Forms - Supplier Validation', async ({ browser }) => {
.getByLabel('text-field-description', { exact: true }) .getByLabel('text-field-description', { exact: true })
.fill('A description'); .fill('A description');
await page.waitForTimeout(250); await page.waitForTimeout(250);
await page.getByText('This field may not be blank.').waitFor(); await page.getByText('This field is required').waitFor();
await page.getByText('Enter a valid URL.').waitFor(); await page.getByText('Enter a valid URL.').waitFor();
// Generate a unique supplier name // Generate a unique supplier name

View File

@@ -116,9 +116,15 @@ test('Importing - BOM', async ({ browser }) => {
// Delete selected rows // Delete selected rows
await page await page
.getByRole('dialog', { name: 'Importing Data Upload File 2' }) .getByRole('dialog', { name: 'Importing Data Upload File' })
.getByLabel('action-button-delete-selected')
.waitFor();
await page.waitForTimeout(200);
await page
.getByRole('dialog', { name: 'Importing Data Upload File' })
.getByLabel('action-button-delete-selected') .getByLabel('action-button-delete-selected')
.click(); .click();
await page.getByRole('button', { name: 'Delete', exact: true }).click(); await page.getByRole('button', { name: 'Delete', exact: true }).click();
await page.getByText('Success', { exact: true }).waitFor(); await page.getByText('Success', { exact: true }).waitFor();