diff --git a/src/frontend/src/components/forms/ApiForm.tsx b/src/frontend/src/components/forms/ApiForm.tsx index dbdc37e132..742b1e94bd 100644 --- a/src/frontend/src/components/forms/ApiForm.tsx +++ b/src/frontend/src/components/forms/ApiForm.tsx @@ -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 ; - } - return ; } @@ -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>(() => { @@ -387,67 +398,81 @@ export function ApiForm({ id, props }: { id: string; props: ApiFormProps }) { return ( - - - {(!isValid || nonFieldErrors.length > 0) && ( - - {nonFieldErrors.length > 0 && ( - - {nonFieldErrors.map((message) => ( - {message} - ))} - + {/* Show loading overlay while fetching fields */} + {/* zIndex used to force overlay on top of modal header bar */} + + + {/* Attempt at making fixed footer with scroll area */} + +
+ {/* Form Fields */} + + {(!isValid || nonFieldErrors.length > 0) && ( + + {nonFieldErrors.length > 0 && ( + + {nonFieldErrors.map((message) => ( + {message} + ))} + + )} + )} - - )} - {props.preFormContent} - {props.preFormSuccess && ( - - {props.preFormSuccess} - - )} - {props.preFormWarning && ( - - {props.preFormWarning} - - )} - - - {Object.entries(props.fields ?? {}).map(([fieldName, field]) => ( - - ))} + {props.preFormContent} + {props.preFormSuccess && ( + + {props.preFormSuccess} + + )} + {props.preFormWarning && ( + + {props.preFormWarning} + + )} + + + {Object.entries(props.fields ?? {}).map( + ([fieldName, field]) => ( + + ) + )} + + + {props.postFormContent} - - {props.postFormContent} - - - - {props.actions?.map((action, i) => ( +
+
+ + {/* Footer with Action Buttons */} +
+ + {props.actions?.map((action, i) => ( + + ))} - ))} - - + +
); } diff --git a/src/frontend/src/tables/settings/UserTable.tsx b/src/frontend/src/tables/settings/UserTable.tsx index 87ccdc6d04..e2793d1c8d 100644 --- a/src/frontend/src/tables/settings/UserTable.tsx +++ b/src/frontend/src/tables/settings/UserTable.tsx @@ -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}`} /> - - <Trans>Groups</Trans> - - - {userDetail?.groups && userDetail?.groups?.length > 0 ? ( - - {userDetail?.groups?.map((group) => ( - - - - ))} - - ) : ( - No groups - )} - + + + <Trans>Groups</Trans> + + + + {userDetail?.groups && userDetail?.groups?.length > 0 ? ( + + {userDetail?.groups?.map((group) => ( + + + + ))} + + ) : ( + No groups + )} + + +
); } @@ -194,6 +208,9 @@ export function UserTable() { ]; }, []); + // Row Actions + const [selectedUser, setSelectedUser] = useState(-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( - + ); return actions; @@ -241,6 +264,8 @@ export function UserTable() { return ( <> + {newUser.modal} + {deleteUser.modal} {