diff --git a/src/frontend/src/components/forms/ApiForm.tsx b/src/frontend/src/components/forms/ApiForm.tsx index 50dc588f01..3370f368eb 100644 --- a/src/frontend/src/components/forms/ApiForm.tsx +++ b/src/frontend/src/components/forms/ApiForm.tsx @@ -1,11 +1,5 @@ import { t } from '@lingui/macro'; -import { - Alert, - Divider, - LoadingOverlay, - ScrollArea, - Text -} from '@mantine/core'; +import { Alert, Divider, LoadingOverlay, Text } from '@mantine/core'; import { Button, Group, Stack } from '@mantine/core'; import { useForm } from '@mantine/form'; import { modals } from '@mantine/modals'; @@ -277,24 +271,22 @@ export function ApiForm({ )} {preFormElement} - - - {Object.entries(props.fields ?? {}).map( - ([fieldName, field]) => - !field.hidden && ( - - ) - )} - - + + {Object.entries(props.fields ?? {}).map( + ([fieldName, field]) => + !field.hidden && ( + + ) + )} + {postFormElement} diff --git a/src/frontend/src/components/images/Thumbnail.tsx b/src/frontend/src/components/images/Thumbnail.tsx index 2e7524014c..3798d1ae10 100644 --- a/src/frontend/src/components/images/Thumbnail.tsx +++ b/src/frontend/src/components/images/Thumbnail.tsx @@ -19,7 +19,7 @@ export function Thumbnail({ return ( void; +}; + +/** + * A simple Menu component which renders a set of actions. + * + * If no "active" actions are provided, the menu will not be rendered + */ +export function ActionDropdown({ + icon, + tooltip, + actions +}: { + icon: ReactNode; + tooltip?: string; + actions: ActionDropdownItem[]; +}) { + const hasActions = useMemo(() => { + return actions.some((action) => !action.disabled); + }, [actions]); + + return hasActions ? ( + + + + + {icon} + + + + + {actions.map((action, index) => + action.disabled ? null : ( + + { + if (action.onClick != undefined) { + action.onClick(); + } else { + notYetImplemented(); + } + }} + disabled={action.disabled} + > + {action.name} + + + ) + )} + + + ) : null; +} diff --git a/src/frontend/src/components/nav/Layout.tsx b/src/frontend/src/components/nav/Layout.tsx index fdcbe6c578..d685a2c6ad 100644 --- a/src/frontend/src/components/nav/Layout.tsx +++ b/src/frontend/src/components/nav/Layout.tsx @@ -1,7 +1,8 @@ -import { Container, Flex, Space } from '@mantine/core'; +import { Container, Flex, LoadingOverlay, Space } from '@mantine/core'; import { Navigate, Outlet } from 'react-router-dom'; import { InvenTreeStyle } from '../../globalStyle'; +import { useModalState } from '../../states/ModalState'; import { useSessionState } from '../../states/SessionState'; import { Footer } from './Footer'; import { Header } from './Header'; @@ -19,9 +20,12 @@ export const ProtectedRoute = ({ children }: { children: JSX.Element }) => { export default function LayoutComponent() { const { classes } = InvenTreeStyle(); + const modalState = useModalState(); + return ( +
diff --git a/src/frontend/src/components/nav/PageDetail.tsx b/src/frontend/src/components/nav/PageDetail.tsx index be9257f618..6ecd565fcf 100644 --- a/src/frontend/src/components/nav/PageDetail.tsx +++ b/src/frontend/src/components/nav/PageDetail.tsx @@ -41,7 +41,11 @@ export function PageDetail({ - {actions && {actions}} + {actions && ( + + {actions} + + )} diff --git a/src/frontend/src/components/tables/general/AttachmentTable.tsx b/src/frontend/src/components/tables/general/AttachmentTable.tsx index 86907aa81f..391be18191 100644 --- a/src/frontend/src/components/tables/general/AttachmentTable.tsx +++ b/src/frontend/src/components/tables/general/AttachmentTable.tsx @@ -224,20 +224,22 @@ export function AttachmentTable({ return ( - + {pk && pk > 0 && ( + + )} {allowEdit && validPk && ( diff --git a/src/frontend/src/components/tables/general/CompanyTable.tsx b/src/frontend/src/components/tables/general/CompanyTable.tsx index 5880c9fe8c..2a7cfe0582 100644 --- a/src/frontend/src/components/tables/general/CompanyTable.tsx +++ b/src/frontend/src/components/tables/general/CompanyTable.tsx @@ -1,6 +1,7 @@ import { t } from '@lingui/macro'; import { Group, Text } from '@mantine/core'; import { useMemo } from 'react'; +import { useNavigate } from 'react-router-dom'; import { useTableRefresh } from '../../../hooks/TableRefresh'; import { ApiPaths, apiUrl } from '../../../states/ApiState'; @@ -11,9 +12,17 @@ import { InvenTreeTable } from '../InvenTreeTable'; * A table which displays a list of company records, * based on the provided filter parameters */ -export function CompanyTable({ params }: { params?: any }) { +export function CompanyTable({ + params, + path +}: { + params?: any; + path?: string; +}) { const { tableKey } = useTableRefresh('company'); + const navigate = useNavigate(); + const columns = useMemo(() => { return [ { @@ -24,7 +33,7 @@ export function CompanyTable({ params }: { params?: any }) { return ( @@ -56,6 +65,10 @@ export function CompanyTable({ params }: { params?: any }) { props={{ params: { ...params + }, + onRowClick: (row: any) => { + let base = path ?? 'company'; + navigate(`/${base}/${row.pk}`); } }} /> diff --git a/src/frontend/src/functions/forms.tsx b/src/frontend/src/functions/forms.tsx index a93376fdec..eb46c51145 100644 --- a/src/frontend/src/functions/forms.tsx +++ b/src/frontend/src/functions/forms.tsx @@ -7,6 +7,7 @@ import { api } from '../App'; import { ApiForm, ApiFormProps } from '../components/forms/ApiForm'; import { ApiFormFieldType } from '../components/forms/fields/ApiFormField'; import { apiUrl } from '../states/ApiState'; +import { useModalState } from '../states/ModalState'; import { invalidResponse, permissionDenied } from './notifications'; import { generateUniqueId } from './uid'; @@ -97,6 +98,10 @@ export function openModalApiForm(props: ApiFormProps) { let url = constructFormUrl(props); + // let modalState = useModalState(); + + useModalState.getState().lock(); + // Make OPTIONS request first api .options(url) @@ -119,6 +124,7 @@ export function openModalApiForm(props: ApiFormProps) { modals.open({ title: props.title, modalId: modalId, + size: 'xl', onClose: () => { props.onClose ? props.onClose() : null; }, @@ -126,8 +132,12 @@ export function openModalApiForm(props: ApiFormProps) { ) }); + + useModalState.getState().unlock(); }) .catch((error) => { + useModalState.getState().unlock(); + console.log('Error:', error); if (error.response) { invalidResponse(error.response.status); diff --git a/src/frontend/src/functions/forms/CompanyForms.tsx b/src/frontend/src/functions/forms/CompanyForms.tsx new file mode 100644 index 0000000000..84a87554f1 --- /dev/null +++ b/src/frontend/src/functions/forms/CompanyForms.tsx @@ -0,0 +1,57 @@ +import { t } from '@lingui/macro'; +import { + IconAt, + IconCurrencyDollar, + IconGlobe, + IconPhone +} from '@tabler/icons-react'; + +import { ApiFormFieldSet } from '../../components/forms/fields/ApiFormField'; +import { ApiPaths } from '../../states/ApiState'; +import { openEditApiForm } from '../forms'; + +/** + * Field set for editing a company instance + */ +export function companyFields(): ApiFormFieldSet { + return { + name: {}, + description: {}, + website: { + icon: + }, + currency: { + icon: + }, + phone: { + icon: + }, + email: { + icon: + }, + is_supplier: {}, + is_manufacturer: {}, + is_customer: {} + }; +} + +/** + * Edit a company instance + */ +export function editCompany({ + pk, + callback +}: { + pk: number; + callback?: () => void; +}) { + openEditApiForm({ + name: 'company-edit', + title: t`Edit Company`, + url: ApiPaths.company_list, + pk: pk, + fields: companyFields(), + successMessage: t`Company updated`, + onFormSuccess: callback + }); +} diff --git a/src/frontend/src/pages/build/BuildDetail.tsx b/src/frontend/src/pages/build/BuildDetail.tsx index b77416aed4..72e60b57eb 100644 --- a/src/frontend/src/pages/build/BuildDetail.tsx +++ b/src/frontend/src/pages/build/BuildDetail.tsx @@ -3,16 +3,26 @@ import { Alert, LoadingOverlay, Stack, Text } from '@mantine/core'; import { IconClipboardCheck, IconClipboardList, + IconCopy, + IconDots, + IconEdit, + IconFileTypePdf, IconInfoCircle, + IconLink, IconList, IconListCheck, IconNotes, IconPaperclip, - IconSitemap + IconPrinter, + IconQrcode, + IconSitemap, + IconTrash, + IconUnlink } from '@tabler/icons-react'; import { useMemo } from 'react'; import { useParams } from 'react-router-dom'; +import { ActionDropdown } from '../../components/items/ActionDropdown'; import { PlaceholderPanel, PlaceholderPill @@ -25,6 +35,7 @@ import { StockItemTable } from '../../components/tables/stock/StockItemTable'; import { NotesEditor } from '../../components/widgets/MarkdownEditor'; import { useInstance } from '../../hooks/UseInstance'; import { ApiPaths, apiUrl } from '../../states/ApiState'; +import { useUserState } from '../../states/UserState'; /** * Detail page for a single Build Order @@ -44,6 +55,8 @@ export default function BuildDetail() { } }); + const user = useUserState(); + const buildPanels: PanelType[] = useMemo(() => { return [ { @@ -130,22 +143,78 @@ export default function BuildDetail() { ]; }, [build]); + const buildActions = useMemo(() => { + // TODO: Disable certain actions based on user permissions + return [ + } + actions={[ + { + icon: , + name: t`View`, + tooltip: t`View part barcode` + }, + { + icon: , + name: t`Link Barcode`, + tooltip: t`Link custom barcode to part`, + disabled: build?.barcode_hash + }, + { + icon: , + name: t`Unlink Barcode`, + tooltip: t`Unlink custom barcode from part`, + disabled: !build?.barcode_hash + } + ]} + />, + } + actions={[ + { + icon: , + name: t`Report`, + tooltip: t`Print build report` + } + ]} + />, + } + actions={[ + { + icon: , + name: t`Edit`, + tooltip: t`Edit build order` + }, + { + icon: , + name: t`Duplicate`, + tooltip: t`Duplicate build order` + }, + { + icon: , + name: t`Delete`, + tooltip: t`Delete build order` + } + ]} + /> + ]; + }, [id, build, user]); + return ( <> - TODO: Build details - - } breadcrumbs={[ { name: t`Build Orders`, url: '/build' }, { name: build.reference, url: `/build/${build.pk}` } ]} - actions={[]} + actions={buildActions} /> diff --git a/src/frontend/src/pages/company/CompanyDetail.tsx b/src/frontend/src/pages/company/CompanyDetail.tsx new file mode 100644 index 0000000000..f080fbb6e2 --- /dev/null +++ b/src/frontend/src/pages/company/CompanyDetail.tsx @@ -0,0 +1,226 @@ +import { t } from '@lingui/macro'; +import { Group, LoadingOverlay, Stack, Text } from '@mantine/core'; +import { + IconBuildingFactory2, + IconBuildingWarehouse, + IconDots, + IconEdit, + IconInfoCircle, + IconMap2, + IconNotes, + IconPackageExport, + IconPackages, + IconPaperclip, + IconShoppingCart, + IconTrash, + IconTruckDelivery, + IconTruckReturn, + IconUsersGroup +} from '@tabler/icons-react'; +import { useMemo } from 'react'; +import { useParams } from 'react-router-dom'; + +import { Thumbnail } from '../../components/images/Thumbnail'; +import { ActionDropdown } from '../../components/items/ActionDropdown'; +import { Breadcrumb } from '../../components/nav/BreadcrumbList'; +import { PageDetail } from '../../components/nav/PageDetail'; +import { PanelGroup } from '../../components/nav/PanelGroup'; +import { PanelType } from '../../components/nav/PanelGroup'; +import { AttachmentTable } from '../../components/tables/general/AttachmentTable'; +import { PurchaseOrderTable } from '../../components/tables/purchasing/PurchaseOrderTable'; +import { ReturnOrderTable } from '../../components/tables/sales/ReturnOrderTable'; +import { SalesOrderTable } from '../../components/tables/sales/SalesOrderTable'; +import { StockItemTable } from '../../components/tables/stock/StockItemTable'; +import { NotesEditor } from '../../components/widgets/MarkdownEditor'; +import { editCompany } from '../../functions/forms/CompanyForms'; +import { useInstance } from '../../hooks/UseInstance'; +import { ApiPaths, apiUrl } from '../../states/ApiState'; +import { useUserState } from '../../states/UserState'; + +export type CompanyDetailProps = { + title: string; + breadcrumbs: Breadcrumb[]; +}; + +/** + * Detail view for a single company instance + */ +export default function CompanyDetail(props: CompanyDetailProps) { + const { id } = useParams(); + + const user = useUserState(); + + const { + instance: company, + refreshInstance, + instanceQuery + } = useInstance({ + endpoint: ApiPaths.company_list, + pk: id, + params: {}, + refetchOnMount: true + }); + + const companyPanels: PanelType[] = useMemo(() => { + return [ + { + name: 'details', + label: t`Details`, + icon: + }, + { + name: 'manufactured-parts', + label: t`Manufactured Parts`, + icon: , + hidden: !company?.is_manufacturer + }, + { + name: 'supplied-parts', + label: t`Supplied Parts`, + icon: , + hidden: !company?.is_supplier + }, + { + name: 'purchase-orders', + label: t`Purchase Orders`, + icon: , + hidden: !company?.is_supplier, + content: company?.pk && ( + + ) + }, + { + name: 'stock-items', + label: t`Stock Items`, + icon: , + hidden: !company?.is_manufacturer && !company?.is_supplier, + content: company?.pk && ( + + ) + }, + { + name: 'sales-orders', + label: t`Sales Orders`, + icon: , + hidden: !company?.is_customer, + content: company?.pk && ( + + ) + }, + { + name: 'return-orders', + label: t`Return Orders`, + icon: , + hidden: !company?.is_customer, + content: company.pk && ( + + ) + }, + { + name: 'assigned-stock', + label: t`Assigned Stock`, + icon: , + hidden: !company?.is_customer + }, + { + name: 'contacts', + label: t`Contacts`, + icon: + }, + { + name: 'addresses', + label: t`Addresses`, + icon: + }, + { + name: 'attachments', + label: t`Attachments`, + icon: , + content: ( + + ) + }, + { + name: 'notes', + label: t`Notes`, + icon: , + content: ( + + ) + } + ]; + }, [id, company]); + + const companyDetail = useMemo(() => { + return ( + + + + + {company.name} + + {company.description} + + + ); + }, [id, company]); + + const companyActions = useMemo(() => { + // TODO: Finer fidelity on these permissions, perhaps? + let canEdit = user.checkUserRole('purchase_order', 'change'); + let canDelete = user.checkUserRole('purchase_order', 'delete'); + + return [ + } + actions={[ + { + icon: , + name: t`Edit`, + tooltip: t`Edit company`, + disabled: !canEdit, + onClick: () => { + if (company?.pk) { + editCompany({ + pk: company?.pk, + callback: refreshInstance + }); + } + } + }, + { + icon: , + name: t`Delete`, + tooltip: t`Delete company`, + disabled: !canDelete + } + ]} + /> + ]; + }, [id, company, user]); + + return ( + + + + + + ); +} diff --git a/src/frontend/src/pages/company/CustomerDetail.tsx b/src/frontend/src/pages/company/CustomerDetail.tsx new file mode 100644 index 0000000000..2725180dbe --- /dev/null +++ b/src/frontend/src/pages/company/CustomerDetail.tsx @@ -0,0 +1,12 @@ +import { t } from '@lingui/macro'; + +import CompanyDetail from './CompanyDetail'; + +export default function CustomerDetail() { + return ( + + ); +} diff --git a/src/frontend/src/pages/company/ManufacturerDetail.tsx b/src/frontend/src/pages/company/ManufacturerDetail.tsx new file mode 100644 index 0000000000..aa04b0405c --- /dev/null +++ b/src/frontend/src/pages/company/ManufacturerDetail.tsx @@ -0,0 +1,12 @@ +import { t } from '@lingui/macro'; + +import CompanyDetail from './CompanyDetail'; + +export default function ManufacturerDetail() { + return ( + + ); +} diff --git a/src/frontend/src/pages/company/SupplierDetail.tsx b/src/frontend/src/pages/company/SupplierDetail.tsx new file mode 100644 index 0000000000..5be35dda8e --- /dev/null +++ b/src/frontend/src/pages/company/SupplierDetail.tsx @@ -0,0 +1,12 @@ +import { t } from '@lingui/macro'; + +import CompanyDetail from './CompanyDetail'; + +export default function SupplierDetail() { + return ( + + ); +} diff --git a/src/frontend/src/pages/part/PartDetail.tsx b/src/frontend/src/pages/part/PartDetail.tsx index 7b722191f4..3179a65ae7 100644 --- a/src/frontend/src/pages/part/PartDetail.tsx +++ b/src/frontend/src/pages/part/PartDetail.tsx @@ -1,34 +1,37 @@ import { t } from '@lingui/macro'; -import { - Alert, - Button, - Group, - LoadingOverlay, - Stack, - Text -} from '@mantine/core'; +import { Group, LoadingOverlay, Stack, Text } from '@mantine/core'; import { IconBuilding, + IconCalendarStats, + IconClipboardList, + IconCopy, IconCurrencyDollar, + IconDots, + IconEdit, IconInfoCircle, IconLayersLinked, + IconLink, IconList, IconListTree, IconNotes, IconPackages, IconPaperclip, + IconQrcode, IconShoppingCart, IconStack2, IconTestPipe, IconTools, + IconTransfer, + IconTrash, IconTruckDelivery, + IconUnlink, IconVersions } from '@tabler/icons-react'; import { useMemo } from 'react'; import { useParams } from 'react-router-dom'; import { ApiImage } from '../../components/images/ApiImage'; -import { PlaceholderPanel } from '../../components/items/Placeholder'; +import { ActionDropdown } from '../../components/items/ActionDropdown'; import { PageDetail } from '../../components/nav/PageDetail'; import { PanelGroup, PanelType } from '../../components/nav/PanelGroup'; import { AttachmentTable } from '../../components/tables/general/AttachmentTable'; @@ -40,6 +43,7 @@ import { NotesEditor } from '../../components/widgets/MarkdownEditor'; import { editPart } from '../../functions/forms/PartForms'; import { useInstance } from '../../hooks/UseInstance'; import { ApiPaths, apiUrl } from '../../states/ApiState'; +import { useUserState } from '../../states/UserState'; /** * Detail view for a single Part instance @@ -47,6 +51,8 @@ import { ApiPaths, apiUrl } from '../../states/ApiState'; export default function PartDetail() { const { id } = useParams(); + const user = useUserState(); + const { instance: part, refreshInstance, @@ -66,8 +72,7 @@ export default function PartDetail() { { name: 'details', label: t`Details`, - icon: , - content: + icon: }, { name: 'parameters', @@ -98,55 +103,57 @@ export default function PartDetail() { name: 'bom', label: t`Bill of Materials`, icon: , - hidden: !part.assembly, - content: + hidden: !part.assembly }, { name: 'builds', label: t`Build Orders`, icon: , - hidden: !part.assembly && !part.component, - content: + hidden: !part.assembly && !part.component }, { name: 'used_in', label: t`Used In`, icon: , - hidden: !part.component, - content: + hidden: !part.component }, { name: 'pricing', label: t`Pricing`, - icon: , - content: + icon: }, { name: 'suppliers', label: t`Suppliers`, icon: , - hidden: !part.purchaseable, - content: + hidden: !part.purchaseable }, { name: 'purchase_orders', label: t`Purchase Orders`, icon: , - content: , hidden: !part.purchaseable }, { name: 'sales_orders', label: t`Sales Orders`, icon: , - content: , hidden: !part.salable }, + { + name: 'scheduling', + label: t`Scheduling`, + icon: + }, + { + name: 'stocktake', + label: t`Stocktake`, + icon: + }, { name: 'test_templates', label: t`Test Templates`, icon: , - content: , hidden: !part.trackable }, { @@ -212,6 +219,79 @@ export default function PartDetail() { ); }, [part, id]); + const partActions = useMemo(() => { + // TODO: Disable actions based on user permissions + return [ + } + actions={[ + { + icon: , + name: t`View`, + tooltip: t`View part barcode` + }, + { + icon: , + name: t`Link Barcode`, + tooltip: t`Link custom barcode to part`, + disabled: part?.barcode_hash + }, + { + icon: , + name: t`Unlink Barcode`, + tooltip: t`Unlink custom barcode from part`, + disabled: !part?.barcode_hash + } + ]} + />, + } + actions={[ + { + icon: , + name: t`Count Stock`, + tooltip: t`Count part stock` + }, + { + icon: , + name: t`Transfer Stock`, + tooltip: t`Transfer part stock` + } + ]} + />, + } + actions={[ + { + icon: , + name: t`Edit`, + tooltip: t`Edit part`, + onClick: () => { + part.pk && + editPart({ + part_id: part.pk, + callback: refreshInstance + }); + } + }, + { + icon: , + name: t`Duplicate`, + tooltip: t`Duplicate part` + }, + { + icon: , + name: t`Delete`, + tooltip: t`Delete part` + } + ]} + /> + ]; + }, [id, part, user]); + return ( <> @@ -219,21 +299,7 @@ export default function PartDetail() { - part.pk && - editPart({ - part_id: part.pk, - callback: refreshInstance - }) - } - > - Edit Part - - ]} + actions={partActions} /> diff --git a/src/frontend/src/pages/purchasing/PurchasingIndex.tsx b/src/frontend/src/pages/purchasing/PurchasingIndex.tsx index 4e2c7969d0..4209d76a1e 100644 --- a/src/frontend/src/pages/purchasing/PurchasingIndex.tsx +++ b/src/frontend/src/pages/purchasing/PurchasingIndex.tsx @@ -26,13 +26,23 @@ export default function PurchasingIndex() { name: 'suppliers', label: t`Suppliers`, icon: , - content: + content: ( + + ) }, { name: 'manufacturer', label: t`Manufacturers`, icon: , - content: + content: ( + + ) } ]; }, []); diff --git a/src/frontend/src/pages/sales/SalesIndex.tsx b/src/frontend/src/pages/sales/SalesIndex.tsx index 9ed47387f7..0bfe2a5b74 100644 --- a/src/frontend/src/pages/sales/SalesIndex.tsx +++ b/src/frontend/src/pages/sales/SalesIndex.tsx @@ -32,7 +32,9 @@ export default function PurchasingIndex() { name: 'suppliers', label: t`Customers`, icon: , - content: + content: ( + + ) } ]; }, []); diff --git a/src/frontend/src/router.tsx b/src/frontend/src/router.tsx index 33c1e65119..f67a85ece8 100644 --- a/src/frontend/src/router.tsx +++ b/src/frontend/src/router.tsx @@ -12,6 +12,22 @@ export const Playground = Loadable( lazy(() => import('./pages/Index/Playground')) ); +export const CompanyDetail = Loadable( + lazy(() => import('./pages/company/CompanyDetail')) +); + +export const CustomerDetail = Loadable( + lazy(() => import('./pages/company/CustomerDetail')) +); + +export const SupplierDetail = Loadable( + lazy(() => import('./pages/company/SupplierDetail')) +); + +export const ManufacturerDetail = Loadable( + lazy(() => import('./pages/company/ManufacturerDetail')) +); + export const CategoryDetail = Loadable( lazy(() => import('./pages/part/CategoryDetail')) ); @@ -109,9 +125,13 @@ export const routes = ( } /> + } /> + } /> + } /> } /> + } /> } /> diff --git a/src/frontend/src/states/ApiState.tsx b/src/frontend/src/states/ApiState.tsx index d5069b1d3b..47fe1bbd5f 100644 --- a/src/frontend/src/states/ApiState.tsx +++ b/src/frontend/src/states/ApiState.tsx @@ -55,6 +55,7 @@ export enum ApiPaths { // Company URLs company_list = 'api-company-list', + company_attachment_list = 'api-company-attachment-list', supplier_part_list = 'api-supplier-part-list', // Stock Item URLs @@ -137,6 +138,8 @@ export function apiEndpoint(path: ApiPaths): string { return 'part/attachment/'; case ApiPaths.company_list: return 'company/'; + case ApiPaths.company_attachment_list: + return 'company/attachment/'; case ApiPaths.supplier_part_list: return 'company/part/'; case ApiPaths.stock_item_list: diff --git a/src/frontend/src/states/ModalState.tsx b/src/frontend/src/states/ModalState.tsx new file mode 100644 index 0000000000..6c6e585102 --- /dev/null +++ b/src/frontend/src/states/ModalState.tsx @@ -0,0 +1,16 @@ +import { create } from 'zustand'; + +interface ModalStateProps { + loading: boolean; + lock: () => void; + unlock: () => void; +} + +/** + * Global state manager for modal forms. + */ +export const useModalState = create((set) => ({ + loading: false, + lock: () => set(() => ({ loading: true })), + unlock: () => set(() => ({ loading: false })) +})); diff --git a/src/frontend/src/states/UserState.tsx b/src/frontend/src/states/UserState.tsx index d8edeee061..05ae161660 100644 --- a/src/frontend/src/states/UserState.tsx +++ b/src/frontend/src/states/UserState.tsx @@ -10,6 +10,7 @@ interface UserStateProps { username: () => string; setUser: (newUser: UserProps) => void; fetchUserState: () => void; + checkUserRole: (role: string, permission: string) => boolean; } /**