mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
* Quick attempt at fixed form footer * slightly improve on lower res devices * Squashed commit of the following: commit06c7ebfc21
Author: Oliver <oliver.henry.walters@gmail.com> Date: Sat Mar 16 09:11:57 2024 +1100 Update docker_install.md (#6723) * Update docker_install.md Add note about external access * Update docker_install.md commita00d5ab4b5
Author: Oliver <oliver.henry.walters@gmail.com> Date: Fri Mar 15 17:53:58 2024 +1100 Disable BOM requirement (#6719) * Add new setting STOCK_ENFORCE_BOM_INSTALLATION - Defaults to True (legacy) * Add logic to bypass BOM check * Update CUI to reflect new logic * Render InstalledItemsTable in PUI commit160d014e44
Author: Oliver <oliver.henry.walters@gmail.com> Date: Fri Mar 15 17:12:53 2024 +1100 [PUI] Details Pages (#6718) * Add "details" view to SupplierPart page * Fix PartActions * Add placeholder for actions * Add "title" option to DetailsTable * Add edit form to supplier part page * Fix link to manufacturer part * Add "details" view to ManufacturerPartDetail page * Add edit for ManufacturerPart * Create new manufacturer part from company table * Tweak ActionIcon commit57a1a81e9b
Author: Oliver <oliver.henry.walters@gmail.com> Date: Fri Mar 15 12:24:17 2024 +1100 Reporting: Build line label fix (#6717) * Fix "BuildLine" label in PUI - Point to "buildline" not "build" * Prevent escape closing template ediror * Update report docs * Fix for format_number - Prevent number from being represented as scientific notation commit0196dd2f60
Author: Lavissa <lavissawow@gmail.com> Date: Fri Mar 15 02:06:18 2024 +0100 [PUI/Feature] Integrate Part "Default Location" into UX (#5972) * Add default parts to location page * Fix name strings * Add Stock Transfer modal * Add ApiForm Table field * temp * Add stock transfer form to part, stock item and location * All stock operations for Item, Part, and Location added (except order new) * Add default_location category traversal, and initial PO Line Item Receive form * . * Remove debug values * Added PO line receive form * Add functionality to PO receive extra fields * . * Forgot to bump API version * Add Category Default to details panel * Fix stockItem query count * Fix reviewed issues * . * . * . * Prevent root category from checking parent for default location commit6abd33f060
Author: Oliver <oliver.henry.walters@gmail.com> Date: Fri Mar 15 00:24:48 2024 +1100 Report enhancements (#6714) * Add "enabled" filter to template table * Cleanup * API endpoints - Add API endpoints for report snippet - List endpoint - Details endpoint * Update serializers - Add asset serializer - Update * Check for duplicate asset files - Prevent upload of duplicate asset files - Allow re-upload for same PK * Duplicate checks for ReportSnippet * Bump API version commitcbd94fc4b5
Author: Oliver <oliver.henry.walters@gmail.com> Date: Thu Mar 14 23:06:11 2024 +1100 Fix for caddyfile (#6712) - Add "authorization" to Access-Control-Allow-Headers - CORS requests actually *work* now commitec5ff6408d
Author: Lukas <76838159+wolflu05@users.noreply.github.com> Date: Thu Mar 14 13:03:30 2024 +0100 handle report previewing errors (#6709) commit267ff67f05
Author: Oliver <oliver.henry.walters@gmail.com> Date: Thu Mar 14 15:11:27 2024 +1100 [PUI] Updates (#6707) * Add button to edit part category * Fix useMemo() * Edit stock location commit610ea7b0b1
Author: Oliver <oliver.henry.walters@gmail.com> Date: Thu Mar 14 12:09:14 2024 +1100 Report: Add date rendering (#6706) * Validate timezone in settings.py * Add helper functions for timezone information - Extract server timezone - Convert provided time to specified timezone * Add more unit tests * Remove debug print * Test fix * Add report helper tags - format_date - format_datetime - Update report templates - Unit tests * Add setting to control report errors - Only log errors to DB if setting is enabled * Update example report * Fixes for to_local_time * Update type hinting * Fix unit test typo commit7de87383b5
Author: Oliver <oliver.henry.walters@gmail.com> Date: Wed Mar 13 21:37:56 2024 +1100 Update .env (#6700) Fix comment - no need to change Caddyfile in most cases commit2fef34852c
Author: Oliver <oliver.henry.walters@gmail.com> Date: Wed Mar 13 20:37:05 2024 +1100 Unit tests for HOST settings (#6698) - CORS - ALLOWED_HOSTS * Make ApiForms shrinkable, spoiler long group list * Improve API Form Scroll Behavior * Fix incorrect modal component * Force load all modal fields to trigger loading animation * Show loading overlay while fetching fields
This commit is contained in:
parent
5de56f5cd8
commit
97ec4d00ef
@ -2,15 +2,15 @@ import { t } from '@lingui/macro';
|
||||
import {
|
||||
Alert,
|
||||
DefaultMantineColor,
|
||||
Divider,
|
||||
LoadingOverlay,
|
||||
Paper,
|
||||
Text
|
||||
} from '@mantine/core';
|
||||
import { Button, Group, Stack } from '@mantine/core';
|
||||
import { useId } from '@mantine/hooks';
|
||||
import { notifications } from '@mantine/notifications';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
FieldValues,
|
||||
@ -139,6 +139,12 @@ export function OptionsApiForm({
|
||||
const formProps: ApiFormProps = useMemo(() => {
|
||||
const _props = { ...props };
|
||||
|
||||
// This forcefully overrides initial data
|
||||
// Currently, most modals do not get pre-loaded correctly
|
||||
if (!data) {
|
||||
_props.fields = undefined;
|
||||
}
|
||||
|
||||
if (!_props.fields) return _props;
|
||||
|
||||
for (const [k, v] of Object.entries(_props.fields)) {
|
||||
@ -158,10 +164,6 @@ export function OptionsApiForm({
|
||||
return _props;
|
||||
}, [data, props]);
|
||||
|
||||
if (!data) {
|
||||
return <LoadingOverlay visible={true} />;
|
||||
}
|
||||
|
||||
return <ApiForm id={id} props={formProps} />;
|
||||
}
|
||||
|
||||
@ -222,41 +224,46 @@ export function ApiForm({ id, props }: { id: string; props: ApiFormProps }) {
|
||||
props.pathParams
|
||||
],
|
||||
queryFn: async () => {
|
||||
return api
|
||||
.get(url)
|
||||
.then((response) => {
|
||||
const processFields = (fields: ApiFormFieldSet, data: NestedDict) => {
|
||||
const res: NestedDict = {};
|
||||
try {
|
||||
// Await API call
|
||||
let response = await api.get(url);
|
||||
// Define function to process API response
|
||||
const processFields = (fields: ApiFormFieldSet, data: NestedDict) => {
|
||||
const res: NestedDict = {};
|
||||
|
||||
for (const [k, field] of Object.entries(fields)) {
|
||||
const dataValue = data[k];
|
||||
// TODO: replace with .map()
|
||||
for (const [k, field] of Object.entries(fields)) {
|
||||
const dataValue = data[k];
|
||||
|
||||
if (
|
||||
field.field_type === 'nested object' &&
|
||||
field.children &&
|
||||
typeof dataValue === 'object'
|
||||
) {
|
||||
res[k] = processFields(field.children, dataValue);
|
||||
} else {
|
||||
res[k] = dataValue;
|
||||
}
|
||||
if (
|
||||
field.field_type === 'nested object' &&
|
||||
field.children &&
|
||||
typeof dataValue === 'object'
|
||||
) {
|
||||
res[k] = processFields(field.children, dataValue);
|
||||
} else {
|
||||
res[k] = dataValue;
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
const initialData: any = processFields(
|
||||
props.fields ?? {},
|
||||
response.data
|
||||
);
|
||||
return res;
|
||||
};
|
||||
|
||||
// Update form values, but only for the fields specified for this form
|
||||
form.reset(initialData);
|
||||
// Process API response
|
||||
const initialData: any = processFields(
|
||||
props.fields ?? {},
|
||||
response.data
|
||||
);
|
||||
|
||||
return response;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error fetching initial data:', error);
|
||||
});
|
||||
// Update form values, but only for the fields specified for this form
|
||||
form.reset(initialData);
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('Error fetching initial data:', error);
|
||||
// Re-throw error to allow react-query to handle error
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -377,8 +384,12 @@ export function ApiForm({ id, props }: { id: string; props: ApiFormProps }) {
|
||||
};
|
||||
|
||||
const isLoading = useMemo(
|
||||
() => isFormLoading || initialDataQuery.isFetching || isSubmitting,
|
||||
[isFormLoading, initialDataQuery.isFetching, isSubmitting]
|
||||
() =>
|
||||
isFormLoading ||
|
||||
initialDataQuery.isFetching ||
|
||||
isSubmitting ||
|
||||
!props.fields,
|
||||
[isFormLoading, initialDataQuery.isFetching, isSubmitting, props.fields]
|
||||
);
|
||||
|
||||
const onFormError = useCallback<SubmitErrorHandler<FieldValues>>(() => {
|
||||
@ -387,67 +398,81 @@ export function ApiForm({ id, props }: { id: string; props: ApiFormProps }) {
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Stack spacing="sm">
|
||||
<LoadingOverlay visible={isLoading} />
|
||||
{(!isValid || nonFieldErrors.length > 0) && (
|
||||
<Alert radius="sm" color="red" title={t`Form Errors Exist`}>
|
||||
{nonFieldErrors.length > 0 && (
|
||||
<Stack spacing="xs">
|
||||
{nonFieldErrors.map((message) => (
|
||||
<Text key={message}>{message}</Text>
|
||||
))}
|
||||
</Stack>
|
||||
{/* Show loading overlay while fetching fields */}
|
||||
{/* zIndex used to force overlay on top of modal header bar */}
|
||||
<LoadingOverlay visible={isLoading} zIndex={1010} />
|
||||
|
||||
{/* Attempt at making fixed footer with scroll area */}
|
||||
<Paper mah={'65vh'} style={{ overflowY: 'auto' }}>
|
||||
<div>
|
||||
{/* Form Fields */}
|
||||
<Stack spacing="sm">
|
||||
{(!isValid || nonFieldErrors.length > 0) && (
|
||||
<Alert radius="sm" color="red" title={t`Form Errors Exist`}>
|
||||
{nonFieldErrors.length > 0 && (
|
||||
<Stack spacing="xs">
|
||||
{nonFieldErrors.map((message) => (
|
||||
<Text key={message}>{message}</Text>
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
</Alert>
|
||||
)}
|
||||
</Alert>
|
||||
)}
|
||||
{props.preFormContent}
|
||||
{props.preFormSuccess && (
|
||||
<Alert color="green" radius="sm">
|
||||
{props.preFormSuccess}
|
||||
</Alert>
|
||||
)}
|
||||
{props.preFormWarning && (
|
||||
<Alert color="orange" radius="sm">
|
||||
{props.preFormWarning}
|
||||
</Alert>
|
||||
)}
|
||||
<FormProvider {...form}>
|
||||
<Stack spacing="xs">
|
||||
{Object.entries(props.fields ?? {}).map(([fieldName, field]) => (
|
||||
<ApiFormField
|
||||
key={fieldName}
|
||||
fieldName={fieldName}
|
||||
definition={field}
|
||||
control={form.control}
|
||||
/>
|
||||
))}
|
||||
{props.preFormContent}
|
||||
{props.preFormSuccess && (
|
||||
<Alert color="green" radius="sm">
|
||||
{props.preFormSuccess}
|
||||
</Alert>
|
||||
)}
|
||||
{props.preFormWarning && (
|
||||
<Alert color="orange" radius="sm">
|
||||
{props.preFormWarning}
|
||||
</Alert>
|
||||
)}
|
||||
<FormProvider {...form}>
|
||||
<Stack spacing="xs">
|
||||
{Object.entries(props.fields ?? {}).map(
|
||||
([fieldName, field]) => (
|
||||
<ApiFormField
|
||||
key={fieldName}
|
||||
fieldName={fieldName}
|
||||
definition={field}
|
||||
control={form.control}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</Stack>
|
||||
</FormProvider>
|
||||
{props.postFormContent}
|
||||
</Stack>
|
||||
</FormProvider>
|
||||
{props.postFormContent}
|
||||
</Stack>
|
||||
<Divider />
|
||||
<Group position="right">
|
||||
{props.actions?.map((action, i) => (
|
||||
</div>
|
||||
</Paper>
|
||||
|
||||
{/* Footer with Action Buttons */}
|
||||
<div>
|
||||
<Group position="right">
|
||||
{props.actions?.map((action, i) => (
|
||||
<Button
|
||||
key={i}
|
||||
onClick={action.onClick}
|
||||
variant={action.variant ?? 'outline'}
|
||||
radius="sm"
|
||||
color={action.color}
|
||||
>
|
||||
{action.text}
|
||||
</Button>
|
||||
))}
|
||||
<Button
|
||||
key={i}
|
||||
onClick={action.onClick}
|
||||
variant={action.variant ?? 'outline'}
|
||||
onClick={form.handleSubmit(submitForm, onFormError)}
|
||||
variant="filled"
|
||||
radius="sm"
|
||||
color={action.color}
|
||||
color={props.submitColor ?? 'green'}
|
||||
disabled={isLoading || (props.fetchInitialData && !isDirty)}
|
||||
>
|
||||
{action.text}
|
||||
{props.submitText ?? t`Submit`}
|
||||
</Button>
|
||||
))}
|
||||
<Button
|
||||
onClick={form.handleSubmit(submitForm, onFormError)}
|
||||
variant="filled"
|
||||
radius="sm"
|
||||
color={props.submitColor ?? 'green'}
|
||||
disabled={isLoading || (props.fetchInitialData && !isDirty)}
|
||||
>
|
||||
{props.submitText ?? t`Submit`}
|
||||
</Button>
|
||||
</Group>
|
||||
</Group>
|
||||
</div>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
@ -1,9 +1,16 @@
|
||||
import { Trans, t } from '@lingui/macro';
|
||||
import { Alert, List, LoadingOverlay, Stack, Text, Title } from '@mantine/core';
|
||||
import {
|
||||
Alert,
|
||||
List,
|
||||
LoadingOverlay,
|
||||
Spoiler,
|
||||
Stack,
|
||||
Text,
|
||||
Title
|
||||
} from '@mantine/core';
|
||||
import { IconInfoCircle } from '@tabler/icons-react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { AddItemButton } from '../../components/buttons/AddItemButton';
|
||||
import { EditApiForm } from '../../components/forms/ApiForm';
|
||||
@ -12,7 +19,10 @@ import {
|
||||
DetailDrawerLink
|
||||
} from '../../components/nav/DetailDrawer';
|
||||
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
||||
import { openCreateApiForm, openDeleteApiForm } from '../../functions/forms';
|
||||
import {
|
||||
useCreateApiFormModal,
|
||||
useDeleteApiFormModal
|
||||
} from '../../hooks/UseForm';
|
||||
import { useInstance } from '../../hooks/UseInstance';
|
||||
import { useTable } from '../../hooks/UseTable';
|
||||
import { apiUrl } from '../../states/ApiState';
|
||||
@ -120,25 +130,29 @@ export function UserDrawer({
|
||||
id={`user-detail-drawer-${id}`}
|
||||
/>
|
||||
|
||||
<Title order={5}>
|
||||
<Trans>Groups</Trans>
|
||||
</Title>
|
||||
<Text ml={'md'}>
|
||||
{userDetail?.groups && userDetail?.groups?.length > 0 ? (
|
||||
<List>
|
||||
{userDetail?.groups?.map((group) => (
|
||||
<List.Item key={group.pk}>
|
||||
<DetailDrawerLink
|
||||
to={`../group-${group.pk}`}
|
||||
text={group.name}
|
||||
/>
|
||||
</List.Item>
|
||||
))}
|
||||
</List>
|
||||
) : (
|
||||
<Trans>No groups</Trans>
|
||||
)}
|
||||
</Text>
|
||||
<Stack>
|
||||
<Title order={5}>
|
||||
<Trans>Groups</Trans>
|
||||
</Title>
|
||||
<Spoiler maxHeight={125} showLabel="Show More" hideLabel="Show Less">
|
||||
<Text ml={'md'}>
|
||||
{userDetail?.groups && userDetail?.groups?.length > 0 ? (
|
||||
<List>
|
||||
{userDetail?.groups?.map((group) => (
|
||||
<List.Item key={group.pk}>
|
||||
<DetailDrawerLink
|
||||
to={`../group-${group.pk}`}
|
||||
text={group.name}
|
||||
/>
|
||||
</List.Item>
|
||||
))}
|
||||
</List>
|
||||
) : (
|
||||
<Trans>No groups</Trans>
|
||||
)}
|
||||
</Text>
|
||||
</Spoiler>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@ -194,6 +208,9 @@ export function UserTable() {
|
||||
];
|
||||
}, []);
|
||||
|
||||
// Row Actions
|
||||
const [selectedUser, setSelectedUser] = useState<number>(-1);
|
||||
|
||||
const rowActions = useCallback((record: UserDetailI): RowAction[] => {
|
||||
return [
|
||||
RowEditAction({
|
||||
@ -201,39 +218,45 @@ export function UserTable() {
|
||||
}),
|
||||
RowDeleteAction({
|
||||
onClick: () => {
|
||||
openDeleteApiForm({
|
||||
url: ApiEndpoints.user_list,
|
||||
pk: record.pk,
|
||||
title: t`Delete user`,
|
||||
successMessage: t`User deleted`,
|
||||
onFormSuccess: table.refreshTable,
|
||||
preFormWarning: t`Are you sure you want to delete this user?`
|
||||
});
|
||||
setSelectedUser(record.pk);
|
||||
deleteUser.open();
|
||||
}
|
||||
})
|
||||
];
|
||||
}, []);
|
||||
|
||||
const addUser = useCallback(() => {
|
||||
openCreateApiForm({
|
||||
url: ApiEndpoints.user_list,
|
||||
title: t`Add user`,
|
||||
fields: {
|
||||
username: {},
|
||||
email: {},
|
||||
first_name: {},
|
||||
last_name: {}
|
||||
},
|
||||
onFormSuccess: table.refreshTable,
|
||||
successMessage: t`Added user`
|
||||
});
|
||||
}, []);
|
||||
const deleteUser = useDeleteApiFormModal({
|
||||
url: ApiEndpoints.user_list,
|
||||
pk: selectedUser,
|
||||
title: t`Delete user`,
|
||||
successMessage: t`User deleted`,
|
||||
onFormSuccess: table.refreshTable,
|
||||
preFormWarning: t`Are you sure you want to delete this user?`
|
||||
});
|
||||
|
||||
// Table Actions - Add New User
|
||||
const newUser = useCreateApiFormModal({
|
||||
url: ApiEndpoints.user_list,
|
||||
title: t`Add user`,
|
||||
fields: {
|
||||
username: {},
|
||||
email: {},
|
||||
first_name: {},
|
||||
last_name: {}
|
||||
},
|
||||
onFormSuccess: table.refreshTable,
|
||||
successMessage: t`Added user`
|
||||
});
|
||||
|
||||
const tableActions = useMemo(() => {
|
||||
let actions = [];
|
||||
|
||||
actions.push(
|
||||
<AddItemButton key="add-user" onClick={addUser} tooltip={t`Add user`} />
|
||||
<AddItemButton
|
||||
key="add-user"
|
||||
onClick={newUser.open}
|
||||
tooltip={t`Add user`}
|
||||
/>
|
||||
);
|
||||
|
||||
return actions;
|
||||
@ -241,6 +264,8 @@ export function UserTable() {
|
||||
|
||||
return (
|
||||
<>
|
||||
{newUser.modal}
|
||||
{deleteUser.modal}
|
||||
<DetailDrawer
|
||||
title={t`Edit user`}
|
||||
renderContent={(id) => {
|
||||
|
Loading…
Reference in New Issue
Block a user