WIP [PUI] Migrate to Mantine v7 (#7028)

* bump deps

* upgrade all deps

* adapt theme context

* add vanilla extract

* add basic theme

* reformat global state

* fix imports

* fix spotlight

* update args

* adapt arg names

* fix more arg renames

* fix italic

* switch sx to style

* fix types

* fix theme refs

* misc fixes

* misc fixes

* fix type

* fix selects

* misc fixes

* bug fix

* update to new style

* set text args

* fix spotlight

* dumb spotlight down

* change ActionIcons back to default

* fix name

* fix test

* adjust test to new spotlight

* package fix

* fix new code to v7

* fix building

* fix group aligment

* remove unneeded imports

* add new type

* import cleanups

* add notification style

* move context to loadable

* reorder contexts

* make test less flaky

* fix missing theming

* fix color schema switcher

* increase timeouts

* update package refs

* add missing style for datatables

* fix missing nesting

* organize imports

* move language context around

* make sure license keys are unique

* add keys to badges

* fix import

* fix missing keys

* fix missing key issue in badge section

* update packages

* fix new code to v7 style

* dummy change

* fix up test

* fix btn style

* fix merge issues

* remove placeholders

* fix color schema usage

* fix usage of ColorScheme

* fix style issues

* fix test

* fix choice field to fit stricter validation

* make test more reproducible

* wait for dash before proceeding

* bump deps

* add missing style

* do loops

* fix css

* change carousel sizing

* fix merge for v7

* fix image ratio

* Revert "bump deps"

This reverts commit 91cdae5a3e.

* fix userstate to ensure it always renders

* await dashboard loading before resuming wiht wuick login

* fix spotlight and remove testing changes

* Catch API error

* Update breadcrumb list

* Update panel icon

* Cleanup notification drawer

* Some more tweaks

* Fix for notification count indicator

* Fix stack prop

* fix type error

* fix double timeout key

* use div instead of text

---------

Co-authored-by: Oliver Walters <oliver.henry.walters@gmail.com>
This commit is contained in:
Matthias Mair 2024-05-08 09:42:57 +02:00 committed by GitHub
parent 08b1bdb564
commit 6700a4625d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
153 changed files with 1936 additions and 1556 deletions

View File

@ -11,66 +11,78 @@
"compile": "lingui compile --typescript"
},
"dependencies": {
"@codemirror/autocomplete": ">=6.0.0",
"@codemirror/lang-liquid": "^6.2.1",
"@codemirror/language": ">=6.0.0",
"@codemirror/lint": ">=6.0.0",
"@codemirror/search": ">=6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/theme-one-dark": ">=6.0.0",
"@codemirror/view": ">=6.0.0",
"@emotion/react": "^11.11.4",
"@fortawesome/fontawesome-svg-core": "^6.5.2",
"@fortawesome/free-regular-svg-icons": "^6.5.2",
"@fortawesome/free-solid-svg-icons": "^6.5.2",
"@fortawesome/react-fontawesome": "^0.2.0",
"@lingui/core": "^4.7.1",
"@lingui/core": "^4.10.0",
"@lingui/react": "^4.10.0",
"@mantine/carousel": "<7",
"@mantine/core": "<7",
"@mantine/dates": "<7",
"@mantine/dropzone": "<7",
"@mantine/form": "<8",
"@mantine/hooks": "<7",
"@mantine/modals": "<7",
"@mantine/notifications": "<7",
"@mantine/spotlight": "<7",
"@mantine/carousel": "^7.8.0",
"@mantine/core": "^7.8.0",
"@mantine/dates": "^7.8.0",
"@mantine/dropzone": "^7.8.0",
"@mantine/form": "^7.8.0",
"@mantine/hooks": "^7.8.0",
"@mantine/modals": "^7.8.0",
"@mantine/notifications": "^7.8.0",
"@mantine/spotlight": "^7.8.0",
"@mantine/vanilla-extract": "^7.8.0",
"@naisutech/react-tree": "^3.1.0",
"@sentry/react": "^7.109.0",
"@tabler/icons-react": "^3.1.0",
"@tanstack/react-query": "^5.28.14",
"@sentry/react": "^7.110.0",
"@tabler/icons-react": "^3.2.0",
"@tanstack/react-query": "^5.29.2",
"@uiw/codemirror-theme-vscode": "^4.21.25",
"@uiw/react-codemirror": "^4.21.25",
"@uiw/react-split": "^5.9.3",
"axios": "^1.6.7",
"@vanilla-extract/css": "^1.14.2",
"axios": "^1.6.8",
"clsx": "^2.1.0",
"codemirror": ">=6.0.0",
"dayjs": "^1.11.10",
"easymde": "^2.18.0",
"embla-carousel-react": "^8.0.2",
"html5-qrcode": "^2.3.8",
"mantine-datatable": "<7",
"mantine-datatable": "^7.8.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-grid-layout": "^1.4.4",
"react-hook-form": "^7.51.3",
"react-is": "^18.2.0",
"react-router-dom": "^6.22.1",
"react-router-dom": "^6.22.3",
"react-select": "^5.8.0",
"react-simplemde-editor": "^5.2.0",
"recharts": "^2.12.4",
"styled-components": "^5.3.6",
"zustand": "^4.5.1"
"styled-components": "^6.1.8",
"zustand": "^4.5.2"
},
"devDependencies": {
"@babel/core": "^7.23.9",
"@babel/preset-react": "^7.23.3",
"@babel/preset-typescript": "^7.23.3",
"@lingui/cli": "^4.7.2",
"@babel/core": "^7.24.4",
"@babel/preset-react": "^7.24.1",
"@babel/preset-typescript": "^7.24.1",
"@lingui/cli": "^4.10.0",
"@lingui/macro": "^4.10.0",
"@playwright/test": "^1.43.1",
"@types/node": "^20.12.3",
"@types/react": "^18.2.74",
"@types/react-dom": "^18.2.23",
"@types/node": "^20.12.7",
"@types/react": "^18.2.79",
"@types/react-dom": "^18.2.25",
"@types/react-grid-layout": "^1.3.5",
"@types/react-router-dom": "^5.3.3",
"@vanilla-extract/vite-plugin": "^4.0.7",
"@vitejs/plugin-react": "^4.2.1",
"babel-plugin-macros": "^3.1.0",
"nyc": "^15.1.0",
"rollup-plugin-license": "^3.3.1",
"typescript": "^5.3.3",
"vite": "^5.2.7",
"typescript": "^5.4.5",
"vite": "^5.2.8",
"vite-plugin-babel-macros": "^1.0.6",
"vite-plugin-istanbul": "^6.0.0"
}

View File

