mirror of
https://github.com/inventree/InvenTree.git
synced 2025-06-18 04:55:44 +00:00
Merge remote-tracking branch 'upstream/master' into barcode-generation
This commit is contained in:
@ -384,21 +384,40 @@ export function ApiForm({
|
||||
let method = props.method?.toLowerCase() ?? 'get';
|
||||
|
||||
let hasFiles = false;
|
||||
mapFields(fields, (_path, field) => {
|
||||
if (field.field_type === 'file upload') {
|
||||
hasFiles = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Optionally pre-process the data before submitting it
|
||||
if (props.processFormData) {
|
||||
data = props.processFormData(data);
|
||||
}
|
||||
|
||||
let dataForm = new FormData();
|
||||
|
||||
Object.keys(data).forEach((key: string) => {
|
||||
let value: any = data[key];
|
||||
let field_type = fields[key]?.field_type;
|
||||
|
||||
if (field_type == 'file upload') {
|
||||
hasFiles = true;
|
||||
}
|
||||
|
||||
// Stringify any JSON objects
|
||||
if (typeof value === 'object') {
|
||||
switch (field_type) {
|
||||
case 'file upload':
|
||||
break;
|
||||
default:
|
||||
value = JSON.stringify(value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
dataForm.append(key, value);
|
||||
});
|
||||
|
||||
return api({
|
||||
method: method,
|
||||
url: url,
|
||||
data: data,
|
||||
data: hasFiles ? dataForm : data,
|
||||
timeout: props.timeout,
|
||||
headers: {
|
||||
'Content-Type': hasFiles ? 'multipart/form-data' : 'application/json'
|
||||
@ -462,7 +481,11 @@ export function ApiForm({
|
||||
for (const [k, v] of Object.entries(errors)) {
|
||||
const path = _path ? `${_path}.${k}` : k;
|
||||
|
||||
if (k === 'non_field_errors' || k === '__all__') {
|
||||
// Determine if field "k" is valid (exists and is visible)
|
||||
let field = fields[k];
|
||||
let valid = field && !field.hidden;
|
||||
|
||||
if (!valid || k === 'non_field_errors' || k === '__all__') {
|
||||
if (Array.isArray(v)) {
|
||||
_nonFieldErrors.push(...v);
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ import { DependentField } from './DependentField';
|
||||
import { NestedObjectField } from './NestedObjectField';
|
||||
import { RelatedModelField } from './RelatedModelField';
|
||||
import { TableField } from './TableField';
|
||||
import TextField from './TextField';
|
||||
|
||||
export type ApiFormData = UseFormReturnType<Record<string, unknown>>;
|
||||
|
||||
@ -223,21 +224,11 @@ export function ApiFormField({
|
||||
case 'url':
|
||||
case 'string':
|
||||
return (
|
||||
<TextInput
|
||||
{...reducedDefinition}
|
||||
ref={field.ref}
|
||||
id={fieldId}
|
||||
aria-label={`text-field-${field.name}`}
|
||||
type={definition.field_type}
|
||||
value={value || ''}
|
||||
error={error?.message}
|
||||
radius="sm"
|
||||
onChange={(event) => onChange(event.currentTarget.value)}
|
||||
rightSection={
|
||||
value && !definition.required ? (
|
||||
<IconX size="1rem" color="red" onClick={() => onChange('')} />
|
||||
) : null
|
||||
}
|
||||
<TextField
|
||||
definition={reducedDefinition}
|
||||
controller={controller}
|
||||
fieldName={fieldName}
|
||||
onChange={onChange}
|
||||
/>
|
||||
);
|
||||
case 'boolean':
|
||||
|
66
src/frontend/src/components/forms/fields/TextField.tsx
Normal file
66
src/frontend/src/components/forms/fields/TextField.tsx
Normal file
@ -0,0 +1,66 @@
|
||||
import { TextInput } from '@mantine/core';
|
||||
import { useDebouncedValue } from '@mantine/hooks';
|
||||
import { IconX } from '@tabler/icons-react';
|
||||
import { useCallback, useEffect, useId, useState } from 'react';
|
||||
import { FieldValues, UseControllerReturn } from 'react-hook-form';
|
||||
|
||||
/*
|
||||
* Custom implementation of the mantine <TextInput> component,
|
||||
* used for rendering text input fields in forms.
|
||||
* Uses a debounced value to prevent excessive re-renders.
|
||||
*/
|
||||
export default function TextField({
|
||||
controller,
|
||||
fieldName,
|
||||
definition,
|
||||
onChange
|
||||
}: {
|
||||
controller: UseControllerReturn<FieldValues, any>;
|
||||
definition: any;
|
||||
fieldName: string;
|
||||
onChange: (value: any) => void;
|
||||
}) {
|
||||
const fieldId = useId();
|
||||
const {
|
||||
field,
|
||||
fieldState: { error }
|
||||
} = controller;
|
||||
|
||||
const { value } = field;
|
||||
|
||||
const [rawText, setRawText] = useState(value);
|
||||
const [debouncedText] = useDebouncedValue(rawText, 250);
|
||||
|
||||
useEffect(() => {
|
||||
setRawText(value);
|
||||
}, [value]);
|
||||
|
||||
const onTextChange = useCallback((value: any) => {
|
||||
setRawText(value);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (debouncedText !== value) {
|
||||
onChange(debouncedText);
|
||||
}
|
||||
}, [debouncedText]);
|
||||
|
||||
return (
|
||||
<TextInput
|
||||
{...definition}
|
||||
ref={field.ref}
|
||||
id={fieldId}
|
||||
aria-label={`text-field-${field.name}`}
|
||||
type={definition.field_type}
|
||||
value={rawText || ''}
|
||||
error={error?.message}
|
||||
radius="sm"
|
||||
onChange={(event) => onTextChange(event.currentTarget.value)}
|
||||
rightSection={
|
||||
value && !definition.required ? (
|
||||
<IconX size="1rem" color="red" onClick={() => onTextChange('')} />
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import { t } from '@lingui/macro';
|
||||
import { Group, HoverCard, Stack, Text } from '@mantine/core';
|
||||
import { Group, HoverCard, Paper, Space, Stack, Text } from '@mantine/core';
|
||||
import { notifications } from '@mantine/notifications';
|
||||
import {
|
||||
IconArrowRight,
|
||||
@ -26,6 +26,7 @@ import { RowDeleteAction, RowEditAction } from '../../tables/RowActions';
|
||||
import { ActionButton } from '../buttons/ActionButton';
|
||||
import { YesNoButton } from '../buttons/YesNoButton';
|
||||
import { ApiFormFieldSet } from '../forms/fields/ApiFormField';
|
||||
import { ProgressBar } from '../items/ProgressBar';
|
||||
import { RenderRemoteInstance } from '../render/Instance';
|
||||
|
||||
function ImporterDataCell({
|
||||
@ -178,6 +179,8 @@ export default function ImporterDataSelector({
|
||||
table.clearSelectedRecords();
|
||||
notifications.hide('importing-rows');
|
||||
table.refreshTable();
|
||||
|
||||
session.refreshSession();
|
||||
});
|
||||
},
|
||||
[session.sessionId, table.refreshTable]
|
||||
@ -191,6 +194,7 @@ export default function ImporterDataSelector({
|
||||
title: t`Edit Data`,
|
||||
fields: selectedFields,
|
||||
initialData: selectedRow.data,
|
||||
fetchInitialData: false,
|
||||
processFormData: (data: any) => {
|
||||
// Construct fields back into a single object
|
||||
return {
|
||||
@ -374,6 +378,18 @@ export default function ImporterDataSelector({
|
||||
{editRow.modal}
|
||||
{deleteRow.modal}
|
||||
<Stack gap="xs">
|
||||
<Paper shadow="xs" p="xs">
|
||||
<Group grow justify="apart">
|
||||
<Text size="lg">{t`Processing Data`}</Text>
|
||||
<Space />
|
||||
<ProgressBar
|
||||
maximum={session.rowCount}
|
||||
value={session.completedRowCount}
|
||||
progressLabel
|
||||
/>
|
||||
<Space />
|
||||
</Group>
|
||||
</Paper>
|
||||
<InvenTreeTable
|
||||
tableState={table}
|
||||
columns={columns}
|
||||
@ -388,7 +404,10 @@ export default function ImporterDataSelector({
|
||||
enableColumnSwitching: true,
|
||||
enableColumnCaching: false,
|
||||
enableSelection: true,
|
||||
enableBulkDelete: true
|
||||
enableBulkDelete: true,
|
||||
afterBulkDelete: () => {
|
||||
session.refreshSession();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
|
@ -2,19 +2,23 @@ import { t } from '@lingui/macro';
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Divider,
|
||||
Group,
|
||||
Paper,
|
||||
Select,
|
||||
SimpleGrid,
|
||||
Space,
|
||||
Stack,
|
||||
Table,
|
||||
Text
|
||||
} from '@mantine/core';
|
||||
import { IconCheck } from '@tabler/icons-react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { api } from '../../App';
|
||||
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
||||
import { ImportSessionState } from '../../hooks/UseImportSession';
|
||||
import { apiUrl } from '../../states/ApiState';
|
||||
import { StandaloneField } from '../forms/StandaloneField';
|
||||
import { ApiFormFieldType } from '../forms/fields/ApiFormField';
|
||||
|
||||
function ImporterColumn({ column, options }: { column: any; options: any[] }) {
|
||||
const [errorMessage, setErrorMessage] = useState<string>('');
|
||||
@ -54,6 +58,7 @@ function ImporterColumn({ column, options }: { column: any; options: any[] }) {
|
||||
<Select
|
||||
error={errorMessage}
|
||||
clearable
|
||||
searchable
|
||||
placeholder={t`Select column, or leave blank to ignore this field.`}
|
||||
label={undefined}
|
||||
data={options}
|
||||
@ -63,6 +68,92 @@ function ImporterColumn({ column, options }: { column: any; options: any[] }) {
|
||||
);
|
||||
}
|
||||
|
||||
function ImporterDefaultField({
|
||||
fieldName,
|
||||
session
|
||||
}: {
|
||||
fieldName: string;
|
||||
session: ImportSessionState;
|
||||
}) {
|
||||
const onChange = useCallback(
|
||||
(value: any) => {
|
||||
// Update the default value for the field
|
||||
let defaults = {
|
||||
...session.fieldDefaults,
|
||||
[fieldName]: value
|
||||
};
|
||||
|
||||
api
|
||||
.patch(apiUrl(ApiEndpoints.import_session_list, session.sessionId), {
|
||||
field_defaults: defaults
|
||||
})
|
||||
.then((response: any) => {
|
||||
session.setSessionData(response.data);
|
||||
})
|
||||
.catch(() => {
|
||||
// TODO: Error message?
|
||||
});
|
||||
},
|
||||
[fieldName, session, session.fieldDefaults]
|
||||
);
|
||||
|
||||
const fieldDef: ApiFormFieldType = useMemo(() => {
|
||||
let def: any = session.availableFields[fieldName];
|
||||
|
||||
if (def) {
|
||||
def = {
|
||||
...def,
|
||||
value: session.fieldDefaults[fieldName],
|
||||
field_type: def.type,
|
||||
description: def.help_text,
|
||||
onValueChange: onChange
|
||||
};
|
||||
}
|
||||
|
||||
return def;
|
||||
}, [fieldName, session.availableFields, session.fieldDefaults]);
|
||||
|
||||
return (
|
||||
fieldDef && <StandaloneField fieldDefinition={fieldDef} hideLabels={true} />
|
||||
);
|
||||
}
|
||||
|
||||
function ImporterColumnTableRow({
|
||||
session,
|
||||
column,
|
||||
options
|
||||
}: {
|
||||
session: ImportSessionState;
|
||||
column: any;
|
||||
options: any;
|
||||
}) {
|
||||
return (
|
||||
<Table.Tr key={column.label ?? column.field}>
|
||||
<Table.Td>
|
||||
<Group gap="xs">
|
||||
<Text fw={column.required ? 700 : undefined}>
|
||||
{column.label ?? column.field}
|
||||
</Text>
|
||||
{column.required && (
|
||||
<Text c="red" fw={700}>
|
||||
*
|
||||
</Text>
|
||||
)}
|
||||
</Group>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Text size="sm">{column.description}</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<ImporterColumn column={column} options={options} />
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<ImporterDefaultField fieldName={column.field} session={session} />
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ImporterColumnSelector({
|
||||
session
|
||||
}: {
|
||||
@ -88,7 +179,7 @@ export default function ImporterColumnSelector({
|
||||
|
||||
const columnOptions: any[] = useMemo(() => {
|
||||
return [
|
||||
{ value: '', label: t`Select a column from the data file` },
|
||||
{ value: '', label: t`Ignore this field` },
|
||||
...session.availableColumns.map((column: any) => {
|
||||
return {
|
||||
value: column,
|
||||
@ -100,45 +191,44 @@ export default function ImporterColumnSelector({
|
||||
|
||||
return (
|
||||
<Stack gap="xs">
|
||||
<Group justify="apart">
|
||||
<Text>{t`Map data columns to database fields`}</Text>
|
||||
<Button
|
||||
color="green"
|
||||
variant="filled"
|
||||
onClick={acceptMapping}
|
||||
>{t`Accept Column Mapping`}</Button>
|
||||
</Group>
|
||||
<Paper shadow="xs" p="xs">
|
||||
<Group grow justify="apart">
|
||||
<Text size="lg">{t`Mapping data columns to database fields`}</Text>
|
||||
<Space />
|
||||
<Button color="green" variant="filled" onClick={acceptMapping}>
|
||||
<Group>
|
||||
<IconCheck />
|
||||
{t`Accept Column Mapping`}
|
||||
</Group>
|
||||
</Button>
|
||||
</Group>
|
||||
</Paper>
|
||||
{errorMessage && (
|
||||
<Alert color="red" title={t`Error`}>
|
||||
<Text>{errorMessage}</Text>
|
||||
</Alert>
|
||||
)}
|
||||
<SimpleGrid cols={3} spacing="xs">
|
||||
<Text fw={700}>{t`Database Field`}</Text>
|
||||
<Text fw={700}>{t`Field Description`}</Text>
|
||||
<Text fw={700}>{t`Imported Column Name`}</Text>
|
||||
<Divider />
|
||||
<Divider />
|
||||
<Divider />
|
||||
{session.columnMappings.map((column: any) => {
|
||||
return [
|
||||
<Group gap="xs">
|
||||
<Text fw={column.required ? 700 : undefined}>
|
||||
{column.label ?? column.field}
|
||||
</Text>
|
||||
{column.required && (
|
||||
<Text c="red" fw={700}>
|
||||
*
|
||||
</Text>
|
||||
)}
|
||||
</Group>,
|
||||
<Text size="sm" fs="italic">
|
||||
{column.description}
|
||||
</Text>,
|
||||
<ImporterColumn column={column} options={columnOptions} />
|
||||
];
|
||||
})}
|
||||
</SimpleGrid>
|
||||
<Table>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>{t`Database Field`}</Table.Th>
|
||||
<Table.Th>{t`Field Description`}</Table.Th>
|
||||
<Table.Th>{t`Imported Column`}</Table.Th>
|
||||
<Table.Th>{t`Default Value`}</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{session.columnMappings.map((column: any) => {
|
||||
return (
|
||||
<ImporterColumnTableRow
|
||||
session={session}
|
||||
column={column}
|
||||
options={columnOptions}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
@ -1,26 +1,26 @@
|
||||
import { t } from '@lingui/macro';
|
||||
import {
|
||||
ActionIcon,
|
||||
Alert,
|
||||
Button,
|
||||
Divider,
|
||||
Drawer,
|
||||
Group,
|
||||
Loader,
|
||||
LoadingOverlay,
|
||||
Paper,
|
||||
Space,
|
||||
Stack,
|
||||
Stepper,
|
||||
Text,
|
||||
Tooltip
|
||||
Text
|
||||
} from '@mantine/core';
|
||||
import { IconCircleX } from '@tabler/icons-react';
|
||||
import { ReactNode, useCallback, useMemo, useState } from 'react';
|
||||
import { IconCheck } from '@tabler/icons-react';
|
||||
import { ReactNode, useMemo } from 'react';
|
||||
|
||||
import { ModelType } from '../../enums/ModelType';
|
||||
import {
|
||||
ImportSessionStatus,
|
||||
useImportSession
|
||||
} from '../../hooks/UseImportSession';
|
||||
import { StylishText } from '../items/StylishText';
|
||||
import { StatusRenderer } from '../render/StatusRenderer';
|
||||
import ImporterDataSelector from './ImportDataSelector';
|
||||
import ImporterColumnSelector from './ImporterColumnSelector';
|
||||
import ImporterImportProgress from './ImporterImportProgress';
|
||||
@ -39,10 +39,12 @@ function ImportDrawerStepper({ currentStep }: { currentStep: number }) {
|
||||
active={currentStep}
|
||||
onStepClick={undefined}
|
||||
allowNextStepsSelect={false}
|
||||
iconSize={20}
|
||||
size="xs"
|
||||
>
|
||||
<Stepper.Step label={t`Import Data`} />
|
||||
<Stepper.Step label={t`Upload File`} />
|
||||
<Stepper.Step label={t`Map Columns`} />
|
||||
<Stepper.Step label={t`Import Data`} />
|
||||
<Stepper.Step label={t`Process Data`} />
|
||||
<Stepper.Step label={t`Complete Import`} />
|
||||
</Stepper>
|
||||
@ -60,7 +62,28 @@ export default function ImporterDrawer({
|
||||
}) {
|
||||
const session = useImportSession({ sessionId: sessionId });
|
||||
|
||||
// Map from import steps to stepper steps
|
||||
const currentStep = useMemo(() => {
|
||||
switch (session.status) {
|
||||
default:
|
||||
case ImportSessionStatus.INITIAL:
|
||||
return 0;
|
||||
case ImportSessionStatus.MAPPING:
|
||||
return 1;
|
||||
case ImportSessionStatus.IMPORTING:
|
||||
return 2;
|
||||
case ImportSessionStatus.PROCESSING:
|
||||
return 3;
|
||||
case ImportSessionStatus.COMPLETE:
|
||||
return 4;
|
||||
}
|
||||
}, [session.status]);
|
||||
|
||||
const widget = useMemo(() => {
|
||||
if (session.sessionQuery.isLoading || session.sessionQuery.isFetching) {
|
||||
return <Loader />;
|
||||
}
|
||||
|
||||
switch (session.status) {
|
||||
case ImportSessionStatus.INITIAL:
|
||||
return <Text>Initial : TODO</Text>;
|
||||
@ -71,11 +94,29 @@ export default function ImporterDrawer({
|
||||
case ImportSessionStatus.PROCESSING:
|
||||
return <ImporterDataSelector session={session} />;
|
||||
case ImportSessionStatus.COMPLETE:
|
||||
return <Text>Complete!</Text>;
|
||||
return (
|
||||
<Stack gap="xs">
|
||||
<Alert
|
||||
color="green"
|
||||
title={t`Import Complete`}
|
||||
icon={<IconCheck />}
|
||||
>
|
||||
{t`Data has been imported successfully`}
|
||||
</Alert>
|
||||
<Button color="blue" onClick={onClose}>{t`Close`}</Button>
|
||||
</Stack>
|
||||
);
|
||||
default:
|
||||
return <Text>Unknown status code: {session?.status}</Text>;
|
||||
return (
|
||||
<Stack gap="xs">
|
||||
<Alert color="red" title={t`Unknown Status`} icon={<IconCheck />}>
|
||||
{t`Import session has unknown status`}: {session.status}
|
||||
</Alert>
|
||||
<Button color="red" onClick={onClose}>{t`Close`}</Button>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
}, [session.status]);
|
||||
}, [session.status, session.sessionQuery]);
|
||||
|
||||
const title: ReactNode = useMemo(() => {
|
||||
return (
|
||||
@ -87,18 +128,11 @@ export default function ImporterDrawer({
|
||||
grow
|
||||
preventGrowOverflow={false}
|
||||
>
|
||||
<StylishText>
|
||||
<StylishText size="lg">
|
||||
{session.sessionData?.statusText ?? t`Importing Data`}
|
||||
</StylishText>
|
||||
{StatusRenderer({
|
||||
status: session.status,
|
||||
type: ModelType.importsession
|
||||
})}
|
||||
<Tooltip label={t`Cancel import session`}>
|
||||
<ActionIcon color="red" variant="transparent" onClick={onClose}>
|
||||
<IconCircleX />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<ImportDrawerStepper currentStep={currentStep} />
|
||||
<Space />
|
||||
</Group>
|
||||
<Divider />
|
||||
</Stack>
|
||||
@ -112,7 +146,7 @@ export default function ImporterDrawer({
|
||||
title={title}
|
||||
opened={opened}
|
||||
onClose={onClose}
|
||||
withCloseButton={false}
|
||||
withCloseButton={true}
|
||||
closeOnEscape={false}
|
||||
closeOnClickOutside={false}
|
||||
styles={{
|
||||
|
@ -134,7 +134,11 @@ export function RenderRemoteInstance({
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return <Text>${pk}</Text>;
|
||||
return (
|
||||
<Text>
|
||||
{model}: {pk}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
return <RenderInstance model={model} instance={data} />;
|
||||
|
@ -4,8 +4,13 @@ export function dataImporterSessionFields(): ApiFormFieldSet {
|
||||
return {
|
||||
data_file: {},
|
||||
model_type: {},
|
||||
field_detauls: {
|
||||
hidden: true
|
||||
field_defaults: {
|
||||
hidden: true,
|
||||
value: {}
|
||||
},
|
||||
field_overrides: {
|
||||
hidden: true,
|
||||
value: {}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ export enum ImportSessionStatus {
|
||||
export type ImportSessionState = {
|
||||
sessionId: number;
|
||||
sessionData: any;
|
||||
setSessionData: (data: any) => void;
|
||||
refreshSession: () => void;
|
||||
sessionQuery: any;
|
||||
status: ImportSessionStatus;
|
||||
@ -28,6 +29,10 @@ export type ImportSessionState = {
|
||||
availableColumns: string[];
|
||||
mappedFields: any[];
|
||||
columnMappings: any[];
|
||||
fieldDefaults: any;
|
||||
fieldOverrides: any;
|
||||
rowCount: number;
|
||||
completedRowCount: number;
|
||||
};
|
||||
|
||||
export function useImportSession({
|
||||
@ -38,6 +43,7 @@ export function useImportSession({
|
||||
// Query manager for the import session
|
||||
const {
|
||||
instance: sessionData,
|
||||
setInstance,
|
||||
refreshInstance: refreshSession,
|
||||
instanceQuery: sessionQuery
|
||||
} = useInstance({
|
||||
@ -46,6 +52,12 @@ export function useImportSession({
|
||||
defaultValue: {}
|
||||
});
|
||||
|
||||
const setSessionData = useCallback((data: any) => {
|
||||
console.log('setting session data:');
|
||||
console.log(data);
|
||||
setInstance(data);
|
||||
}, []);
|
||||
|
||||
// Current step of the import process
|
||||
const status: ImportSessionStatus = useMemo(() => {
|
||||
return sessionData?.status ?? ImportSessionStatus.INITIAL;
|
||||
@ -93,8 +105,25 @@ export function useImportSession({
|
||||
);
|
||||
}, [sessionData]);
|
||||
|
||||
const fieldDefaults: any = useMemo(() => {
|
||||
return sessionData?.field_defaults ?? {};
|
||||
}, [sessionData]);
|
||||
|
||||
const fieldOverrides: any = useMemo(() => {
|
||||
return sessionData?.field_overrides ?? {};
|
||||
}, [sessionData]);
|
||||
|
||||
const rowCount: number = useMemo(() => {
|
||||
return sessionData?.row_count ?? 0;
|
||||
}, [sessionData]);
|
||||
|
||||
const completedRowCount: number = useMemo(() => {
|
||||
return sessionData?.completed_row_count ?? 0;
|
||||
}, [sessionData]);
|
||||
|
||||
return {
|
||||
sessionData,
|
||||
setSessionData,
|
||||
sessionId,
|
||||
refreshSession,
|
||||
sessionQuery,
|
||||
@ -102,6 +131,10 @@ export function useImportSession({
|
||||
availableFields,
|
||||
availableColumns,
|
||||
columnMappings,
|
||||
mappedFields
|
||||
mappedFields,
|
||||
fieldDefaults,
|
||||
fieldOverrides,
|
||||
rowCount,
|
||||
completedRowCount
|
||||
};
|
||||
}
|
||||
|
@ -93,5 +93,11 @@ export function useInstance<T = any>({
|
||||
instanceQuery.refetch();
|
||||
}, []);
|
||||
|
||||
return { instance, refreshInstance, instanceQuery, requestStatus };
|
||||
return {
|
||||
instance,
|
||||
setInstance,
|
||||
refreshInstance,
|
||||
instanceQuery,
|
||||
requestStatus
|
||||
};
|
||||
}
|
||||
|
@ -57,7 +57,6 @@ import {
|
||||
useEditApiFormModal
|
||||
} from '../../hooks/UseForm';
|
||||
import { useInstance } from '../../hooks/UseInstance';
|
||||
import { apiUrl } from '../../states/ApiState';
|
||||
import { useUserState } from '../../states/UserState';
|
||||
import { AttachmentTable } from '../../tables/general/AttachmentTable';
|
||||
import InstalledItemsTable from '../../tables/stock/InstalledItemsTable';
|
||||
|
@ -103,6 +103,7 @@ export type InvenTreeTableProps<T = any> = {
|
||||
enableColumnCaching?: boolean;
|
||||
enableLabels?: boolean;
|
||||
enableReports?: boolean;
|
||||
afterBulkDelete?: () => void;
|
||||
pageSize?: number;
|
||||
barcodeActions?: React.ReactNode[];
|
||||
tableFilters?: TableFilter[];
|
||||
@ -547,6 +548,9 @@ export function InvenTreeTable<T = any>({
|
||||
})
|
||||
.finally(() => {
|
||||
tableState.clearSelectedRecords();
|
||||
if (props.afterBulkDelete) {
|
||||
props.afterBulkDelete();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -4,6 +4,7 @@ import { showNotification } from '@mantine/notifications';
|
||||
import {
|
||||
IconArrowRight,
|
||||
IconCircleCheck,
|
||||
IconFileArrowLeft,
|
||||
IconLock,
|
||||
IconSwitch3
|
||||
} from '@tabler/icons-react';
|
||||
@ -15,11 +16,13 @@ import { ActionButton } from '../../components/buttons/ActionButton';
|
||||
import { AddItemButton } from '../../components/buttons/AddItemButton';
|
||||
import { YesNoButton } from '../../components/buttons/YesNoButton';
|
||||
import { Thumbnail } from '../../components/images/Thumbnail';
|
||||
import ImporterDrawer from '../../components/importer/ImporterDrawer';
|
||||
import { formatDecimal, formatPriceRange } from '../../defaults/formatters';
|
||||
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
||||
import { ModelType } from '../../enums/ModelType';
|
||||
import { UserRoles } from '../../enums/Roles';
|
||||
import { bomItemFields } from '../../forms/BomForms';
|
||||
import { dataImporterSessionFields } from '../../forms/ImporterForms';
|
||||
import {
|
||||
useApiFormModal,
|
||||
useCreateApiFormModal,
|
||||
@ -70,6 +73,12 @@ export function BomTable({
|
||||
const table = useTable('bom');
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [importOpened, setImportOpened] = useState<boolean>(false);
|
||||
|
||||
const [selectedSession, setSelectedSession] = useState<number | undefined>(
|
||||
undefined
|
||||
);
|
||||
|
||||
const tableColumns: TableColumn[] = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
@ -345,6 +354,29 @@ export function BomTable({
|
||||
|
||||
const [selectedBomItem, setSelectedBomItem] = useState<number>(0);
|
||||
|
||||
const importSessionFields = useMemo(() => {
|
||||
let fields = dataImporterSessionFields();
|
||||
|
||||
fields.model_type.hidden = true;
|
||||
fields.model_type.value = 'bomitem';
|
||||
|
||||
fields.field_overrides.value = {
|
||||
part: partId
|
||||
};
|
||||
|
||||
return fields;
|
||||
}, [partId]);
|
||||
|
||||
const importBomItem = useCreateApiFormModal({
|
||||
url: ApiEndpoints.import_session_list,
|
||||
title: t`Import BOM Data`,
|
||||
fields: importSessionFields,
|
||||
onFormSuccess: (response: any) => {
|
||||
setSelectedSession(response.pk);
|
||||
setImportOpened(true);
|
||||
}
|
||||
});
|
||||
|
||||
const newBomItem = useCreateApiFormModal({
|
||||
url: ApiEndpoints.bom_list,
|
||||
title: t`Add BOM Item`,
|
||||
@ -467,6 +499,12 @@ export function BomTable({
|
||||
|
||||
const tableActions = useMemo(() => {
|
||||
return [
|
||||
<ActionButton
|
||||
hidden={partLocked || !user.hasAddRole(UserRoles.part)}
|
||||
tooltip={t`Import BOM Data`}
|
||||
icon={<IconFileArrowLeft />}
|
||||
onClick={() => importBomItem.open()}
|
||||
/>,
|
||||
<ActionButton
|
||||
hidden={partLocked || !user.hasChangeRole(UserRoles.part)}
|
||||
tooltip={t`Validate BOM`}
|
||||
@ -483,6 +521,7 @@ export function BomTable({
|
||||
|
||||
return (
|
||||
<>
|
||||
{importBomItem.modal}
|
||||
{newBomItem.modal}
|
||||
{editBomItem.modal}
|
||||
{validateBom.modal}
|
||||
@ -515,10 +554,20 @@ export function BomTable({
|
||||
modelField: 'sub_part',
|
||||
rowActions: rowActions,
|
||||
enableSelection: !partLocked,
|
||||
enableBulkDelete: !partLocked
|
||||
enableBulkDelete: !partLocked,
|
||||
enableDownload: true
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
<ImporterDrawer
|
||||
sessionId={selectedSession ?? -1}
|
||||
opened={selectedSession !== undefined && importOpened}
|
||||
onClose={() => {
|
||||
setSelectedSession(undefined);
|
||||
setImportOpened(false);
|
||||
table.refreshTable();
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ import { t } from '@lingui/macro';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
||||
import { ModelType } from '../../enums/ModelType';
|
||||
import { UserRoles } from '../../enums/Roles';
|
||||
import {
|
||||
useDeleteApiFormModal,
|
||||
@ -58,6 +59,13 @@ export default function BuildAllocatedStockTable({
|
||||
sortable: true,
|
||||
switchable: false
|
||||
},
|
||||
{
|
||||
accessor: 'serial',
|
||||
title: t`Serial Number`,
|
||||
sortable: false,
|
||||
switchable: true,
|
||||
render: (record: any) => record?.stock_item_detail?.serial
|
||||
},
|
||||
{
|
||||
accessor: 'batch',
|
||||
title: t`Batch Code`,
|
||||
@ -150,7 +158,9 @@ export default function BuildAllocatedStockTable({
|
||||
enableDownload: true,
|
||||
enableSelection: true,
|
||||
rowActions: rowActions,
|
||||
tableFilters: tableFilters
|
||||
tableFilters: tableFilters,
|
||||
modelField: 'stock_item',
|
||||
modelType: ModelType.stockitem
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
|
@ -19,10 +19,10 @@ test('PUI - Pages - Build Order', async ({ page }) => {
|
||||
await page.getByRole('tab', { name: 'Allocated Stock' }).click();
|
||||
|
||||
// Check for expected text in the table
|
||||
await page.getByText('R_10R_0402_1%').click();
|
||||
await page.getByText('R_10R_0402_1%').waitFor();
|
||||
await page
|
||||
.getByRole('cell', { name: 'R38, R39, R40, R41, R42, R43' })
|
||||
.click();
|
||||
.waitFor();
|
||||
|
||||
// Click through to the "parent" build
|
||||
await page.getByRole('tab', { name: 'Build Details' }).click();
|
||||
|
@ -180,6 +180,10 @@ test('PUI - Pages - Part - Attachments', async ({ page }) => {
|
||||
await page.getByLabel('action-button-add-external-').click();
|
||||
await page.getByLabel('text-field-link').fill('https://www.google.com');
|
||||
await page.getByLabel('text-field-comment').fill('a sample comment');
|
||||
|
||||
// Note: Text field values are debounced for 250ms
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
await page.getByRole('button', { name: 'Submit' }).click();
|
||||
await page.getByRole('cell', { name: 'a sample comment' }).first().waitFor();
|
||||
|
||||
|
Reference in New Issue
Block a user