mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
[React] Settings interface (#5679)
* Add some dummy pages for settings * Add some placeholder pages for settings * Refactor 'useInstance' hook - Allow use without a pk * Make response status code available * Cleanup user settings page * Fill out (empty) panels for system settings * Update URLs * Add more user information to "me" API endpoint * Implement global user context - User information available globally - Add placeholder page for plugin settings * remove debug entry * Add first-pass implementation of plugins table * Add icon to plugin table * plugin table improvements * Add SettingsContext component - Allows list of settings to be drilled down through props - Also provides a way of reloading the settings list * Update settings page * Update settings detail API endpoints - No longer case sensitive * Display boolean values, and allow them to be edited - Still some work to be done here - Need to pass the base URL down through useContext * Allow editing of non-boolean settings too - Required some adjustment to existing forms interface * Fix rendering of choice field within modal * Display correct value for choice fields * Expose settings units to API * Updates - Display units for setting (if available) - Rename fieldType to field_type - React does not like snakeCase props * Improve form handling * Add global server settings keys * Add table for project codes * Use cache to ensure that settings are not rebuilt too often * Update api version notes * Add username helper function to user state * Remove SettingsContext - Replace with global state manager - Does not currently refresh properly * Remove UserContext * Update BaseContext * Cleanup to match master - Integrated many improvements from current master * Get settings items working again - Now integrates nicely with userSettingsState and globalSettingsState * Improve generation of user name * Handle user settings too * url endpoint fix * Add new table for custom unit management * Update global settings
This commit is contained in:
parent
f409cd6894
commit
8bc750bc06
@ -43,7 +43,7 @@ import { ApiFormField, ApiFormFieldSet } from './fields/ApiFormField';
|
||||
export interface ApiFormProps {
|
||||
name: string;
|
||||
url: ApiPaths;
|
||||
pk?: number | string;
|
||||
pk?: number | string | undefined;
|
||||
title: string;
|
||||
fields?: ApiFormFieldSet;
|
||||
cancelText?: string;
|
||||
@ -51,6 +51,7 @@ export interface ApiFormProps {
|
||||
submitColor?: string;
|
||||
cancelColor?: string;
|
||||
fetchInitialData?: boolean;
|
||||
ignorePermissionCheck?: boolean;
|
||||
method?: string;
|
||||
preFormContent?: JSX.Element | (() => JSX.Element);
|
||||
postFormContent?: JSX.Element | (() => JSX.Element);
|
||||
|
@ -3,7 +3,7 @@ import { Input } from '@mantine/core';
|
||||
import { UseFormReturnType } from '@mantine/form';
|
||||
import { useDebouncedValue } from '@mantine/hooks';
|
||||
import { useId } from '@mantine/hooks';
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { ReactNode, useEffect, useMemo, useState } from 'react';
|
||||
import Select from 'react-select';
|
||||
|
||||
|
@ -75,10 +75,10 @@ export function Header() {
|
||||
<NavTabs />
|
||||
</Group>
|
||||
<Group>
|
||||
<ScanButton />
|
||||
<ActionIcon onClick={openSearchDrawer}>
|
||||
<IconSearch />
|
||||
</ActionIcon>
|
||||
<ScanButton />
|
||||
<ActionIcon onClick={openNotificationDrawer}>
|
||||
<Indicator
|
||||
radius="lg"
|
||||
|
@ -2,10 +2,11 @@ import { Trans } from '@lingui/macro';
|
||||
import { Group, Menu, Skeleton, Text, UnstyledButton } from '@mantine/core';
|
||||
import {
|
||||
IconChevronDown,
|
||||
IconHeart,
|
||||
IconLogout,
|
||||
IconPlugConnected,
|
||||
IconSettings,
|
||||
IconUserCircle
|
||||
IconUserCircle,
|
||||
IconUserCog
|
||||
} from '@tabler/icons-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
@ -16,17 +17,16 @@ import { PlaceholderPill } from '../items/Placeholder';
|
||||
|
||||
export function MainMenu() {
|
||||
const { classes, theme } = InvenTreeStyle();
|
||||
const [username] = useUserState((state) => [
|
||||
state.user?.name ?? state.user?.username
|
||||
]);
|
||||
const userState = useUserState();
|
||||
|
||||
return (
|
||||
<Menu width={260} position="bottom-end">
|
||||
<Menu.Target>
|
||||
<UnstyledButton className={classes.layoutHeaderUser}>
|
||||
<Group spacing={7}>
|
||||
<Text weight={500} size="sm" sx={{ lineHeight: 1 }} mr={3}>
|
||||
{username ? (
|
||||
username
|
||||
{userState.username() ? (
|
||||
userState.username()
|
||||
) : (
|
||||
<Skeleton height={20} width={40} radius={theme.defaultRadius} />
|
||||
)}
|
||||
@ -46,6 +46,25 @@ export function MainMenu() {
|
||||
<Menu.Item icon={<IconSettings />} component={Link} to="/profile/user">
|
||||
<Trans>Account settings</Trans>
|
||||
</Menu.Item>
|
||||
<Menu.Item icon={<IconUserCog />} component={Link} to="/settings/user">
|
||||
<Trans>Account settings</Trans>
|
||||
</Menu.Item>
|
||||
{userState.user?.is_staff && (
|
||||
<Menu.Item icon={<IconSettings />} component={Link} to="/settings/">
|
||||
<Trans>System Settings</Trans>
|
||||
</Menu.Item>
|
||||
)}
|
||||
{userState.user?.is_staff && (
|
||||
<Menu.Item
|
||||
icon={<IconPlugConnected />}
|
||||
component={Link}
|
||||
to="/settings/plugin"
|
||||
>
|
||||
<Trans>Plugins</Trans>
|
||||
</Menu.Item>
|
||||
)}
|
||||
<Menu.Divider />
|
||||
|
||||
<Menu.Item
|
||||
icon={<IconLogout />}
|
||||
onClick={() => {
|
||||
|
@ -14,6 +14,7 @@ import {
|
||||
import { ReactNode } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { PlaceholderPanel } from '../items/Placeholder';
|
||||
import { StylishText } from '../items/StylishText';
|
||||
|
||||
/**
|
||||
@ -23,7 +24,7 @@ export type PanelType = {
|
||||
name: string;
|
||||
label: string;
|
||||
icon?: ReactNode;
|
||||
content: ReactNode;
|
||||
content?: ReactNode;
|
||||
hidden?: boolean;
|
||||
disabled?: boolean;
|
||||
};
|
||||
@ -119,7 +120,7 @@ export function PanelGroup({
|
||||
<Stack spacing="md">
|
||||
<StylishText size="lg">{panel.label}</StylishText>
|
||||
<Divider />
|
||||
{panel.content}
|
||||
{panel.content ?? <PlaceholderPanel />}
|
||||
</Stack>
|
||||
</Tabs.Panel>
|
||||
)
|
||||
|
147
src/frontend/src/components/settings/SettingItem.tsx
Normal file
147
src/frontend/src/components/settings/SettingItem.tsx
Normal file
@ -0,0 +1,147 @@
|
||||
import { t } from '@lingui/macro';
|
||||
import { Button, Group, Space, Stack, Switch, Text } from '@mantine/core';
|
||||
import { showNotification } from '@mantine/notifications';
|
||||
import { IconEdit } from '@tabler/icons-react';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { api } from '../../App';
|
||||
import { openModalApiForm } from '../../functions/forms';
|
||||
import { ApiPaths, apiUrl } from '../../states/ApiState';
|
||||
import { SettingsStateProps } from '../../states/SettingsState';
|
||||
import { Setting } from '../../states/states';
|
||||
|
||||
/**
|
||||
* Render a single setting value
|
||||
*/
|
||||
function SettingValue({
|
||||
settingsState,
|
||||
setting
|
||||
}: {
|
||||
settingsState: SettingsStateProps;
|
||||
setting: Setting;
|
||||
}) {
|
||||
// Callback function when a boolean value is changed
|
||||
function onToggle(value: boolean) {
|
||||
api
|
||||
.patch(apiUrl(settingsState.endpoint, setting.key), { value: value })
|
||||
.then(() => {
|
||||
showNotification({
|
||||
title: t`Setting updated`,
|
||||
message: t`${setting?.name} updated successfully`,
|
||||
color: 'green'
|
||||
});
|
||||
settingsState.fetchSettings();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log('Error editing setting', error);
|
||||
showNotification({
|
||||
title: t`Error editing setting`,
|
||||
message: error.message,
|
||||
color: 'red'
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Callback function to open the edit dialog (for non-boolean settings)
|
||||
function onEditButton() {
|
||||
let field_type: string = setting?.type ?? 'string';
|
||||
|
||||
if (setting?.choices && setting?.choices?.length > 0) {
|
||||
field_type = 'choice';
|
||||
}
|
||||
|
||||
openModalApiForm({
|
||||
name: 'setting-edit',
|
||||
url: settingsState.endpoint,
|
||||
pk: setting.key,
|
||||
method: 'PATCH',
|
||||
title: t`Edit Setting`,
|
||||
ignorePermissionCheck: true,
|
||||
fields: {
|
||||
value: {
|
||||
value: setting?.value ?? '',
|
||||
field_type: field_type,
|
||||
choices: setting?.choices || [],
|
||||
label: setting?.name,
|
||||
description: setting?.description
|
||||
}
|
||||
},
|
||||
onFormSuccess() {
|
||||
showNotification({
|
||||
title: t`Setting updated`,
|
||||
message: t`${setting?.name} updated successfully`,
|
||||
color: 'green'
|
||||
});
|
||||
settingsState.fetchSettings();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Determine the text to display for the setting value
|
||||
const valueText: string = useMemo(() => {
|
||||
let value = setting.value;
|
||||
|
||||
// If the setting has a choice, display the choice label
|
||||
if (setting?.choices && setting?.choices?.length > 0) {
|
||||
const choice = setting.choices.find((c) => c.value == setting.value);
|
||||
value = choice?.display_name || setting.value;
|
||||
}
|
||||
|
||||
if (setting?.units) {
|
||||
value = `${value} ${setting.units}`;
|
||||
}
|
||||
|
||||
return value;
|
||||
}, [setting]);
|
||||
|
||||
switch (setting?.type || 'string') {
|
||||
case 'boolean':
|
||||
return (
|
||||
<Switch
|
||||
size="sm"
|
||||
radius="lg"
|
||||
checked={setting.value.toLowerCase() == 'true'}
|
||||
onChange={(event) => onToggle(event.currentTarget.checked)}
|
||||
style={{
|
||||
paddingRight: '20px'
|
||||
}}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return valueText ? (
|
||||
<Group spacing="xs" position="right">
|
||||
<Space />
|
||||
<Button variant="subtle" onClick={onEditButton}>
|
||||
{valueText}
|
||||
</Button>
|
||||
</Group>
|
||||
) : (
|
||||
<Button variant="subtle" onClick={onEditButton}>
|
||||
<IconEdit />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a single setting item, and allow editing of the value
|
||||
*/
|
||||
export function SettingItem({
|
||||
settingsState,
|
||||
setting
|
||||
}: {
|
||||
settingsState: SettingsStateProps;
|
||||
setting: Setting;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<Group position="apart" p="10">
|
||||
<Stack spacing="2">
|
||||
<Text>{setting.name}</Text>
|
||||
<Text size="xs">{setting.description}</Text>
|
||||
</Stack>
|
||||
<SettingValue settingsState={settingsState} setting={setting} />
|
||||
</Group>
|
||||
</>
|
||||
);
|
||||
}
|
59
src/frontend/src/components/settings/SettingList.tsx
Normal file
59
src/frontend/src/components/settings/SettingList.tsx
Normal file
@ -0,0 +1,59 @@
|
||||
import { LoadingOverlay, Stack, Text } from '@mantine/core';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import {
|
||||
SettingsStateProps,
|
||||
useGlobalSettingsState,
|
||||
useUserSettingsState
|
||||
} from '../../states/SettingsState';
|
||||
import { SettingItem } from './SettingItem';
|
||||
|
||||
/**
|
||||
* Display a list of setting items, based on a list of provided keys
|
||||
*/
|
||||
export function SettingList({
|
||||
settingsState,
|
||||
keys
|
||||
}: {
|
||||
settingsState: SettingsStateProps;
|
||||
keys: string[];
|
||||
}) {
|
||||
useEffect(() => {
|
||||
settingsState.fetchSettings();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack spacing="xs">
|
||||
{keys.map((key) => {
|
||||
const setting = settingsState?.settings?.find(
|
||||
(s: any) => s.key === key
|
||||
);
|
||||
return (
|
||||
<div key={key}>
|
||||
{setting ? (
|
||||
<SettingItem settingsState={settingsState} setting={setting} />
|
||||
) : (
|
||||
<Text size="sm" italic color="red">
|
||||
Setting {key} not found
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function UserSettingList({ keys }: { keys: string[] }) {
|
||||
const userSettings = useUserSettingsState();
|
||||
|
||||
return <SettingList settingsState={userSettings} keys={keys} />;
|
||||
}
|
||||
|
||||
export function GlobalSettingList({ keys }: { keys: string[] }) {
|
||||
const globalSettings = useGlobalSettingsState();
|
||||
|
||||
return <SettingList settingsState={globalSettings} keys={keys} />;
|
||||
}
|
@ -188,8 +188,8 @@ function partTableFilters(): TableFilter[] {
|
||||
* @returns
|
||||
*/
|
||||
export function PartListTable({ props }: { props: InvenTreeTableProps }) {
|
||||
let tableColumns = useMemo(() => partTableColumns(), []);
|
||||
let tableFilters = useMemo(() => partTableFilters(), []);
|
||||
const tableColumns = useMemo(() => partTableColumns(), []);
|
||||
const tableFilters = useMemo(() => partTableFilters(), []);
|
||||
|
||||
const { tableKey, refreshTable } = useTableRefresh('part');
|
||||
|
||||
|
160
src/frontend/src/components/tables/plugin/PluginListTable.tsx
Normal file
160
src/frontend/src/components/tables/plugin/PluginListTable.tsx
Normal file
@ -0,0 +1,160 @@
|
||||
import { t } from '@lingui/macro';
|
||||
import { Group, Text, Tooltip } from '@mantine/core';
|
||||
import {
|
||||
IconCircleCheck,
|
||||
IconCircleX,
|
||||
IconHelpCircle
|
||||
} from '@tabler/icons-react';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { notYetImplemented } from '../../../functions/notifications';
|
||||
import { useTableRefresh } from '../../../hooks/TableRefresh';
|
||||
import { ApiPaths, apiUrl } from '../../../states/ApiState';
|
||||
import { TableColumn } from '../Column';
|
||||
import { InvenTreeTable, InvenTreeTableProps } from '../InvenTreeTable';
|
||||
import { RowAction } from '../RowActions';
|
||||
|
||||
/**
|
||||
* Construct an indicator icon for a single plugin
|
||||
*/
|
||||
function PluginIcon(plugin: any) {
|
||||
if (plugin.is_installed) {
|
||||
if (plugin.active) {
|
||||
return (
|
||||
<Tooltip label={t`Plugin is active`}>
|
||||
<IconCircleCheck color="green" />
|
||||
</Tooltip>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Tooltip label={t`Plugin is inactive`}>
|
||||
<IconCircleX color="red" />
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return (
|
||||
<Tooltip label={t`Plugin is not installed`}>
|
||||
<IconHelpCircle />
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Table displaying list of available plugins
|
||||
*/
|
||||
export function PluginListTable({ props }: { props: InvenTreeTableProps }) {
|
||||
const { tableKey, refreshTable } = useTableRefresh('plugin');
|
||||
|
||||
const pluginTableColumns: TableColumn[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
accessor: 'name',
|
||||
title: t`Plugin`,
|
||||
sortable: true,
|
||||
render: function (record: any) {
|
||||
// TODO: Add link to plugin detail page
|
||||
// TODO: Add custom badges
|
||||
return (
|
||||
<Group position="left">
|
||||
<PluginIcon {...record} />
|
||||
<Text>{record.name}</Text>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
accessor: 'meta.description',
|
||||
title: t`Description`,
|
||||
sortable: false,
|
||||
switchable: true,
|
||||
render: function (record: any) {
|
||||
if (record.active) {
|
||||
return record.meta.description;
|
||||
} else {
|
||||
return <Text italic>{t`Description not available`}</Text>;
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
accessor: 'meta.version',
|
||||
title: t`Version`,
|
||||
sortable: false,
|
||||
switchable: true
|
||||
// TODO: Display date information if available
|
||||
},
|
||||
{
|
||||
accessor: 'meta.author',
|
||||
title: 'Author',
|
||||
sortable: false,
|
||||
switchable: true
|
||||
}
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
// Determine available actions for a given plugin
|
||||
function rowActions(record: any): RowAction[] {
|
||||
let actions: RowAction[] = [];
|
||||
|
||||
if (!record.is_builtin && record.is_installed) {
|
||||
if (record.active) {
|
||||
actions.push({
|
||||
title: t`Deactivate`,
|
||||
color: 'red',
|
||||
onClick: () => {
|
||||
notYetImplemented();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
actions.push({
|
||||
title: t`Activate`,
|
||||
onClick: () => {
|
||||
notYetImplemented();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return actions;
|
||||
}
|
||||
|
||||
return (
|
||||
<InvenTreeTable
|
||||
url={apiUrl(ApiPaths.plugin_list)}
|
||||
tableKey={tableKey}
|
||||
columns={pluginTableColumns}
|
||||
props={{
|
||||
...props,
|
||||
enableDownload: false,
|
||||
params: {
|
||||
...props.params
|
||||
},
|
||||
rowActions: rowActions,
|
||||
customFilters: [
|
||||
{
|
||||
name: 'active',
|
||||
label: t`Active`,
|
||||
type: 'boolean'
|
||||
},
|
||||
{
|
||||
name: 'builtin',
|
||||
label: t`Builtin`,
|
||||
type: 'boolean'
|
||||
},
|
||||
{
|
||||
name: 'sample',
|
||||
label: t`Sample`,
|
||||
type: 'boolean'
|
||||
},
|
||||
{
|
||||
name: 'installed',
|
||||
label: t`Installed`,
|
||||
type: 'boolean'
|
||||
}
|
||||
]
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
125
src/frontend/src/components/tables/settings/CustomUnitsTable.tsx
Normal file
125
src/frontend/src/components/tables/settings/CustomUnitsTable.tsx
Normal file
@ -0,0 +1,125 @@
|
||||
import { t } from '@lingui/macro';
|
||||
import { ActionIcon, Text, Tooltip } from '@mantine/core';
|
||||
import { IconCirclePlus } from '@tabler/icons-react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
import {
|
||||
openCreateApiForm,
|
||||
openDeleteApiForm,
|
||||
openEditApiForm
|
||||
} from '../../../functions/forms';
|
||||
import { useTableRefresh } from '../../../hooks/TableRefresh';
|
||||
import { ApiPaths, apiUrl } from '../../../states/ApiState';
|
||||
import { TableColumn } from '../Column';
|
||||
import { InvenTreeTable } from '../InvenTreeTable';
|
||||
import { RowAction } from '../RowActions';
|
||||
|
||||
/**
|
||||
* Table for displaying list of custom physical units
|
||||
*/
|
||||
export function CustomUnitsTable() {
|
||||
const { tableKey, refreshTable } = useTableRefresh('custom-units');
|
||||
|
||||
const columns: TableColumn[] = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
accessor: 'name',
|
||||
title: t`Name`,
|
||||
switchable: false,
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
accessor: 'definition',
|
||||
title: t`Definition`,
|
||||
switchable: false,
|
||||
sortable: false
|
||||
},
|
||||
{
|
||||
accessor: 'symbol',
|
||||
title: t`Symbol`,
|
||||
switchable: false,
|
||||
sortable: true
|
||||
}
|
||||
];
|
||||
}, []);
|
||||
|
||||
const rowActions = useCallback((record: any): RowAction[] => {
|
||||
return [
|
||||
{
|
||||
title: t`Edit`,
|
||||
onClick: () => {
|
||||
openEditApiForm({
|
||||
name: 'edit-custom-unit',
|
||||
url: ApiPaths.custom_unit_list,
|
||||
pk: record.pk,
|
||||
title: t`Edit custom unit`,
|
||||
fields: {
|
||||
name: {},
|
||||
definition: {},
|
||||
symbol: {}
|
||||
},
|
||||
onFormSuccess: refreshTable,
|
||||
successMessage: t`Custom unit updated`
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
title: t`Delete`,
|
||||
onClick: () => {
|
||||
openDeleteApiForm({
|
||||
name: 'delete-custom-unit',
|
||||
url: ApiPaths.custom_unit_list,
|
||||
pk: record.pk,
|
||||
title: t`Delete custom unit`,
|
||||
successMessage: t`Custom unit deleted`,
|
||||
onFormSuccess: refreshTable,
|
||||
preFormContent: (
|
||||
<Text>{t`Are you sure you want to remove this custom unit?`}</Text>
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
];
|
||||
}, []);
|
||||
|
||||
const addCustomUnit = useCallback(() => {
|
||||
openCreateApiForm({
|
||||
name: 'add-custom-unit',
|
||||
url: ApiPaths.custom_unit_list,
|
||||
title: t`Add custom unit`,
|
||||
fields: {
|
||||
name: {},
|
||||
definition: {},
|
||||
symbol: {}
|
||||
},
|
||||
successMessage: t`Custom unit created`,
|
||||
onFormSuccess: refreshTable
|
||||
});
|
||||
}, []);
|
||||
|
||||
const tableActions = useMemo(() => {
|
||||
let actions = [];
|
||||
|
||||
actions.push(
|
||||
<Tooltip label={t`Add custom unit`}>
|
||||
<ActionIcon radius="sm" onClick={addCustomUnit}>
|
||||
<IconCirclePlus color="green" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
return actions;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<InvenTreeTable
|
||||
url={apiUrl(ApiPaths.custom_unit_list)}
|
||||
tableKey={tableKey}
|
||||
columns={columns}
|
||||
props={{
|
||||
rowActions: rowActions,
|
||||
customActionGroups: tableActions
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
118
src/frontend/src/components/tables/settings/ProjectCodeTable.tsx
Normal file
118
src/frontend/src/components/tables/settings/ProjectCodeTable.tsx
Normal file
@ -0,0 +1,118 @@
|
||||
import { t } from '@lingui/macro';
|
||||
import { ActionIcon, Text, Tooltip } from '@mantine/core';
|
||||
import { showNotification } from '@mantine/notifications';
|
||||
import { IconCirclePlus } from '@tabler/icons-react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
import {
|
||||
openCreateApiForm,
|
||||
openDeleteApiForm,
|
||||
openEditApiForm
|
||||
} from '../../../functions/forms';
|
||||
import { notYetImplemented } from '../../../functions/notifications';
|
||||
import { useTableRefresh } from '../../../hooks/TableRefresh';
|
||||
import { ApiPaths, apiUrl } from '../../../states/ApiState';
|
||||
import { TableColumn } from '../Column';
|
||||
import { InvenTreeTable } from '../InvenTreeTable';
|
||||
import { RowAction } from '../RowActions';
|
||||
|
||||
/**
|
||||
* Table for displaying list of project codes
|
||||
*/
|
||||
export function ProjectCodeTable() {
|
||||
const { tableKey, refreshTable } = useTableRefresh('project-code');
|
||||
|
||||
const columns: TableColumn[] = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
accessor: 'code',
|
||||
sortable: true,
|
||||
title: t`Project Code`
|
||||
},
|
||||
{
|
||||
accessor: 'description',
|
||||
sortable: false,
|
||||
title: t`Description`
|
||||
}
|
||||
];
|
||||
}, []);
|
||||
|
||||
const rowActions = useCallback((record: any): RowAction[] => {
|
||||
return [
|
||||
{
|
||||
title: t`Edit`,
|
||||
onClick: () => {
|
||||
openEditApiForm({
|
||||
name: 'edit-project-code',
|
||||
url: ApiPaths.project_code_list,
|
||||
pk: record.pk,
|
||||
title: t`Edit project code`,
|
||||
fields: {
|
||||
code: {},
|
||||
description: {}
|
||||
},
|
||||
onFormSuccess: refreshTable,
|
||||
successMessage: t`Project code updated`
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
title: t`Delete`,
|
||||
color: 'red',
|
||||
onClick: () => {
|
||||
openDeleteApiForm({
|
||||
name: 'delete-project-code',
|
||||
url: ApiPaths.project_code_list,
|
||||
pk: record.pk,
|
||||
title: t`Delete project code`,
|
||||
successMessage: t`Project code deleted`,
|
||||
onFormSuccess: refreshTable,
|
||||
preFormContent: (
|
||||
<Text>{t`Are you sure you want to remove this project code?`}</Text>
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
];
|
||||
}, []);
|
||||
|
||||
const addProjectCode = useCallback(() => {
|
||||
openCreateApiForm({
|
||||
name: 'add-project-code',
|
||||
url: ApiPaths.project_code_list,
|
||||
title: t`Add project code`,
|
||||
fields: {
|
||||
code: {},
|
||||
description: {}
|
||||
},
|
||||
onFormSuccess: refreshTable,
|
||||
successMessage: t`Added project code`
|
||||
});
|
||||
}, []);
|
||||
|
||||
const tableActions = useMemo(() => {
|
||||
let actions = [];
|
||||
|
||||
actions.push(
|
||||
<Tooltip label={t`Add project code`}>
|
||||
<ActionIcon radius="sm" onClick={addProjectCode}>
|
||||
<IconCirclePlus color="green" />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
return actions;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<InvenTreeTable
|
||||
url={apiUrl(ApiPaths.project_code_list)}
|
||||
tableKey={tableKey}
|
||||
columns={columns}
|
||||
props={{
|
||||
rowActions: rowActions,
|
||||
customActionGroups: tableActions
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
@ -1,10 +1,15 @@
|
||||
import { QueryClientProvider } from '@tanstack/react-query';
|
||||
|
||||
import { queryClient } from '../App';
|
||||
import { LanguageContext } from './LanguageContext';
|
||||
import { ThemeContext } from './ThemeContext';
|
||||
|
||||
export const BaseContext = ({ children }: { children: any }) => {
|
||||
return (
|
||||
<LanguageContext>
|
||||
<ThemeContext>{children}</ThemeContext>
|
||||
</LanguageContext>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<LanguageContext>
|
||||
<ThemeContext>{children}</ThemeContext>
|
||||
</LanguageContext>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
};
|
||||
|
@ -8,9 +8,7 @@ import {
|
||||
import { useColorScheme, useLocalStorage } from '@mantine/hooks';
|
||||
import { ModalsProvider } from '@mantine/modals';
|
||||
import { Notifications } from '@mantine/notifications';
|
||||
import { QueryClientProvider } from '@tanstack/react-query';
|
||||
|
||||
import { queryClient } from '../App';
|
||||
import { QrCodeModal } from '../components/modals/QrCodeModal';
|
||||
import { useLocalState } from '../states/LocalState';
|
||||
|
||||
@ -60,14 +58,12 @@ export function ThemeContext({ children }: { children: JSX.Element }) {
|
||||
>
|
||||
<MantineProvider theme={myTheme} withGlobalStyles withNormalizeCSS>
|
||||
<Notifications />
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ModalsProvider
|
||||
labels={{ confirm: t`Submit`, cancel: t`Cancel` }}
|
||||
modals={{ qr: QrCodeModal }}
|
||||
>
|
||||
{children}
|
||||
</ModalsProvider>
|
||||
</QueryClientProvider>
|
||||
<ModalsProvider
|
||||
labels={{ confirm: t`Submit`, cancel: t`Cancel` }}
|
||||
modals={{ qr: QrCodeModal }}
|
||||
>
|
||||
{children}
|
||||
</ModalsProvider>
|
||||
</MantineProvider>
|
||||
</ColorSchemeProvider>
|
||||
);
|
||||
|
@ -98,11 +98,15 @@ export function openModalApiForm(props: ApiFormProps) {
|
||||
.options(url)
|
||||
.then((response) => {
|
||||
// Extract available fields from the OPTIONS response (and handle any errors)
|
||||
let fields: Record<string, ApiFormFieldType> | null =
|
||||
extractAvailableFields(response, props.method);
|
||||
|
||||
if (fields == null) {
|
||||
return;
|
||||
let fields: Record<string, ApiFormFieldType> | null = {};
|
||||
|
||||
if (!props.ignorePermissionCheck) {
|
||||
fields = extractAvailableFields(response, props.method);
|
||||
|
||||
if (fields == null) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Generate a random modal ID for controller
|
||||
|
@ -51,7 +51,7 @@ const vals: LayoutItemType[] = [
|
||||
];
|
||||
|
||||
export default function Home() {
|
||||
const [username] = useUserState((state) => [state.user?.name]);
|
||||
const [username] = useUserState((state) => [state.username()]);
|
||||
return (
|
||||
<>
|
||||
<Title order={1}>
|
||||
|
52
src/frontend/src/pages/Index/PluginSettings.tsx
Normal file
52
src/frontend/src/pages/Index/PluginSettings.tsx
Normal file
@ -0,0 +1,52 @@
|
||||
import { t } from '@lingui/macro';
|
||||
import { LoadingOverlay, Stack } from '@mantine/core';
|
||||
import { IconPlugConnected } from '@tabler/icons-react';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { PageDetail } from '../../components/nav/PageDetail';
|
||||
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
|
||||
import { PluginListTable } from '../../components/tables/plugin/PluginListTable';
|
||||
import { useInstance } from '../../hooks/UseInstance';
|
||||
import { ApiPaths, apiUrl } from '../../states/ApiState';
|
||||
|
||||
/**
|
||||
* Plugins settings page
|
||||
*/
|
||||
export default function PluginSettings() {
|
||||
// Query manager for global plugin settings
|
||||
const {
|
||||
instance: settings,
|
||||
refreshInstance: reloadSettings,
|
||||
instanceQuery: settingsQuery
|
||||
} = useInstance({
|
||||
endpoint: ApiPaths.settings_global_list,
|
||||
hasPrimaryKey: false,
|
||||
refetchOnMount: true,
|
||||
defaultValue: []
|
||||
});
|
||||
|
||||
const pluginPanels: PanelType[] = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
name: 'plugins',
|
||||
label: t`Plugins`,
|
||||
icon: <IconPlugConnected />,
|
||||
content: (
|
||||
<Stack spacing="xs">
|
||||
<PluginListTable props={{}} />
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
];
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack spacing="xs">
|
||||
<LoadingOverlay visible={settingsQuery.isFetching} />
|
||||
<PageDetail title={t`Plugin Settings`} />
|
||||
<PanelGroup pageKey="plugin-settings" panels={pluginPanels} />
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
}
|
261
src/frontend/src/pages/Index/SystemSettings.tsx
Normal file
261
src/frontend/src/pages/Index/SystemSettings.tsx
Normal file
@ -0,0 +1,261 @@
|
||||
import { t } from '@lingui/macro';
|
||||
import { Divider, Stack } from '@mantine/core';
|
||||
import {
|
||||
IconBellCog,
|
||||
IconCategory,
|
||||
IconClipboardCheck,
|
||||
IconCurrencyDollar,
|
||||
IconFileAnalytics,
|
||||
IconFingerprint,
|
||||
IconList,
|
||||
IconListDetails,
|
||||
IconPackages,
|
||||
IconQrcode,
|
||||
IconScale,
|
||||
IconServerCog,
|
||||
IconShoppingCart,
|
||||
IconSitemap,
|
||||
IconTag,
|
||||
IconTools,
|
||||
IconTruckDelivery
|
||||
} from '@tabler/icons-react';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { PageDetail } from '../../components/nav/PageDetail';
|
||||
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
|
||||
import { GlobalSettingList } from '../../components/settings/SettingList';
|
||||
import { CustomUnitsTable } from '../../components/tables/settings/CustomUnitsTable';
|
||||
import { ProjectCodeTable } from '../../components/tables/settings/ProjectCodeTable';
|
||||
|
||||
/**
|
||||
* System settings page
|
||||
*/
|
||||
export default function SystemSettings() {
|
||||
const systemSettingsPanels: PanelType[] = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
name: 'server',
|
||||
label: t`Server`,
|
||||
icon: <IconServerCog />,
|
||||
content: (
|
||||
<GlobalSettingList
|
||||
keys={[
|
||||
'INVENTREE_BASE_URL',
|
||||
'INVENTREE_COMPANY_NAME',
|
||||
'INVENTREE_INSTANCE',
|
||||
'INVENTREE_INSTANCE_TITLE',
|
||||
'INVENTREE_RESTRICT_ABOUT',
|
||||
'INVENTREE_UPDATE_CHECK_INTERVAL',
|
||||
'INVENTREE_DOWNLOAD_FROM_URL',
|
||||
'INVENTREE_DOWNLOAD_IMAGE_MAX_SIZE',
|
||||
'INVENTREE_DOWNLOAD_FROM_URL_USER_AGENT',
|
||||
'INVENTREE_REQUIRE_CONFIRM',
|
||||
'INVENTREE_TREE_DEPTH',
|
||||
'INVENTREE_BACKUP_ENABLE',
|
||||
'INVENTREE_BACKUP_DAYS',
|
||||
'INVENTREE_DELETE_TASKS_DAYS',
|
||||
'INVENTREE_DELETE_ERRORS_DAYS',
|
||||
'INVENTREE_DELETE_NOTIFICATIONS_DAYS'
|
||||
]}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'login',
|
||||
label: t`Login`,
|
||||
icon: <IconFingerprint />,
|
||||
content: (
|
||||
<GlobalSettingList
|
||||
keys={[
|
||||
'LOGIN_ENABLE_PWD_FORGOT',
|
||||
'LOGIN_MAIL_REQUIRED',
|
||||
'LOGIN_ENFORCE_MFA',
|
||||
'LOGIN_ENABLE_REG',
|
||||
'LOGIN_SIGNUP_MAIL_TWICE',
|
||||
'LOGIN_SIGNUP_PWD_TWICE',
|
||||
'SIGNUP_GROUP',
|
||||
'LOGIN_SIGNUP_MAIL_RESTRICTION',
|
||||
'LOGIN_ENABLE_SSO',
|
||||
'LOGIN_ENABLE_SSO_REG',
|
||||
'LOGIN_SIGNUP_SSO_AUTO'
|
||||
]}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'barcode',
|
||||
label: t`Barcodes`,
|
||||
icon: <IconQrcode />,
|
||||
content: (
|
||||
<GlobalSettingList
|
||||
keys={[
|
||||
'BARCODE_ENABLE',
|
||||
'BARCODE_INPUT_DELAY',
|
||||
'BARCODE_WEBCAM_SUPPORT'
|
||||
]}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'projectcodes',
|
||||
label: t`Project Codes`,
|
||||
icon: <IconListDetails />,
|
||||
content: (
|
||||
<Stack spacing="xs">
|
||||
<GlobalSettingList keys={['PROJECT_CODES_ENABLED']} />
|
||||
<Divider />
|
||||
<ProjectCodeTable />
|
||||
</Stack>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'physicalunits',
|
||||
label: t`Physical Units`,
|
||||
icon: <IconScale />,
|
||||
content: <CustomUnitsTable />
|
||||
},
|
||||
{
|
||||
name: 'notifications',
|
||||
label: t`Notifications`,
|
||||
icon: <IconBellCog />
|
||||
},
|
||||
{
|
||||
name: 'pricing',
|
||||
label: t`Pricing`,
|
||||
icon: <IconCurrencyDollar />
|
||||
},
|
||||
{
|
||||
name: 'labels',
|
||||
label: t`Labels`,
|
||||
icon: <IconTag />,
|
||||
content: <GlobalSettingList keys={['LABEL_ENABLE', 'LABEL_DPI']} />
|
||||
},
|
||||
{
|
||||
name: 'reporting',
|
||||
label: t`Reporting`,
|
||||
icon: <IconFileAnalytics />,
|
||||
content: (
|
||||
<GlobalSettingList
|
||||
keys={[
|
||||
'REPORT_ENABLE',
|
||||
'REPORT_DEFAULT_PAGE_SIZE',
|
||||
'REPORT_DEBUG_MODE',
|
||||
'REPORT_ENABLE_TEST_REPORT',
|
||||
'REPORT_ATTACH_TEST_REPORT'
|
||||
]}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'categories',
|
||||
label: t`Part Categories`,
|
||||
icon: <IconSitemap />
|
||||
},
|
||||
{
|
||||
name: 'parts',
|
||||
label: t`Parts`,
|
||||
icon: <IconCategory />,
|
||||
content: (
|
||||
<GlobalSettingList
|
||||
keys={[
|
||||
'PART_ENABLE_REVISION',
|
||||
'PART_IPN_REGEX',
|
||||
'PART_ALLOW_DUPLICATE_IPN',
|
||||
'PART_ALLOW_EDIT_IPN',
|
||||
'PART_NAME_FORMAT',
|
||||
'PART_SHOW_RELATED',
|
||||
'PART_CREATE_INITIAL',
|
||||
'PART_CREATE_SUPPLIER', // TODO: Break here
|
||||
'PART_TEMPLATE',
|
||||
'PART_ASSEMBLY',
|
||||
'PART_COMPONENT',
|
||||
'PART_TRACKABLE',
|
||||
'PART_PURCHASEABLE',
|
||||
'PART_SALABLE',
|
||||
'PART_VIRTUAL', // TODO: Break here
|
||||
'PART_COPY_BOM',
|
||||
'PART_COPY_PARAMETERS',
|
||||
'PART_COPY_TESTS',
|
||||
'PART_CATEGORY_PARAMETERS',
|
||||
'PART_CATEGORY_DEFAULT_ICON' // TODO: Move to part category settings page
|
||||
]}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'parameters',
|
||||
label: t`Part Parameters`,
|
||||
icon: <IconList />
|
||||
},
|
||||
{
|
||||
name: 'stock',
|
||||
label: t`Stock`,
|
||||
icon: <IconPackages />,
|
||||
content: (
|
||||
<GlobalSettingList
|
||||
keys={[
|
||||
'SERIAL_NUMBER_GLOBALLY_UNIQUE',
|
||||
'SERIAL_NUMBER_AUTOFILL',
|
||||
'STOCK_DELETE_DEPLETED_DEFAULT',
|
||||
'STOCK_BATCH_CODE_TEMPLATE',
|
||||
'STOCK_ENABLE_EXPIRY',
|
||||
'STOCK_STALE_DAYS',
|
||||
'STOCK_ALLOW_EXPIRED_SALE',
|
||||
'STOCK_ALLOW_EXPIRED_BUILD',
|
||||
'STOCK_OWNERSHIP_CONTROL',
|
||||
'STOCK_LOCATION_DEFAULT_ICON',
|
||||
'STOCK_SHOW_INSTALLED_ITEMS'
|
||||
]}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'stocktake',
|
||||
label: t`Stocktake`,
|
||||
icon: <IconClipboardCheck />
|
||||
},
|
||||
{
|
||||
name: 'buildorders',
|
||||
label: t`Build Orders`,
|
||||
icon: <IconTools />,
|
||||
content: <GlobalSettingList keys={['BUILDORDER_REFERENCE_PATTERN']} />
|
||||
},
|
||||
{
|
||||
name: 'purchaseorders',
|
||||
label: t`Purchase Orders`,
|
||||
icon: <IconShoppingCart />,
|
||||
content: (
|
||||
<GlobalSettingList
|
||||
keys={[
|
||||
'PURCHASEORDER_REFERENCE_PATTERN',
|
||||
'PURCHASEORDER_EDIT_COMPLETED_ORDERS'
|
||||
]}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'salesorders',
|
||||
label: t`Sales Orders`,
|
||||
icon: <IconTruckDelivery />,
|
||||
content: (
|
||||
<GlobalSettingList
|
||||
keys={[
|
||||
'SALESORDER_REFERENCE_PATTERN',
|
||||
'SALESORDER_DEFAULT_SHIPMENT',
|
||||
'SALESORDER_EDIT_COMPLETED_ORDERS'
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
];
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack spacing="xs">
|
||||
<PageDetail title={t`System Settings`} />
|
||||
<PanelGroup pageKey="system-settings" panels={systemSettingsPanels} />
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
}
|
110
src/frontend/src/pages/Index/UserSettings.tsx
Normal file
110
src/frontend/src/pages/Index/UserSettings.tsx
Normal file
@ -0,0 +1,110 @@
|
||||
import { t } from '@lingui/macro';
|
||||
import { Stack, Text } from '@mantine/core';
|
||||
import {
|
||||
IconBellCog,
|
||||
IconDeviceDesktop,
|
||||
IconDeviceDesktopAnalytics,
|
||||
IconFileAnalytics,
|
||||
IconSearch,
|
||||
IconUserCircle
|
||||
} from '@tabler/icons-react';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { PageDetail } from '../../components/nav/PageDetail';
|
||||
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
|
||||
import { UserSettingList } from '../../components/settings/SettingList';
|
||||
|
||||
/**
|
||||
* User settings page
|
||||
*/
|
||||
export default function UserSettings() {
|
||||
const userSettingsPanels: PanelType[] = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
name: 'account',
|
||||
label: t`Account`,
|
||||
icon: <IconUserCircle />
|
||||
},
|
||||
{
|
||||
name: 'dashboard',
|
||||
label: t`Dashboard`,
|
||||
icon: <IconDeviceDesktopAnalytics />
|
||||
},
|
||||
{
|
||||
name: 'display',
|
||||
label: t`Display Options`,
|
||||
icon: <IconDeviceDesktop />,
|
||||
content: (
|
||||
<UserSettingList
|
||||
keys={[
|
||||
'STICKY_HEADER',
|
||||
'DATE_DISPLAY_FORMAT',
|
||||
'FORMS_CLOSE_USING_ESCAPE',
|
||||
'PART_SHOW_QUANTITY_IN_FORMS',
|
||||
'DISPLAY_SCHEDULE_TAB',
|
||||
'DISPLAY_STOCKTAKE_TAB',
|
||||
'TABLE_STRING_MAX_LENGTH'
|
||||
]}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'search',
|
||||
label: t`Search`,
|
||||
icon: <IconSearch />,
|
||||
content: (
|
||||
<UserSettingList
|
||||
keys={[
|
||||
'SEARCH_WHOLE',
|
||||
'SEARCH_REGEX',
|
||||
'SEARCH_PREVIEW_RESULTS',
|
||||
'SEARCH_PREVIEW_SHOW_PARTS',
|
||||
'SEARCH_HIDE_INACTIVE_PARTS',
|
||||
'SEARCH_PREVIEW_SHOW_SUPPLIER_PARTS',
|
||||
'SEARCH_PREVIEW_SHOW_MANUFACTURER_PARTS',
|
||||
'SEARCH_PREVIEW_SHOW_CATEGORIES',
|
||||
'SEARCH_PREVIEW_SHOW_STOCK',
|
||||
'SEARCH_PREVIEW_HIDE_UNAVAILABLE_STOCK',
|
||||
'SEARCH_PREVIEW_SHOW_LOCATIONS',
|
||||
'SEARCH_PREVIEW_SHOW_COMPANIES',
|
||||
'SEARCH_PREVIEW_SHOW_BUILD_ORDERS',
|
||||
'SEARCH_PREVIEW_SHOW_PURCHASE_ORDERS',
|
||||
'SEARCH_PREVIEW_EXCLUDE_INACTIVE_PURCHASE_ORDERS',
|
||||
'SEARCH_PREVIEW_SHOW_SALES_ORDERS',
|
||||
'SEARCH_PREVIEW_EXCLUDE_INACTIVE_SALES_ORDERS',
|
||||
'SEARCH_PREVIEW_SHOW_RETURN_ORDERS',
|
||||
'SEARCH_PREVIEW_EXCLUDE_INACTIVE_RETURN_ORDERS'
|
||||
]}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'notifications',
|
||||
label: t`Notifications`,
|
||||
icon: <IconBellCog />
|
||||
},
|
||||
{
|
||||
name: 'reporting',
|
||||
label: t`Reporting`,
|
||||
icon: <IconFileAnalytics />,
|
||||
content: (
|
||||
<UserSettingList
|
||||
keys={['REPORT_INLINE', 'LABEL_INLINE', 'LABEL_DEFAULT_PRINTER']}
|
||||
/>
|
||||
)
|
||||
}
|
||||
];
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack spacing="xs">
|
||||
<PageDetail
|
||||
title={t`User Settings`}
|
||||
detail={<Text>TODO: Filler</Text>}
|
||||
/>
|
||||
<PanelGroup pageKey="user-settings" panels={userSettingsPanels} />
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
}
|
@ -49,6 +49,18 @@ export const Profile = Loadable(
|
||||
lazy(() => import('./pages/Index/Profile/Profile'))
|
||||
);
|
||||
|
||||
export const UserSettings = Loadable(
|
||||
lazy(() => import('./pages/Index/UserSettings'))
|
||||
);
|
||||
|
||||
export const SystemSettings = Loadable(
|
||||
lazy(() => import('./pages/Index/SystemSettings'))
|
||||
);
|
||||
|
||||
export const PluginSettings = Loadable(
|
||||
lazy(() => import('./pages/Index/PluginSettings'))
|
||||
);
|
||||
|
||||
export const NotFound = Loadable(lazy(() => import('./pages/NotFound')));
|
||||
export const Login = Loadable(lazy(() => import('./pages/Auth/Login')));
|
||||
export const Logged_In = Loadable(lazy(() => import('./pages/Auth/Logged-In')));
|
||||
@ -68,6 +80,11 @@ export const routes = (
|
||||
<Route path="notifications/" element={<Notifications />} />,
|
||||
<Route path="playground/" element={<Playground />} />,
|
||||
<Route path="scan/" element={<Scan />} />,
|
||||
<Route path="settings/">
|
||||
<Route index element={<SystemSettings />} />
|
||||
<Route path="user/" element={<UserSettings />} />
|
||||
<Route path="plugin/" element={<PluginSettings />} />
|
||||
</Route>
|
||||
<Route path="part/">
|
||||
<Route index element={<CategoryDetail />} />
|
||||
<Route path="category/:id" element={<CategoryDetail />} />
|
||||
|
@ -65,7 +65,13 @@ export enum ApiPaths {
|
||||
purchase_order_list = 'api-purchase-order-list',
|
||||
|
||||
// Sales Order URLs
|
||||
sales_order_list = 'api-sales-order-list'
|
||||
sales_order_list = 'api-sales-order-list',
|
||||
|
||||
// Plugin URLs
|
||||
plugin_list = 'api-plugin-list',
|
||||
|
||||
project_code_list = 'api-project-code-list',
|
||||
custom_unit_list = 'api-custom-unit-list'
|
||||
}
|
||||
|
||||
/**
|
||||
@ -137,7 +143,12 @@ export function apiEndpoint(path: ApiPaths): string {
|
||||
return 'order/po/';
|
||||
case ApiPaths.sales_order_list:
|
||||
return 'order/so/';
|
||||
|
||||
case ApiPaths.plugin_list:
|
||||
return 'plugins/';
|
||||
case ApiPaths.project_code_list:
|
||||
return 'project-code/';
|
||||
case ApiPaths.custom_unit_list:
|
||||
return 'units/';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
|
@ -7,9 +7,10 @@ import { api } from '../App';
|
||||
import { ApiPaths, apiUrl } from './ApiState';
|
||||
import { Setting } from './states';
|
||||
|
||||
interface SettingsStateProps {
|
||||
export interface SettingsStateProps {
|
||||
settings: Setting[];
|
||||
fetchSettings: () => void;
|
||||
endpoint: ApiPaths;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -18,6 +19,7 @@ interface SettingsStateProps {
|
||||
export const useGlobalSettingsState = create<SettingsStateProps>(
|
||||
(set, get) => ({
|
||||
settings: [],
|
||||
endpoint: ApiPaths.settings_global_list,
|
||||
fetchSettings: async () => {
|
||||
await api
|
||||
.get(apiUrl(ApiPaths.settings_global_list))
|
||||
@ -36,6 +38,7 @@ export const useGlobalSettingsState = create<SettingsStateProps>(
|
||||
*/
|
||||
export const useUserSettingsState = create<SettingsStateProps>((set, get) => ({
|
||||
settings: [],
|
||||
endpoint: ApiPaths.settings_user_list,
|
||||
fetchSettings: async () => {
|
||||
await api
|
||||
.get(apiUrl(ApiPaths.settings_user_list))
|
||||
|
@ -6,6 +6,7 @@ import { UserProps } from './states';
|
||||
|
||||
interface UserStateProps {
|
||||
user: UserProps | undefined;
|
||||
username: () => string;
|
||||
setUser: (newUser: UserProps) => void;
|
||||
fetchUserState: () => void;
|
||||
}
|
||||
@ -15,6 +16,15 @@ interface UserStateProps {
|
||||
*/
|
||||
export const useUserState = create<UserStateProps>((set, get) => ({
|
||||
user: undefined,
|
||||
username: () => {
|
||||
const user: UserProps = get().user as UserProps;
|
||||
|
||||
if (user.first_name || user.last_name) {
|
||||
return `${user.first_name} ${user.last_name}`.trim();
|
||||
} else {
|
||||
return user.username;
|
||||
}
|
||||
},
|
||||
setUser: (newUser: UserProps) => set({ user: newUser }),
|
||||
fetchUserState: async () => {
|
||||
// Fetch user data
|
||||
@ -22,7 +32,8 @@ export const useUserState = create<UserStateProps>((set, get) => ({
|
||||
.get(apiUrl(ApiPaths.user_me))
|
||||
.then((response) => {
|
||||
const user: UserProps = {
|
||||
name: `${response.data.first_name} ${response.data.last_name}`,
|
||||
first_name: response.data?.first_name ?? '',
|
||||
last_name: response.data?.last_name ?? '',
|
||||
email: response.data.email,
|
||||
username: response.data.username
|
||||
};
|
||||
|
@ -9,9 +9,10 @@ export interface HostList {
|
||||
|
||||
// Type interface fully defining the current user
|
||||
export interface UserProps {
|
||||
name: string;
|
||||
email: string;
|
||||
username: string;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
email: string;
|
||||
is_staff?: boolean;
|
||||
is_superuser?: boolean;
|
||||
roles?: Record<string, string[]>;
|
||||
@ -61,7 +62,8 @@ export enum SettingTyp {
|
||||
export enum SettingType {
|
||||
Boolean = 'boolean',
|
||||
Integer = 'integer',
|
||||
String = 'string'
|
||||
String = 'string',
|
||||
Choice = 'choice'
|
||||
}
|
||||
|
||||
export interface PluginProps {
|
||||
|
Loading…
Reference in New Issue
Block a user