@ -5,7 +5,6 @@ export default defineConfig({
fullyParallel: true,
timeout: 60000,
forbidOnly: !!process.env.CI,
timeout: 5 * 60 * 1000,
retries: process.env.CI ? 1 : 0,
workers: process.env.CI ? 2 : undefined,
reporter: process.env.CI ? [['html', { open: 'never' }], ['github']] : 'list',

View File

@ -1,5 +1,4 @@
import { ActionIcon, Group, Tooltip } from '@mantine/core';
import { FloatingPosition } from '@mantine/core/lib/Floating';
import { ActionIcon, FloatingPosition, Group, Tooltip } from '@mantine/core';
import { ReactNode } from 'react';
import { notYetImplemented } from '../../functions/notifications';
@ -41,7 +40,7 @@ export function ActionButton(props: ActionButtonProps) {
onClick={props.onClick ?? notYetImplemented}
variant={props.variant ?? 'light'}
>
<Group spacing="xs" noWrap={true}>
<Group gap="xs" wrap="nowrap">
{props.icon}
</Group>
</ActionIcon>

View File

@ -18,7 +18,7 @@ export function ButtonMenu({
return (
<Menu shadow="xs">
<Menu.Target>
<ActionIcon>
<ActionIcon variant="default">
<Tooltip label={tooltip}>{icon}</Tooltip>
</ActionIcon>
</Menu.Target>

View File

@ -17,7 +17,7 @@ export function CopyButton({
onClick={copy}
title={t`Copy to clipboard`}
variant="subtle"
compact
size="compact-md"
>
<IconCopy size={10} />
{label && <div>&nbsp;</div>}

View File

@ -14,7 +14,11 @@ export function EditButton({
}) {
saveIcon = saveIcon || <IconDeviceFloppy />;
return (
<ActionIcon onClick={() => setEditing()} disabled={disabled}>
<ActionIcon
onClick={() => setEditing()}
disabled={disabled}
variant="default"
>
{editing ? saveIcon : <IconEdit />}
</ActionIcon>
);

View File

@ -50,7 +50,7 @@ export function SsoButton({ provider }: { provider: Provider }) {
return (
<Button
leftIcon={getBrandIcon(provider)}
leftSection={getBrandIcon(provider)}
radius="xl"
component="a"
onClick={login}

View File

@ -16,6 +16,7 @@ export function ScanButton() {
innerProps: {}
})
}
variant="transparent"
title={t`Open QR code scanner`}
>
<IconQrcode />

View File

@ -0,0 +1,19 @@
import { style } from '@vanilla-extract/css';
import { vars } from '../../theme';
export const button = style({
borderTopRightRadius: 0,
borderBottomRightRadius: 0,
':before': {
borderRadius: '0 !important'
}
});
export const icon = style({
borderTopLeftRadius: 0,
borderBottomLeftRadius: 0,
border: 0,
borderLeft: `1px solid ${vars.colors.primaryShade}`
});

View File

@ -5,13 +5,13 @@ import {
Menu,
Text,
Tooltip,
createStyles,
useMantineTheme
} from '@mantine/core';
import { IconChevronDown } from '@tabler/icons-react';
import { useEffect, useMemo, useState } from 'react';
import { TablerIconType } from '../../functions/icons';
import * as classes from './SplitButton.css';
interface SplitButtonOption {
key: string;
@ -30,22 +30,6 @@ interface SplitButtonProps {
loading?: boolean;
}
const useStyles = createStyles((theme) => ({
button: {
borderTopRightRadius: 0,
borderBottomRightRadius: 0,
'&::before': {
borderRadius: '0 !important'
}
},
icon: {
borderTopLeftRadius: 0,
borderBottomLeftRadius: 0,
border: 0,
borderLeft: `1px solid ${theme.primaryShade}`
}
}));
export function SplitButton({
options,
defaultSelected,
@ -54,7 +38,6 @@ export function SplitButton({
loading
}: Readonly<SplitButtonProps>) {
const [current, setCurrent] = useState<string>(defaultSelected);
const { classes } = useStyles();
useEffect(() => {
setSelected?.(current);
@ -72,7 +55,7 @@ export function SplitButton({
const theme = useMantineTheme();
return (
<Group noWrap style={{ gap: 0 }}>
<Group wrap="nowrap" style={{ gap: 0 }}>
<Button
onClick={currentOption?.onClick}
disabled={loading ? false : currentOption?.disabled}
@ -106,7 +89,7 @@ export function SplitButton({
option.onClick();
}}
disabled={option.disabled}
icon={<option.icon />}
leftSection={<option.icon />}
>
<Tooltip label={option.tooltip} position="right">
<Text>{option.name}</Text>

View File

@ -1,14 +1,19 @@
import { t } from '@lingui/macro';
import { ActionIcon } from '@mantine/core';
import { spotlight } from '@mantine/spotlight';
import { IconCommand } from '@tabler/icons-react';
import { firstSpotlight } from '../nav/Layout';
/**
* A button which opens the quick command modal
*/
export function SpotlightButton() {
return (
<ActionIcon onClick={() => spotlight.open()} title={t`Open spotlight`}>
<ActionIcon
onClick={() => firstSpotlight.open()}
title={t`Open spotlight`}
variant="transparent"
>
<IconCommand />
</ActionIcon>
);

View File

@ -324,7 +324,12 @@ function CopyField({ value }: { value: string }) {
<CopyButton value={value}>
{({ copied, copy }) => (
<Tooltip label={copied ? t`Copied` : t`Copy`} withArrow>
<ActionIcon color={copied ? 'teal' : 'gray'} onClick={copy}>
<ActionIcon
color={copied ? 'teal' : 'gray'}
onClick={copy}
variant="transparent"
size="sm"
>
{copied ? (
<InvenTreeIcon icon="check" />
) : (
@ -398,9 +403,14 @@ export function DetailsTable({
}) {
return (
<Paper p="xs" withBorder radius="xs">
<Stack spacing="xs">
<Stack gap="xs">
{title && <StylishText size="lg">{title}</StylishText>}
<Table striped>
<Table
striped
verticalSpacing="sm"
horizontalSpacing="md"
withColumnBorders
>
<tbody>
{fields
.filter((field: DetailsField) => !field.hidden)

View File

@ -8,7 +8,7 @@ import {
Paper,
Text,
rem,
useMantineTheme
useMantineColorScheme
} from '@mantine/core';
import { Dropzone, FileWithPath, IMAGE_MIME_TYPE } from '@mantine/dropzone';
import { useHover } from '@mantine/hooks';
@ -21,6 +21,7 @@ import { cancelEvent } from '../../functions/events';
import { InvenTreeIcon } from '../../functions/icons';
import { useUserState } from '../../states/UserState';
import { PartThumbTable } from '../../tables/part/PartThumbTable';
import { vars } from '../../theme';
import { ActionButton } from '../buttons/ActionButton';
import { ApiImage } from '../images/ApiImage';
import { StylishText } from '../items/StylishText';
@ -87,8 +88,6 @@ function UploadModal({
const [file1, setFile] = useState<FileWithPath | null>(null);
let uploading = false;
const theme = useMantineTheme();
// Components to show in the Dropzone when no file is selected
const noFileIdle = (
<Group>
@ -122,7 +121,7 @@ function UploadModal({
>
<Image
src={imageUrl}
imageProps={{ onLoad: () => URL.revokeObjectURL(imageUrl) }}
onLoad={() => URL.revokeObjectURL(imageUrl)}
radius="sm"
height={75}
fit="contain"
@ -160,12 +159,14 @@ function UploadModal({
}
};
const { colorScheme } = useMantineColorScheme();
const primaryColor =
theme.colors[theme.primaryColor][theme.colorScheme === 'dark' ? 4 : 6];
const redColor = theme.colors.red[theme.colorScheme === 'dark' ? 4 : 6];
vars.colors.primaryColors[colorScheme === 'dark' ? 4 : 6];
const redColor = vars.colors.red[colorScheme === 'dark' ? 4 : 6];
return (
<Paper sx={{ height: '220px' }}>
<Paper style={{ height: '220px' }}>
<Dropzone
onDrop={(files) => setFile(files[0])}
maxFiles={1}
@ -173,8 +174,8 @@ function UploadModal({
loading={uploading}
>
<Group
position="center"
spacing="xl"
justify="center"
gap="xl"
style={{ minHeight: rem(140), pointerEvents: 'none' }}
>
<Dropzone.Accept>
@ -252,7 +253,7 @@ function ImageActionButtons({
<>
{visible && (
<Group
spacing="xs"
gap="xs"
style={{ zIndex: 2, position: 'absolute', top: '10px', left: '10px' }}
>
{actions.selectExisting && (
@ -358,8 +359,8 @@ export function DetailsImage(props: Readonly<DetailImageProps>) {
<>
<ApiImage
src={img}
height={IMAGE_DIMENSION}
width={IMAGE_DIMENSION}
mah={IMAGE_DIMENSION}
maw={IMAGE_DIMENSION}
onClick={expandImage}
/>
{permissions.hasChangeRole(props.appRole) &&

View File

@ -222,7 +222,7 @@ export function TemplateEditor(props: Readonly<TemplateEditorProps>) {
<Split style={{ gap: '10px' }}>
<Tabs
value={editorValue}
onTabChange={async (v) => {
onChange={async (v) => {
codeRef.current = await getCodeFromEditor();
setEditorValue(v);
}}
@ -239,13 +239,13 @@ export function TemplateEditor(props: Readonly<TemplateEditorProps>) {
<Tabs.Tab
key={Editor.key}
value={Editor.key}
icon={<Editor.icon size="0.8rem" />}
leftSection={<Editor.icon size="0.8rem" />}
>
{Editor.name}
</Tabs.Tab>
))}
<Group position="right" style={{ flex: '1' }} noWrap>
<Group justify="right" style={{ flex: '1' }} wrap="nowrap">
<SplitButton
loading={isPreviewLoading}
defaultSelected="preview_save"
@ -288,7 +288,7 @@ export function TemplateEditor(props: Readonly<TemplateEditorProps>) {
<Tabs
value={previewValue}
onTabChange={setPreviewValue}
onChange={setPreviewValue}
style={{
minWidth: '200px',
display: 'flex',
@ -300,7 +300,7 @@ export function TemplateEditor(props: Readonly<TemplateEditorProps>) {
<Tabs.Tab
key={PreviewArea.key}
value={PreviewArea.key}
icon={<PreviewArea.icon size="0.8rem" />}
leftSection={<PreviewArea.icon size="0.8rem" />}
>
{PreviewArea.name}
</Tabs.Tab>

View File

@ -482,11 +482,11 @@ export function ApiForm({
<Paper mah={'65vh'} style={{ overflowY: 'auto' }}>
<div>
{/* Form Fields */}
<Stack spacing="sm">
<Stack gap="sm">
{(!isValid || nonFieldErrors.length > 0) && (
<Alert radius="sm" color="red" title={t`Form Errors Exist`}>
{nonFieldErrors.length > 0 && (
<Stack spacing="xs">
<Stack gap="xs">
{nonFieldErrors.map((message) => (
<Text key={message}>{message}</Text>
))}
@ -509,7 +509,7 @@ export function ApiForm({
</Boundary>
<Boundary label={`ApiForm-${id}-FormContent`}>
<FormProvider {...form}>
<Stack spacing="xs">
<Stack gap="xs">
{!optionsLoading &&
Object.entries(fields).map(([fieldName, field]) => (
<ApiFormField
@ -532,7 +532,7 @@ export function ApiForm({
{/* Footer with Action Buttons */}
<Divider />
<div>
<Group position="right">
<Group justify="right">
{props.actions?.map((action, i) => (
<Button
key={i}

View File

@ -100,7 +100,7 @@ export function AuthenticationForm() {
) : null}
<form onSubmit={classicForm.onSubmit(() => {})}>
{classicLoginMode ? (
<Stack spacing={0}>
<Stack gap={0}>
<TextInput
required
label={t`Username`}
@ -114,7 +114,7 @@ export function AuthenticationForm() {
{...classicForm.getInputProps('password')}
/>
{auth_settings?.password_forgotten_enabled === true && (
<Group position="apart" mt="0">
<Group justify="space-between" mt="0">
<Anchor
component="button"
type="button"
@ -139,7 +139,7 @@ export function AuthenticationForm() {
</Stack>
)}
<Group position="apart" mt="xl">
<Group justify="space-between" mt="xl">
<Anchor
component="button"
type="button"
@ -221,7 +221,7 @@ export function RegistrationForm() {
<>
{auth_settings?.registration_enabled && (
<form onSubmit={registrationForm.onSubmit(() => {})}>
<Stack spacing={0}>
<Stack gap={0}>
<TextInput
required
label={t`Username`}
@ -249,7 +249,7 @@ export function RegistrationForm() {
/>
</Stack>
<Group position="apart" mt="xl">
<Group justify="space-between" mt="xl">
<Button
type="submit"
disabled={isRegistering}

View File

@ -35,13 +35,13 @@ export function HostOptionsForm({
<TextInput
placeholder={t`Host`}
withAsterisk
sx={{ flex: 1 }}
style={{ flex: 1 }}
{...form.getInputProps(`${key}.host`)}
/>
<TextInput
placeholder={t`Name`}
withAsterisk
sx={{ flex: 1 }}
style={{ flex: 1 }}
{...form.getInputProps(`${key}.name`)}
/>
<ActionIcon
@ -49,6 +49,7 @@ export function HostOptionsForm({
onClick={() => {
deleteItem(key);
}}
variant="default"
>
<IconTrash />
</ActionIcon>
@ -59,18 +60,18 @@ export function HostOptionsForm({
return (
<form onSubmit={form.onSubmit(saveOptions)}>
<Box sx={{ maxWidth: 500 }} mx="auto">
<Box style={{ maxWidth: 500 }} mx="auto">
{fields.length > 0 ? (
<Group mb="xs">
<Text weight={500} size="sm" sx={{ flex: 1 }}>
<Text fw={500} size="sm" style={{ flex: 1 }}>
<Trans>Host</Trans>
</Text>
<Text weight={500} size="sm" sx={{ flex: 1 }}>
<Text fw={500} size="sm" style={{ flex: 1 }}>
<Trans>Name</Trans>
</Text>
</Group>
) : (
<Text color="dimmed" align="center">
<Text c="dimmed" ta="center">
<Trans>No one here...</Trans>
</Text>
)}
@ -84,7 +85,7 @@ export function HostOptionsForm({
<IconSquarePlus />
<Trans>Add Host</Trans>
</Button>
<Space sx={{ flex: 1 }} />
<Space style={{ flex: 1 }} />
<Button type="submit">
<Trans>Save</Trans>
</Button>

View File

@ -15,7 +15,7 @@ export function InstanceOptions({
setHostEdit
}: {
hostKey: string;
ChangeHost: (newHost: string) => void;
ChangeHost: (newHost: string | null) => void;
setHostEdit: () => void;
}) {
const [HostListEdit, setHostListEdit] = useToggle([false, true] as const);

View File

@ -230,9 +230,8 @@ export function ApiFormField({
id={fieldId}
value={numericalValue}
error={error?.message}
precision={definition.field_type == 'integer' ? 0 : 10}
onChange={(value: number) => onChange(value)}
removeTrailingZeros
decimalScale={definition.field_type == 'integer' ? 0 : 10}
onChange={(value: number | string | null) => onChange(value)}
step={1}
/>
);

View File

@ -31,8 +31,8 @@ export function ChoiceField({
return choices.map((choice) => {
return {
value: choice.value,
label: choice.display_name
value: choice.value.toString(),
label: choice.display_name.toString()
};
});
}, [definition.choices]);
@ -62,8 +62,8 @@ export function ChoiceField({
placeholder={definition.placeholder}
required={definition.required}
disabled={definition.disabled}
icon={definition.icon}
withinPortal={true}
leftSection={definition.icon}
comboboxProps={{ withinPortal: true }}
/>
);
}

View File

@ -61,7 +61,7 @@ export default function DateField({
label={definition.label}
description={definition.description}
placeholder={definition.placeholder}
icon={definition.icon}
leftSection={definition.icon}
/>
);
}

View File

@ -19,8 +19,8 @@ export function NestedObjectField({
<Text>{definition.label}</Text>
</Accordion.Control>
<Accordion.Panel>
<Divider sx={{ marginTop: '-10px', marginBottom: '10px' }} />
<Stack spacing="xs">
<Divider style={{ marginTop: '-10px', marginBottom: '10px' }} />
<Stack gap="xs">
{Object.entries(definition.children ?? {}).map(
([childFieldName, field]) => (
<ApiFormField

View File

@ -1,5 +1,10 @@
import { t } from '@lingui/macro';
import { Input, useMantineTheme } from '@mantine/core';
import {
Input,
darken,
useMantineColorScheme,
useMantineTheme
} from '@mantine/core';
import { useDebouncedValue, useId } from '@mantine/hooks';
import { useQuery } from '@tanstack/react-query';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
@ -11,6 +16,7 @@ import {
import Select from 'react-select';
import { api } from '../../../App';
import { vars } from '../../../theme';
import { RenderInstance } from '../../render/Instance';
import { ApiFormFieldType } from './ApiFormField';
@ -217,43 +223,46 @@ export function RelatedModelField({
// Define color theme to pass to field based on Mantine theme
const theme = useMantineTheme();
const colorschema = vars.colors.primaryColors;
const { colorScheme } = useMantineColorScheme();
const colors = useMemo(() => {
let colors: any;
if (theme.colorScheme === 'dark') {
if (colorScheme === 'dark') {
colors = {
neutral0: theme.colors[theme.colorScheme][6],
neutral5: theme.colors[theme.colorScheme][4],
neutral10: theme.colors[theme.colorScheme][4],
neutral20: theme.colors[theme.colorScheme][4],
neutral30: theme.colors[theme.colorScheme][3],
neutral40: theme.colors[theme.colorScheme][2],
neutral50: theme.colors[theme.colorScheme][1],
neutral60: theme.colors[theme.colorScheme][0],
neutral70: theme.colors[theme.colorScheme][0],
neutral80: theme.colors[theme.colorScheme][0],
neutral90: theme.colors[theme.colorScheme][0],
primary: theme.colors[theme.primaryColor][7],
primary25: theme.colors[theme.primaryColor][6],
primary50: theme.colors[theme.primaryColor][5],
primary75: theme.colors[theme.primaryColor][4]
neutral0: colorschema[6],
neutral5: colorschema[4],
neutral10: colorschema[4],
neutral20: colorschema[4],
neutral30: colorschema[3],
neutral40: colorschema[2],
neutral50: colorschema[1],
neutral60: colorschema[0],
neutral70: colorschema[0],
neutral80: colorschema[0],
neutral90: colorschema[0],
primary: vars.colors.primaryColors[7],
primary25: vars.colors.primaryColors[6],
primary50: vars.colors.primaryColors[5],
primary75: vars.colors.primaryColors[4]
};
} else {
colors = {
neutral0: theme.white,
neutral5: theme.fn.darken(theme.white, 0.05),
neutral10: theme.fn.darken(theme.white, 0.1),
neutral20: theme.fn.darken(theme.white, 0.2),
neutral30: theme.fn.darken(theme.white, 0.3),
neutral40: theme.fn.darken(theme.white, 0.4),
neutral50: theme.fn.darken(theme.white, 0.5),
neutral60: theme.fn.darken(theme.white, 0.6),
neutral70: theme.fn.darken(theme.white, 0.7),
neutral80: theme.fn.darken(theme.white, 0.8),
neutral90: theme.fn.darken(theme.white, 0.9),
primary: theme.colors[theme.primaryColor][7],
primary25: theme.colors[theme.primaryColor][4],
primary50: theme.colors[theme.primaryColor][5],
primary75: theme.colors[theme.primaryColor][6]
neutral0: vars.colors.white,
neutral5: darken(vars.colors.white, 0.05),
neutral10: darken(vars.colors.white, 0.1),
neutral20: darken(vars.colors.white, 0.2),
neutral30: darken(vars.colors.white, 0.3),
neutral40: darken(vars.colors.white, 0.4),
neutral50: darken(vars.colors.white, 0.5),
neutral60: darken(vars.colors.white, 0.6),
neutral70: darken(vars.colors.white, 0.7),
neutral80: darken(vars.colors.white, 0.8),
neutral90: darken(vars.colors.white, 0.9),
primary: vars.colors.primaryColors[7],
primary25: vars.colors.primaryColors[4],
primary50: vars.colors.primaryColors[5],
primary75: vars.colors.primaryColors[6]
};
}
return colors;

View File

@ -8,10 +8,14 @@ import { useMemo } from 'react';
import { useLocalState } from '../../states/LocalState';
interface ApiImageProps extends ImageProps {
onClick?: (event: any) => void;
}
/**
* Construct an image container which will load and display the image
*/
export function ApiImage(props: ImageProps) {
export function ApiImage(props: Readonly<ApiImageProps>) {
const { host } = useLocalState.getState();
const imageUrl = useMemo(() => {
@ -21,12 +25,9 @@ export function ApiImage(props: ImageProps) {
return (
<Stack>
{imageUrl ? (
<Image {...props} src={imageUrl} withPlaceholder fit="contain" />
<Image {...props} src={imageUrl} fit="contain" />
) : (
<Skeleton
height={props?.height ?? props.width}
width={props?.width ?? props.height}
/>
<Skeleton h={props?.h ?? props.w} w={props?.w ?? props.h} />
)}
</Stack>
);

View File

@ -37,19 +37,14 @@ export function Thumbnail({
}, [link, text]);
return (
<Group align={align ?? 'left'} spacing="xs" noWrap={true}>
<Group align={align ?? 'left'} gap="xs" wrap="nowrap">
<ApiImage
src={src || backup_image}
alt={alt}
width={size}
aria-label={alt}
w={size}
fit="contain"
radius="xs"
withPlaceholder
imageProps={{
style: {
maxHeight: size
}
}}
style={{ maxHeight: size }}
/>
{inner}
</Group>
@ -71,7 +66,7 @@ export function ThumbnailHoverCard({
}) {
const card = useMemo(() => {
return (
<Group position="left" spacing={10} noWrap={true}>
<Group justify="left" gap={10} wrap="nowrap">
<Thumbnail src={src} alt={alt} size={size} />
<Text>{text}</Text>
</Group>

View File

@ -79,7 +79,7 @@ export function ActionDropdown({
>
<Tooltip label={action.tooltip}>
<Menu.Item
icon={action.icon}
leftSection={action.icon}
onClick={() => {
if (action.onClick != undefined) {
action.onClick();

View File

@ -72,7 +72,7 @@ export function AttachmentLink({
}, [host, attachment, external]);
return (
<Group position="left" spacing="sm">
<Group justify="left" gap="sm">
{external ? <IconLink /> : attachmentIcon(attachment)}
<Anchor href={url} target="_blank" rel="noopener noreferrer">
{text}

View File

@ -1,20 +1,21 @@
import { ActionIcon, Group, useMantineColorScheme } from '@mantine/core';
import { IconMoonStars, IconSun } from '@tabler/icons-react';
import { vars } from '../../theme';
export function ColorToggle() {
const { colorScheme, toggleColorScheme } = useMantineColorScheme();
return (
<Group position="center">
<Group justify="center">
<ActionIcon
onClick={() => toggleColorScheme()}
onClick={toggleColorScheme}
size="lg"
sx={(theme) => ({
style={{
color:
theme.colorScheme === 'dark'
? theme.colors.yellow[4]
: theme.colors.blue[6]
})}
colorScheme === 'dark' ? vars.colors.yellow[4] : vars.colors.blue[6]
}}
variant="transparent"
>
{colorScheme === 'dark' ? <IconSun /> : <IconMoonStars />}
</ActionIcon>

View File

@ -1,6 +1,6 @@
import { Group, LoadingOverlay, Paper, Text } from '@mantine/core';
import { InvenTreeStyle } from '../../globalStyle';
import * as classes from '../../main.css';
export interface StatisticItemProps {
title: string;
@ -16,18 +16,16 @@ export function StatisticItem({
data: StatisticItemProps;
isLoading: boolean;
}) {
const { classes } = InvenTreeStyle();
return (
<Paper withBorder p="xs" key={id} pos="relative">
<LoadingOverlay visible={isLoading} overlayBlur={2} />
<Group position="apart">
<LoadingOverlay visible={isLoading} overlayProps={{ blur: 2 }} />
<Group justify="space-between">
<Text size="xs" color="dimmed" className={classes.dashboardItemTitle}>
{data.title}
</Text>
</Group>
<Group align="flex-end" spacing="xs" mt={25}>
<Group align="flex-end" gap="xs" mt={25}>
<Text className={classes.dashboardItemValue}>{data.value}</Text>
</Group>
</Paper>

View File

@ -2,7 +2,7 @@ import { Trans } from '@lingui/macro';
import { Anchor, Container, HoverCard, ScrollArea, Text } from '@mantine/core';
import { useEffect, useRef, useState } from 'react';
import { InvenTreeStyle } from '../../globalStyle';
import * as classes from '../../main.css';
export interface BaseDocProps {
text: string | JSX.Element;
@ -22,8 +22,6 @@ export function DocTooltip({
link,
docchildren
}: Readonly<DocTooltipProps>) {
const { classes } = InvenTreeStyle();
return (
<HoverCard
shadow="md"

View File

@ -0,0 +1,30 @@
import { rem } from '@mantine/core';
import { style } from '@vanilla-extract/css';
import { vars } from '../../theme';
export const card = style({
height: rem(170),
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
alignItems: 'flex-start',
backgroundSize: 'cover',
backgroundPosition: 'center'
});
export const title = style({
fontWeight: 900,
lineHeight: 1.2,
fontSize: rem(32),
marginTop: 0,
[vars.lightSelector]: { color: vars.colors.dark[5] },
[vars.darkSelector]: { color: vars.colors.white[0] }
});
export const category = style({
opacity: 0.7,
fontWeight: 700,
[vars.lightSelector]: { color: vars.colors.dark[5] },
[vars.darkSelector]: { color: vars.colors.white[0] }
});

View File

@ -1,54 +1,17 @@
import { Trans } from '@lingui/macro';
import { Carousel } from '@mantine/carousel';
import {
Anchor,
Button,
Paper,
Text,
Title,
createStyles,
rem
} from '@mantine/core';
import { Anchor, Button, Paper, Text, Title, rem } from '@mantine/core';
import { DocumentationLinkItem } from './DocumentationLinks';
import * as classes from './GettingStartedCarousel.css';
import { PlaceholderPill } from './Placeholder';
const useStyles = createStyles((theme) => ({
card: {
height: rem(170),
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
alignItems: 'flex-start',
backgroundSize: 'cover',
backgroundPosition: 'center'
},
title: {
fontWeight: 900,
color:
theme.colorScheme === 'dark' ? theme.colors.white : theme.colors.dark,
lineHeight: 1.2,
fontSize: rem(32),
marginTop: 0
},
category: {
color:
theme.colorScheme === 'dark' ? theme.colors.white : theme.colors.dark,
opacity: 0.7,
fontWeight: 700
}
}));
function StartedCard({
title,
description,
link,
placeholder
}: DocumentationLinkItem) {
const { classes } = useStyles();
return (
<Paper shadow="md" p="xl" radius="md" className={classes.card}>
<div>
@ -81,10 +44,11 @@ export function GettingStartedCarousel({
return (
<Carousel
slideSize="50%"
breakpoints={[{ maxWidth: 'sm', slideSize: '100%', slideGap: rem(2) }]}
slideGap="xl"
slideSize={{ base: '100%', sm: '50%', md: '33.333333%' }}
slideGap={{ base: 0, sm: 'md' }}
slidesToScroll={3}
align="start"
loop
>
{slides}
</Carousel>

View File

@ -43,7 +43,7 @@ export function InfoItem({
}
return (
<Group position="apart">
<Group justify="space-between">
<Text fz="sm" fw={700}>
{name}:
</Text>

View File

@ -10,7 +10,7 @@ export const InvenTreeLogoHomeButton = forwardRef<HTMLDivElement>(
return (
<div ref={ref} {...props}>
<NavLink to={'/'}>
<ActionIcon size={28}>
<ActionIcon size={28} variant="transparent">
<InvenTreeLogo />
</ActionIcon>
</NavLink>

View File

@ -1,4 +1,4 @@
import { Select, SelectItem } from '@mantine/core';
import { Select } from '@mantine/core';
import { useEffect, useState } from 'react';
import { getSupportedLanguages } from '../../contexts/LanguageContext';
@ -10,7 +10,7 @@ export function LanguageSelect({ width = 80 }: { width?: number }) {
state.language,
state.setLanguage
]);
const [langOptions, setLangOptions] = useState<SelectItem[]>([]);
const [langOptions, setLangOptions] = useState<any[]>([]);
// change global language on change
useEffect(() => {

View File

@ -9,14 +9,18 @@ export function LanguageToggle() {
return (
<Group
position="center"
justify="center"
style={{
border: open === true ? `1px dashed ` : ``,
margin: open === true ? 2 : 12,
padding: open === true ? 8 : 0
}}
>
<ActionIcon onClick={() => toggle.toggle()} size="lg">
<ActionIcon
onClick={() => toggle.toggle()}
size="lg"
variant="transparent"
>
<IconLanguage />
</ActionIcon>
{open && (

View File

@ -2,7 +2,7 @@ import { SimpleGrid, Text, UnstyledButton } from '@mantine/core';
import React from 'react';
import { Link } from 'react-router-dom';
import { InvenTreeStyle } from '../../globalStyle';
import * as classes from '../../main.css';
import { DocTooltip } from './DocTooltip';
export interface MenuLinkItem {
@ -50,8 +50,6 @@ export function MenuLinks({
links: MenuLinkItem[];
highlighted?: boolean;
}) {
const { classes } = InvenTreeStyle();
const filteredLinks = links.filter(
(item) => !highlighted || item.highlight === true
);

View File

@ -9,7 +9,7 @@ export function PlaceholderPill() {
return (
<Tooltip
multiline
width={220}
w={220}
withArrow
label={t`This feature/button/site is a placeholder for a feature that is not implemented, only partial or intended for testing.`}
>
@ -31,7 +31,7 @@ export function PlaceholderPanel() {
title={t`This panel is a placeholder.`}
icon={<IconInfoCircle />}
>
<Text color="gray">This panel has not yet been implemented</Text>
<Text c="gray">This panel has not yet been implemented</Text>
</Alert>
</Stack>
);

View File

@ -23,9 +23,9 @@ export function ProgressBar(props: Readonly<ProgressBarProps>) {
}, [props]);
return (
<Stack spacing={2} style={{ flexGrow: 1, minWidth: '100px' }}>
<Stack gap={2} style={{ flexGrow: 1, minWidth: '100px' }}>
{props.progressLabel && (
<Text align="center" size="xs">
<Text ta="center" size="xs">
{props.value} / {props.maximum}
</Text>
)}

View File

@ -1,6 +1,6 @@
import { Text } from '@mantine/core';
import { InvenTreeStyle } from '../../globalStyle';
import * as classes from '../../main.css';
export function StylishText({
children,
@ -9,7 +9,6 @@ export function StylishText({
children: JSX.Element | string;
size?: string;
}) {
const { classes } = InvenTreeStyle();
return (
<Text size={size} className={classes.signText} variant="gradient">
{children}

View File

@ -59,7 +59,7 @@ export function AboutInvenTreeModal({
<tr key={idx}>
<td>{map.title}</td>
<td>
<Group position="apart" spacing="xs">
<Group justify="space-between" gap="xs">
{alwaysLink ? (
<Anchor href={data[map.ref]} target="_blank">
{data[map.ref]}
@ -177,7 +177,7 @@ export function AboutInvenTreeModal({
</tbody>
</Table>
<Divider />
<Group position="apart">
<Group justify="space-between">
<CopyButton
value={copyval}
label={<Trans>Copy version information</Trans>}

View File

@ -18,7 +18,7 @@ import { apiUrl } from '../../states/ApiState';
export function LicenceView(entries: Readonly<any[]>) {
return (
<Stack spacing="xs">
<Stack gap="xs">
<Divider />
{entries?.length > 0 ? (
<Accordion variant="contained" defaultValue="-">
@ -28,7 +28,7 @@ export function LicenceView(entries: Readonly<any[]>) {
value={`entry-${index}`}
>
<Accordion.Control>
<Group position="apart" grow>
<Group justify="space-between" grow>
<Text>{entry.name}</Text>
<Text>{entry.license}</Text>
<Space />
@ -63,7 +63,7 @@ export function LicenseModal() {
const rspdata = !data ? [] : Object.keys(data ?? {});
return (
<Stack spacing="xs">
<Stack gap="xs">
<Divider />
<LoadingOverlay visible={isFetching} />
{isFetching && (

View File

@ -150,7 +150,7 @@ export function QrCodeModal({
<Stack>
<Group>
<Text size="sm">{camId?.label}</Text>
<Space sx={{ flex: 1 }} />
<Space style={{ flex: 1 }} />
<Badge>{ScanningEnabled ? t`Scanning` : t`Not scanning`}</Badge>
</Group>
<Container px={0} id="reader" w={'100%'} mih="300px" />
@ -162,14 +162,14 @@ export function QrCodeModal({
<>
<Group>
<Button
sx={{ flex: 1 }}
style={{ flex: 1 }}
onClick={() => startScanning()}
disabled={camId != undefined && ScanningEnabled}
>
<Trans>Start scanning</Trans>
</Button>
<Button
sx={{ flex: 1 }}
style={{ flex: 1 }}
onClick={() => stopScanning()}
disabled={!ScanningEnabled}
>
@ -177,11 +177,11 @@ export function QrCodeModal({
</Button>
</Group>
{values.length == 0 ? (
<Text color={'grey'}>
<Text c={'grey'}>
<Trans>No scans yet!</Trans>
</Text>
) : (
<ScrollArea sx={{ height: 200 }} type="auto" offsetScrollbars>
<ScrollArea style={{ height: 200 }} type="auto" offsetScrollbars>
{values.map((value, index) => (
<div key={index}>{value}</div>
))}

View File

@ -137,7 +137,7 @@ export function ServerInfoModal({
</tbody>
</Table>
<Divider />
<Group position="right">
<Group justify="right">
<Button
color="red"
onClick={() => {

View File

@ -41,10 +41,14 @@ export function BreadcrumbList({
}, [breadcrumbs]);
return (
<Paper p="3" radius="xs">
<Group spacing="xs">
<Paper p="7" radius="xs" shadow="xs">
<Group gap="xs">
{navCallback && (
<ActionIcon key="nav-action" onClick={navCallback}>
<ActionIcon
key="nav-action"
onClick={navCallback}
variant="transparent"
>
<IconMenu2 />
</ActionIcon>
)}

View File

@ -0,0 +1,6 @@
import { style } from '@vanilla-extract/css';
export const flex = style({
display: 'flex',
flex: 1
});

View File

@ -1,19 +1,12 @@
import {
ActionIcon,
Divider,
Drawer,
Group,
MantineNumberSize,
Stack,
Text,
createStyles
} from '@mantine/core';
import { ActionIcon, Divider, Drawer, Group, Stack, Text } from '@mantine/core';
import { IconChevronLeft } from '@tabler/icons-react';
import { useCallback, useMemo } from 'react';
import { Link, Route, Routes, useNavigate, useParams } from 'react-router-dom';
import type { To } from 'react-router-dom';
import { UiSizeType } from '../../defaults/formatters';
import { useLocalState } from '../../states/LocalState';
import * as classes from './DetailDrawer.css';
/**
* @param title - drawer title
@ -26,17 +19,10 @@ export interface DrawerProps {
position?: 'right' | 'left';
renderContent: (id?: string) => React.ReactNode;
urlPrefix?: string;
size?: MantineNumberSize;
size?: UiSizeType;
closeOnEscape?: boolean;
}
const useStyles = createStyles(() => ({
flex: {
display: 'flex',
flex: 1
}
}));
function DetailDrawerComponent({
title,
position = 'right',
@ -46,7 +32,6 @@ function DetailDrawerComponent({
}: Readonly<DrawerProps>) {
const navigate = useNavigate();
const { id } = useParams();
const { classes } = useStyles();
const content = renderContent(id);
const opened = useMemo(() => !!id && !!content, [id, content]);
@ -87,7 +72,7 @@ function DetailDrawerComponent({
</Group>
}
>
<Stack spacing={'xs'} className={classes.flex}>
<Stack gap={'xs'} className={classes.flex}>
<Divider />
{content}
</Stack>

View File

@ -1,14 +1,13 @@
import { Anchor, Container, Group } from '@mantine/core';
import { footerLinks } from '../../defaults/links';
import { InvenTreeStyle } from '../../globalStyle';
import * as classes from '../../main.css';
import { InvenTreeLogoHomeButton } from '../items/InvenTreeLogo';
export function Footer() {
const { classes } = InvenTreeStyle();
const items = footerLinks.map((link) => (
<Anchor<'a'>
color="dimmed"
c="dimmed"
key={link.key}
href={link.link}
onClick={(event) => event.preventDefault()}

View File

@ -8,7 +8,7 @@ import { useMatch, useNavigate } from 'react-router-dom';
import { api } from '../../App';
import { navTabs as mainNavTabs } from '../../defaults/links';
import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { InvenTreeStyle } from '../../globalStyle';
import * as classes from '../../main.css';
import { apiUrl } from '../../states/ApiState';
import { useLocalState } from '../../states/LocalState';
import { useUserState } from '../../states/UserState';
@ -21,7 +21,6 @@ import { NotificationDrawer } from './NotificationDrawer';
import { SearchDrawer } from './SearchDrawer';
export function Header() {
const { classes } = InvenTreeStyle();
const [setNavigationOpen, navigationOpen] = useLocalState((state) => [
state.setNavigationOpen,
state.navigationOpen
@ -54,12 +53,13 @@ export function Header() {
limit: 1
}
};
let response = await api.get(
apiUrl(ApiEndpoints.notifications_list),
params
);
setNotificationCount(response.data.count);
return response.data;
let response = await api
.get(apiUrl(ApiEndpoints.notifications_list), params)
.catch(() => {
return null;
});
setNotificationCount(response?.data?.count ?? 0);
return response?.data;
} catch (error) {
return error;
}
@ -93,28 +93,32 @@ export function Header() {
}}
/>
<Container className={classes.layoutHeaderSection} size="100%">
<Group position="apart">
<Group justify="space-between">
<Group>
<NavHoverMenu openDrawer={openNavDrawer} />
<NavTabs />
</Group>
<Group>
<ActionIcon onClick={openSearchDrawer}>
<ActionIcon onClick={openSearchDrawer} variant="transparent">
<IconSearch />
</ActionIcon>
<SpotlightButton />
<ScanButton />
<ActionIcon onClick={openNotificationDrawer}>
<Indicator
radius="lg"
size="18"
label={notificationCount}
color="red"
disabled={notificationCount <= 0}
<Indicator
radius="lg"
size="18"
label={notificationCount}
color="red"
disabled={notificationCount <= 0}
inline
>
<ActionIcon
onClick={openNotificationDrawer}
variant="transparent"
>
<IconBell />
</Indicator>
</ActionIcon>
</ActionIcon>
</Indicator>
<MainMenu />
</Group>
</Group>
@ -124,7 +128,6 @@ export function Header() {
}
function NavTabs() {
const { classes } = InvenTreeStyle();
const navigate = useNavigate();
const match = useMatch(':tabName/*');
const tabValue = match?.params.tabName;
@ -134,11 +137,11 @@ function NavTabs() {
defaultValue="home"
classNames={{
root: classes.tabs,
tabsList: classes.tabsList,
list: classes.tabsList,
tab: classes.tab
}}
value={tabValue}
onTabChange={(value) =>
onChange={(value) =>
value == '/' ? navigate('/') : navigate(`/${value}`)
}
>

View File

@ -1,12 +1,12 @@
import { t } from '@lingui/macro';
import { Container, Flex, Space } from '@mantine/core';
import { SpotlightProvider } from '@mantine/spotlight';
import { Spotlight, createSpotlight } from '@mantine/spotlight';
import { IconSearch } from '@tabler/icons-react';
import { useEffect, useState } from 'react';
import { Navigate, Outlet, useLocation, useNavigate } from 'react-router-dom';
import { getActions } from '../../defaults/actions';
import { InvenTreeStyle } from '../../globalStyle';
import * as classes from '../../main.css';
import { useUserState } from '../../states/UserState';
import { Boundary } from '../Boundary';
import { Footer } from './Footer';
@ -25,8 +25,9 @@ export const ProtectedRoute = ({ children }: { children: JSX.Element }) => {
return children;
};
export const [firstStore, firstSpotlight] = createSpotlight();
export default function LayoutComponent() {
const { classes } = InvenTreeStyle();
const navigate = useNavigate();
const location = useLocation();
@ -38,6 +39,9 @@ export default function LayoutComponent() {
if (change.length > defaultActions.length) setCustomActions(true);
setActions(change);
}
// firstStore.subscribe(actionsAreChanging);
// clear additional actions on location change
useEffect(() => {
if (customActions) {
setActions(defaultActions);
@ -47,26 +51,28 @@ export default function LayoutComponent() {
return (
<ProtectedRoute>
<SpotlightProvider
actions={actions}
onActionsChange={actionsAreChanging}
searchIcon={<IconSearch size="1.2rem" />}
searchPlaceholder={t`Search...`}
shortcut={['mod + K', '/']}
nothingFoundMessage={t`Nothing found...`}
>
<Flex direction="column" mih="100vh">
<Header />
<Container className={classes.layoutContent} size="100%">
<Boundary label={'layout'}>
<Outlet />
</Boundary>
{/* </ErrorBoundary> */}
</Container>
<Space h="xl" />
<Footer />
</Flex>
</SpotlightProvider>
<Flex direction="column" mih="100vh">
<Header />
<Container className={classes.layoutContent} size="100%">
<Boundary label={'layout'}>
<Outlet />
</Boundary>
{/* </ErrorBoundary> */}
</Container>
<Space h="xl" />
<Footer />
<Spotlight
actions={actions}
store={firstStore}
highlightQuery
searchProps={{
leftSection: <IconSearch size="1.2rem" />,
placeholder: t`Search...`
}}
shortcut={['mod + K', '/']}
nothingFound={t`Nothing found...`}
/>
</Flex>
</ProtectedRoute>
);
}

View File

@ -10,26 +10,29 @@ import {
import { Link, useNavigate } from 'react-router-dom';
import { doLogout } from '../../functions/auth';
import { InvenTreeStyle } from '../../globalStyle';
import * as classes from '../../main.css';
import { useUserState } from '../../states/UserState';
import { vars } from '../../theme';
export function MainMenu() {
const navigate = useNavigate();
const { classes, theme } = InvenTreeStyle();
const userState = useUserState();
const [user, username] = useUserState((state) => [
state.user,
state.username
]);
return (
<Menu width={260} position="bottom-end">
<Menu.Target>
<UnstyledButton className={classes.layoutHeaderUser}>
<Group spacing={7}>
<Text weight={500} size="sm" sx={{ lineHeight: 1 }} mr={3}>
{userState.username() ? (
userState.username()
) : (
<Skeleton height={20} width={40} radius={theme.defaultRadius} />
)}
</Text>
<Group gap={7}>
{username() ? (
<Text fw={500} size="sm" style={{ lineHeight: 1 }} mr={3}>
{username()}
</Text>
) : (
<Skeleton height={20} width={40} radius={vars.radiusDefault} />
)}
<IconChevronDown />
</Group>
</UnstyledButton>
@ -38,22 +41,26 @@ export function MainMenu() {
<Menu.Label>
<Trans>Settings</Trans>
</Menu.Label>
<Menu.Item icon={<IconUserCog />} component={Link} to="/settings/user">
<Menu.Item
leftSection={<IconUserCog />}
component={Link}
to="/settings/user"
>
<Trans>Account settings</Trans>
</Menu.Item>
{userState.user?.is_staff && (
{user?.is_staff && (
<Menu.Item
icon={<IconSettings />}
leftSection={<IconSettings />}
component={Link}
to="/settings/system"
>
<Trans>System Settings</Trans>
</Menu.Item>
)}
{userState.user?.is_staff && <Menu.Divider />}
{userState.user?.is_staff && (
{user?.is_staff && <Menu.Divider />}
{user?.is_staff && (
<Menu.Item
icon={<IconUserBolt />}
leftSection={<IconUserBolt />}
component={Link}
to="/settings/admin"
>
@ -62,7 +69,7 @@ export function MainMenu() {
)}
<Menu.Divider />
<Menu.Item
icon={<IconLogout />}
leftSection={<IconLogout />}
onClick={() => {
doLogout(navigate);
}}

View File

@ -8,15 +8,17 @@ import {
HoverCard,
Skeleton,
Text,
UnstyledButton
UnstyledButton,
useMantineColorScheme
} from '@mantine/core';
import { IconLayoutSidebar } from '@tabler/icons-react';
import { useEffect, useState } from 'react';
import { menuItems } from '../../defaults/menuItems';
import { InvenTreeStyle } from '../../globalStyle';
import * as classes from '../../main.css';
import { useServerApiState } from '../../states/ApiState';
import { useLocalState } from '../../states/LocalState';
import { vars } from '../../theme';
import { InvenTreeLogo } from '../items/InvenTreeLogo';
import { MenuLinks } from '../items/MenuLinks';
@ -27,13 +29,13 @@ export function NavHoverMenu({
}: {
openDrawer: () => void;
}) {
const { classes, theme } = InvenTreeStyle();
const [hostKey, hostList] = useLocalState((state) => [
state.hostKey,
state.hostList
]);
const [servername] = useServerApiState((state) => [state.server.instance]);
const [instanceName, setInstanceName] = useState<string>();
const { colorScheme } = useMantineColorScheme();
useEffect(() => {
if (hostKey && hostList[hostKey]) {
@ -55,26 +57,27 @@ export function NavHoverMenu({
</UnstyledButton>
</HoverCard.Target>
<HoverCard.Dropdown sx={{ overflow: 'hidden' }}>
<Group position="apart" px="md">
<HoverCard.Dropdown style={{ overflow: 'hidden' }}>
<Group justify="space-between" px="md">
<ActionIcon
onClick={openDrawer}
onMouseOver={openDrawer}
title={t`Open Navigation`}
variant="default"
>
<IconLayoutSidebar />
</ActionIcon>
<Group spacing={'xs'}>
<Group gap={'xs'}>
{instanceName ? (
instanceName
) : (
<Skeleton height={20} width={40} radius={theme.defaultRadius} />
<Skeleton height={20} width={40} radius={vars.radiusDefault} />
)}{' '}
|{' '}
{servername ? (
servername
) : (
<Skeleton height={20} width={40} radius={theme.defaultRadius} />
<Skeleton height={20} width={40} radius={vars.radiusDefault} />
)}
</Group>
<Anchor href="#" fz="xs" onClick={openDrawer}>
@ -85,11 +88,13 @@ export function NavHoverMenu({
<Divider
my="sm"
mx="-md"
color={theme.colorScheme === 'dark' ? 'dark.5' : 'gray.1'}
color={
colorScheme === 'dark' ? vars.colors.dark[5] : vars.colors.gray[1]
}
/>
<MenuLinks links={onlyItems} highlighted={true} />
<div className={classes.headerDropdownFooter}>
<Group position="apart">
<Group justify="space-between">
<div>
<Text fw={500} fz="sm">
<Trans>Get started</Trans>

View File

@ -12,7 +12,7 @@ import { useEffect, useRef, useState } from 'react';
import { aboutLinks, navDocLinks } from '../../defaults/links';
import { menuItems } from '../../defaults/menuItems';
import { InvenTreeStyle } from '../../globalStyle';
import * as classes from '../../main.css';
import { DocumentationLinks } from '../items/DocumentationLinks';
import { MenuLinkItem, MenuLinks } from '../items/MenuLinks';
@ -27,8 +27,6 @@ export function NavigationDrawer({
opened: boolean;
close: () => void;
}) {
const { classes } = InvenTreeStyle();
return (
<Drawer
opened={opened}
@ -44,7 +42,6 @@ export function NavigationDrawer({
);
}
function DrawerContent() {
const { classes } = InvenTreeStyle();
const [scrollHeight, setScrollHeight] = useState(0);
const ref = useRef(null);
const { height } = useViewportSize();

View File

@ -2,9 +2,11 @@ import { t } from '@lingui/macro';
import {
ActionIcon,
Alert,
Center,
Divider,
Drawer,
Group,
Loader,
LoadingOverlay,
Space,
Stack,
@ -13,6 +15,7 @@ import {
} from '@mantine/core';
import { IconBellCheck, IconBellPlus } from '@tabler/icons-react';
import { useQuery } from '@tanstack/react-query';
import { useMemo } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { api } from '../../App';
@ -51,6 +54,10 @@ export function NotificationDrawer({
refetchOnWindowFocus: false
});
const hasNotifications: boolean = useMemo(() => {
return (notificationQuery.data?.results?.length ?? 0) > 0;
}, [notificationQuery.data]);
return (
<Drawer
opened={opened}
@ -67,74 +74,80 @@ export function NotificationDrawer({
}
}}
title={
<Group position="apart" noWrap={true}>
<Group justify="space-between" wrap="nowrap">
<StylishText size="lg">{t`Notifications`}</StylishText>
<ActionIcon
onClick={() => {
onClose();
navigate('/notifications/unread');
}}
variant="transparent"
>
<IconBellPlus />
</ActionIcon>
</Group>
}
>
<Stack spacing="xs">
<Stack gap="xs">
<Divider />
<LoadingOverlay visible={notificationQuery.isFetching} />
{(notificationQuery.data?.results?.length ?? 0) == 0 && (
{!hasNotifications && (
<Alert color="green">
<Text size="sm">{t`You have no unread notifications.`}</Text>
</Alert>
)}
{notificationQuery.data?.results?.map((notification: any) => (
<Group position="apart" key={notification.pk}>
<Stack spacing="3">
{notification?.target?.link ? (
<Text
size="sm"
component={Link}
to={notification?.target?.link}
target="_blank"
>
{notification.target?.name ??
notification.name ??
t`Notification`}
</Text>
) : (
<Text size="sm">
{notification.target?.name ??
notification.name ??
t`Notification`}
</Text>
)}
<Text size="xs">{notification.age_human ?? ''}</Text>
</Stack>
<Space />
<ActionIcon
color="gray"
variant="hover"
onClick={() => {
let url = apiUrl(
ApiEndpoints.notifications_list,
notification.pk
);
api
.patch(url, {
read: true
})
.then((response) => {
notificationQuery.refetch();
});
}}
>
<Tooltip label={t`Mark as read`}>
<IconBellCheck />
</Tooltip>
</ActionIcon>
</Group>
))}
{hasNotifications &&
notificationQuery.data?.results?.map((notification: any) => (
<Group justify="space-between" key={notification.pk}>
<Stack gap="3">
{notification?.target?.link ? (
<Text
size="sm"
component={Link}
to={notification?.target?.link}
target="_blank"
>
{notification.target?.name ??
notification.name ??
t`Notification`}
</Text>
) : (
<Text size="sm">
{notification.target?.name ??
notification.name ??
t`Notification`}
</Text>
)}
<Text size="xs">{notification.age_human ?? ''}</Text>
</Stack>
<Space />
<ActionIcon
color="gray"
variant="hover"
onClick={() => {
let url = apiUrl(
ApiEndpoints.notifications_list,
notification.pk
);
api
.patch(url, {
read: true
})
.then((response) => {
notificationQuery.refetch();
});
}}
>
<Tooltip label={t`Mark as read`}>
<IconBellCheck />
</Tooltip>
</ActionIcon>
</Group>
))}
{notificationQuery.isFetching && (
<Center>
<Loader size="sm" />
</Center>
)}
</Stack>
</Drawer>
);

View File

@ -33,23 +33,21 @@ export function PageDetail({
actions
}: Readonly<PageDetailInterface>) {
return (
<Stack spacing="xs">
<Stack gap="xs">
{breadcrumbs && breadcrumbs.length > 0 && (
<Paper p="xs" radius="xs" shadow="xs">
<BreadcrumbList
navCallback={breadcrumbAction}
breadcrumbs={breadcrumbs}
/>
</Paper>
<BreadcrumbList
navCallback={breadcrumbAction}
breadcrumbs={breadcrumbs}
/>
)}
<Paper p="xs" radius="xs" shadow="xs">
<Stack spacing="xs">
<Group position="apart" noWrap={true}>
<Group position="left" noWrap={true}>
<Stack gap="xs">
<Group justify="space-between" wrap="nowrap">
<Group justify="left" wrap="nowrap">
{imageUrl && (
<ApiImage src={imageUrl} radius="sm" height={64} width={64} />
<ApiImage src={imageUrl} radius="sm" h={64} w={64} />
)}
<Stack spacing="xs">
<Stack gap="xs">
{title && <StylishText size="lg">{title}</StylishText>}
{subtitle && (
<Text size="md" truncate>
@ -60,14 +58,14 @@ export function PageDetail({
</Group>
<Space />
{detail}
<Group position="right" spacing="xs" noWrap>
<Group justify="right" gap="xs" wrap="nowrap">
{badges?.map((badge, idx) => (
<Fragment key={idx}>{badge}</Fragment>
))}
</Group>
<Space />
{actions && (
<Group spacing={5} position="right">
<Group gap={5} justify="right">
{actions.map((action, idx) => (
<Fragment key={idx}>{action}</Fragment>
))}

View File

@ -72,7 +72,7 @@ function BasePanelGroup({
}, [setLastUsedPanel]);
// Callback when the active panel changes
function handlePanelChange(panel: string) {
function handlePanelChange(panel: string | null) {
if (activePanels.findIndex((p) => p.name === panel) === -1) {
setLastUsedPanel('');
return navigate('../');
@ -81,7 +81,7 @@ function BasePanelGroup({
navigate(`../${panel}`);
// Optionally call external callback hook
if (onPanelChange) {
if (panel && onPanelChange) {
onPanelChange(panel);
}
}
@ -109,10 +109,10 @@ function BasePanelGroup({
<Tabs
value={panel}
orientation="vertical"
onTabChange={handlePanelChange}
onChange={handlePanelChange}
keepMounted={false}
>
<Tabs.List position="left">
<Tabs.List justify="left">
{panels.map(
(panel) =>
!panel.hidden && (
@ -125,7 +125,7 @@ function BasePanelGroup({
p="xs"
value={panel.name}
// icon={(<InvenTreeIcon icon={panel.name}/>)} // Enable when implementing Icon manager everywhere
icon={panel.icon}
leftSection={panel.icon}
hidden={panel.hidden}
disabled={panel.disabled}
style={{ cursor: panel.disabled ? 'unset' : 'pointer' }}
@ -141,6 +141,8 @@ function BasePanelGroup({
paddingLeft: '10px'
}}
onClick={() => setExpanded(!expanded)}
variant="transparent"
size="md"
>
{expanded ? (
<IconLayoutSidebarLeftCollapse opacity={0.5} />
@ -162,7 +164,7 @@ function BasePanelGroup({
width: '100%'
}}
>
<Stack spacing="md">
<Stack gap="md">
{panel.showHeadline !== false && (
<>
<StylishText size="xl">{panel.label}</StylishText>

View File

@ -5,7 +5,7 @@ import {
LoadingOverlay,
Stack,
Text,
useMantineTheme
useMantineColorScheme
} from '@mantine/core';
import { ReactTree, ThemeSettings } from '@naisutech/react-tree';
import {
@ -20,6 +20,7 @@ import { useNavigate } from 'react-router-dom';
import { api } from '../../App';
import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { apiUrl } from '../../states/ApiState';
import { theme, vars } from '../../theme';
import { StylishText } from '../items/StylishText';
export function PartCategoryTree({
@ -59,9 +60,9 @@ export function PartCategoryTree({
function renderNode({ node }: { node: any }) {
return (
<Group
position="apart"
justify="space-between"
key={node.id}
noWrap={true}
wrap="nowrap"
onClick={() => {
onClose();
navigate(`/part/category/${node.id}`);
@ -80,57 +81,61 @@ export function PartCategoryTree({
return open ? <IconChevronDown /> : <IconChevronRight />;
}
const mantineTheme = useMantineTheme();
const { colorScheme } = useMantineColorScheme();
const themes: ThemeSettings = useMemo(() => {
const currentTheme =
mantineTheme.colorScheme === 'dark'
? mantineTheme.colorScheme
: mantineTheme.primaryColor;
colorScheme === 'dark'
? vars.colors.defaultColor
: vars.colors.primaryColors;
return {
dark: {
text: {
...mantineTheme.fn.fontStyles()
fontFamily: vars.fontFamily,
//fontSize: vars.fontSizes.md,
color: vars.colors.text
},
nodes: {
height: '2.5rem',
folder: {
selectedBgColor: mantineTheme.colors[currentTheme][4],
hoverBgColor: mantineTheme.colors[currentTheme][6]
selectedBgColor: currentTheme[4],
hoverBgColor: currentTheme[6]
},
leaf: {
selectedBgColor: mantineTheme.colors[currentTheme][4],
hoverBgColor: mantineTheme.colors[currentTheme][6]
selectedBgColor: currentTheme[4],
hoverBgColor: currentTheme[6]
},
icons: {
folderColor: mantineTheme.colors[currentTheme][3],
leafColor: mantineTheme.colors[currentTheme][3]
folderColor: currentTheme[3],
leafColor: currentTheme[3]
}
}
},
light: {
text: {
...mantineTheme.fn.fontStyles()
fontFamily: vars.fontFamily,
//fontSize: vars.fontSizes.md,
color: vars.colors.text
},
nodes: {
height: '2.5rem',
folder: {
selectedBgColor: mantineTheme.colors[currentTheme][4],
hoverBgColor: mantineTheme.colors[currentTheme][2]
selectedBgColor: currentTheme[4],
hoverBgColor: currentTheme[2]
},
leaf: {
selectedBgColor: mantineTheme.colors[currentTheme][4],
hoverBgColor: mantineTheme.colors[currentTheme][2]
selectedBgColor: currentTheme[4],
hoverBgColor: currentTheme[2]
},
icons: {
folderColor: mantineTheme.colors[currentTheme][8],
leafColor: mantineTheme.colors[currentTheme][6]
folderColor: currentTheme[8],
leafColor: currentTheme[6]
}
}
}
};
}, [mantineTheme]);
}, [theme]);
return (
<Drawer
@ -148,13 +153,13 @@ export function PartCategoryTree({
}
}}
title={
<Group position="left" p="ms" spacing="md" noWrap={true}>
<Group justify="left" p="ms" gap="md" wrap="nowrap">
<IconSitemap />
<StylishText size="lg">{t`Part Categories`}</StylishText>
</Group>
}
>
<Stack spacing="xs">
<Stack gap="xs">
<LoadingOverlay visible={treeQuery.isFetching} />
<ReactTree
nodes={treeQuery.data ?? []}
@ -162,7 +167,7 @@ export function PartCategoryTree({
RenderIcon={renderIcon}
defaultSelectedNodes={selectedCategory ? [selectedCategory] : []}
showEmptyItems={false}
theme={mantineTheme.colorScheme}
theme={colorScheme}
themes={themes}
/>
</Stack>

View File

@ -69,10 +69,10 @@ function QueryResultGroup({
return (
<Paper shadow="sm" radius="xs" p="md" key={`paper-${query.model}`}>
<Stack key={`stack-${query.model}`}>
<Group position="apart" noWrap={true}>
<Group position="left" spacing={5} noWrap={true}>
<Group justify="space-between" wrap="nowrap">
<Group justify="left" gap={5} wrap="nowrap">
<Text size="lg">{model.label_multiple}</Text>
<Text size="sm" italic>
<Text size="sm" style={{ fontStyle: 'italic' }}>
{' '}
- {query.results.count} <Trans>results</Trans>
</Text>
@ -332,13 +332,13 @@ export function SearchDrawer({
withCloseButton={false}
styles={{ header: { width: '100%' }, title: { width: '100%' } }}
title={
<Group position="apart" spacing={1} noWrap={true}>
<Group justify="space-between" gap={1} wrap="nowrap">
<TextInput
placeholder={t`Enter search text`}
radius="xs"
value={value}
onChange={(event) => setValue(event.currentTarget.value)}
icon={<IconSearch size="0.8rem" />}
leftSection={<IconSearch size="0.8rem" />}
rightSection={
value && (
<IconBackspace color="red" onClick={() => setValue('')} />
@ -394,7 +394,7 @@ export function SearchDrawer({
</Center>
)}
{!searchQuery.isFetching && !searchQuery.isError && (
<Stack spacing="md">
<Stack gap="md">
{queryResults.map((query, idx) => (
<QueryResultGroup
key={idx}

View File

@ -24,7 +24,7 @@ export function SettingsHeader({
switch_link
}: Readonly<SettingsHeaderInterface>) {
return (
<Stack spacing="0" ml={'sm'}>
<Stack gap="0" ml={'sm'}>
<Group>
<Title order={3}>{title}</Title>
{shorthand && <Text c="dimmed">({shorthand})</Text>}

View File

@ -51,9 +51,9 @@ export function StockLocationTree({
function renderNode({ node }: { node: any }) {
return (
<Group
position="apart"
justify="space-between"
key={node.id}
noWrap={true}
wrap="nowrap"
onClick={() => {
onClose();
navigate(`/stock/location/${node.id}`);
@ -88,13 +88,13 @@ export function StockLocationTree({
}
}}
title={
<Group position="left" noWrap={true} spacing="md" p="md">
<Group justify="left" wrap="nowrap" gap="md" p="md">
<IconSitemap />
<StylishText size="lg">{t`Stock Locations`}</StylishText>
</Group>
}
>
<Stack spacing="xs">
<Stack gap="xs">
<LoadingOverlay visible={treeQuery.isFetching} />
<ReactTree
nodes={treeQuery.data ?? []}

View File

@ -112,8 +112,8 @@ export function RenderInlineModel({
// TODO: Handle URL
return (
<Group spacing="xs" position="apart" noWrap={true}>
<Group spacing="xs" position="left" noWrap={true}>
<Group gap="xs" justify="space-between" wrap="nowrap">
<Group gap="xs" justify="left" wrap="nowrap">
{image && Thumbnail({ src: image, size: 18 })}
<Text size="sm">{primary}</Text>
{secondary && <Text size="xs">{secondary}</Text>}
@ -121,7 +121,7 @@ export function RenderInlineModel({
{suffix && (
<>
<Space />
<Text size="xs">{suffix}</Text>
<div style={{ fontSize: 'xs', lineHeight: 'xs' }}>{suffix}</div>
</>
)}
</Group>

View File

@ -7,7 +7,7 @@ import {
Stack,
Switch,
Text,
useMantineTheme
useMantineColorScheme
} from '@mantine/core';
import { showNotification } from '@mantine/notifications';
import { IconEdit } from '@tabler/icons-react';
@ -19,6 +19,7 @@ import { openModalApiForm } from '../../functions/forms';
import { apiUrl } from '../../states/ApiState';
import { SettingsStateProps } from '../../states/SettingsState';
import { Setting, SettingType } from '../../states/states';
import { vars } from '../../theme';
import { ApiFormFieldType } from '../forms/fields/ApiFormField';
/**
@ -137,7 +138,7 @@ function SettingValue({
);
default:
return valueText ? (
<Group spacing="xs" position="right">
<Group gap="xs" justify="right">
<Space />
<Button variant="subtle" onClick={onEditButton}>
{valueText}
@ -165,20 +166,18 @@ export function SettingItem({
shaded: boolean;
onChange?: () => void;
}) {
const theme = useMantineTheme();
const { colorScheme } = useMantineColorScheme();
const style: Record<string, string> = { paddingLeft: '8px' };
if (shaded) {
style['backgroundColor'] =
theme.colorScheme === 'light'
? theme.colors.gray[1]
: theme.colors.gray[9];
colorScheme === 'light' ? vars.colors.gray[1] : vars.colors.gray[9];
}
return (
<Paper style={style}>
<Group position="apart" p="3">
<Stack spacing="2" p="4px">
<Group justify="space-between" p="3">
<Stack gap="2" p="4px">
<Text>
{setting.name}
{setting.required ? ' *' : ''}

View File

@ -35,7 +35,7 @@ export function SettingList({
return (
<>
<Stack spacing="xs">
<Stack gap="xs">
{(keys || allKeys).map((key, i) => {
const setting = settingsState?.settings?.find(
(s: any) => s.key === key
@ -51,7 +51,7 @@ export function SettingList({
onChange={onChange}
/>
) : (
<Text size="sm" italic color="red">
<Text size="sm" style={{ fontStyle: 'italic' }} color="red">
Setting {key} not found
</Text>
)}
@ -59,7 +59,7 @@ export function SettingList({
);
})}
{(keys || allKeys).length === 0 && (
<Text italic>
<Text style={{ fontStyle: 'italic' }}>
<Trans>No settings specified</Trans>
</Text>
)}

View File

@ -1,17 +1,18 @@
import { Trans } from '@lingui/macro';
import { Button, Stack, Title } from '@mantine/core';
import { Button, Stack, Title, useMantineColorScheme } from '@mantine/core';
import { IconExternalLink } from '@tabler/icons-react';
import { vars } from '../../theme';
export default function FeedbackWidget() {
const { colorScheme } = useMantineColorScheme();
return (
<Stack
sx={(theme) => ({
style={{
backgroundColor:
theme.colorScheme === 'dark'
? theme.colors.gray[9]
: theme.colors.gray[1],
borderRadius: theme.radius.md
})}
colorScheme === 'dark' ? vars.colors.gray[9] : vars.colors.gray[1],
borderRadius: vars.radius.md
}}
p={15}
>
<Title order={5}>
@ -26,7 +27,7 @@ export default function FeedbackWidget() {
component="a"
href="https://github.com/inventree/InvenTree/discussions/5328"
variant="outline"
leftIcon={<IconExternalLink size="0.9rem" />}
leftSection={<IconExternalLink size="0.9rem" />}
>
<Trans>Provide Feedback</Trans>
</Button>

View File

@ -0,0 +1,16 @@
import { style } from '@vanilla-extract/css';
import { vars } from '../../theme';
export const backgroundItem = style({
maxWidth: '100%',
padding: '8px',
boxShadow: vars.shadows.md,
[vars.lightSelector]: { backgroundColor: vars.colors.white },
[vars.darkSelector]: { backgroundColor: vars.colors.dark[5] }
});
export const baseItem = style({
maxWidth: '100%',
padding: '8px'
});

View File

@ -5,8 +5,7 @@ import {
Group,
Indicator,
Menu,
Text,
createStyles
Text
} from '@mantine/core';
import { useDisclosure, useHotkeys } from '@mantine/hooks';
import {
@ -19,6 +18,8 @@ import {
import { useEffect, useState } from 'react';
import { Responsive, WidthProvider } from 'react-grid-layout';
import * as classes from './WidgetLayout.css';
const ReactGridLayout = WidthProvider(Responsive);
interface LayoutStorage {
@ -27,21 +28,6 @@ interface LayoutStorage {
const compactType = 'vertical';
const useItemStyle = createStyles((theme) => ({
backgroundItem: {
backgroundColor:
theme.colorScheme === 'dark' ? theme.colors.dark[5] : theme.white,
maxWidth: '100%',
padding: '8px',
boxShadow: theme.shadows.md
},
baseItem: {
maxWidth: '100%',
padding: '8px'
}
}));
export interface LayoutItemType {
i: number;
val: string | JSX.Element | JSX.Element[] | (() => JSX.Element);
@ -66,7 +52,6 @@ export function WidgetLayout({
const [layouts, setLayouts] = useState({});
const [editable, setEditable] = useDisclosure(false);
const [boxShown, setBoxShown] = useDisclosure(true);
const { classes } = useItemStyle();
useEffect(() => {
let layout = getFromLS('layouts') || [];
@ -155,7 +140,7 @@ function WidgetControlBar({
useHotkeys([['mod+E', () => editFnc()]]);
return (
<Group position="right">
<Group justify="right">
<Menu
shadow="md"
width={200}
@ -181,13 +166,13 @@ function WidgetControlBar({
<Trans>Layout</Trans>
</Menu.Label>
<Menu.Item
icon={<IconArrowBackUpDouble size={14} />}
leftSection={<IconArrowBackUpDouble size={14} />}
onClick={resetLayout}
>
<Trans>Reset Layout</Trans>
</Menu.Item>
<Menu.Item
icon={
leftSection={
<IconLayout2 size={14} color={editable ? 'red' : undefined} />
}
onClick={editFnc}
@ -206,7 +191,7 @@ function WidgetControlBar({
<Trans>Appearance</Trans>
</Menu.Label>
<Menu.Item
icon={
leftSection={
boxShown ? (
<IconSquareCheck size={14} />
) : (

View File

@ -1,15 +1,12 @@
import { QueryClientProvider } from '@tanstack/react-query';
import { queryClient } from '../App';
import { LanguageContext } from './LanguageContext';
import { ThemeContext } from './ThemeContext';
export const BaseContext = ({ children }: { children: any }) => {
return (
<QueryClientProvider client={queryClient}>
<LanguageContext>
<ThemeContext>{children}</ThemeContext>
</LanguageContext>
<ThemeContext>{children}</ThemeContext>
</QueryClientProvider>
);
};

View File

@ -1,11 +1,5 @@
import { t } from '@lingui/macro';
import {
ColorScheme,
ColorSchemeProvider,
MantineProvider,
MantineThemeOverride
} from '@mantine/core';
import { useColorScheme, useLocalStorage } from '@mantine/hooks';
import { MantineProvider, createTheme } from '@mantine/core';
import { ModalsProvider } from '@mantine/modals';
import { Notifications } from '@mantine/notifications';
@ -14,36 +8,24 @@ import { LicenseModal } from '../components/modals/LicenseModal';
import { QrCodeModal } from '../components/modals/QrCodeModal';
import { ServerInfoModal } from '../components/modals/ServerInfoModal';
import { useLocalState } from '../states/LocalState';
import { LanguageContext } from './LanguageContext';
import { colorSchema } from './colorSchema';
export function ThemeContext({ children }: { children: JSX.Element }) {
const [primaryColor, whiteColor, blackColor, radius, loader] = useLocalState(
const [primaryColor, whiteColor, blackColor, radius] = useLocalState(
(state) => [
state.primaryColor,
state.whiteColor,
state.blackColor,
state.radius,
state.loader
state.radius
]
);
// Color Scheme
const preferredColorScheme = useColorScheme();
const [colorScheme, setColorScheme] = useLocalStorage<ColorScheme>({
key: 'scheme',
defaultValue: preferredColorScheme
});
const toggleColorScheme = (value?: ColorScheme) => {
setColorScheme(value || (colorScheme === 'dark' ? 'light' : 'dark'));
myTheme.colorScheme = colorScheme;
};
// Theme
const myTheme: MantineThemeOverride = {
colorScheme: colorScheme,
const myTheme = createTheme({
primaryColor: primaryColor,
white: whiteColor,
black: blackColor,
loader: loader,
defaultRadius: radius,
breakpoints: {
xs: '30em',
@ -52,15 +34,11 @@ export function ThemeContext({ children }: { children: JSX.Element }) {
lg: '74em',
xl: '90em'
}
};
});
return (
<ColorSchemeProvider
colorScheme={colorScheme}
toggleColorScheme={toggleColorScheme}
>
<MantineProvider theme={myTheme} withGlobalStyles withNormalizeCSS>
<Notifications />
<MantineProvider theme={myTheme} colorSchemeManager={colorSchema}>
<LanguageContext>
<ModalsProvider
labels={{ confirm: t`Submit`, cancel: t`Cancel` }}
modals={{
@ -70,9 +48,10 @@ export function ThemeContext({ children }: { children: JSX.Element }) {
license: LicenseModal
}}
>
<Notifications />
{children}
</ModalsProvider>
</MantineProvider>
</ColorSchemeProvider>
</LanguageContext>
</MantineProvider>
);
}

View File

@ -0,0 +1,67 @@
import {
MantineColorScheme,
MantineColorSchemeManager,
isMantineColorScheme
} from '@mantine/core';
export interface LocalStorageColorSchemeManagerOptions {
/** Local storage key used to retrieve value with `localStorage.getItem(key)`, `mantine-color-scheme` by default */
key?: string;
}
export function localStorageColorSchemeManager({
key = 'mantine-color-scheme'
}: LocalStorageColorSchemeManagerOptions = {}): MantineColorSchemeManager {
let handleStorageEvent: (event: StorageEvent) => void;
return {
get: (defaultValue) => {
if (typeof window === 'undefined') {
return defaultValue;
}
try {
return (
(window.localStorage.getItem(key) as MantineColorScheme) ||
defaultValue
);
} catch {
return defaultValue;
}
},
set: (value) => {
try {
window.localStorage.setItem(key, value);
} catch (error) {
// eslint-disable-next-line no-console
console.warn(
'[@mantine/core] Local storage color scheme manager was unable to save color scheme.',
error
);
}
},
subscribe: (onUpdate) => {
handleStorageEvent = (event) => {
if (event.storageArea === window.localStorage && event.key === key) {
isMantineColorScheme(event.newValue) && onUpdate(event.newValue);
}
};
window.addEventListener('storage', handleStorageEvent);
},
unsubscribe: () => {
window.removeEventListener('storage', handleStorageEvent);
},
clear: () => {
window.localStorage.removeItem(key);
}
};
}
export const colorSchema = localStorageColorSchemeManager({
key: 'scheme'
});

View File

@ -1,5 +1,5 @@
import { t } from '@lingui/macro';
import type { SpotlightAction } from '@mantine/spotlight';
import type { SpotlightActionData } from '@mantine/spotlight';
import { IconHome, IconLink, IconPointer } from '@tabler/icons-react';
import { NavigateFunction } from 'react-router-dom';
@ -10,48 +10,55 @@ import { menuItems } from './menuItems';
export function getActions(navigate: NavigateFunction) {
const setNavigationOpen = useLocalState((state) => state.setNavigationOpen);
const actions: SpotlightAction[] = [
const actions: SpotlightActionData[] = [
{
title: t`Home`,
id: 'home',
label: t`Home`,
description: `Go to the home page`,
onTrigger: () => navigate(menuItems.home.link),
icon: <IconHome size="1.2rem" />
onClick: () => navigate(menuItems.home.link),
leftSection: <IconHome size="1.2rem" />
},
{
title: t`Dashboard`,
id: 'dashboard',
label: t`Dashboard`,
description: t`Go to the InvenTree dashboard`,
onTrigger: () => navigate(menuItems.dashboard.link),
icon: <IconLink size="1.2rem" />
onClick: () => navigate(menuItems.dashboard.link),
leftSection: <IconLink size="1.2rem" />
},
{
title: t`Documentation`,
id: 'documentation',
label: t`Documentation`,
description: t`Visit the documentation to learn more about InvenTree`,
onTrigger: () => (window.location.href = docLinks.faq),
icon: <IconLink size="1.2rem" />
onClick: () => (window.location.href = docLinks.faq),
leftSection: <IconLink size="1.2rem" />
},
{
title: t`About InvenTree`,
id: 'about',
label: t`About InvenTree`,
description: t`About the InvenTree org`,
onTrigger: () => aboutInvenTree(),
icon: <IconLink size="1.2rem" />
onClick: () => aboutInvenTree(),
leftSection: <IconLink size="1.2rem" />
},
{
title: t`Server Information`,
id: 'server-info',
label: t`Server Information`,
description: t`About this Inventree instance`,
onTrigger: () => serverInfo(),
icon: <IconLink size="1.2rem" />
onClick: () => serverInfo(),
leftSection: <IconLink size="1.2rem" />
},
{
title: t`License Information`,
id: 'license-info',
label: t`License Information`,
description: t`Licenses for dependencies of the service`,
onTrigger: () => licenseInfo(),
icon: <IconLink size="1.2rem" />
onClick: () => licenseInfo(),
leftSection: <IconLink size="1.2rem" />
},
{
title: t`Open Navigation`,
id: 'navigation',
label: t`Open Navigation`,
description: t`Open the main navigation menu`,
onTrigger: () => setNavigationOpen(true),
icon: <IconPointer size="1.2rem" />
onClick: () => setNavigationOpen(true),
leftSection: <IconPointer size="1.2rem" />
}
];

View File

@ -1,3 +1,4 @@
import { MantineSize } from '@mantine/core';
import dayjs from 'dayjs';
import {
@ -154,3 +155,5 @@ export function renderDate(
return date;
}
}
export type UiSizeType = MantineSize | string | number;

View File

@ -392,7 +392,7 @@ export function useCancelBuildOutputsForm({
const preFormContent = useMemo(() => {
return (
<Stack spacing="xs">
<Stack gap="xs">
<Alert color="red" title={t`Cancel Build Outputs`}>
<Text>{t`Selected build outputs will be deleted`}</Text>
</Alert>

View File

@ -311,7 +311,7 @@ function StockOperationsRow({
<td>{record.location ? record.location_detail?.pathstring : '-'}</td>
<td>
<Flex align="center" gap="xs">
<Group position="apart">
<Group justify="space-between">
<Text>{stockString}</Text>
<StatusRenderer status={record.status} type={ModelType.stockitem} />
</Group>

View File

@ -244,7 +244,7 @@ export function openModalApiForm(props: OpenApiFormProps) {
props.onClose ? props.onClose() : null;
},
children: (
<Stack spacing={'xs'}>
<Stack gap={'xs'}>
<Divider />
<ApiForm id={modalId} props={props} optionsLoading={false} />
</Stack>

View File

@ -1,13 +1,18 @@
import { Center, Loader, Stack } from '@mantine/core';
import { Center, Loader, MantineProvider, Stack } from '@mantine/core';
import { Suspense } from 'react';
import { colorSchema } from '../contexts/colorSchema';
import { theme } from '../theme';
function LoadingFallback() {
return (
<Stack>
<Center>
<Loader />
</Center>
</Stack>
<MantineProvider theme={theme} colorSchemeManager={colorSchema}>
<Stack>
<Center>
<Loader />
</Center>
</Stack>
</MantineProvider>
);
}

View File

@ -1,184 +0,0 @@
import { createStyles, rem } from '@mantine/core';
export const InvenTreeStyle = createStyles((theme) => ({
layoutHeader: {
paddingTop: theme.spacing.sm,
backgroundColor:
theme.colorScheme === 'dark'
? theme.colors.dark[6]
: theme.colors.gray[0],
borderBottom: `1px solid ${
theme.colorScheme === 'dark' ? 'transparent' : theme.colors.gray[2]
}`,
marginBottom: 10
},
layoutFooter: {
marginTop: 10,
borderTop: `1px solid ${
theme.colorScheme === 'dark' ? theme.colors.dark[5] : theme.colors.gray[2]
}`
},
layoutHeaderSection: {
paddingBottom: theme.spacing.sm
},
layoutHeaderUser: {
color: theme.colorScheme === 'dark' ? theme.colors.dark[0] : theme.black,
padding: `${theme.spacing.xs}px ${theme.spacing.sm}px`,
borderRadius: theme.defaultRadius,
transition: 'background-color 100ms ease',
[theme.fn.smallerThan('xs')]: {
display: 'none'
}
},
headerDropdownFooter: {
backgroundColor:
theme.colorScheme === 'dark'
? theme.colors.dark[7]
: theme.colors.gray[0],
margin: `calc(${theme.spacing.md} * -1)`,
marginTop: theme.spacing.sm,
padding: `${theme.spacing.md} calc(${theme.spacing.md} * 2)`,
paddingBottom: theme.spacing.xl,
borderTop: `${rem(1)} solid ${
theme.colorScheme === 'dark' ? theme.colors.dark[5] : theme.colors.gray[1]
}`
},
link: {
display: 'flex',
alignItems: 'center',
height: '100%',
paddingLeft: theme.spacing.md,
paddingRight: theme.spacing.md,
textDecoration: 'none',
color: theme.colorScheme === 'dark' ? theme.white : theme.black,
fontWeight: 500,
fontSize: theme.fontSizes.sm,
[theme.fn.smallerThan('sm')]: {
height: rem(42),
display: 'flex',
alignItems: 'center',
width: '100%'
},
...theme.fn.hover({
backgroundColor:
theme.colorScheme === 'dark'
? theme.colors.dark[6]
: theme.colors.gray[0]
})
},
subLink: {
width: '100%',
padding: `${theme.spacing.xs} ${theme.spacing.md}`,
borderRadius: theme.defaultRadius,
...theme.fn.hover({
backgroundColor:
theme.colorScheme === 'dark'
? theme.colors.dark[7]
: theme.colors.gray[0]
}),
'&:active': theme.activeStyles
},
docHover: {
border: `1px dashed `
},
layoutContent: {
flex: 1,
width: '100%'
},
layoutFooterLinks: {
[theme.fn.smallerThan('xs')]: {
marginTop: theme.spacing.md
}
},
layoutFooterInner: {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
paddingTop: theme.spacing.xl,
paddingBottom: theme.spacing.xl,
[theme.fn.smallerThan('xs')]: {
flexDirection: 'column'
}
},
tabs: {
[theme.fn.smallerThan('sm')]: {
display: 'none'
}
},
tabsList: {
borderBottom: '0 !important',
'& > button:first-of-type': {
paddingLeft: '0 !important'
},
'& > button:last-of-type': {
paddingRight: '0 !important'
}
},
tab: {
fontWeight: 500,
height: 38,
backgroundColor: 'transparent',
'&:hover': {
backgroundColor:
theme.colorScheme === 'dark'
? theme.colors.dark[5]
: theme.colors.gray[1]
}
},
signText: {
fontSize: 'xl',
fontWeight: 700
},
error: {
backgroundColor: theme.colors.gray[0],
color: theme.colors.red[6]
},
dashboardItemValue: {
fontSize: 24,
fontWeight: 700,
lineHeight: 1
},
dashboardItemTitle: {
fontWeight: 700
},
card: {
backgroundColor:
theme.colorScheme === 'dark' ? theme.colors.dark[7] : theme.white
},
itemTopBorder: {
borderTop: `1px solid ${
theme.colorScheme === 'dark' ? theme.colors.dark[4] : theme.colors.gray[2]
}`
},
navigationDrawer: {
padding: 0
}
}));

View File

@ -1,9 +1,10 @@
import { t } from '@lingui/macro';
import { Alert, Divider, MantineNumberSize, Stack } from '@mantine/core';
import { Alert, Divider, Stack } from '@mantine/core';
import { useId } from '@mantine/hooks';
import { useEffect, useMemo, useRef } from 'react';
import { ApiFormProps, OptionsApiForm } from '../components/forms/ApiForm';
import { UiSizeType } from '../defaults/formatters';
import { useModal } from './UseModal';
/**
@ -20,7 +21,7 @@ export interface ApiFormModalProps extends ApiFormProps {
onClose?: () => void;
onOpen?: () => void;
closeOnClickOutside?: boolean;
size?: MantineNumberSize;
size?: UiSizeType;
}
/**
@ -62,7 +63,7 @@ export function useApiFormModal(props: ApiFormModalProps) {
closeOnClickOutside: formProps.closeOnClickOutside,
size: props.size ?? 'xl',
children: (
<Stack spacing={'xs'}>
<Stack gap={'xs'}>
<Divider />
<OptionsApiForm props={formProps} id={id} />
</Stack>

View File

@ -1,13 +1,14 @@
import { MantineNumberSize, Modal } from '@mantine/core';
import { Modal } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
import React, { useCallback } from 'react';
import { StylishText } from '../components/items/StylishText';
import { UiSizeType } from '../defaults/formatters';
export interface UseModalProps {
title: string;
children: React.ReactElement;
size?: MantineNumberSize;
size?: UiSizeType;
onOpen?: () => void;
onClose?: () => void;
closeOnClickOutside?: boolean;

View File

@ -0,0 +1,180 @@
import { rem } from '@mantine/core';
import { style } from '@vanilla-extract/css';
import { vars } from './theme';
export const layoutHeader = style({
paddingTop: vars.spacing.sm,
marginBottom: 10,
[vars.lightSelector]: {
backgroundColor: vars.colors.gray[0],
borderBottom: `${rem(1)} solid ${vars.colors.gray[2]}`
},
[vars.darkSelector]: {
backgroundColor: vars.colors.dark[6],
borderBottom: `${rem(1)} solid transparent`
}
});
export const layoutFooter = style({
marginTop: 10,
[vars.lightSelector]: { borderTop: `1px solid ${vars.colors.gray[2]}` },
[vars.darkSelector]: { borderTop: `1px solid ${vars.colors.dark[5]}` }
});
export const layoutHeaderSection = style({
paddingBottom: vars.spacing.sm
});
export const layoutHeaderUser = style({
padding: `${vars.spacing.xs}px ${vars.spacing.sm}px`,
borderRadius: vars.radiusDefault,
transition: 'background-color 100ms ease',
[vars.lightSelector]: { color: vars.colors.black },
[vars.darkSelector]: { color: vars.colors.dark[0] },
[vars.smallerThan('xs')]: {
display: 'none'
}
});
export const headerDropdownFooter = style({
margin: `calc(${vars.spacing.md} * -1)`,
marginTop: vars.spacing.sm,
padding: `${vars.spacing.md} calc(${vars.spacing.md} * 2)`,
paddingBottom: vars.spacing.xl,
[vars.lightSelector]: {
backgroundColor: vars.colors.gray[0],
borderTop: `${rem(1)} solid ${vars.colors.gray[1]}`
},
[vars.darkSelector]: {
backgroundColor: vars.colors.dark[7],
borderTop: `${rem(1)} solid ${vars.colors.dark[5]}`
}
});
export const link = style({
display: 'flex',
alignItems: 'center',
height: '100%',
paddingLeft: vars.spacing.md,
paddingRight: vars.spacing.md,
textDecoration: 'none',
fontWeight: 500,
fontSize: vars.fontSizes.sm,
[vars.lightSelector]: { color: vars.colors.black },
[vars.darkSelector]: { color: vars.colors.white },
[vars.smallerThan('sm')]: {
height: rem(42),
display: 'flex',
alignItems: 'center',
width: '100%'
},
':hover': {
[vars.lightSelector]: { backgroundColor: vars.colors.gray[0] },
[vars.darkSelector]: { backgroundColor: vars.colors.dark[6] }
}
});
export const subLink = style({
width: '100%',
padding: `${vars.spacing.xs} ${vars.spacing.md}`,
borderRadius: vars.radiusDefault,
':hover': {
[vars.lightSelector]: { backgroundColor: vars.colors.gray[0] },
[vars.darkSelector]: { backgroundColor: vars.colors.dark[7] }
},
':active': {
color: vars.colors.defaultHover
}
});
export const docHover = style({
border: `1px dashed `
});
export const layoutContent = style({
flex: 1,
width: '100%'
});
export const layoutFooterLinks = style({
[vars.smallerThan('xs')]: {
marginTop: vars.spacing.md
}
});
export const layoutFooterInner = style({
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
paddingTop: vars.spacing.xl,
paddingBottom: vars.spacing.xl,
[vars.smallerThan('xs')]: {
flexDirection: 'column'
}
});
export const tabs = style({
[vars.smallerThan('sm')]: {
display: 'none'
}
});
export const tabsList = style({
borderBottom: '0 !important'
});
export const tab = style({
fontWeight: 500,
height: 38,
backgroundColor: 'transparent',
':hover': {
[vars.lightSelector]: { backgroundColor: vars.colors.gray[1] },
[vars.darkSelector]: { backgroundColor: vars.colors.dark[5] }
}
});
export const signText = style({
fontSize: 'xl',
fontWeight: 700
});
export const error = style({
backgroundColor: vars.colors.gray[0],
color: vars.colors.red[6]
});
export const dashboardItemValue = style({
fontSize: 24,
fontWeight: 700,
lineHeight: 1
});
export const dashboardItemTitle = style({
fontWeight: 700
});
export const card = style({
[vars.lightSelector]: { backgroundColor: vars.colors.white },
[vars.darkSelector]: { backgroundColor: vars.colors.dark[7] }
});
export const itemTopBorder = style({
[vars.lightSelector]: { borderTop: `1px solid ${vars.colors.gray[2]}` },
[vars.darkSelector]: { borderTop: `1px solid ${vars.colors.dark[4]}` }
});
export const navigationDrawer = style({
padding: 0
});

View File

@ -1,4 +1,9 @@
import '@mantine/carousel/styles.css';
import '@mantine/core/styles.css';
import '@mantine/notifications/styles.css';
import '@mantine/spotlight/styles.css';
import * as Sentry from '@sentry/react';
import 'mantine-datatable/styles.css';
import React from 'react';
import ReactDOM from 'react-dom/client';
import 'react-grid-layout/css/styles.css';

View File

@ -22,7 +22,7 @@ export default function Logged_In() {
<Text size="lg">
<Trans>Checking if you are already logged in</Trans>
</Text>
<Group position="center">
<Group justify="center">
<Loader />
</Group>
</Stack>

View File

@ -36,7 +36,8 @@ export default function Login() {
const [searchParams] = useSearchParams();
// Data manipulation functions
function ChangeHost(newHost: string): void {
function ChangeHost(newHost: string | null): void {
if (newHost === null) return;
setHost(hostList[newHost]?.host, newHost);
setApiDefaults();
fetchServerApiState();
@ -81,7 +82,7 @@ export default function Login() {
) : (
<>
<Paper radius="md" p="xl" withBorder>
<Text size="lg" weight={500}>
<Text size="lg" fw={500}>
{loginMode ? (
<Trans>Welcome, log in below</Trans>
) : (

View File

@ -22,7 +22,7 @@ export default function Logout() {
<Text size="lg">
<Trans>Logging out</Trans>
</Text>
<Group position="center">
<Group justify="center">
<Loader />
</Group>
</Stack>

View File

@ -8,13 +8,14 @@ import {
Text,
TextInput
} from '@mantine/core';
import { spotlight } from '@mantine/spotlight';
import { SpotlightActionData } from '@mantine/spotlight';
import { IconAlien } from '@tabler/icons-react';
import { ReactNode, useMemo, useState } from 'react';
import { OptionsApiForm } from '../../components/forms/ApiForm';
import { PlaceholderPill } from '../../components/items/Placeholder';
import { StylishText } from '../../components/items/StylishText';
import { firstSpotlight } from '../../components/nav/Layout';
import { StatusRenderer } from '../../components/render/StatusRenderer';
import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType';
@ -135,7 +136,7 @@ function ApiFormsPlayground() {
<Button onClick={() => openCreatePart()}>Create Part new Modal</Button>
{createPartModal}
</Group>
<Card sx={{ padding: '30px' }}>
<Card style={{ padding: '30px' }}>
<OptionsApiForm
props={{
url: ApiEndpoints.part_list,
@ -181,25 +182,28 @@ function SpotlighPlayground() {
<Button
variant="outline"
onClick={() => {
spotlight.registerActions([
const setAdditionalActions = (value: SpotlightActionData[]) => {
console.log('would add', value);
};
setAdditionalActions([
{
id: 'secret-action-1',
title: 'Secret action',
description: 'It was registered with a button click',
icon: <IconAlien size="1.2rem" />,
onTrigger: () => console.log('Secret')
leftSection: <IconAlien size="1.2rem" />,
onClick: () => console.log('Secret')
},
{
id: 'secret-action-2',
title: 'Another secret action',
description:
'You can register multiple actions with just one command',
icon: <IconAlien size="1.2rem" />,
onTrigger: () => console.log('Secret')
leftSection: <IconAlien size="1.2rem" />,
onClick: () => console.log('Secret')
}
]);
console.log('registed');
spotlight.open();
firstSpotlight.open();
}}
>
Register extra actions

View File

@ -4,7 +4,6 @@ import {
Badge,
Button,
Checkbox,
Col,
Container,
Grid,
Group,
@ -42,7 +41,7 @@ import {
} from '@tabler/icons-react';
import { Html5Qrcode } from 'html5-qrcode';
import { CameraDevice } from 'html5-qrcode/camera/core';
import { ReactNode, useEffect, useState } from 'react';
import { ReactNode, useEffect, useMemo, useState } from 'react';
import { api } from '../../App';
import { DocInfo } from '../../components/items/DocInfo';
@ -243,14 +242,14 @@ export default function Scan() {
if (uniqueObjectTypes.length === 0) {
return (
<Group spacing={0}>
<Group gap={0}>
<IconQuestionMark color="orange" />
<Trans>Selected elements are not known</Trans>
</Group>
);
} else if (uniqueObjectTypes.length > 1) {
return (
<Group spacing={0}>
<Group gap={0}>
<IconAlertCircle color="orange" />
<Trans>Multiple object types selected</Trans>
</Group>
@ -262,7 +261,11 @@ export default function Scan() {
<Trans>Actions for {uniqueObjectTypes[0]} </Trans>
</Text>
<Group>
<ActionIcon onClick={notYetImplemented} title={t`Count`}>
<ActionIcon
onClick={notYetImplemented}
title={t`Count`}
variant="default"
>
<IconNumber />
</ActionIcon>
</Group>
@ -273,8 +276,8 @@ export default function Scan() {
// rendering
return (
<>
<Group position="apart">
<Group position="left">
<Group justify="space-between">
<Group justify="left">
<StylishText>
<Trans>Scan Page</Trans>
</StylishText>
@ -293,10 +296,10 @@ export default function Scan() {
</Group>
<Space h={'md'} />
<Grid maw={'100%'}>
<Col span={4}>
<Grid.Col span={4}>
<Stack>
<Stack spacing="xs">
<Group position="apart">
<Stack gap="xs">
<Group justify="space-between">
<TitleWithDoc
order={3}
text={t`Select the input method you want to use to scan items.`}
@ -309,12 +312,12 @@ export default function Scan() {
data={inputOptions}
searchable
placeholder={t`Select input method`}
nothingFound={t`Nothing found`}
nothingFoundMessage={t`Nothing found`}
/>
</Group>
{inp}
</Stack>
<Stack spacing={0}>
<Stack gap={0}>
<TitleWithDoc
order={3}
text={t`Depending on the selected parts actions will be shown here. Not all barcode types are supported currently.`}
@ -338,6 +341,7 @@ export default function Scan() {
color="red"
onClick={btnDeleteHistory}
title={t`Delete`}
variant="default"
>
<IconTrash />
</ActionIcon>
@ -345,6 +349,7 @@ export default function Scan() {
onClick={btnRunSelectedBarcode}
disabled={selection.length > 1}
title={t`Lookup part`}
variant="default"
>
<IconSearch />
</ActionIcon>
@ -352,6 +357,7 @@ export default function Scan() {
onClick={btnOpenSelectedLink}
disabled={!selectionLinked}
title={t`Open Link`}
variant="default"
>
<IconLink />
</ActionIcon>
@ -361,9 +367,9 @@ export default function Scan() {
)}
</Stack>
</Stack>
</Col>
<Col span={8}>
<Group position="apart">
</Grid.Col>
<Grid.Col span={8}>
<Group justify="space-between">
<TitleWithDoc
order={3}
text={t`History is locally kept in this browser.`}
@ -374,6 +380,7 @@ export default function Scan() {
<ActionIcon
color="red"
onClick={btnDeleteFullHistory}
variant="default"
title={t`Delete History`}
>
<IconTrash />
@ -384,7 +391,7 @@ export default function Scan() {
selection={selection}
setSelection={setSelection}
/>
</Col>
</Grid.Col>
</Grid>
</>
);
@ -409,34 +416,30 @@ function HistoryTable({
setSelection((current) =>
current.length === data.length ? [] : data.map((item) => item.id)
);
const [rows, setRows] = useState<ReactNode>();
useEffect(() => {
setRows(
data.map((item) => {
return (
<tr key={item.id}>
<td>
<Checkbox
checked={selection.includes(item.id)}
onChange={() => toggleRow(item.id)}
transitionDuration={0}
/>
</td>
<td>
{item.pk && item.model && item.instance ? (
<RenderInstance model={item.model} instance={item.instance} />
) : (
item.ref
)}
</td>
<td>{item.model}</td>
<td>{item.source}</td>
<td>{item.timestamp?.toString()}</td>
</tr>
);
})
);
const rows = useMemo(() => {
return data.map((item) => {
return (
<tr key={item.id}>
<td>
<Checkbox
checked={selection.includes(item.id)}
onChange={() => toggleRow(item.id)}
/>
</td>
<td>
{item.pk && item.model && item.instance ? (
<RenderInstance model={item.model} instance={item.instance} />
) : (
item.ref
)}
</td>
<td>{item.model}</td>
<td>{item.source}</td>
<td>{item.timestamp?.toString()}</td>
</tr>
);
});
}, [data, selection]);
// rendering
@ -458,7 +461,6 @@ function HistoryTable({
indeterminate={
selection.length > 0 && selection.length !== data.length
}
transitionDuration={0}
/>
</th>
<th>
@ -528,7 +530,7 @@ function InputManual({ action }: Readonly<ScanInputInterface>) {
onChange={(event) => setValue(event.currentTarget.value)}
onKeyDown={getHotkeyHandler([['Enter', btnAddItem]])}
/>
<ActionIcon onClick={btnAddItem} w={16}>
<ActionIcon onClick={btnAddItem} w={16} variant="default">
<IconPlus />
</ActionIcon>
</Group>
@ -712,8 +714,8 @@ function InputImageBarcode({ action }: Readonly<ScanInputInterface>) {
}, [cameraValue]);
return (
<Stack spacing="xs">
<Group spacing="xs">
<Stack gap="xs">
<Group gap="xs">
<Select
value={cameraValue}
onChange={setCameraValue}
@ -723,7 +725,11 @@ function InputImageBarcode({ action }: Readonly<ScanInputInterface>) {
size="sm"
/>
{ScanningEnabled ? (
<ActionIcon onClick={btnStopScanning} title={t`Stop scanning`}>
<ActionIcon
onClick={btnStopScanning}
title={t`Stop scanning`}
variant="default"
>
<IconPlayerStopFilled />
</ActionIcon>
) : (
@ -731,11 +737,12 @@ function InputImageBarcode({ action }: Readonly<ScanInputInterface>) {
onClick={btnStartScanning}
title={t`Start scanning`}
disabled={!camId}
variant="default"
>
<IconPlayerPlayFilled />
</ActionIcon>
)}
<Space sx={{ flex: 1 }} />
<Space style={{ flex: 1 }} />
<Badge color={ScanningEnabled ? 'green' : 'orange'}>
{ScanningEnabled ? t`Scanning` : t`Not scanning`}
</Badge>

View File

@ -40,7 +40,7 @@ export function AccountDetailPanel() {
</Group>
<Group>
{editing ? (
<Stack spacing="xs">
<Stack gap="xs">
<TextInput
label="first name"
placeholder={t`First name`}
@ -51,14 +51,14 @@ export function AccountDetailPanel() {
placeholder={t`Last name`}
{...form.getInputProps('last_name')}
/>
<Group position="right" mt="md">
<Group justify="right" mt="md">
<Button type="submit">
<Trans>Submit</Trans>
</Button>
</Group>
</Stack>
) : (
<Stack spacing="0">
<Stack gap="0">
<Text>
<Trans>First name: </Trans>
{form.values.first_name}

View File

@ -137,7 +137,7 @@ function EmailContent({}: {}) {
key={link.id}
value={String(link.id)}
label={
<Group position="apart">
<Group justify="space-between">
{link.email}
{link.primary && (
<Badge color="blue">
@ -168,7 +168,7 @@ function EmailContent({}: {}) {
<TextInput
label={t`E-Mail`}
placeholder={t`E-Mail address`}
icon={<IconAt />}
leftSection={<IconAt />}
value={newEmailValue}
onChange={(event) => setNewEmailValue(event.currentTarget.value)}
/>
@ -251,7 +251,7 @@ function SsoContent({ dataProvider }: { dataProvider: any | undefined }) {
variant="outline"
disabled={!provider.configured}
>
<Group position="apart">
<Group justify="space-between">
{provider.display_name}
{provider.configured == false && <IconAlertCircle />}
</Group>
@ -308,7 +308,7 @@ function SsoContent({ dataProvider }: { dataProvider: any | undefined }) {
{currentProviders === undefined ? (
<Trans>Loading</Trans>
) : (
<Stack spacing="xs">
<Stack gap="xs">
{currentProviders.map((provider: any) => (
<ProviderButton key={provider.id} provider={provider} />
))}

View File

@ -11,12 +11,11 @@ import {
Table,
Title
} from '@mantine/core';
import { LoaderType } from '@mantine/styles/lib/theme/types/MantineTheme';
import { useState } from 'react';
import { SizeMarks } from '../../../../defaults/defaults';
import { InvenTreeStyle } from '../../../../globalStyle';
import { useLocalState } from '../../../../states/LocalState';
import { theme } from '../../../../theme';
function getLkp(color: string) {
return { [DEFAULT_THEME.colors[color][6]]: color };
@ -27,8 +26,6 @@ const LOOKUP = Object.assign(
);
export function UserTheme({ height }: { height: number }) {
const { theme } = InvenTreeStyle();
// primary color
function changePrimary(color: string) {
useLocalState.setState({ primaryColor: LOOKUP[color] });
@ -69,10 +66,13 @@ export function UserTheme({ height }: { height: number }) {
{ value: 'oval', label: t`oval` },
{ value: 'dots', label: t`dots` }
];
const [loader, setLoader] = useState<LoaderType>(theme.loader);
function changeLoader(value: LoaderType) {
setLoader(value);
useLocalState.setState({ loader: value });
const [themeLoader, setThemeLoader] = useLocalState((state) => [
state.loader,
state.setLoader
]);
function changeLoader(value: string | null) {
if (value === null) return;
setThemeLoader(value);
}
return (
@ -135,10 +135,10 @@ export function UserTheme({ height }: { height: number }) {
<Group align="center">
<Select
data={loaderDate}
value={loader}
value={themeLoader}
onChange={changeLoader}
/>
<Loader type={loader} mah={18} />
<Loader type={themeLoader} mah={18} />
</Group>
</td>
</tr>

View File

@ -97,7 +97,7 @@ export default function AdminCenter() {
label: t`Project Codes`,
icon: <IconListDetails />,
content: (
<Stack spacing="xs">
<Stack gap="xs">
<GlobalSettingList keys={['PROJECT_CODES_ENABLED']} />
<Divider />
<ProjectCodeTable />
@ -144,7 +144,7 @@ export default function AdminCenter() {
}, []);
const QuickAction = () => (
<Stack spacing={'xs'} ml={'sm'}>
<Stack gap={'xs'} ml={'sm'}>
<Title order={5}>
<Trans>Quick Actions</Trans>
</Title>
@ -167,7 +167,7 @@ export default function AdminCenter() {
);
return (
<Stack spacing="xs">
<Stack gap="xs">
<SettingsHeader
title={t`Admin Center`}
subtitle={t`Advanced Options`}

View File

@ -38,7 +38,7 @@ export default function MachineManagementPanel() {
<Space h="10px" />
<Stack spacing={'xs'}>
<Stack gap={'xs'}>
<Title order={5}>
<Trans>Machine types</Trans>
</Title>
@ -47,7 +47,7 @@ export default function MachineManagementPanel() {
<Space h="10px" />
<Stack spacing={'xs'}>
<Stack gap={'xs'}>
<Group>
<Title order={5}>
<Trans>Machine Error Stack</Trans>
@ -58,7 +58,7 @@ export default function MachineManagementPanel() {
</Group>
{registryStatus?.registry_errors &&
registryStatus.registry_errors.length === 0 ? (
<Text italic>
<Text style={{ fontStyle: 'italic' }}>
<Trans>There are no machine registry errors.</Trans>
</Text>
) : (

View File

@ -8,7 +8,7 @@ import { UserTable } from '../../../../tables/settings/UserTable';
export default function UserManagementPanel() {
return (
<Stack spacing="xs">
<Stack gap="xs">
<Title order={5}>
<Trans>Users</Trans>
</Title>
@ -21,7 +21,7 @@ export default function UserManagementPanel() {
<Divider />
<Stack spacing={0}>
<Stack gap={0}>
<Text>
<Trans>Settings</Trans>
</Text>

View File

@ -287,7 +287,7 @@ export default function SystemSettings() {
return (
<>
<Stack spacing="xs">
<Stack gap="xs">
<SettingsHeader
title={t`System Settings`}
subtitle={server.instance || ''}

View File

@ -110,7 +110,7 @@ export default function UserSettings() {
return (
<>
<Stack spacing="xs">
<Stack gap="xs">
<SettingsHeader
title={t`Account Settings`}
subtitle={`${user?.first_name} ${user?.last_name}`}

View File

@ -415,7 +415,7 @@ export default function BuildDetail() {
{editBuild.modal}
{duplicateBuild.modal}
{cancelBuild.modal}
<Stack spacing="xs">
<Stack gap="xs">
<LoadingOverlay visible={instanceQuery.isFetching} />
<PageDetail
title={build.reference}

View File

@ -81,7 +81,8 @@ export default function CompanyDetail(props: Readonly<CompanyDetailProps>) {
{
type: 'text',
name: 'description',
label: t`Description`
label: t`Description`,
copy: true
},
{
type: 'link',
@ -314,7 +315,7 @@ export default function CompanyDetail(props: Readonly<CompanyDetailProps>) {
return (
<>
{editCompany.modal}
<Stack spacing="xs">
<Stack gap="xs">
<LoadingOverlay visible={instanceQuery.isFetching} />
<PageDetail
title={t`Company` + `: ${company.name}`}

View File

@ -241,8 +241,7 @@ export default function ManufacturerPartDetail() {
return (
<>
{editManufacturerPart.modal}
{duplicateManufacturerPart.modal}
<Stack spacing="xs">
<Stack gap="xs">
<LoadingOverlay visible={instanceQuery.isFetching} />
<PageDetail
title={t`ManufacturerPart`}

View File

@ -311,8 +311,7 @@ export default function SupplierPartDetail() {
return (
<>
{editSuppliertPart.modal}
{duplicateSupplierPart.modal}
<Stack spacing="xs">
<Stack gap="xs">
<LoadingOverlay visible={instanceQuery.isFetching} />
<PageDetail
title={t`Supplier Part`}

View File

@ -222,7 +222,7 @@ export default function CategoryDetail({}: {}) {
return (
<>
{editCategory.modal}
<Stack spacing="xs">
<Stack gap="xs">
<LoadingOverlay visible={instanceQuery.isFetching} />
<PartCategoryTree
opened={treeOpen}

View File

@ -442,7 +442,7 @@ export default function PartDetail() {
/>
</Grid.Col>
<Grid.Col span={8}>
<Stack spacing="xs">
<Stack gap="xs">
<table>
<tbody>
<tr>
@ -782,7 +782,7 @@ export default function PartDetail() {
<>
{duplicatePart.modal}
{editPart.modal}
<Stack spacing="xs">
<Stack gap="xs">
<LoadingOverlay visible={instanceQuery.isFetching} />
<PartCategoryTree
opened={treeOpen}

View File

@ -65,7 +65,7 @@ export default function PartPricingPanel({ part }: { part: any }) {
}
return (
<Stack spacing="xs">
<Stack gap="xs">
<LoadingOverlay visible={instanceQuery.isLoading} />
{!pricing && !instanceQuery.isLoading && (
<Alert color="ref" title={t`Error`}>

Some files were not shown because too many files have changed in this diff Show More