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 { useMemo, useState } from 'react'; | ||||
|  | ||||
| import type { SettingsStateProps } from '@lib/types/Settings'; | ||||
| import { t } from '@lingui/core/macro'; | ||||
| import { useShallow } from 'zustand/react/shallow'; | ||||
| import { docLinks } from '../../defaults/links'; | ||||
| import { useServerApiState } from '../../states/ServerApiState'; | ||||
| import { useGlobalSettingsState } from '../../states/SettingsStates'; | ||||
| import { useUserState } from '../../states/UserState'; | ||||
| import type { ServerAPIProps } from '../../states/states'; | ||||
|  | ||||
| interface AlertInfo { | ||||
|   key: string; | ||||
| @@ -32,64 +34,21 @@ export function Alerts() { | ||||
|  | ||||
|   const [dismissed, setDismissed] = useState<string[]>([]); | ||||
|  | ||||
|   const alerts: AlertInfo[] = useMemo(() => { | ||||
|     const _alerts: AlertInfo[] = []; | ||||
|  | ||||
|     if (server?.debug_mode) { | ||||
|       _alerts.push({ | ||||
|         key: 'debug', | ||||
|         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 alerts: AlertInfo[] = useMemo( | ||||
|     () => | ||||
|       getAlerts(server, globalSettings).filter( | ||||
|         (alert) => !dismissed.includes(alert.key) | ||||
|       ), | ||||
|     [server, dismissed, globalSettings] | ||||
|   ); | ||||
|  | ||||
|   const anyErrors: boolean = useMemo( | ||||
|     () => alerts.some((alert) => alert.error), | ||||
|     [alerts] | ||||
|   ); | ||||
|   function closeAlert(key: string) { | ||||
|     setDismissed([...dismissed, key]); | ||||
|   } | ||||
|  | ||||
|   if (user.isStaff() && alerts.length > 0) | ||||
|     return ( | ||||
| @@ -108,22 +67,7 @@ export function Alerts() { | ||||
|         <Menu.Dropdown> | ||||
|           {alerts.map((alert) => ( | ||||
|             <Menu.Item key={`alert-item-${alert.key}`}> | ||||
|               <Alert | ||||
|                 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> | ||||
|               <ServerAlert alert={alert} closeAlert={closeAlert} /> | ||||
|             </Menu.Item> | ||||
|           ))} | ||||
|         </Menu.Dropdown> | ||||
| @@ -131,6 +75,84 @@ export function Alerts() { | ||||
|     ); | ||||
|   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) { | ||||
|   return ( | ||||
|     <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, | ||||
|   IconFileDownload, | ||||
|   IconFileUpload, | ||||
|   IconHome, | ||||
|   IconList, | ||||
|   IconListDetails, | ||||
|   IconMail, | ||||
| @@ -40,6 +41,8 @@ const ReportTemplatePanel = Loadable( | ||||
|  | ||||
| const LabelTemplatePanel = Loadable(lazy(() => import('./LabelTemplatePanel'))); | ||||
|  | ||||
| const HomePanel = Loadable(lazy(() => import('./HomePanel'))); | ||||
|  | ||||
| const UserManagementPanel = Loadable( | ||||
|   lazy(() => import('./UserManagementPanel')) | ||||
| ); | ||||
| @@ -107,6 +110,13 @@ export default function AdminCenter() { | ||||
|  | ||||
|   const adminCenterPanels: PanelType[] = useMemo(() => { | ||||
|     return [ | ||||
|       { | ||||
|         name: 'home', | ||||
|         label: t`Home`, | ||||
|         icon: <IconHome />, | ||||
|         content: <HomePanel />, | ||||
|         showHeadline: false | ||||
|       }, | ||||
|       { | ||||
|         name: 'user', | ||||
|         label: t`Users / Access`, | ||||
| @@ -231,6 +241,7 @@ export default function AdminCenter() { | ||||
|   }, [user]); | ||||
|   const grouping: PanelGroupType[] = useMemo(() => { | ||||
|     return [ | ||||
|       { id: 'home', label: '', panelIDs: ['home'] }, | ||||
|       { | ||||
|         id: 'ops', | ||||
|         label: t`Operations`, | ||||
|   | ||||
| @@ -13,8 +13,8 @@ import { ApiEndpoints } from '@lib/enums/ApiEndpoints'; | ||||
| import { ModelType } from '@lib/enums/ModelType'; | ||||
| import { UserRoles } from '@lib/enums/Roles'; | ||||
| import { apiUrl } from '@lib/functions/Api'; | ||||
| import { getDetailUrl } from '@lib/index'; | ||||
| import type { TableColumn } from '@lib/types/Tables'; | ||||
| import { type ApiFormModalProps, getDetailUrl } from '@lib/index'; | ||||
| import type { TableColumn, TableState } from '@lib/types/Tables'; | ||||
| import { IconUsersGroup } from '@tabler/icons-react'; | ||||
| import { useNavigate } from 'react-router-dom'; | ||||
| import { EditApiForm } from '../../components/forms/ApiForm'; | ||||
| @@ -188,17 +188,7 @@ export function GroupTable({ | ||||
|     preFormWarning: t`Are you sure you want to delete this group?` | ||||
|   }); | ||||
|  | ||||
|   const newGroup = useCreateApiFormModal({ | ||||
|     url: ApiEndpoints.group_list, | ||||
|     title: t`Add Group`, | ||||
|     fields: { | ||||
|       name: { | ||||
|         label: t`Name`, | ||||
|         description: t`Name of the user group` | ||||
|       } | ||||
|     }, | ||||
|     table: table | ||||
|   }); | ||||
|   const newGroup = useCreateApiFormModal(groupFields(table)); | ||||
|  | ||||
|   const tableActions = useMemo(() => { | ||||
|     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 { UserRoles } from '@lib/enums/Roles'; | ||||
| 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 { TableColumn } from '@lib/types/Tables'; | ||||
| import type { TableColumn, TableState } from '@lib/types/Tables'; | ||||
| import { showNotification } from '@mantine/notifications'; | ||||
| import { useNavigate } from 'react-router-dom'; | ||||
| import { useShallow } from 'zustand/react/shallow'; | ||||
| @@ -363,18 +363,7 @@ export function UserTable({ | ||||
|   }); | ||||
|  | ||||
|   // Table Actions - Add New User | ||||
|   const newUser = useCreateApiFormModal({ | ||||
|     url: ApiEndpoints.user_list, | ||||
|     title: t`Add User`, | ||||
|     fields: { | ||||
|       username: {}, | ||||
|       email: {}, | ||||
|       first_name: {}, | ||||
|       last_name: {} | ||||
|     }, | ||||
|     table: table, | ||||
|     successMessage: t`Added user` | ||||
|   }); | ||||
|   const newUser = useCreateApiFormModal(userFields(table)); | ||||
|  | ||||
|   const setPassword = useApiFormModal({ | ||||
|     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) { | ||||
|   try { | ||||
|     await api.patch(apiUrl(ApiEndpoints.user_list, userId), { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user