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,12 +224,14 @@ 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);
// Define function to process API response
const processFields = (fields: ApiFormFieldSet, data: NestedDict) => { const processFields = (fields: ApiFormFieldSet, data: NestedDict) => {
const res: NestedDict = {}; const res: NestedDict = {};
// TODO: replace with .map()
for (const [k, field] of Object.entries(fields)) { for (const [k, field] of Object.entries(fields)) {
const dataValue = data[k]; const dataValue = data[k];
@ -244,6 +248,8 @@ export function ApiForm({ id, props }: { id: string; props: ApiFormProps }) {
return res; return res;
}; };
// Process API response
const initialData: any = processFields( const initialData: any = processFields(
props.fields ?? {}, props.fields ?? {},
response.data response.data
@ -253,10 +259,11 @@ export function ApiForm({ id, props }: { id: string; props: ApiFormProps }) {
form.reset(initialData); form.reset(initialData);
return response; return response;
}) } catch (error) {
.catch((error) => {
console.error('Error fetching initial data:', 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,8 +398,15 @@ export function ApiForm({ id, props }: { id: string; props: ApiFormProps }) {
return ( return (
<Stack> <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"> <Stack spacing="sm">
<LoadingOverlay visible={isLoading} />
{(!isValid || nonFieldErrors.length > 0) && ( {(!isValid || nonFieldErrors.length > 0) && (
<Alert radius="sm" color="red" title={t`Form Errors Exist`}> <Alert radius="sm" color="red" title={t`Form Errors Exist`}>
{nonFieldErrors.length > 0 && ( {nonFieldErrors.length > 0 && (
@ -413,19 +431,25 @@ export function ApiForm({ id, props }: { id: string; props: ApiFormProps }) {
)} )}
<FormProvider {...form}> <FormProvider {...form}>
<Stack spacing="xs"> <Stack spacing="xs">
{Object.entries(props.fields ?? {}).map(([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> </Stack>
</FormProvider> </FormProvider>
{props.postFormContent} {props.postFormContent}
</Stack> </Stack>
<Divider /> </div>
</Paper>
{/* Footer with Action Buttons */}
<div>
<Group position="right"> <Group position="right">
{props.actions?.map((action, i) => ( {props.actions?.map((action, i) => (
<Button <Button
@ -448,6 +472,7 @@ export function ApiForm({ id, props }: { id: string; props: ApiFormProps }) {
{props.submitText ?? t`Submit`} {props.submitText ?? t`Submit`}
</Button> </Button>
</Group> </Group>
</div>
</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,9 +130,11 @@ export function UserDrawer({
id={`user-detail-drawer-${id}`} id={`user-detail-drawer-${id}`}
/> />
<Stack>
<Title order={5}> <Title order={5}>
<Trans>Groups</Trans> <Trans>Groups</Trans>
</Title> </Title>
<Spoiler maxHeight={125} showLabel="Show More" hideLabel="Show Less">
<Text ml={'md'}> <Text ml={'md'}>
{userDetail?.groups && userDetail?.groups?.length > 0 ? ( {userDetail?.groups && userDetail?.groups?.length > 0 ? (
<List> <List>
@ -139,6 +151,8 @@ export function UserDrawer({
<Trans>No groups</Trans> <Trans>No groups</Trans>
)} )}
</Text> </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,21 +218,24 @@ 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,
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, url: ApiEndpoints.user_list,
title: t`Add user`, title: t`Add user`,
fields: { fields: {
@ -227,13 +247,16 @@ export function UserTable() {
onFormSuccess: table.refreshTable, onFormSuccess: table.refreshTable,
successMessage: t`Added user` 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) => {