mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-10-31 05:05:42 +00:00 
			
		
		
		
	feat(frontend): Add start page with quick actions to Admin Center (#7995)
* add option to set leftMargin * Add home tab and action button * make home button actually go to home * Add general info text * Add dependeant quick action section * Add Quickaction to home page * use Carousel * style check * small fixes * add permanent alerts to Admin Center Home * also show inactive alerts * fix order of alerts * simplify attrs * remove security section for now * bring quick actions alive * adjust text * Use StylishText * Make alert columns reactive * Adjust text formatting * Refactor <QuickActions /> - Use responsive grid instead of carousel - Add icons - Translate text --------- Co-authored-by: Oliver Walters <oliver.henry.walters@gmail.com>
This commit is contained in:
		| @@ -2,12 +2,14 @@ import { ActionIcon, Alert, Group, Menu, Stack, Tooltip } from '@mantine/core'; | |||||||
| import { IconExclamationCircle } from '@tabler/icons-react'; | import { IconExclamationCircle } from '@tabler/icons-react'; | ||||||
| import { useMemo, useState } from 'react'; | import { useMemo, useState } from 'react'; | ||||||
|  |  | ||||||
|  | import type { SettingsStateProps } from '@lib/types/Settings'; | ||||||
| import { t } from '@lingui/core/macro'; | import { t } from '@lingui/core/macro'; | ||||||
| import { useShallow } from 'zustand/react/shallow'; | import { useShallow } from 'zustand/react/shallow'; | ||||||
| import { docLinks } from '../../defaults/links'; | import { docLinks } from '../../defaults/links'; | ||||||
| import { useServerApiState } from '../../states/ServerApiState'; | import { useServerApiState } from '../../states/ServerApiState'; | ||||||
| import { useGlobalSettingsState } from '../../states/SettingsStates'; | import { useGlobalSettingsState } from '../../states/SettingsStates'; | ||||||
| import { useUserState } from '../../states/UserState'; | import { useUserState } from '../../states/UserState'; | ||||||
|  | import type { ServerAPIProps } from '../../states/states'; | ||||||
|  |  | ||||||
| interface AlertInfo { | interface AlertInfo { | ||||||
|   key: string; |   key: string; | ||||||
| @@ -32,64 +34,21 @@ export function Alerts() { | |||||||
|  |  | ||||||
|   const [dismissed, setDismissed] = useState<string[]>([]); |   const [dismissed, setDismissed] = useState<string[]>([]); | ||||||
|  |  | ||||||
|   const alerts: AlertInfo[] = useMemo(() => { |   const alerts: AlertInfo[] = useMemo( | ||||||
|     const _alerts: AlertInfo[] = []; |     () => | ||||||
|  |       getAlerts(server, globalSettings).filter( | ||||||
|     if (server?.debug_mode) { |         (alert) => !dismissed.includes(alert.key) | ||||||
|       _alerts.push({ |       ), | ||||||
|         key: 'debug', |     [server, dismissed, globalSettings] | ||||||
|         title: t`Debug Mode`, |   ); | ||||||
|         code: 'INVE-W4', |  | ||||||
|         message: t`The server is running in debug mode.` |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     if (!server?.worker_running) { |  | ||||||
|       _alerts.push({ |  | ||||||
|         key: 'worker', |  | ||||||
|         title: t`Background Worker`, |  | ||||||
|         code: 'INVE-W5', |  | ||||||
|         message: t`The background worker process is not running.` |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     if (!server?.email_configured) { |  | ||||||
|       _alerts.push({ |  | ||||||
|         key: 'email', |  | ||||||
|         title: t`Email settings`, |  | ||||||
|         code: 'INVE-W7', |  | ||||||
|         message: t`Email settings not configured.` |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     if (globalSettings.isSet('SERVER_RESTART_REQUIRED')) { |  | ||||||
|       _alerts.push({ |  | ||||||
|         key: 'restart', |  | ||||||
|         title: t`Server Restart`, |  | ||||||
|         code: 'INVE-W6', |  | ||||||
|         message: t`The server requires a restart to apply changes.` |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     const n_migrations = |  | ||||||
|       Number.parseInt(globalSettings.getSetting('_PENDING_MIGRATIONS')) ?? 0; |  | ||||||
|  |  | ||||||
|     if (n_migrations > 0) { |  | ||||||
|       _alerts.push({ |  | ||||||
|         key: 'migrations', |  | ||||||
|         title: t`Database Migrations`, |  | ||||||
|         code: 'INVE-W8', |  | ||||||
|         message: t`There are pending database migrations.` |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     return _alerts.filter((alert) => !dismissed.includes(alert.key)); |  | ||||||
|   }, [server, dismissed, globalSettings]); |  | ||||||
|  |  | ||||||
|   const anyErrors: boolean = useMemo( |   const anyErrors: boolean = useMemo( | ||||||
|     () => alerts.some((alert) => alert.error), |     () => alerts.some((alert) => alert.error), | ||||||
|     [alerts] |     [alerts] | ||||||
|   ); |   ); | ||||||
|  |   function closeAlert(key: string) { | ||||||
|  |     setDismissed([...dismissed, key]); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   if (user.isStaff() && alerts.length > 0) |   if (user.isStaff() && alerts.length > 0) | ||||||
|     return ( |     return ( | ||||||
| @@ -108,22 +67,7 @@ export function Alerts() { | |||||||
|         <Menu.Dropdown> |         <Menu.Dropdown> | ||||||
|           {alerts.map((alert) => ( |           {alerts.map((alert) => ( | ||||||
|             <Menu.Item key={`alert-item-${alert.key}`}> |             <Menu.Item key={`alert-item-${alert.key}`}> | ||||||
|               <Alert |               <ServerAlert alert={alert} closeAlert={closeAlert} /> | ||||||
|                 withCloseButton |  | ||||||
|                 color={alert.error ? 'red' : 'orange'} |  | ||||||
|                 title={ |  | ||||||
|                   <Group gap='xs'> |  | ||||||
|                     {alert.code && `${alert.code}: `} |  | ||||||
|                     {alert.title} |  | ||||||
|                   </Group> |  | ||||||
|                 } |  | ||||||
|                 onClose={() => setDismissed([...dismissed, alert.key])} |  | ||||||
|               > |  | ||||||
|                 <Stack gap='xs'> |  | ||||||
|                   {alert.message} |  | ||||||
|                   {alert.code && errorCodeLink(alert.code)} |  | ||||||
|                 </Stack> |  | ||||||
|               </Alert> |  | ||||||
|             </Menu.Item> |             </Menu.Item> | ||||||
|           ))} |           ))} | ||||||
|         </Menu.Dropdown> |         </Menu.Dropdown> | ||||||
| @@ -131,6 +75,84 @@ export function Alerts() { | |||||||
|     ); |     ); | ||||||
|   return null; |   return null; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | export function ServerAlert({ | ||||||
|  |   alert, | ||||||
|  |   closeAlert | ||||||
|  | }: { alert: AlertInfo; closeAlert?: (key: string) => void }) { | ||||||
|  |   return ( | ||||||
|  |     <Alert | ||||||
|  |       withCloseButton={!!closeAlert} | ||||||
|  |       color={alert.error ? 'red' : 'orange'} | ||||||
|  |       title={ | ||||||
|  |         <Group gap='xs'> | ||||||
|  |           {alert.code && `${alert.code}: `} | ||||||
|  |           {alert.title} | ||||||
|  |         </Group> | ||||||
|  |       } | ||||||
|  |       onClose={closeAlert ? () => closeAlert(alert.key) : undefined} | ||||||
|  |     > | ||||||
|  |       <Stack gap='xs'> | ||||||
|  |         {alert.message} | ||||||
|  |         {alert.code && errorCodeLink(alert.code)} | ||||||
|  |       </Stack> | ||||||
|  |     </Alert> | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type ExtendedAlertInfo = AlertInfo & { | ||||||
|  |   condition: boolean; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export function getAlerts( | ||||||
|  |   server: ServerAPIProps, | ||||||
|  |   globalSettings: SettingsStateProps, | ||||||
|  |   inactive = false | ||||||
|  | ): ExtendedAlertInfo[] { | ||||||
|  |   const n_migrations = | ||||||
|  |     Number.parseInt(globalSettings.getSetting('_PENDING_MIGRATIONS')) ?? 0; | ||||||
|  |  | ||||||
|  |   const allalerts: ExtendedAlertInfo[] = [ | ||||||
|  |     { | ||||||
|  |       key: 'debug', | ||||||
|  |       title: t`Debug Mode`, | ||||||
|  |       code: 'INVE-W4', | ||||||
|  |       message: t`The server is running in debug mode.`, | ||||||
|  |       condition: server?.debug_mode || false | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       key: 'worker', | ||||||
|  |       title: t`Background Worker`, | ||||||
|  |       code: 'INVE-W5', | ||||||
|  |       message: t`The background worker process is not running.`, | ||||||
|  |       condition: !server?.worker_running | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       key: 'restart', | ||||||
|  |       title: t`Server Restart`, | ||||||
|  |       code: 'INVE-W6', | ||||||
|  |       message: t`The server requires a restart to apply changes.`, | ||||||
|  |       condition: globalSettings.isSet('SERVER_RESTART_REQUIRED') | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       key: 'email', | ||||||
|  |       title: t`Email settings`, | ||||||
|  |       code: 'INVE-W7', | ||||||
|  |       message: t`Email settings not configured.`, | ||||||
|  |       condition: !server?.email_configured | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       key: 'migrations', | ||||||
|  |       title: t`Database Migrations`, | ||||||
|  |       code: 'INVE-W8', | ||||||
|  |       message: t`There are pending database migrations.`, | ||||||
|  |       condition: n_migrations > 0 | ||||||
|  |     } | ||||||
|  |   ]; | ||||||
|  |  | ||||||
|  |   return allalerts.filter((alert) => inactive || alert.condition); | ||||||
|  | } | ||||||
|  |  | ||||||
| export function errorCodeLink(code: string) { | export function errorCodeLink(code: string) { | ||||||
|   return ( |   return ( | ||||||
|     <a |     <a | ||||||
|   | |||||||
							
								
								
									
										138
									
								
								src/frontend/src/components/settings/QuickAction.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										138
									
								
								src/frontend/src/components/settings/QuickAction.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,138 @@ | |||||||
|  | import { t } from '@lingui/core/macro'; | ||||||
|  | import { Trans } from '@lingui/react/macro'; | ||||||
|  | import { Button, Group, Paper, SimpleGrid, Stack, Text } from '@mantine/core'; | ||||||
|  | import { | ||||||
|  |   IconBrandGithub, | ||||||
|  |   IconListCheck, | ||||||
|  |   IconUserPlus, | ||||||
|  |   IconUsersGroup, | ||||||
|  |   type ReactNode | ||||||
|  | } from '@tabler/icons-react'; | ||||||
|  |  | ||||||
|  | import { ApiEndpoints } from '@lib/index'; | ||||||
|  | import { | ||||||
|  |   projectCodeFields, | ||||||
|  |   useCustomStateFields | ||||||
|  | } from '../../forms/CommonForms'; | ||||||
|  | import { useCreateApiFormModal } from '../../hooks/UseForm'; | ||||||
|  | import { groupFields } from '../../tables/settings/GroupTable'; | ||||||
|  | import { userFields } from '../../tables/settings/UserTable'; | ||||||
|  |  | ||||||
|  | interface ActionItem { | ||||||
|  |   id: string; | ||||||
|  |   title: string; | ||||||
|  |   description: string; | ||||||
|  |   icon?: ReactNode; | ||||||
|  |   buttonText?: string; | ||||||
|  |   action: () => void; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function ActionGrid({ items }: { items: ActionItem[] }) { | ||||||
|  |   const slides = items.map((image) => ( | ||||||
|  |     <Paper shadow='xs' p='sm' withBorder> | ||||||
|  |       <Group justify='space-between' wrap='nowrap'> | ||||||
|  |         <Stack> | ||||||
|  |           <Text> | ||||||
|  |             <strong>{image.title}</strong> | ||||||
|  |             <br /> | ||||||
|  |             {image.description} | ||||||
|  |           </Text> | ||||||
|  |         </Stack> | ||||||
|  |         <Button | ||||||
|  |           size='sm' | ||||||
|  |           variant='light' | ||||||
|  |           onClick={image.action} | ||||||
|  |           leftSection={image.icon} | ||||||
|  |         > | ||||||
|  |           {image.buttonText ?? <Trans>Act</Trans>} | ||||||
|  |         </Button> | ||||||
|  |       </Group> | ||||||
|  |     </Paper> | ||||||
|  |   )); | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <SimpleGrid | ||||||
|  |       cols={{ | ||||||
|  |         base: 1, | ||||||
|  |         '600px': 2, | ||||||
|  |         '1200px': 3 | ||||||
|  |       }} | ||||||
|  |       type='container' | ||||||
|  |       spacing='sm' | ||||||
|  |     > | ||||||
|  |       {slides} | ||||||
|  |     </SimpleGrid> | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export const QuickAction = () => { | ||||||
|  |   const newUser = useCreateApiFormModal(userFields()); | ||||||
|  |   const newGroup = useCreateApiFormModal(groupFields()); | ||||||
|  |   const newProjectCode = useCreateApiFormModal({ | ||||||
|  |     url: ApiEndpoints.project_code_list, | ||||||
|  |     title: t`Add Project Code`, | ||||||
|  |     fields: projectCodeFields() | ||||||
|  |   }); | ||||||
|  |   const newCustomState = useCreateApiFormModal({ | ||||||
|  |     url: ApiEndpoints.custom_state_list, | ||||||
|  |     title: t`Add State`, | ||||||
|  |     fields: useCustomStateFields() | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   const items = [ | ||||||
|  |     { | ||||||
|  |       id: '0', | ||||||
|  |       title: t`Open an Issue`, | ||||||
|  |       description: t`Report a bug or request a feature on GitHub`, | ||||||
|  |       icon: <IconBrandGithub />, | ||||||
|  |       buttonText: t`Open Issue`, | ||||||
|  |       action: () => | ||||||
|  |         window.open( | ||||||
|  |           'https://github.com/inventree/inventree/issues/new', | ||||||
|  |           '_blank' | ||||||
|  |         ) | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       id: '1', | ||||||
|  |       title: t`Add New Group`, | ||||||
|  |       description: t`Create a new group to manage your users`, | ||||||
|  |       icon: <IconUsersGroup />, | ||||||
|  |       buttonText: t`New Group`, | ||||||
|  |       action: () => newGroup.open() | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       id: '2', | ||||||
|  |       title: t`Add New User`, | ||||||
|  |       description: t`Create a new user to manage your groups`, | ||||||
|  |       icon: <IconUserPlus />, | ||||||
|  |       buttonText: t`New User`, | ||||||
|  |       action: () => newUser.open() | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       id: '3', | ||||||
|  |       title: t`Add Project Code`, | ||||||
|  |       description: t`Create a new project code to organize your items`, | ||||||
|  |       icon: <IconListCheck />, | ||||||
|  |       buttonText: t`Add Code`, | ||||||
|  |       action: () => newProjectCode.open() | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       id: '4', | ||||||
|  |       title: t`Add Custom State`, | ||||||
|  |       description: t`Create a new custom state for your workflow`, | ||||||
|  |       icon: <IconListCheck />, | ||||||
|  |       buttonText: t`Add State`, | ||||||
|  |       action: () => newCustomState.open() | ||||||
|  |     } | ||||||
|  |   ]; | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <Stack gap={'xs'} ml={'sm'}> | ||||||
|  |       <ActionGrid items={items} /> | ||||||
|  |       {newUser.modal} | ||||||
|  |       {newGroup.modal} | ||||||
|  |       {newProjectCode.modal} | ||||||
|  |       {newCustomState.modal} | ||||||
|  |     </Stack> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
							
								
								
									
										111
									
								
								src/frontend/src/pages/Index/Settings/AdminCenter/HomePanel.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										111
									
								
								src/frontend/src/pages/Index/Settings/AdminCenter/HomePanel.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,111 @@ | |||||||
|  | import { t } from '@lingui/core/macro'; | ||||||
|  | import { Trans } from '@lingui/react/macro'; | ||||||
|  | import { Accordion, Alert, SimpleGrid, Stack, Text } from '@mantine/core'; | ||||||
|  | import { type JSX, useMemo, useState } from 'react'; | ||||||
|  | import { useShallow } from 'zustand/react/shallow'; | ||||||
|  | import { StylishText } from '../../../../components/items/StylishText'; | ||||||
|  | import { ServerAlert, getAlerts } from '../../../../components/nav/Alerts'; | ||||||
|  | import { QuickAction } from '../../../../components/settings/QuickAction'; | ||||||
|  | import { useServerApiState } from '../../../../states/ServerApiState'; | ||||||
|  | import { useGlobalSettingsState } from '../../../../states/SettingsStates'; | ||||||
|  |  | ||||||
|  | export default function HomePanel(): JSX.Element { | ||||||
|  |   const [dismissed, setDismissed] = useState<boolean>(false); | ||||||
|  |   const [server] = useServerApiState(useShallow((state) => [state.server])); | ||||||
|  |   const globalSettings = useGlobalSettingsState(); | ||||||
|  |  | ||||||
|  |   const accElements = useMemo(() => { | ||||||
|  |     const _alerts = getAlerts(server, globalSettings, true); | ||||||
|  |     return [ | ||||||
|  |       { | ||||||
|  |         key: 'active', | ||||||
|  |         text: t`Active Alerts`, | ||||||
|  |         elements: _alerts.filter((alert) => alert.condition) | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         key: 'inactive', | ||||||
|  |         text: t`Inactive Alerts`, | ||||||
|  |         elements: _alerts.filter((alert) => !alert.condition) | ||||||
|  |       } | ||||||
|  |     ]; | ||||||
|  |   }, [server, globalSettings]); | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <Stack gap='xs'> | ||||||
|  |       {dismissed ? null : ( | ||||||
|  |         <Alert | ||||||
|  |           color='blue' | ||||||
|  |           title={t`Admin Center Information`} | ||||||
|  |           withCloseButton | ||||||
|  |           onClose={() => setDismissed(true)} | ||||||
|  |         > | ||||||
|  |           <Stack gap='xs'> | ||||||
|  |             <Text> | ||||||
|  |               <Trans> | ||||||
|  |                 The home panel (and the whole Admin Center) is a new feature | ||||||
|  |                 starting with the new UI and was previously (before 1.0) not | ||||||
|  |                 available. | ||||||
|  |               </Trans> | ||||||
|  |             </Text> | ||||||
|  |             <Text> | ||||||
|  |               <Trans> | ||||||
|  |                 The admin center provides a centralized location for all | ||||||
|  |                 administration functionality and is meant to replace all | ||||||
|  |                 interaction with the (django) backend admin interface. | ||||||
|  |               </Trans> | ||||||
|  |             </Text> | ||||||
|  |             <Text> | ||||||
|  |               <Trans> | ||||||
|  |                 Please open feature requests (after checking the tracker) for | ||||||
|  |                 any existing backend admin functionality you are missing in this | ||||||
|  |                 UI. The backend admin interface should be used carefully and | ||||||
|  |                 seldom. | ||||||
|  |               </Trans> | ||||||
|  |             </Text> | ||||||
|  |           </Stack> | ||||||
|  |         </Alert> | ||||||
|  |       )} | ||||||
|  |       <Accordion | ||||||
|  |         multiple | ||||||
|  |         defaultValue={['quick-actions', 'active']} | ||||||
|  |         variant='contained' | ||||||
|  |       > | ||||||
|  |         <Accordion.Item value='quick-actions'> | ||||||
|  |           <Accordion.Control> | ||||||
|  |             <StylishText size='md'> | ||||||
|  |               <Trans>Quick Actions</Trans> | ||||||
|  |             </StylishText> | ||||||
|  |           </Accordion.Control> | ||||||
|  |           <Accordion.Panel> | ||||||
|  |             <QuickAction /> | ||||||
|  |           </Accordion.Panel> | ||||||
|  |         </Accordion.Item> | ||||||
|  |         {accElements.map( | ||||||
|  |           (item) => | ||||||
|  |             item.elements.length > 0 && ( | ||||||
|  |               <Accordion.Item key={item.key} value={item.key}> | ||||||
|  |                 <Accordion.Control> | ||||||
|  |                   <StylishText size='md'>{item.text}</StylishText> | ||||||
|  |                 </Accordion.Control> | ||||||
|  |                 <Accordion.Panel> | ||||||
|  |                   <SimpleGrid | ||||||
|  |                     cols={{ | ||||||
|  |                       base: 1, | ||||||
|  |                       '600px': 2, | ||||||
|  |                       '1200px': 3 | ||||||
|  |                     }} | ||||||
|  |                     type='container' | ||||||
|  |                     spacing='sm' | ||||||
|  |                   > | ||||||
|  |                     {item.elements.map((alert) => ( | ||||||
|  |                       <ServerAlert alert={alert} /> | ||||||
|  |                     ))} | ||||||
|  |                   </SimpleGrid> | ||||||
|  |                 </Accordion.Panel> | ||||||
|  |               </Accordion.Item> | ||||||
|  |             ) | ||||||
|  |         )} | ||||||
|  |       </Accordion> | ||||||
|  |     </Stack> | ||||||
|  |   ); | ||||||
|  | } | ||||||
| @@ -7,6 +7,7 @@ import { | |||||||
|   IconExclamationCircle, |   IconExclamationCircle, | ||||||
|   IconFileDownload, |   IconFileDownload, | ||||||
|   IconFileUpload, |   IconFileUpload, | ||||||
|  |   IconHome, | ||||||
|   IconList, |   IconList, | ||||||
|   IconListDetails, |   IconListDetails, | ||||||
|   IconMail, |   IconMail, | ||||||
| @@ -40,6 +41,8 @@ const ReportTemplatePanel = Loadable( | |||||||
|  |  | ||||||
| const LabelTemplatePanel = Loadable(lazy(() => import('./LabelTemplatePanel'))); | const LabelTemplatePanel = Loadable(lazy(() => import('./LabelTemplatePanel'))); | ||||||
|  |  | ||||||
|  | const HomePanel = Loadable(lazy(() => import('./HomePanel'))); | ||||||
|  |  | ||||||
| const UserManagementPanel = Loadable( | const UserManagementPanel = Loadable( | ||||||
|   lazy(() => import('./UserManagementPanel')) |   lazy(() => import('./UserManagementPanel')) | ||||||
| ); | ); | ||||||
| @@ -107,6 +110,13 @@ export default function AdminCenter() { | |||||||
|  |  | ||||||
|   const adminCenterPanels: PanelType[] = useMemo(() => { |   const adminCenterPanels: PanelType[] = useMemo(() => { | ||||||
|     return [ |     return [ | ||||||
|  |       { | ||||||
|  |         name: 'home', | ||||||
|  |         label: t`Home`, | ||||||
|  |         icon: <IconHome />, | ||||||
|  |         content: <HomePanel />, | ||||||
|  |         showHeadline: false | ||||||
|  |       }, | ||||||
|       { |       { | ||||||
|         name: 'user', |         name: 'user', | ||||||
|         label: t`Users / Access`, |         label: t`Users / Access`, | ||||||
| @@ -231,6 +241,7 @@ export default function AdminCenter() { | |||||||
|   }, [user]); |   }, [user]); | ||||||
|   const grouping: PanelGroupType[] = useMemo(() => { |   const grouping: PanelGroupType[] = useMemo(() => { | ||||||
|     return [ |     return [ | ||||||
|  |       { id: 'home', label: '', panelIDs: ['home'] }, | ||||||
|       { |       { | ||||||
|         id: 'ops', |         id: 'ops', | ||||||
|         label: t`Operations`, |         label: t`Operations`, | ||||||
|   | |||||||
| @@ -13,8 +13,8 @@ import { ApiEndpoints } from '@lib/enums/ApiEndpoints'; | |||||||
| import { ModelType } from '@lib/enums/ModelType'; | import { ModelType } from '@lib/enums/ModelType'; | ||||||
| import { UserRoles } from '@lib/enums/Roles'; | import { UserRoles } from '@lib/enums/Roles'; | ||||||
| import { apiUrl } from '@lib/functions/Api'; | import { apiUrl } from '@lib/functions/Api'; | ||||||
| import { getDetailUrl } from '@lib/index'; | import { type ApiFormModalProps, getDetailUrl } from '@lib/index'; | ||||||
| import type { TableColumn } from '@lib/types/Tables'; | import type { TableColumn, TableState } from '@lib/types/Tables'; | ||||||
| import { IconUsersGroup } from '@tabler/icons-react'; | import { IconUsersGroup } from '@tabler/icons-react'; | ||||||
| import { useNavigate } from 'react-router-dom'; | import { useNavigate } from 'react-router-dom'; | ||||||
| import { EditApiForm } from '../../components/forms/ApiForm'; | import { EditApiForm } from '../../components/forms/ApiForm'; | ||||||
| @@ -188,17 +188,7 @@ export function GroupTable({ | |||||||
|     preFormWarning: t`Are you sure you want to delete this group?` |     preFormWarning: t`Are you sure you want to delete this group?` | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   const newGroup = useCreateApiFormModal({ |   const newGroup = useCreateApiFormModal(groupFields(table)); | ||||||
|     url: ApiEndpoints.group_list, |  | ||||||
|     title: t`Add Group`, |  | ||||||
|     fields: { |  | ||||||
|       name: { |  | ||||||
|         label: t`Name`, |  | ||||||
|         description: t`Name of the user group` |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     table: table |  | ||||||
|   }); |  | ||||||
|  |  | ||||||
|   const tableActions = useMemo(() => { |   const tableActions = useMemo(() => { | ||||||
|     const actions = []; |     const actions = []; | ||||||
| @@ -256,3 +246,17 @@ export function GroupTable({ | |||||||
|     </> |     </> | ||||||
|   ); |   ); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | export function groupFields(table?: TableState): ApiFormModalProps { | ||||||
|  |   return { | ||||||
|  |     url: ApiEndpoints.group_list, | ||||||
|  |     title: t`Add Group`, | ||||||
|  |     fields: { | ||||||
|  |       name: { | ||||||
|  |         label: t`Name`, | ||||||
|  |         description: t`Name of the user group` | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     table: table ?? undefined | ||||||
|  |   }; | ||||||
|  | } | ||||||
|   | |||||||
| @@ -20,9 +20,9 @@ import { ApiEndpoints } from '@lib/enums/ApiEndpoints'; | |||||||
| import { ModelType } from '@lib/enums/ModelType'; | import { ModelType } from '@lib/enums/ModelType'; | ||||||
| import { UserRoles } from '@lib/enums/Roles'; | import { UserRoles } from '@lib/enums/Roles'; | ||||||
| import { apiUrl } from '@lib/functions/Api'; | import { apiUrl } from '@lib/functions/Api'; | ||||||
| import { getDetailUrl } from '@lib/index'; | import { type ApiFormModalProps, getDetailUrl } from '@lib/index'; | ||||||
| import type { TableFilter } from '@lib/types/Filters'; | import type { TableFilter } from '@lib/types/Filters'; | ||||||
| import type { TableColumn } from '@lib/types/Tables'; | import type { TableColumn, TableState } from '@lib/types/Tables'; | ||||||
| import { showNotification } from '@mantine/notifications'; | import { showNotification } from '@mantine/notifications'; | ||||||
| import { useNavigate } from 'react-router-dom'; | import { useNavigate } from 'react-router-dom'; | ||||||
| import { useShallow } from 'zustand/react/shallow'; | import { useShallow } from 'zustand/react/shallow'; | ||||||
| @@ -363,18 +363,7 @@ export function UserTable({ | |||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   // Table Actions - Add New User |   // Table Actions - Add New User | ||||||
|   const newUser = useCreateApiFormModal({ |   const newUser = useCreateApiFormModal(userFields(table)); | ||||||
|     url: ApiEndpoints.user_list, |  | ||||||
|     title: t`Add User`, |  | ||||||
|     fields: { |  | ||||||
|       username: {}, |  | ||||||
|       email: {}, |  | ||||||
|       first_name: {}, |  | ||||||
|       last_name: {} |  | ||||||
|     }, |  | ||||||
|     table: table, |  | ||||||
|     successMessage: t`Added user` |  | ||||||
|   }); |  | ||||||
|  |  | ||||||
|   const setPassword = useApiFormModal({ |   const setPassword = useApiFormModal({ | ||||||
|     url: ApiEndpoints.user_set_password, |     url: ApiEndpoints.user_set_password, | ||||||
| @@ -468,6 +457,21 @@ export function UserTable({ | |||||||
|   ); |   ); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | export function userFields(table?: TableState): ApiFormModalProps { | ||||||
|  |   return { | ||||||
|  |     url: ApiEndpoints.user_list, | ||||||
|  |     title: t`Add User`, | ||||||
|  |     fields: { | ||||||
|  |       username: {}, | ||||||
|  |       email: {}, | ||||||
|  |       first_name: {}, | ||||||
|  |       last_name: {} | ||||||
|  |     }, | ||||||
|  |     table: table ?? undefined, | ||||||
|  |     successMessage: t`Added user` | ||||||
|  |   }; | ||||||
|  | } | ||||||
|  |  | ||||||
| async function setUserActiveState(userId: number, active: boolean) { | async function setUserActiveState(userId: number, active: boolean) { | ||||||
|   try { |   try { | ||||||
|     await api.patch(apiUrl(ApiEndpoints.user_list, userId), { |     await api.patch(apiUrl(ApiEndpoints.user_list, userId), { | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user