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
(cherry picked from commit ba9b5438b4)
Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
This commit is contained in:
committed by
GitHub
parent
56f09e1aa6
commit
8cbce3f335
@@ -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);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -111,9 +111,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();
|
||||||
|
|||||||
Reference in New Issue
Block a user