P-UI: Fast language / theme / server selection (#5301)

* added language toggle component

* added language and theme controls onto start page

* moved host selection out of auth

* optimized rendering

* make server option less obvious

* changed EditButton save symbol

* longer welcome text

* removed ColorToggle color schema

* reduced code

* disabled host selection when options are changing

* fix type error

* use GH reporter

* fix tests?

* compile frontend

* fix assertation

* revert unneeded change

* split up into more components

* separated functions / use cases for LanguageToggle more

* moved color toggle to profile

* moved language out of main menu into profile

* remapped settings link
This commit is contained in:
Matthias Mair 2023-07-22 14:19:19 +02:00 committed by GitHub
parent f70294b247
commit f227315ad1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 226 additions and 151 deletions

View File

@ -414,7 +414,7 @@ jobs:
- name: Set up test data - name: Set up test data
run: invoke setup-test -i run: invoke setup-test -i
- name: Install dependencies - name: Install dependencies
run: cd src/frontend && yarn install run: inv frontend-compile
- name: Install Playwright Browsers - name: Install Playwright Browsers
run: cd src/frontend && npx playwright install --with-deps run: cd src/frontend && npx playwright install --with-deps
- name: Run Playwright tests - name: Run Playwright tests

View File

@ -6,7 +6,7 @@ export default defineConfig({
forbidOnly: !!process.env.CI, forbidOnly: !!process.env.CI,
retries: process.env.CI ? 1 : 0, retries: process.env.CI ? 1 : 0,
workers: process.env.CI ? 2 : undefined, workers: process.env.CI ? 2 : undefined,
reporter: 'html', reporter: process.env.CI ? 'github' : 'list',
/* Configure projects for major browsers */ /* Configure projects for major browsers */
projects: [ projects: [

View File

@ -0,0 +1,25 @@
import { Center, Group, Tooltip } from '@mantine/core';
import { IconServer } from '@tabler/icons-react';
import { ColorToggle } from '../items/ColorToggle';
import { LanguageToggle } from '../items/LanguageToggle';
export function AuthFormOptions({
hostname,
toggleHostEdit
}: {
hostname: string;
toggleHostEdit: () => void;
}) {
return (
<Center mx={'md'}>
<Group>
<ColorToggle />
<LanguageToggle />
<Tooltip label={hostname}>
<IconServer onClick={toggleHostEdit} />
</Tooltip>
</Group>
</Center>
);
}

View File

@ -16,19 +16,8 @@ import { IconCheck } from '@tabler/icons-react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { doClassicLogin, doSimpleLogin } from '../../functions/auth'; import { doClassicLogin, doSimpleLogin } from '../../functions/auth';
import { EditButton } from '../items/EditButton';
export function AuthenticationForm({ export function AuthenticationForm() {
hostname,
editing,
setEditing,
selectElement
}: {
hostname: string;
editing: boolean;
setEditing: (value?: React.SetStateAction<boolean> | undefined) => void;
selectElement: JSX.Element;
}) {
const classicForm = useForm({ const classicForm = useForm({
initialValues: { username: '', password: '' } initialValues: { username: '', password: '' }
}); });
@ -82,10 +71,7 @@ export function AuthenticationForm({
return ( return (
<Paper radius="md" p="xl" withBorder> <Paper radius="md" p="xl" withBorder>
<Text size="lg" weight={500}> <Text size="lg" weight={500}>
<Group> <Trans>Welcome, log in below</Trans>
{!editing ? hostname : selectElement}
<EditButton setEditing={setEditing} editing={editing} />
</Group>
</Text> </Text>
<form onSubmit={classicForm.onSubmit(() => {})}> <form onSubmit={classicForm.onSubmit(() => {})}>
{classicLoginMode ? ( {classicLoginMode ? (

View File

@ -0,0 +1,77 @@
import { Trans } from '@lingui/macro';
import { Divider, Group, Select, Text, Title } from '@mantine/core';
import { useToggle } from '@mantine/hooks';
import { IconCheck } from '@tabler/icons-react';
import { useLocalState } from '../../states/LocalState';
import { HostList } from '../../states/states';
import { EditButton } from '../items/EditButton';
import { HostOptionsForm } from './HostOptionsForm';
export function InstanceOptions({
hostKey,
ChangeHost,
setHostEdit
}: {
hostKey: string;
ChangeHost: (newHost: string) => void;
setHostEdit: () => void;
}) {
const [HostListEdit, setHostListEdit] = useToggle([false, true] as const);
const [setHost, setHostList, hostList] = useLocalState((state) => [
state.setHost,
state.setHostList,
state.hostList
]);
const hostListData = Object.keys(hostList).map((key) => ({
value: key,
label: hostList[key].name
}));
function SaveOptions(newHostList: HostList): void {
setHostList(newHostList);
if (newHostList[hostKey] === undefined) {
setHost('', '');
}
setHostListEdit();
}
return (
<>
<Title order={3}>
<Trans>Select destination instance</Trans>
</Title>
<Group>
<Group>
<Select
value={hostKey}
onChange={ChangeHost}
data={hostListData}
disabled={HostListEdit}
/>
<EditButton
setEditing={setHostListEdit}
editing={HostListEdit}
disabled={HostListEdit}
/>
</Group>
<EditButton
setEditing={setHostEdit}
editing={true}
disabled={HostListEdit}
saveIcon={<IconCheck />}
/>
</Group>
{HostListEdit && (
<>
<Divider my={'sm'} />
<Text>
<Trans>Edit possible host options</Trans>
</Text>
<HostOptionsForm data={hostList} saveOptions={SaveOptions} />
</>
)}
</>
);
}

View File

@ -10,10 +10,6 @@ export function ColorToggle() {
onClick={() => toggleColorScheme()} onClick={() => toggleColorScheme()}
size="lg" size="lg"
sx={(theme) => ({ sx={(theme) => ({
backgroundColor:
theme.colorScheme === 'dark'
? theme.colors.dark[6]
: theme.colors.gray[0],
color: color:
theme.colorScheme === 'dark' theme.colorScheme === 'dark'
? theme.colors.yellow[4] ? theme.colors.yellow[4]

View File

@ -4,15 +4,18 @@ import { IconDeviceFloppy, IconEdit } from '@tabler/icons-react';
export function EditButton({ export function EditButton({
setEditing, setEditing,
editing, editing,
disabled disabled,
saveIcon
}: { }: {
setEditing: (value?: React.SetStateAction<boolean> | undefined) => void; setEditing: (value?: React.SetStateAction<boolean> | undefined) => void;
editing: boolean; editing: boolean;
disabled?: boolean; disabled?: boolean;
saveIcon?: JSX.Element;
}) { }) {
saveIcon = saveIcon || <IconDeviceFloppy />;
return ( return (
<ActionIcon onClick={() => setEditing()} disabled={disabled}> <ActionIcon onClick={() => setEditing()} disabled={disabled}>
{editing ? <IconDeviceFloppy /> : <IconEdit />} {editing ? saveIcon : <IconEdit />}
</ActionIcon> </ActionIcon>
); );
} }

View File

@ -0,0 +1,26 @@
import { Select } from '@mantine/core';
import { useEffect, useState } from 'react';
import { Locales, languages } from '../../contexts/LanguageContext';
import { useLocalState } from '../../states/LocalState';
export function LanguageSelect() {
const [value, setValue] = useState<string | null>(null);
const [locale, setLanguage] = useLocalState((state) => [
state.language,
state.setLanguage
]);
// change global language on change
useEffect(() => {
if (value === null) return;
setLanguage(value as Locales);
}, [value]);
// set language on component load
useEffect(() => {
setValue(locale);
}, [locale]);
return <Select w={80} data={languages} value={value} onChange={setValue} />;
}

View File

@ -0,0 +1,29 @@
import { ActionIcon, Group } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
import { IconLanguage } from '@tabler/icons-react';
import { LanguageSelect } from './LanguageSelect';
export function LanguageToggle() {
const [open, toggle] = useDisclosure();
return (
<Group
position="center"
style={{
border: open === true ? `1px dashed ` : ``,
margin: open === true ? 2 : 12,
padding: open === true ? 8 : 0
}}
>
<ActionIcon onClick={() => toggle.toggle()} size="lg">
<IconLanguage />
</ActionIcon>
{open && (
<Group>
<LanguageSelect />
</Group>
)}
</Group>
);
}

View File

@ -4,7 +4,6 @@ import { useNavigate, useParams } from 'react-router-dom';
import { navTabs as mainNavTabs } from '../../defaults/links'; import { navTabs as mainNavTabs } from '../../defaults/links';
import { InvenTreeStyle } from '../../globalStyle'; import { InvenTreeStyle } from '../../globalStyle';
import { ColorToggle } from '../items/ColorToggle';
import { ScanButton } from '../items/ScanButton'; import { ScanButton } from '../items/ScanButton';
import { MainMenu } from './MainMenu'; import { MainMenu } from './MainMenu';
import { NavHoverMenu } from './NavHoverMenu'; import { NavHoverMenu } from './NavHoverMenu';
@ -25,7 +24,6 @@ export function Header() {
</Group> </Group>
<Group> <Group>
<ScanButton /> <ScanButton />
<ColorToggle />
<MainMenu /> <MainMenu />
</Group> </Group>
</Group> </Group>

View File

@ -3,35 +3,20 @@ import { Group, Menu, Skeleton, Text, UnstyledButton } from '@mantine/core';
import { import {
IconChevronDown, IconChevronDown,
IconHeart, IconHeart,
IconLanguage,
IconLogout, IconLogout,
IconSettings, IconSettings,
IconUserCircle IconUserCircle
} from '@tabler/icons-react'; } from '@tabler/icons-react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { languages } from '../../contexts/LanguageContext';
import { doClassicLogout } from '../../functions/auth'; import { doClassicLogout } from '../../functions/auth';
import { InvenTreeStyle } from '../../globalStyle'; import { InvenTreeStyle } from '../../globalStyle';
import { useApiState } from '../../states/ApiState'; import { useApiState } from '../../states/ApiState';
import { useLocalState } from '../../states/LocalState';
import { PlaceholderPill } from '../items/Placeholder'; import { PlaceholderPill } from '../items/Placeholder';
export function MainMenu() { export function MainMenu() {
const { classes, theme } = InvenTreeStyle(); const { classes, theme } = InvenTreeStyle();
const [username] = useApiState((state) => [state.user?.name]); const [username] = useApiState((state) => [state.user?.name]);
const [locale] = useLocalState((state) => [state.language]);
// Language
function switchLanguage() {
useLocalState.setState({
language: languages[(languages.indexOf(locale) + 1) % languages.length]
});
}
function enablePsuedo() {
useLocalState.setState({ language: 'pseudo-LOCALE' });
}
return ( return (
<Menu width={260} position="bottom-end"> <Menu width={260} position="bottom-end">
<Menu.Target> <Menu.Target>
@ -53,26 +38,15 @@ export function MainMenu() {
<Trans>Notifications</Trans> <Trans>Notifications</Trans>
<PlaceholderPill /> <PlaceholderPill />
</Menu.Item> </Menu.Item>
<Menu.Item <Menu.Item icon={<IconUserCircle />}>
icon={<IconUserCircle />} <Trans>Profile</Trans> <PlaceholderPill />
component={Link}
to="/profile/user"
>
<Trans>Profile</Trans>
</Menu.Item> </Menu.Item>
<Menu.Label> <Menu.Label>
<Trans>Settings</Trans> <Trans>Settings</Trans>
</Menu.Label> </Menu.Label>
<Menu.Item icon={<IconLanguage />} onClick={switchLanguage}> <Menu.Item icon={<IconSettings />} component={Link} to="/profile/user">
<Trans>Current language {locale}</Trans>
</Menu.Item>
<Menu.Item icon={<IconLanguage />} onClick={enablePsuedo}>
<Trans>Switch to pseudo language</Trans>
</Menu.Item>
<Menu.Item icon={<IconSettings />}>
<Trans>Account settings</Trans> <Trans>Account settings</Trans>
<PlaceholderPill />
</Menu.Item> </Menu.Item>
<Menu.Item <Menu.Item
icon={<IconLogout />} icon={<IconLogout />}

View File

@ -1,14 +1,13 @@
import { Trans, t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { Center, Container, Group, Select, Stack, Text } from '@mantine/core'; import { Center, Container } from '@mantine/core';
import { useToggle } from '@mantine/hooks'; import { useToggle } from '@mantine/hooks';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { AuthFormOptions } from '../../components/forms/AuthFormOptions';
import { AuthenticationForm } from '../../components/forms/AuthenticationForm'; import { AuthenticationForm } from '../../components/forms/AuthenticationForm';
import { HostOptionsForm } from '../../components/forms/HostOptionsForm'; import { InstanceOptions } from '../../components/forms/InstanceOptions';
import { EditButton } from '../../components/items/EditButton';
import { defaultHostKey } from '../../defaults/defaultHostList'; import { defaultHostKey } from '../../defaults/defaultHostList';
import { useLocalState } from '../../states/LocalState'; import { useLocalState } from '../../states/LocalState';
import { HostList } from '../../states/states';
export default function Login() { export default function Login() {
const [hostKey, setHost, hostList] = useLocalState((state) => [ const [hostKey, setHost, hostList] = useLocalState((state) => [
@ -19,24 +18,12 @@ export default function Login() {
const hostname = const hostname =
hostList[hostKey] === undefined ? t`No selection` : hostList[hostKey].name; hostList[hostKey] === undefined ? t`No selection` : hostList[hostKey].name;
const [hostEdit, setHostEdit] = useToggle([false, true] as const); const [hostEdit, setHostEdit] = useToggle([false, true] as const);
const hostListData = Object.keys(hostList).map((key) => ({
value: key,
label: hostList[key].name
}));
const [HostListEdit, setHostListEdit] = useToggle([false, true] as const);
// Data manipulation functions // Data manipulation functions
function ChangeHost(newHost: string): void { function ChangeHost(newHost: string): void {
setHost(hostList[newHost].host, newHost); setHost(hostList[newHost].host, newHost);
setHostEdit(false);
}
function SaveOptions(newHostList: HostList): void {
useLocalState.setState({ hostList: newHostList });
if (newHostList[hostKey] === undefined) {
setHost('', '');
}
setHostListEdit();
} }
// Set default host to localhost if no host is selected // Set default host to localhost if no host is selected
useEffect(() => { useEffect(() => {
if (hostKey === '') { if (hostKey === '') {
@ -44,87 +31,23 @@ export default function Login() {
} }
}, []); }, []);
// Main rendering block
return ( return (
<Center mih="100vh"> <Center mih="100vh">
<Container w="md" miw={400}> <Container w="md" miw={400}>
<Stack> {hostEdit ? (
<EditHostList <InstanceOptions
hostList={hostList} hostKey={hostKey}
SaveOptions={SaveOptions} ChangeHost={ChangeHost}
HostListEdit={HostListEdit} setHostEdit={setHostEdit}
/> />
{!HostListEdit && ( ) : (
<AuthenticationForm <>
hostname={hostname} <AuthenticationForm />
editing={hostEdit} <AuthFormOptions hostname={hostname} toggleHostEdit={setHostEdit} />
setEditing={setHostEdit} </>
selectElement={ )}
<SelectHost
hostKey={hostKey}
ChangeHost={ChangeHost}
hostListData={hostListData}
HostListEdit={HostListEdit}
hostEdit={hostEdit}
setHostListEdit={setHostListEdit}
/>
}
/>
)}
</Stack>
</Container> </Container>
</Center> </Center>
); );
} }
const SelectHost = ({
hostKey,
ChangeHost,
hostListData,
HostListEdit,
hostEdit,
setHostListEdit
}: {
hostKey: string;
ChangeHost: (newHost: string) => void;
hostListData: any;
HostListEdit: boolean;
hostEdit: boolean;
setHostListEdit: (value?: React.SetStateAction<boolean> | undefined) => void;
}) => {
if (!hostEdit) return <></>;
return (
<Group>
<Select
value={hostKey}
onChange={ChangeHost}
data={hostListData}
disabled={HostListEdit}
/>
<EditButton
setEditing={setHostListEdit}
editing={HostListEdit}
disabled={HostListEdit}
/>
</Group>
);
};
const EditHostList = ({
hostList,
SaveOptions,
HostListEdit
}: {
hostList: HostList;
SaveOptions: (newHostList: HostList) => void;
HostListEdit: boolean;
}) => {
if (!HostListEdit) return null;
return (
<>
<Text>
<Trans>Edit host options</Trans>
</Text>
<HostOptionsForm data={hostList} saveOptions={SaveOptions} />
</>
);
};

View File

@ -16,7 +16,10 @@ import { useToggle } from '@mantine/hooks';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { api, queryClient } from '../../../App'; import { api, queryClient } from '../../../App';
import { ColorToggle } from '../../../components/items/ColorToggle';
import { EditButton } from '../../../components/items/EditButton'; import { EditButton } from '../../../components/items/EditButton';
import { LanguageSelect } from '../../../components/items/LanguageSelect';
import { useLocalState } from '../../../states/LocalState';
import { UserTheme } from './UserTheme'; import { UserTheme } from './UserTheme';
export function UserPanel() { export function UserPanel() {
@ -48,7 +51,7 @@ export function UserPanel() {
<UserTheme height={SECONDARY_COL_HEIGHT} /> <UserTheme height={SECONDARY_COL_HEIGHT} />
</Grid.Col> </Grid.Col>
<Grid.Col span={6}> <Grid.Col span={6}>
<Skeleton height={SECONDARY_COL_HEIGHT} /> <DisplaySettings height={SECONDARY_COL_HEIGHT} />
</Grid.Col> </Grid.Col>
<Grid.Col span={6}> <Grid.Col span={6}>
<Skeleton height={SECONDARY_COL_HEIGHT} /> <Skeleton height={SECONDARY_COL_HEIGHT} />
@ -122,3 +125,34 @@ export function UserInfo({ data }: { data: any }) {
</form> </form>
); );
} }
function DisplaySettings({ 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>
<Group>
<Text>
<Trans>Color Mode</Trans>
</Text>
<ColorToggle />
</Group>
<Group align="top">
<Text>
<Trans>Language</Trans>
</Text>
<Stack>
<LanguageSelect />
<Button onClick={enablePseudoLang} variant="light">
<Trans>Use pseudo language</Trans>
</Button>
</Stack>
</Group>
</Container>
);
}

View File

@ -13,7 +13,9 @@ interface LocalStateProps {
setHost: (newHost: string, newHostKey: string) => void; setHost: (newHost: string, newHostKey: string) => void;
hostKey: string; hostKey: string;
hostList: HostList; hostList: HostList;
setHostList: (newHostList: HostList) => void;
language: Locales; language: Locales;
setLanguage: (newLanguage: Locales) => void;
// theme // theme
primaryColor: string; primaryColor: string;
whiteColor: string; whiteColor: string;
@ -33,7 +35,9 @@ export const useLocalState = create<LocalStateProps>()(
set({ host: newHost, hostKey: newHostKey }), set({ host: newHost, hostKey: newHostKey }),
hostKey: '', hostKey: '',
hostList: {}, hostList: {},
setHostList: (newHostList) => set({ hostList: newHostList }),
language: 'en', language: 'en',
setLanguage: (newLanguage) => set({ language: newLanguage }),
//theme //theme
primaryColor: 'indigo', primaryColor: 'indigo',
whiteColor: '#fff', whiteColor: '#fff',

View File

@ -129,7 +129,7 @@ def node_available(versions: bool = False, bypass_yarn: bool = False):
print('Node is available but yarn is not. Install yarn if you wish to build the frontend.') print('Node is available but yarn is not. Install yarn if you wish to build the frontend.')
# Return the result # Return the result
return ret((not yarn_passes or not node_version), node_version, yarn_version) return ret(yarn_passes and node_version, node_version, yarn_version)
def check_file_existance(filename: str, overwrite: bool = False): def check_file_existance(filename: str, overwrite: bool = False):