mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-28 11:36:44 +00:00
[PUI] User settings panel updates (#7944)
* 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:
parent
a5ab4a30ea
commit
7fbc1fba72
@ -16,6 +16,7 @@ The demo instance has a number of user accounts which you can use to explore the
|
||||
|
||||
| Username | Password | Staff Access | Enabled | Description |
|
||||
| -------- | -------- | ------------ | ------- | ----------- |
|
||||
| noaccess | youshallnotpass | No | Yes | Can login, but has no permissions |
|
||||
| allaccess | nolimits | No | Yes | View / create / edit all pages and items |
|
||||
| reader | readonly | No | Yes | Can view all pages but cannot create, edit or delete database records |
|
||||
| engineer | partsonly | No | Yes | Can manage parts, view stock, but no access to purchase orders or sales orders |
|
||||
|
@ -1,14 +1,17 @@
|
||||
"""InvenTree API version information."""
|
||||
|
||||
# InvenTree API version
|
||||
INVENTREE_API_VERSION = 252
|
||||
INVENTREE_API_VERSION = 253
|
||||
|
||||
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
||||
|
||||
|
||||
INVENTREE_API_TEXT = """
|
||||
|
||||
v252 - 2024-09-30 : https://github.com/inventree/InvenTree/pull/8035
|
||||
v253 - 2024-09-14 : https://github.com/inventree/InvenTree/pull/7944
|
||||
- Adjustments for user API endpoints
|
||||
|
||||
v252 - 2024-09-13 : https://github.com/inventree/InvenTree/pull/8040
|
||||
- Add endpoint for listing all known units
|
||||
|
||||
v251 - 2024-09-06 : https://github.com/inventree/InvenTree/pull/8018
|
||||
|
@ -2,9 +2,13 @@
|
||||
|
||||
import logging
|
||||
|
||||
from rest_framework import serializers
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.http import Http404
|
||||
|
||||
from rest_framework import exceptions, serializers
|
||||
from rest_framework.fields import empty
|
||||
from rest_framework.metadata import SimpleMetadata
|
||||
from rest_framework.request import clone_request
|
||||
from rest_framework.utils import model_meta
|
||||
|
||||
import common.models
|
||||
@ -29,6 +33,40 @@ class InvenTreeMetadata(SimpleMetadata):
|
||||
so we can perform lookup for ForeignKey related fields.
|
||||
"""
|
||||
|
||||
def determine_actions(self, request, view):
|
||||
"""Determine the 'actions' available to the user for the given view.
|
||||
|
||||
Note that this differs from the standard DRF implementation,
|
||||
in that we also allow annotation for the 'GET' method.
|
||||
|
||||
This allows the client to determine what fields are available,
|
||||
even if they are only for a read (GET) operation.
|
||||
|
||||
See SimpleMetadata.determine_actions for more information.
|
||||
"""
|
||||
actions = {}
|
||||
|
||||
for method in {'PUT', 'POST', 'GET'} & set(view.allowed_methods):
|
||||
view.request = clone_request(request, method)
|
||||
try:
|
||||
# Test global permissions
|
||||
if hasattr(view, 'check_permissions'):
|
||||
view.check_permissions(view.request)
|
||||
# Test object permissions
|
||||
if method == 'PUT' and hasattr(view, 'get_object'):
|
||||
view.get_object()
|
||||
except (exceptions.APIException, PermissionDenied, Http404):
|
||||
pass
|
||||
else:
|
||||
# If user has appropriate permissions for the view, include
|
||||
# appropriate metadata about the fields that should be supplied.
|
||||
serializer = view.get_serializer()
|
||||
actions[method] = self.get_serializer_info(serializer)
|
||||
finally:
|
||||
view.request = request
|
||||
|
||||
return actions
|
||||
|
||||
def determine_metadata(self, request, view):
|
||||
"""Overwrite the metadata to adapt to the request user."""
|
||||
self.request = request
|
||||
@ -81,6 +119,7 @@ class InvenTreeMetadata(SimpleMetadata):
|
||||
|
||||
# Map the request method to a permission type
|
||||
rolemap = {
|
||||
'GET': 'view',
|
||||
'POST': 'add',
|
||||
'PUT': 'change',
|
||||
'PATCH': 'change',
|
||||
@ -102,10 +141,6 @@ class InvenTreeMetadata(SimpleMetadata):
|
||||
if 'DELETE' in view.allowed_methods and check(user, table, 'delete'):
|
||||
actions['DELETE'] = {}
|
||||
|
||||
# Add a 'VIEW' action if we are allowed to view
|
||||
if 'GET' in view.allowed_methods and check(user, table, 'view'):
|
||||
actions['GET'] = {}
|
||||
|
||||
metadata['actions'] = actions
|
||||
|
||||
except AttributeError:
|
||||
|
@ -79,6 +79,9 @@ class RolePermission(permissions.BasePermission):
|
||||
# Extract the model name associated with this request
|
||||
model = get_model_for_view(view)
|
||||
|
||||
if model is None:
|
||||
return True
|
||||
|
||||
app_label = model._meta.app_label
|
||||
model_name = model._meta.model_name
|
||||
|
||||
@ -99,6 +102,17 @@ class IsSuperuser(permissions.IsAdminUser):
|
||||
return bool(request.user and request.user.is_superuser)
|
||||
|
||||
|
||||
class IsSuperuserOrReadOnly(permissions.IsAdminUser):
|
||||
"""Allow read-only access to any user, but write access is restricted to superuser users."""
|
||||
|
||||
def has_permission(self, request, view):
|
||||
"""Check if the user is a superuser."""
|
||||
return bool(
|
||||
(request.user and request.user.is_superuser)
|
||||
or request.method in permissions.SAFE_METHODS
|
||||
)
|
||||
|
||||
|
||||
class IsStaffOrReadOnly(permissions.IsAdminUser):
|
||||
"""Allows read-only access to any user, but write access is restricted to staff users."""
|
||||
|
||||
|
@ -403,18 +403,21 @@ class UserSerializer(InvenTreeModelSerializer):
|
||||
read_only_fields = ['username', 'email']
|
||||
|
||||
username = serializers.CharField(label=_('Username'), help_text=_('Username'))
|
||||
|
||||
first_name = serializers.CharField(
|
||||
label=_('First Name'), help_text=_('First name of the user'), allow_blank=True
|
||||
)
|
||||
|
||||
last_name = serializers.CharField(
|
||||
label=_('Last Name'), help_text=_('Last name of the user'), allow_blank=True
|
||||
)
|
||||
|
||||
email = serializers.EmailField(
|
||||
label=_('Email'), help_text=_('Email address of the user'), allow_blank=True
|
||||
)
|
||||
|
||||
|
||||
class ExendedUserSerializer(UserSerializer):
|
||||
class ExtendedUserSerializer(UserSerializer):
|
||||
"""Serializer for a User with a bit more info."""
|
||||
|
||||
from users.serializers import GroupSerializer
|
||||
@ -437,9 +440,11 @@ class ExendedUserSerializer(UserSerializer):
|
||||
is_staff = serializers.BooleanField(
|
||||
label=_('Staff'), help_text=_('Does this user have staff permissions')
|
||||
)
|
||||
|
||||
is_superuser = serializers.BooleanField(
|
||||
label=_('Superuser'), help_text=_('Is this user a superuser')
|
||||
)
|
||||
|
||||
is_active = serializers.BooleanField(
|
||||
label=_('Active'), help_text=_('Is this user account active')
|
||||
)
|
||||
@ -464,9 +469,33 @@ class ExendedUserSerializer(UserSerializer):
|
||||
return super().validate(attrs)
|
||||
|
||||
|
||||
class UserCreateSerializer(ExendedUserSerializer):
|
||||
class MeUserSerializer(ExtendedUserSerializer):
|
||||
"""API serializer specifically for the 'me' endpoint."""
|
||||
|
||||
class Meta(ExtendedUserSerializer.Meta):
|
||||
"""Metaclass options.
|
||||
|
||||
Extends the ExtendedUserSerializer.Meta options,
|
||||
but ensures that certain fields are read-only.
|
||||
"""
|
||||
|
||||
read_only_fields = [
|
||||
*ExtendedUserSerializer.Meta.read_only_fields,
|
||||
'is_active',
|
||||
'is_staff',
|
||||
'is_superuser',
|
||||
]
|
||||
|
||||
|
||||
class UserCreateSerializer(ExtendedUserSerializer):
|
||||
"""Serializer for creating a new User."""
|
||||
|
||||
class Meta(ExtendedUserSerializer.Meta):
|
||||
"""Metaclass options for the UserCreateSerializer."""
|
||||
|
||||
# Prevent creation of users with superuser or staff permissions
|
||||
read_only_fields = ['groups', 'is_staff', 'is_superuser']
|
||||
|
||||
def validate(self, attrs):
|
||||
"""Expanded valiadation for auth."""
|
||||
# Check that the user trying to create a new user is a superuser
|
||||
|
@ -24,6 +24,7 @@ from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
import InvenTree.helpers
|
||||
import InvenTree.permissions
|
||||
from common.models import InvenTreeSetting
|
||||
from InvenTree.filters import SEARCH_ORDER_FILTER
|
||||
from InvenTree.mixins import (
|
||||
@ -33,7 +34,11 @@ from InvenTree.mixins import (
|
||||
RetrieveUpdateAPI,
|
||||
RetrieveUpdateDestroyAPI,
|
||||
)
|
||||
from InvenTree.serializers import ExendedUserSerializer, UserCreateSerializer
|
||||
from InvenTree.serializers import (
|
||||
ExtendedUserSerializer,
|
||||
MeUserSerializer,
|
||||
UserCreateSerializer,
|
||||
)
|
||||
from InvenTree.settings import FRONTEND_URL_BASE
|
||||
from users.models import ApiToken, Owner
|
||||
from users.serializers import (
|
||||
@ -134,24 +139,38 @@ class UserDetail(RetrieveUpdateDestroyAPI):
|
||||
"""Detail endpoint for a single user."""
|
||||
|
||||
queryset = User.objects.all()
|
||||
serializer_class = ExendedUserSerializer
|
||||
serializer_class = ExtendedUserSerializer
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
|
||||
class MeUserDetail(RetrieveUpdateAPI, UserDetail):
|
||||
"""Detail endpoint for current user."""
|
||||
|
||||
serializer_class = MeUserSerializer
|
||||
|
||||
rolemap = {'POST': 'view', 'PUT': 'view', 'PATCH': 'view'}
|
||||
|
||||
def get_object(self):
|
||||
"""Always return the current user object."""
|
||||
return self.request.user
|
||||
|
||||
def get_permission_model(self):
|
||||
"""Return the model for the permission check.
|
||||
|
||||
Note that for this endpoint, the current user can *always* edit their own details.
|
||||
"""
|
||||
return None
|
||||
|
||||
|
||||
class UserList(ListCreateAPI):
|
||||
"""List endpoint for detail on all users."""
|
||||
|
||||
queryset = User.objects.all()
|
||||
serializer_class = UserCreateSerializer
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
permission_classes = [
|
||||
permissions.IsAuthenticated,
|
||||
InvenTree.permissions.IsSuperuserOrReadOnly,
|
||||
]
|
||||
filter_backends = SEARCH_ORDER_FILTER
|
||||
|
||||
search_fields = ['first_name', 'last_name', 'username']
|
||||
|
@ -17,7 +17,10 @@ class UserAPITests(InvenTreeAPITestCase):
|
||||
self.assignRole('admin.add')
|
||||
response = self.options(reverse('api-user-list'), expected_code=200)
|
||||
|
||||
fields = response.data['actions']['POST']
|
||||
# User is *not* a superuser, so user account API is read-only
|
||||
self.assertNotIn('POST', response.data['actions'])
|
||||
|
||||
fields = response.data['actions']['GET']
|
||||
|
||||
# Check some of the field values
|
||||
self.assertEqual(fields['username']['label'], 'Username')
|
||||
|
@ -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 ? (
|
||||
<>
|
||||
{editUser.modal}
|
||||
<Stack gap="xs">
|
||||
<TextInput
|
||||
label="first name"
|
||||
placeholder={t`First name`}
|
||||
{...form.getInputProps('first_name')}
|
||||
<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
|
||||
}
|
||||
]}
|
||||
/>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
) : (
|
||||
<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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -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}
|
||||
|
Loading…
x
Reference in New Issue
Block a user