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 |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 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 information."""
|
||||||
|
|
||||||
# InvenTree API version
|
# 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."""
|
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
||||||
|
|
||||||
|
|
||||||
INVENTREE_API_TEXT = """
|
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
|
- Add endpoint for listing all known units
|
||||||
|
|
||||||
v251 - 2024-09-06 : https://github.com/inventree/InvenTree/pull/8018
|
v251 - 2024-09-06 : https://github.com/inventree/InvenTree/pull/8018
|
||||||
|
@ -2,9 +2,13 @@
|
|||||||
|
|
||||||
import logging
|
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.fields import empty
|
||||||
from rest_framework.metadata import SimpleMetadata
|
from rest_framework.metadata import SimpleMetadata
|
||||||
|
from rest_framework.request import clone_request
|
||||||
from rest_framework.utils import model_meta
|
from rest_framework.utils import model_meta
|
||||||
|
|
||||||
import common.models
|
import common.models
|
||||||
@ -29,6 +33,40 @@ class InvenTreeMetadata(SimpleMetadata):
|
|||||||
so we can perform lookup for ForeignKey related fields.
|
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):
|
def determine_metadata(self, request, view):
|
||||||
"""Overwrite the metadata to adapt to the request user."""
|
"""Overwrite the metadata to adapt to the request user."""
|
||||||
self.request = request
|
self.request = request
|
||||||
@ -81,6 +119,7 @@ class InvenTreeMetadata(SimpleMetadata):
|
|||||||
|
|
||||||
# Map the request method to a permission type
|
# Map the request method to a permission type
|
||||||
rolemap = {
|
rolemap = {
|
||||||
|
'GET': 'view',
|
||||||
'POST': 'add',
|
'POST': 'add',
|
||||||
'PUT': 'change',
|
'PUT': 'change',
|
||||||
'PATCH': 'change',
|
'PATCH': 'change',
|
||||||
@ -102,10 +141,6 @@ class InvenTreeMetadata(SimpleMetadata):
|
|||||||
if 'DELETE' in view.allowed_methods and check(user, table, 'delete'):
|
if 'DELETE' in view.allowed_methods and check(user, table, 'delete'):
|
||||||
actions['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
|
metadata['actions'] = actions
|
||||||
|
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
|
@ -79,6 +79,9 @@ class RolePermission(permissions.BasePermission):
|
|||||||
# Extract the model name associated with this request
|
# Extract the model name associated with this request
|
||||||
model = get_model_for_view(view)
|
model = get_model_for_view(view)
|
||||||
|
|
||||||
|
if model is None:
|
||||||
|
return True
|
||||||
|
|
||||||
app_label = model._meta.app_label
|
app_label = model._meta.app_label
|
||||||
model_name = model._meta.model_name
|
model_name = model._meta.model_name
|
||||||
|
|
||||||
@ -99,6 +102,17 @@ class IsSuperuser(permissions.IsAdminUser):
|
|||||||
return bool(request.user and request.user.is_superuser)
|
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):
|
class IsStaffOrReadOnly(permissions.IsAdminUser):
|
||||||
"""Allows read-only access to any user, but write access is restricted to staff users."""
|
"""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']
|
read_only_fields = ['username', 'email']
|
||||||
|
|
||||||
username = serializers.CharField(label=_('Username'), help_text=_('Username'))
|
username = serializers.CharField(label=_('Username'), help_text=_('Username'))
|
||||||
|
|
||||||
first_name = serializers.CharField(
|
first_name = serializers.CharField(
|
||||||
label=_('First Name'), help_text=_('First name of the user'), allow_blank=True
|
label=_('First Name'), help_text=_('First name of the user'), allow_blank=True
|
||||||
)
|
)
|
||||||
|
|
||||||
last_name = serializers.CharField(
|
last_name = serializers.CharField(
|
||||||
label=_('Last Name'), help_text=_('Last name of the user'), allow_blank=True
|
label=_('Last Name'), help_text=_('Last name of the user'), allow_blank=True
|
||||||
)
|
)
|
||||||
|
|
||||||
email = serializers.EmailField(
|
email = serializers.EmailField(
|
||||||
label=_('Email'), help_text=_('Email address of the user'), allow_blank=True
|
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."""
|
"""Serializer for a User with a bit more info."""
|
||||||
|
|
||||||
from users.serializers import GroupSerializer
|
from users.serializers import GroupSerializer
|
||||||
@ -437,9 +440,11 @@ class ExendedUserSerializer(UserSerializer):
|
|||||||
is_staff = serializers.BooleanField(
|
is_staff = serializers.BooleanField(
|
||||||
label=_('Staff'), help_text=_('Does this user have staff permissions')
|
label=_('Staff'), help_text=_('Does this user have staff permissions')
|
||||||
)
|
)
|
||||||
|
|
||||||
is_superuser = serializers.BooleanField(
|
is_superuser = serializers.BooleanField(
|
||||||
label=_('Superuser'), help_text=_('Is this user a superuser')
|
label=_('Superuser'), help_text=_('Is this user a superuser')
|
||||||
)
|
)
|
||||||
|
|
||||||
is_active = serializers.BooleanField(
|
is_active = serializers.BooleanField(
|
||||||
label=_('Active'), help_text=_('Is this user account active')
|
label=_('Active'), help_text=_('Is this user account active')
|
||||||
)
|
)
|
||||||
@ -464,9 +469,33 @@ class ExendedUserSerializer(UserSerializer):
|
|||||||
return super().validate(attrs)
|
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."""
|
"""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):
|
def validate(self, attrs):
|
||||||
"""Expanded valiadation for auth."""
|
"""Expanded valiadation for auth."""
|
||||||
# Check that the user trying to create a new user is a superuser
|
# 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
|
from rest_framework.views import APIView
|
||||||
|
|
||||||
import InvenTree.helpers
|
import InvenTree.helpers
|
||||||
|
import InvenTree.permissions
|
||||||
from common.models import InvenTreeSetting
|
from common.models import InvenTreeSetting
|
||||||
from InvenTree.filters import SEARCH_ORDER_FILTER
|
from InvenTree.filters import SEARCH_ORDER_FILTER
|
||||||
from InvenTree.mixins import (
|
from InvenTree.mixins import (
|
||||||
@ -33,7 +34,11 @@ from InvenTree.mixins import (
|
|||||||
RetrieveUpdateAPI,
|
RetrieveUpdateAPI,
|
||||||
RetrieveUpdateDestroyAPI,
|
RetrieveUpdateDestroyAPI,
|
||||||
)
|
)
|
||||||
from InvenTree.serializers import ExendedUserSerializer, UserCreateSerializer
|
from InvenTree.serializers import (
|
||||||
|
ExtendedUserSerializer,
|
||||||
|
MeUserSerializer,
|
||||||
|
UserCreateSerializer,
|
||||||
|
)
|
||||||
from InvenTree.settings import FRONTEND_URL_BASE
|
from InvenTree.settings import FRONTEND_URL_BASE
|
||||||
from users.models import ApiToken, Owner
|
from users.models import ApiToken, Owner
|
||||||
from users.serializers import (
|
from users.serializers import (
|
||||||
@ -134,24 +139,38 @@ class UserDetail(RetrieveUpdateDestroyAPI):
|
|||||||
"""Detail endpoint for a single user."""
|
"""Detail endpoint for a single user."""
|
||||||
|
|
||||||
queryset = User.objects.all()
|
queryset = User.objects.all()
|
||||||
serializer_class = ExendedUserSerializer
|
serializer_class = ExtendedUserSerializer
|
||||||
permission_classes = [permissions.IsAuthenticated]
|
permission_classes = [permissions.IsAuthenticated]
|
||||||
|
|
||||||
|
|
||||||
class MeUserDetail(RetrieveUpdateAPI, UserDetail):
|
class MeUserDetail(RetrieveUpdateAPI, UserDetail):
|
||||||
"""Detail endpoint for current user."""
|
"""Detail endpoint for current user."""
|
||||||
|
|
||||||
|
serializer_class = MeUserSerializer
|
||||||
|
|
||||||
|
rolemap = {'POST': 'view', 'PUT': 'view', 'PATCH': 'view'}
|
||||||
|
|
||||||
def get_object(self):
|
def get_object(self):
|
||||||
"""Always return the current user object."""
|
"""Always return the current user object."""
|
||||||
return self.request.user
|
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):
|
class UserList(ListCreateAPI):
|
||||||
"""List endpoint for detail on all users."""
|
"""List endpoint for detail on all users."""
|
||||||
|
|
||||||
queryset = User.objects.all()
|
queryset = User.objects.all()
|
||||||
serializer_class = UserCreateSerializer
|
serializer_class = UserCreateSerializer
|
||||||
permission_classes = [permissions.IsAuthenticated]
|
permission_classes = [
|
||||||
|
permissions.IsAuthenticated,
|
||||||
|
InvenTree.permissions.IsSuperuserOrReadOnly,
|
||||||
|
]
|
||||||
filter_backends = SEARCH_ORDER_FILTER
|
filter_backends = SEARCH_ORDER_FILTER
|
||||||
|
|
||||||
search_fields = ['first_name', 'last_name', 'username']
|
search_fields = ['first_name', 'last_name', 'username']
|
||||||
|
@ -17,7 +17,10 @@ class UserAPITests(InvenTreeAPITestCase):
|
|||||||
self.assignRole('admin.add')
|
self.assignRole('admin.add')
|
||||||
response = self.options(reverse('api-user-list'), expected_code=200)
|
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
|
# Check some of the field values
|
||||||
self.assertEqual(fields['username']['label'], 'Username')
|
self.assertEqual(fields['username']['label'], 'Username')
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { Badge } from '@mantine/core';
|
import { Badge, Skeleton } from '@mantine/core';
|
||||||
|
|
||||||
import { isTrue } from '../../functions/conversion';
|
import { isTrue } from '../../functions/conversion';
|
||||||
|
|
||||||
@ -32,3 +32,11 @@ export function PassFailButton({
|
|||||||
export function YesNoButton({ value }: { value: any }) {
|
export function YesNoButton({ value }: { value: any }) {
|
||||||
return <PassFailButton value={value} passText={t`Yes`} failText={t`No`} />;
|
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 { Trans, t } from '@lingui/macro';
|
||||||
import { Button, Group, Stack, Text, TextInput, Title } from '@mantine/core';
|
import { Group, Stack, Table, Title } from '@mantine/core';
|
||||||
import { useForm } from '@mantine/form';
|
import { IconKey, IconUser } from '@tabler/icons-react';
|
||||||
import { useToggle } from '@mantine/hooks';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
import { api } from '../../../../App';
|
import { YesNoUndefinedButton } from '../../../../components/buttons/YesNoButton';
|
||||||
import { EditButton } from '../../../../components/buttons/EditButton';
|
import { ApiFormFieldSet } from '../../../../components/forms/fields/ApiFormField';
|
||||||
|
import { ActionDropdown } from '../../../../components/items/ActionDropdown';
|
||||||
import { ApiEndpoints } from '../../../../enums/ApiEndpoints';
|
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';
|
import { useUserState } from '../../../../states/UserState';
|
||||||
|
|
||||||
export function AccountDetailPanel() {
|
export function AccountDetailPanel() {
|
||||||
@ -14,66 +16,89 @@ export function AccountDetailPanel() {
|
|||||||
state.user,
|
state.user,
|
||||||
state.fetchUserState
|
state.fetchUserState
|
||||||
]);
|
]);
|
||||||
const form = useForm({ initialValues: user });
|
|
||||||
const [editing, setEditing] = useToggle([false, true] as const);
|
const userFields: ApiFormFieldSet = useMemo(() => {
|
||||||
function SaveData(values: any) {
|
return {
|
||||||
// copy values over to break form rendering link
|
first_name: {},
|
||||||
const urlVals = { ...values };
|
last_name: {}
|
||||||
urlVals.is_active = true;
|
};
|
||||||
// send
|
}, []);
|
||||||
api
|
|
||||||
.put(apiUrl(ApiEndpoints.user_me), urlVals)
|
const editUser = useEditApiFormModal({
|
||||||
.then((res) => {
|
title: t`Edit User Information`,
|
||||||
if (res.status === 200) {
|
url: ApiEndpoints.user_me,
|
||||||
setEditing();
|
onFormSuccess: fetchUserState,
|
||||||
fetchUserState();
|
fields: userFields,
|
||||||
}
|
successMessage: t`User details updated`
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
console.error('ERR: Error saving user data');
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={form.onSubmit((values) => SaveData(values))}>
|
<>
|
||||||
<Group>
|
{editUser.modal}
|
||||||
<Title order={3}>
|
|
||||||
<Trans>Account Details</Trans>
|
|
||||||
</Title>
|
|
||||||
<EditButton setEditing={setEditing} editing={editing} />
|
|
||||||
</Group>
|
|
||||||
<Group>
|
|
||||||
{editing ? (
|
|
||||||
<Stack gap="xs">
|
<Stack gap="xs">
|
||||||
<TextInput
|
<Group justify="space-between">
|
||||||
label="first name"
|
<Title order={3}>
|
||||||
placeholder={t`First name`}
|
<Trans>User Details</Trans>
|
||||||
{...form.getInputProps('first_name')}
|
</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>
|
</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>
|
||||||
) : (
|
</>
|
||||||
<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 { Container, Grid, SimpleGrid } from '@mantine/core';
|
||||||
|
|
||||||
import { AccountDetailPanel } from './AccountDetailPanel';
|
import { AccountDetailPanel } from './AccountDetailPanel';
|
||||||
import { DisplaySettingsPanel } from './DisplaySettingsPanel';
|
|
||||||
import { UserTheme } from './UserThemePanel';
|
import { UserTheme } from './UserThemePanel';
|
||||||
|
|
||||||
export function AccountContent() {
|
export function AccountContent() {
|
||||||
@ -18,9 +17,6 @@ export function AccountContent() {
|
|||||||
<Grid.Col>
|
<Grid.Col>
|
||||||
<UserTheme height={SECONDARY_COL_HEIGHT} />
|
<UserTheme height={SECONDARY_COL_HEIGHT} />
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
<Grid.Col>
|
|
||||||
<DisplaySettingsPanel height={SECONDARY_COL_HEIGHT} />
|
|
||||||
</Grid.Col>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import { Trans, t } from '@lingui/macro';
|
import { Trans, t } from '@lingui/macro';
|
||||||
import {
|
import {
|
||||||
|
ActionIcon,
|
||||||
|
Button,
|
||||||
ColorInput,
|
ColorInput,
|
||||||
ColorPicker,
|
ColorPicker,
|
||||||
Container,
|
Container,
|
||||||
@ -9,13 +11,18 @@ import {
|
|||||||
Select,
|
Select,
|
||||||
Slider,
|
Slider,
|
||||||
Table,
|
Table,
|
||||||
Title
|
Title,
|
||||||
|
useMantineTheme
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
|
import { IconReload, IconRestore } from '@tabler/icons-react';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import { ColorToggle } from '../../../../components/items/ColorToggle';
|
||||||
|
import { LanguageSelect } from '../../../../components/items/LanguageSelect';
|
||||||
import { SizeMarks } from '../../../../defaults/defaults';
|
import { SizeMarks } from '../../../../defaults/defaults';
|
||||||
|
import { notYetImplemented } from '../../../../functions/notifications';
|
||||||
|
import { IS_DEV } from '../../../../main';
|
||||||
import { useLocalState } from '../../../../states/LocalState';
|
import { useLocalState } from '../../../../states/LocalState';
|
||||||
import { theme } from '../../../../theme';
|
|
||||||
|
|
||||||
function getLkp(color: string) {
|
function getLkp(color: string) {
|
||||||
return { [DEFAULT_THEME.colors[color][6]]: color };
|
return { [DEFAULT_THEME.colors[color][6]]: color };
|
||||||
@ -26,18 +33,24 @@ const LOOKUP = Object.assign(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export function UserTheme({ height }: { height: number }) {
|
export function UserTheme({ height }: { height: number }) {
|
||||||
// primary color
|
const theme = useMantineTheme();
|
||||||
function changePrimary(color: string) {
|
|
||||||
useLocalState.setState({ primaryColor: LOOKUP[color] });
|
const [themeLoader, setThemeLoader] = useLocalState((state) => [
|
||||||
}
|
state.loader,
|
||||||
|
state.setLoader
|
||||||
|
]);
|
||||||
|
|
||||||
// white color
|
// white color
|
||||||
const [whiteColor, setWhiteColor] = useState(theme.white);
|
const [whiteColor, setWhiteColor] = useState(theme.white);
|
||||||
|
|
||||||
function changeWhite(color: string) {
|
function changeWhite(color: string) {
|
||||||
useLocalState.setState({ whiteColor: color });
|
useLocalState.setState({ whiteColor: color });
|
||||||
setWhiteColor(color);
|
setWhiteColor(color);
|
||||||
}
|
}
|
||||||
|
|
||||||
// black color
|
// black color
|
||||||
const [blackColor, setBlackColor] = useState(theme.black);
|
const [blackColor, setBlackColor] = useState(theme.black);
|
||||||
|
|
||||||
function changeBlack(color: string) {
|
function changeBlack(color: string) {
|
||||||
useLocalState.setState({ blackColor: color });
|
useLocalState.setState({ blackColor: color });
|
||||||
setBlackColor(color);
|
setBlackColor(color);
|
||||||
@ -48,6 +61,7 @@ export function UserTheme({ height }: { height: number }) {
|
|||||||
if (obj) return obj;
|
if (obj) return obj;
|
||||||
return SizeMarks[0];
|
return SizeMarks[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDefaultRadius() {
|
function getDefaultRadius() {
|
||||||
const obj = SizeMarks.find(
|
const obj = SizeMarks.find(
|
||||||
(mark) => mark.label === useLocalState.getState().radius
|
(mark) => mark.label === useLocalState.getState().radius
|
||||||
@ -60,16 +74,23 @@ export function UserTheme({ height }: { height: number }) {
|
|||||||
setRadius(value);
|
setRadius(value);
|
||||||
useLocalState.setState({ radius: getMark(value).label });
|
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 = [
|
const loaderDate = [
|
||||||
{ value: 'bars', label: t`bars` },
|
{ value: 'bars', label: t`Bars` },
|
||||||
{ value: 'oval', label: t`oval` },
|
{ value: 'oval', label: t`Oval` },
|
||||||
{ value: 'dots', label: t`dots` }
|
{ value: 'dots', label: t`Dots` }
|
||||||
];
|
];
|
||||||
const [themeLoader, setThemeLoader] = useLocalState((state) => [
|
|
||||||
state.loader,
|
|
||||||
state.setLoader
|
|
||||||
]);
|
|
||||||
function changeLoader(value: string | null) {
|
function changeLoader(value: string | null) {
|
||||||
if (value === null) return;
|
if (value === null) return;
|
||||||
setThemeLoader(value);
|
setThemeLoader(value);
|
||||||
@ -78,13 +99,39 @@ export function UserTheme({ height }: { height: number }) {
|
|||||||
return (
|
return (
|
||||||
<Container w="100%" mih={height} p={0}>
|
<Container w="100%" mih={height} p={0}>
|
||||||
<Title order={3}>
|
<Title order={3}>
|
||||||
<Trans>Theme</Trans>
|
<Trans>Display Settings</Trans>
|
||||||
</Title>
|
</Title>
|
||||||
<Table>
|
<Table>
|
||||||
<Table.Tbody>
|
<Table.Tbody>
|
||||||
<Table.Tr>
|
<Table.Tr>
|
||||||
<Table.Td>
|
<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>
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
@ -94,6 +141,11 @@ export function UserTheme({ height }: { height: number }) {
|
|||||||
swatches={Object.keys(LOOKUP)}
|
swatches={Object.keys(LOOKUP)}
|
||||||
/>
|
/>
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
<Button color={theme.primaryColor} variant="light">
|
||||||
|
<Trans>Example</Trans>
|
||||||
|
</Button>
|
||||||
|
</Table.Td>
|
||||||
</Table.Tr>
|
</Table.Tr>
|
||||||
<Table.Tr>
|
<Table.Tr>
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
@ -102,6 +154,14 @@ export function UserTheme({ height }: { height: number }) {
|
|||||||
<Table.Td>
|
<Table.Td>
|
||||||
<ColorInput value={whiteColor} onChange={changeWhite} />
|
<ColorInput value={whiteColor} onChange={changeWhite} />
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
<ActionIcon
|
||||||
|
variant="default"
|
||||||
|
onClick={() => changeWhite('#FFFFFF')}
|
||||||
|
>
|
||||||
|
<IconRestore />
|
||||||
|
</ActionIcon>
|
||||||
|
</Table.Td>
|
||||||
</Table.Tr>
|
</Table.Tr>
|
||||||
<Table.Tr>
|
<Table.Tr>
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
@ -110,6 +170,14 @@ export function UserTheme({ height }: { height: number }) {
|
|||||||
<Table.Td>
|
<Table.Td>
|
||||||
<ColorInput value={blackColor} onChange={changeBlack} />
|
<ColorInput value={blackColor} onChange={changeBlack} />
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
<ActionIcon
|
||||||
|
variant="default"
|
||||||
|
onClick={() => changeBlack('#000000')}
|
||||||
|
>
|
||||||
|
<IconRestore />
|
||||||
|
</ActionIcon>
|
||||||
|
</Table.Td>
|
||||||
</Table.Tr>
|
</Table.Tr>
|
||||||
<Table.Tr>
|
<Table.Tr>
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
@ -132,13 +200,17 @@ export function UserTheme({ height }: { height: number }) {
|
|||||||
<Trans>Loader</Trans>
|
<Trans>Loader</Trans>
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
<Group align="center">
|
<Group justify="left">
|
||||||
<Select
|
<Select
|
||||||
data={loaderDate}
|
data={loaderDate}
|
||||||
value={themeLoader}
|
value={themeLoader}
|
||||||
onChange={changeLoader}
|
onChange={changeLoader}
|
||||||
/>
|
/>
|
||||||
<Loader type={themeLoader} mah={18} />
|
</Group>
|
||||||
|
</Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
<Group justify="left">
|
||||||
|
<Loader type={themeLoader} mah={16} size="sm" />
|
||||||
</Group>
|
</Group>
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
</Table.Tr>
|
</Table.Tr>
|
||||||
|
@ -153,7 +153,8 @@ export function InvenTreeTable<T = any>({
|
|||||||
getTableColumnNames,
|
getTableColumnNames,
|
||||||
setTableColumnNames,
|
setTableColumnNames,
|
||||||
getTableSorting,
|
getTableSorting,
|
||||||
setTableSorting
|
setTableSorting,
|
||||||
|
loader
|
||||||
} = useLocalState();
|
} = useLocalState();
|
||||||
const [fieldNames, setFieldNames] = useState<Record<string, string>>({});
|
const [fieldNames, setFieldNames] = useState<Record<string, string>>({});
|
||||||
|
|
||||||
@ -192,8 +193,9 @@ export function InvenTreeTable<T = any>({
|
|||||||
// Extract field information from the API
|
// Extract field information from the API
|
||||||
|
|
||||||
let names: Record<string, string> = {};
|
let names: Record<string, string> = {};
|
||||||
|
|
||||||
let fields: ApiFormFieldSet =
|
let fields: ApiFormFieldSet =
|
||||||
extractAvailableFields(response, 'POST', true) || {};
|
extractAvailableFields(response, 'GET', true) || {};
|
||||||
|
|
||||||
// Extract flattened map of fields
|
// Extract flattened map of fields
|
||||||
mapFields(fields, (path, field) => {
|
mapFields(fields, (path, field) => {
|
||||||
@ -720,7 +722,7 @@ export function InvenTreeTable<T = any>({
|
|||||||
withColumnBorders
|
withColumnBorders
|
||||||
striped
|
striped
|
||||||
highlightOnHover
|
highlightOnHover
|
||||||
loaderType="dots"
|
loaderType={loader}
|
||||||
pinLastColumn={tableProps.rowActions != undefined}
|
pinLastColumn={tableProps.rowActions != undefined}
|
||||||
idAccessor={tableProps.idAccessor}
|
idAccessor={tableProps.idAccessor}
|
||||||
minHeight={300}
|
minHeight={300}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user