2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-06-23 23:30:54 +00:00

[PUI] User settings panel updates ()

* Simplify user theme settings

* Cleanup

* Fix permission on user list endpoint

* Update AccountDetailPanel to use modal form

* Update components

* UI updates

* Implement default colors

* Display more user details (read only)

* Add specific "MeUserSerializer"

- Prevent certain attributes from being adjusted

* Add <YesNoUndefinedButton>

* Allow role checks to be bypassed for a given view

- Override the 'get_permission_model' attribute with None

* Enable 'GET' metadata

- Required for extracting field information even if we only have 'read' permissions
- e.g. getting table columns for users without write perms
- use 'GET' action when reading table cols

* Add info on new user account

* Fix boolean expression wrapper

* Ruff fixes

* Adjust icon

* Update unit test

* Bummp API version

* Table layout fix
This commit is contained in:
Oliver
2024-09-14 12:30:46 +10:00
committed by GitHub
parent a5ab4a30ea
commit 7fbc1fba72
13 changed files with 311 additions and 155 deletions

@ -1,5 +1,5 @@
import { t } from '@lingui/macro';
import { Badge } from '@mantine/core';
import { Badge, Skeleton } from '@mantine/core';
import { isTrue } from '../../functions/conversion';
@ -32,3 +32,11 @@ export function PassFailButton({
export function YesNoButton({ value }: { value: any }) {
return <PassFailButton value={value} passText={t`Yes`} failText={t`No`} />;
}
export function YesNoUndefinedButton({ value }: { value?: boolean }) {
if (value === undefined) {
return <Skeleton height={15} width={32} />;
} else {
return <YesNoButton value={value} />;
}
}

@ -1,12 +1,14 @@
import { Trans, t } from '@lingui/macro';
import { Button, Group, Stack, Text, TextInput, Title } from '@mantine/core';
import { useForm } from '@mantine/form';
import { useToggle } from '@mantine/hooks';
import { Group, Stack, Table, Title } from '@mantine/core';
import { IconKey, IconUser } from '@tabler/icons-react';
import { useMemo } from 'react';
import { api } from '../../../../App';
import { EditButton } from '../../../../components/buttons/EditButton';
import { YesNoUndefinedButton } from '../../../../components/buttons/YesNoButton';
import { ApiFormFieldSet } from '../../../../components/forms/fields/ApiFormField';
import { ActionDropdown } from '../../../../components/items/ActionDropdown';
import { ApiEndpoints } from '../../../../enums/ApiEndpoints';
import { apiUrl } from '../../../../states/ApiState';
import { notYetImplemented } from '../../../../functions/notifications';
import { useEditApiFormModal } from '../../../../hooks/UseForm';
import { useUserState } from '../../../../states/UserState';
export function AccountDetailPanel() {
@ -14,66 +16,89 @@ export function AccountDetailPanel() {
state.user,
state.fetchUserState
]);
const form = useForm({ initialValues: user });
const [editing, setEditing] = useToggle([false, true] as const);
function SaveData(values: any) {
// copy values over to break form rendering link
const urlVals = { ...values };
urlVals.is_active = true;
// send
api
.put(apiUrl(ApiEndpoints.user_me), urlVals)
.then((res) => {
if (res.status === 200) {
setEditing();
fetchUserState();
}
})
.catch(() => {
console.error('ERR: Error saving user data');
});
}
const userFields: ApiFormFieldSet = useMemo(() => {
return {
first_name: {},
last_name: {}
};
}, []);
const editUser = useEditApiFormModal({
title: t`Edit User Information`,
url: ApiEndpoints.user_me,
onFormSuccess: fetchUserState,
fields: userFields,
successMessage: t`User details updated`
});
return (
<form onSubmit={form.onSubmit((values) => SaveData(values))}>
<Group>
<Title order={3}>
<Trans>Account Details</Trans>
</Title>
<EditButton setEditing={setEditing} editing={editing} />
</Group>
<Group>
{editing ? (
<Stack gap="xs">
<TextInput
label="first name"
placeholder={t`First name`}
{...form.getInputProps('first_name')}
/>
<TextInput
label="Last name"
placeholder={t`Last name`}
{...form.getInputProps('last_name')}
/>
<Group justify="right" mt="md">
<Button type="submit">
<Trans>Submit</Trans>
</Button>
</Group>
</Stack>
) : (
<Stack gap="0">
<Text>
<Trans>First name: </Trans>
{form.values.first_name}
</Text>
<Text>
<Trans>Last name: </Trans>
{form.values.last_name}
</Text>
</Stack>
)}
</Group>
</form>
<>
{editUser.modal}
<Stack gap="xs">
<Group justify="space-between">
<Title order={3}>
<Trans>User Details</Trans>
</Title>
<ActionDropdown
tooltip={t`User Actions`}
icon={<IconUser />}
actions={[
{
name: t`Edit User`,
icon: <IconUser />,
tooltip: t`Edit User Information`,
onClick: editUser.open
},
{
name: t`Set Password`,
icon: <IconKey />,
tooltip: t`Set User Password`,
onClick: notYetImplemented
}
]}
/>
</Group>
<Table>
<Table.Tbody>
<Table.Tr>
<Table.Td>
<Trans>Username</Trans>
</Table.Td>
<Table.Td>{user?.username}</Table.Td>
</Table.Tr>
<Table.Tr>
<Table.Td>
<Trans>First Name</Trans>
</Table.Td>
<Table.Td>{user?.first_name}</Table.Td>
</Table.Tr>
<Table.Tr>
<Table.Td>
<Trans>Last Name</Trans>
</Table.Td>
<Table.Td>{user?.last_name}</Table.Td>
</Table.Tr>
<Table.Tr>
<Table.Td>
<Trans>Staff Access</Trans>
</Table.Td>
<Table.Td>
<YesNoUndefinedButton value={user?.is_staff} />
</Table.Td>
</Table.Tr>
<Table.Tr>
<Table.Td>
<Trans>Superuser</Trans>
</Table.Td>
<Table.Td>
<YesNoUndefinedButton value={user?.is_superuser} />
</Table.Td>
</Table.Tr>
</Table.Tbody>
</Table>
</Stack>
</>
);
}

@ -1,51 +0,0 @@
import { Trans } from '@lingui/macro';
import { Button, Container, Group, Table, Title } from '@mantine/core';
import { ColorToggle } from '../../../../components/items/ColorToggle';
import { LanguageSelect } from '../../../../components/items/LanguageSelect';
import { IS_DEV } from '../../../../main';
import { useLocalState } from '../../../../states/LocalState';
export function DisplaySettingsPanel({ height }: { height: number }) {
function enablePseudoLang(): void {
useLocalState.setState({ language: 'pseudo-LOCALE' });
}
return (
<Container w="100%" mih={height} p={0}>
<Title order={3}>
<Trans>Display Settings</Trans>
</Title>
<Table>
<tbody>
<tr>
<td>
<Trans>Color Mode</Trans>
</td>
<td>
<Group>
<ColorToggle />
</Group>
</td>
</tr>
<tr>
<td>
<Trans>Language</Trans>
</td>
<td>
{' '}
<Group>
<LanguageSelect width={200} />
{IS_DEV && (
<Button onClick={enablePseudoLang} variant="light">
<Trans>Use pseudo language</Trans>
</Button>
)}
</Group>
</td>
</tr>
</tbody>
</Table>
</Container>
);
}

@ -1,7 +1,6 @@
import { Container, Grid, SimpleGrid } from '@mantine/core';
import { AccountDetailPanel } from './AccountDetailPanel';
import { DisplaySettingsPanel } from './DisplaySettingsPanel';
import { UserTheme } from './UserThemePanel';
export function AccountContent() {
@ -18,9 +17,6 @@ export function AccountContent() {
<Grid.Col>
<UserTheme height={SECONDARY_COL_HEIGHT} />
</Grid.Col>
<Grid.Col>
<DisplaySettingsPanel height={SECONDARY_COL_HEIGHT} />
</Grid.Col>
</Grid>
</SimpleGrid>
</div>

@ -1,5 +1,7 @@
import { Trans, t } from '@lingui/macro';
import {
ActionIcon,
Button,
ColorInput,
ColorPicker,
Container,
@ -9,13 +11,18 @@ import {
Select,
Slider,
Table,
Title
Title,
useMantineTheme
} from '@mantine/core';
import { IconReload, IconRestore } from '@tabler/icons-react';
import { useState } from 'react';
import { ColorToggle } from '../../../../components/items/ColorToggle';
import { LanguageSelect } from '../../../../components/items/LanguageSelect';
import { SizeMarks } from '../../../../defaults/defaults';
import { notYetImplemented } from '../../../../functions/notifications';
import { IS_DEV } from '../../../../main';
import { useLocalState } from '../../../../states/LocalState';
import { theme } from '../../../../theme';
function getLkp(color: string) {
return { [DEFAULT_THEME.colors[color][6]]: color };
@ -26,18 +33,24 @@ const LOOKUP = Object.assign(
);
export function UserTheme({ height }: { height: number }) {
// primary color
function changePrimary(color: string) {
useLocalState.setState({ primaryColor: LOOKUP[color] });
}
const theme = useMantineTheme();
const [themeLoader, setThemeLoader] = useLocalState((state) => [
state.loader,
state.setLoader
]);
// white color
const [whiteColor, setWhiteColor] = useState(theme.white);
function changeWhite(color: string) {
useLocalState.setState({ whiteColor: color });
setWhiteColor(color);
}
// black color
const [blackColor, setBlackColor] = useState(theme.black);
function changeBlack(color: string) {
useLocalState.setState({ blackColor: color });
setBlackColor(color);
@ -48,6 +61,7 @@ export function UserTheme({ height }: { height: number }) {
if (obj) return obj;
return SizeMarks[0];
}
function getDefaultRadius() {
const obj = SizeMarks.find(
(mark) => mark.label === useLocalState.getState().radius
@ -60,16 +74,23 @@ export function UserTheme({ height }: { height: number }) {
setRadius(value);
useLocalState.setState({ radius: getMark(value).label });
}
// loader
// Set theme primary color
function changePrimary(color: string) {
useLocalState.setState({ primaryColor: LOOKUP[color] });
}
function enablePseudoLang(): void {
useLocalState.setState({ language: 'pseudo-LOCALE' });
}
// Custom loading indicator
const loaderDate = [
{ value: 'bars', label: t`bars` },
{ value: 'oval', label: t`oval` },
{ value: 'dots', label: t`dots` }
{ value: 'bars', label: t`Bars` },
{ value: 'oval', label: t`Oval` },
{ value: 'dots', label: t`Dots` }
];
const [themeLoader, setThemeLoader] = useLocalState((state) => [
state.loader,
state.setLoader
]);
function changeLoader(value: string | null) {
if (value === null) return;
setThemeLoader(value);
@ -78,13 +99,39 @@ export function UserTheme({ height }: { height: number }) {
return (
<Container w="100%" mih={height} p={0}>
<Title order={3}>
<Trans>Theme</Trans>
<Trans>Display Settings</Trans>
</Title>
<Table>
<Table.Tbody>
<Table.Tr>
<Table.Td>
<Trans>Primary color</Trans>
<Trans>Language</Trans>
</Table.Td>
<Table.Td>
<LanguageSelect width={200} />
</Table.Td>
<Table.Td>
{IS_DEV && (
<Button onClick={enablePseudoLang} variant="light">
<Trans>Use pseudo language</Trans>
</Button>
)}
</Table.Td>
</Table.Tr>
<Table.Tr>
<Table.Td>
<Trans>Color Mode</Trans>
</Table.Td>
<Table.Td>
<Group justify="left">
<ColorToggle />
</Group>
</Table.Td>
<Table.Td></Table.Td>
</Table.Tr>
<Table.Tr>
<Table.Td>
<Trans>Highlight color</Trans>
</Table.Td>
<Table.Td>
<ColorPicker
@ -94,6 +141,11 @@ export function UserTheme({ height }: { height: number }) {
swatches={Object.keys(LOOKUP)}
/>
</Table.Td>
<Table.Td>
<Button color={theme.primaryColor} variant="light">
<Trans>Example</Trans>
</Button>
</Table.Td>
</Table.Tr>
<Table.Tr>
<Table.Td>
@ -102,6 +154,14 @@ export function UserTheme({ height }: { height: number }) {
<Table.Td>
<ColorInput value={whiteColor} onChange={changeWhite} />
</Table.Td>
<Table.Td>
<ActionIcon
variant="default"
onClick={() => changeWhite('#FFFFFF')}
>
<IconRestore />
</ActionIcon>
</Table.Td>
</Table.Tr>
<Table.Tr>
<Table.Td>
@ -110,6 +170,14 @@ export function UserTheme({ height }: { height: number }) {
<Table.Td>
<ColorInput value={blackColor} onChange={changeBlack} />
</Table.Td>
<Table.Td>
<ActionIcon
variant="default"
onClick={() => changeBlack('#000000')}
>
<IconRestore />
</ActionIcon>
</Table.Td>
</Table.Tr>
<Table.Tr>
<Table.Td>
@ -132,13 +200,17 @@ export function UserTheme({ height }: { height: number }) {
<Trans>Loader</Trans>
</Table.Td>
<Table.Td>
<Group align="center">
<Group justify="left">
<Select
data={loaderDate}
value={themeLoader}
onChange={changeLoader}
/>
<Loader type={themeLoader} mah={18} />
</Group>
</Table.Td>
<Table.Td>
<Group justify="left">
<Loader type={themeLoader} mah={16} size="sm" />
</Group>
</Table.Td>
</Table.Tr>

@ -153,7 +153,8 @@ export function InvenTreeTable<T = any>({
getTableColumnNames,
setTableColumnNames,
getTableSorting,
setTableSorting
setTableSorting,
loader
} = useLocalState();
const [fieldNames, setFieldNames] = useState<Record<string, string>>({});
@ -192,8 +193,9 @@ export function InvenTreeTable<T = any>({
// Extract field information from the API
let names: Record<string, string> = {};
let fields: ApiFormFieldSet =
extractAvailableFields(response, 'POST', true) || {};
extractAvailableFields(response, 'GET', true) || {};
// Extract flattened map of fields
mapFields(fields, (path, field) => {
@ -720,7 +722,7 @@ export function InvenTreeTable<T = any>({
withColumnBorders
striped
highlightOnHover
loaderType="dots"
loaderType={loader}
pinLastColumn={tableProps.rowActions != undefined}
idAccessor={tableProps.idAccessor}
minHeight={300}