Make modal footer fixed (#6696) (#6699)

* Quick attempt at fixed form footer

* slightly improve on lower res devices

* Squashed commit of the following:

commit 06c7ebfc21
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

commit a00d5ab4b5
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

commit 160d014e44
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

commit 57a1a81e9b
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

commit 0196dd2f60
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

commit 6abd33f060
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

commit cbd94fc4b5
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

commit ec5ff6408d
Author: Lukas <76838159+wolflu05@users.noreply.github.com>
Date:   Thu Mar 14 13:03:30 2024 +0100

    handle report previewing errors (#6709)

commit 267ff67f05
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

commit 610ea7b0b1
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

commit 7de87383b5
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

commit 2fef34852c
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:
Xander Luciano 2024-03-20 03:00:43 -07:00 committed by GitHub
parent 5de56f5cd8
commit 97ec4d00ef
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 188 additions and 138 deletions

View File

@ -2,15 +2,15 @@ import { t } from '@lingui/macro';
import { import {
Alert, Alert,
DefaultMantineColor, DefaultMantineColor,
Divider,
LoadingOverlay, LoadingOverlay,
Paper,
Text Text
} from '@mantine/core'; } from '@mantine/core';
import { Button, Group, Stack } from '@mantine/core'; import { Button, Group, Stack } from '@mantine/core';
import { useId } from '@mantine/hooks'; import { useId } from '@mantine/hooks';
import { notifications } from '@mantine/notifications'; import { notifications } from '@mantine/notifications';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { useCallback, useEffect, useMemo } from 'react'; import { useCallback, useEffect, useMemo, useRef } from 'react';
import { useState } from 'react'; import { useState } from 'react';
import { import {
FieldValues, FieldValues,
@ -139,6 +139,12 @@ export function OptionsApiForm({
const formProps: ApiFormProps = useMemo(() => { const formProps: ApiFormProps = useMemo(() => {
const _props = { ...props }; 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; if (!_props.fields) return _props;
for (const [k, v] of Object.entries(_props.fields)) { for (const [k, v] of Object.entries(_props.fields)) {
@ -158,10 +164,6 @@ export function OptionsApiForm({
return _props; return _props;
}, [data, props]); }, [data, props]);
if (!data) {
return <LoadingOverlay visible={true} />;
}
return <ApiForm id={id} props={formProps} />; return <ApiForm id={id} props={formProps} />;
} }
@ -222,41 +224,46 @@ export function ApiForm({ id, props }: { id: string; props: ApiFormProps }) {
props.pathParams props.pathParams
], ],
queryFn: async () => { queryFn: async () => {
return api try {
.get(url) // Await API call
.then((response) => { let response = await api.get(url);
const processFields = (fields: ApiFormFieldSet, data: NestedDict) => { // Define function to process API response
const res: NestedDict = {}; const processFields = (fields: ApiFormFieldSet, data: NestedDict) => {
const res: NestedDict = {};
for (const [k, field] of Object.entries(fields)) { // TODO: replace with .map()
const dataValue = data[k]; for (const [k, field] of Object.entries(fields)) {
const dataValue = data[k];
if ( if (
field.field_type === 'nested object' && field.field_type === 'nested object' &&
field.children && field.children &&
typeof dataValue === 'object' typeof dataValue === 'object'
) { ) {
res[k] = processFields(field.children, dataValue); res[k] = processFields(field.children, dataValue);
} else { } else {
res[k] = dataValue; res[k] = dataValue;
}
} }
}
return res; return res;
}; };
const initialData: any = processFields(
props.fields ?? {},
response.data
);
// Update form values, but only for the fields specified for this form // Process API response
form.reset(initialData); const initialData: any = processFields(
props.fields ?? {},
response.data
);
return response; // Update form values, but only for the fields specified for this form
}) form.reset(initialData);
.catch((error) => {
console.error('Error fetching initial data:', error); 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( 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>>(() => { const onFormError = useCallback<SubmitErrorHandler<FieldValues>>(() => {
@ -387,67 +398,81 @@ export function ApiForm({ id, props }: { id: string; props: ApiFormProps }) {
return ( return (
<Stack> <Stack>
<Stack spacing="sm"> {/* Show loading overlay while fetching fields */}
<LoadingOverlay visible={isLoading} /> {/* zIndex used to force overlay on top of modal header bar */}
{(!isValid || nonFieldErrors.length > 0) && ( <LoadingOverlay visible={isLoading} zIndex={1010} />
<Alert radius="sm" color="red" title={t`Form Errors Exist`}>
{nonFieldErrors.length > 0 && ( {/* Attempt at making fixed footer with scroll area */}
<Stack spacing="xs"> <Paper mah={'65vh'} style={{ overflowY: 'auto' }}>
{nonFieldErrors.map((message) => ( <div>
<Text key={message}>{message}</Text> {/* Form Fields */}
))} <Stack spacing="sm">
</Stack> {(!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 && (
{props.preFormContent} <Alert color="green" radius="sm">
{props.preFormSuccess && ( {props.preFormSuccess}
<Alert color="green" radius="sm"> </Alert>
{props.preFormSuccess} )}
</Alert> {props.preFormWarning && (
)} <Alert color="orange" radius="sm">
{props.preFormWarning && ( {props.preFormWarning}
<Alert color="orange" radius="sm"> </Alert>
{props.preFormWarning} )}
</Alert> <FormProvider {...form}>
)} <Stack spacing="xs">
<FormProvider {...form}> {Object.entries(props.fields ?? {}).map(
<Stack spacing="xs"> ([fieldName, field]) => (
{Object.entries(props.fields ?? {}).map(([fieldName, field]) => ( <ApiFormField
<ApiFormField key={fieldName}
key={fieldName} fieldName={fieldName}
fieldName={fieldName} definition={field}
definition={field} control={form.control}
control={form.control} />
/> )
))} )}
</Stack>
</FormProvider>
{props.postFormContent}
</Stack> </Stack>
</FormProvider> </div>
{props.postFormContent} </Paper>
</Stack>
<Divider /> {/* Footer with Action Buttons */}
<Group position="right"> <div>
{props.actions?.map((action, i) => ( <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 <Button
key={i} onClick={form.handleSubmit(submitForm, onFormError)}
onClick={action.onClick} variant="filled"
variant={action.variant ?? 'outline'}
radius="sm" radius="sm"
color={action.color} color={props.submitColor ?? 'green'}
disabled={isLoading || (props.fetchInitialData && !isDirty)}
> >
{action.text} {props.submitText ?? t`Submit`}
</Button> </Button>
))} </Group>
<Button </div>
onClick={form.handleSubmit(submitForm, onFormError)}
variant="filled"
radius="sm"
color={props.submitColor ?? 'green'}
disabled={isLoading || (props.fetchInitialData && !isDirty)}
>
{props.submitText ?? t`Submit`}
</Button>
</Group>
</Stack> </Stack>
); );
} }

View File

@ -1,9 +1,16 @@
import { Trans, t } from '@lingui/macro'; 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 { IconInfoCircle } from '@tabler/icons-react';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { Link } from 'react-router-dom';
import { AddItemButton } from '../../components/buttons/AddItemButton'; import { AddItemButton } from '../../components/buttons/AddItemButton';
import { EditApiForm } from '../../components/forms/ApiForm'; import { EditApiForm } from '../../components/forms/ApiForm';
@ -12,7 +19,10 @@ import {
DetailDrawerLink DetailDrawerLink
} from '../../components/nav/DetailDrawer'; } from '../../components/nav/DetailDrawer';
import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { openCreateApiForm, openDeleteApiForm } from '../../functions/forms'; import {
useCreateApiFormModal,
useDeleteApiFormModal
} from '../../hooks/UseForm';
import { useInstance } from '../../hooks/UseInstance'; import { useInstance } from '../../hooks/UseInstance';
import { useTable } from '../../hooks/UseTable'; import { useTable } from '../../hooks/UseTable';
import { apiUrl } from '../../states/ApiState'; import { apiUrl } from '../../states/ApiState';
@ -120,25 +130,29 @@ export function UserDrawer({
id={`user-detail-drawer-${id}`} id={`user-detail-drawer-${id}`}
/> />
<Title order={5}> <Stack>
<Trans>Groups</Trans> <Title order={5}>
</Title> <Trans>Groups</Trans>
<Text ml={'md'}> </Title>
{userDetail?.groups && userDetail?.groups?.length > 0 ? ( <Spoiler maxHeight={125} showLabel="Show More" hideLabel="Show Less">
<List> <Text ml={'md'}>
{userDetail?.groups?.map((group) => ( {userDetail?.groups && userDetail?.groups?.length > 0 ? (
<List.Item key={group.pk}> <List>
<DetailDrawerLink {userDetail?.groups?.map((group) => (
to={`../group-${group.pk}`} <List.Item key={group.pk}>
text={group.name} <DetailDrawerLink
/> to={`../group-${group.pk}`}
</List.Item> text={group.name}
))} />
</List> </List.Item>
) : ( ))}
<Trans>No groups</Trans> </List>
)} ) : (
</Text> <Trans>No groups</Trans>
)}
</Text>
</Spoiler>
</Stack>
</Stack> </Stack>
); );
} }
@ -194,6 +208,9 @@ export function UserTable() {
]; ];
}, []); }, []);
// Row Actions
const [selectedUser, setSelectedUser] = useState<number>(-1);
const rowActions = useCallback((record: UserDetailI): RowAction[] => { const rowActions = useCallback((record: UserDetailI): RowAction[] => {
return [ return [
RowEditAction({ RowEditAction({
@ -201,39 +218,45 @@ export function UserTable() {
}), }),
RowDeleteAction({ RowDeleteAction({
onClick: () => { onClick: () => {
openDeleteApiForm({ setSelectedUser(record.pk);
url: ApiEndpoints.user_list, deleteUser.open();
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?`
});
} }
}) })
]; ];
}, []); }, []);
const addUser = useCallback(() => { const deleteUser = useDeleteApiFormModal({
openCreateApiForm({ url: ApiEndpoints.user_list,
url: ApiEndpoints.user_list, pk: selectedUser,
title: t`Add user`, title: t`Delete user`,
fields: { successMessage: t`User deleted`,
username: {}, onFormSuccess: table.refreshTable,
email: {}, preFormWarning: t`Are you sure you want to delete this user?`
first_name: {}, });
last_name: {}
}, // Table Actions - Add New User
onFormSuccess: table.refreshTable, const newUser = useCreateApiFormModal({
successMessage: t`Added user` 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(() => { const tableActions = useMemo(() => {
let actions = []; let actions = [];
actions.push( 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; return actions;
@ -241,6 +264,8 @@ export function UserTable() {
return ( return (
<> <>
{newUser.modal}
{deleteUser.modal}
<DetailDrawer <DetailDrawer
title={t`Edit user`} title={t`Edit user`}
renderContent={(id) => { renderContent={(id) => {