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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -16,6 +16,7 @@ export function ScanButton() {
innerProps: {} innerProps: {}
}) })
} }
variant="transparent"
title={t`Open QR code scanner`} title={t`Open QR code scanner`}
> >
<IconQrcode /> <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, Menu,
Text, Text,
Tooltip, Tooltip,
createStyles,
useMantineTheme useMantineTheme
} from '@mantine/core'; } from '@mantine/core';
import { IconChevronDown } from '@tabler/icons-react'; import { IconChevronDown } from '@tabler/icons-react';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { TablerIconType } from '../../functions/icons'; import { TablerIconType } from '../../functions/icons';
import * as classes from './SplitButton.css';
interface SplitButtonOption { interface SplitButtonOption {
key: string; key: string;
@ -30,22 +30,6 @@ interface SplitButtonProps {
loading?: boolean; 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({ export function SplitButton({
options, options,
defaultSelected, defaultSelected,
@ -54,7 +38,6 @@ export function SplitButton({
loading loading
}: Readonly<SplitButtonProps>) { }: Readonly<SplitButtonProps>) {
const [current, setCurrent] = useState<string>(defaultSelected); const [current, setCurrent] = useState<string>(defaultSelected);
const { classes } = useStyles();
useEffect(() => { useEffect(() => {
setSelected?.(current); setSelected?.(current);
@ -72,7 +55,7 @@ export function SplitButton({
const theme = useMantineTheme(); const theme = useMantineTheme();
return ( return (
<Group noWrap style={{ gap: 0 }}> <Group wrap="nowrap" style={{ gap: 0 }}>
<Button <Button
onClick={currentOption?.onClick} onClick={currentOption?.onClick}
disabled={loading ? false : currentOption?.disabled} disabled={loading ? false : currentOption?.disabled}
@ -106,7 +89,7 @@ export function SplitButton({
option.onClick(); option.onClick();
}} }}
disabled={option.disabled} disabled={option.disabled}
icon={<option.icon />} leftSection={<option.icon />}
> >
<Tooltip label={option.tooltip} position="right"> <Tooltip label={option.tooltip} position="right">
<Text>{option.name}</Text> <Text>{option.name}</Text>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,7 +2,7 @@ import { Trans } from '@lingui/macro';
import { Anchor, Container, HoverCard, ScrollArea, Text } from '@mantine/core'; import { Anchor, Container, HoverCard, ScrollArea, Text } from '@mantine/core';
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import { InvenTreeStyle } from '../../globalStyle'; import * as classes from '../../main.css';
export interface BaseDocProps { export interface BaseDocProps {
text: string | JSX.Element; text: string | JSX.Element;
@ -22,8 +22,6 @@ export function DocTooltip({
link, link,
docchildren docchildren
}: Readonly<DocTooltipProps>) { }: Readonly<DocTooltipProps>) {
const { classes } = InvenTreeStyle();
return ( return (
<HoverCard <HoverCard
shadow="md" 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 { Trans } from '@lingui/macro';
import { Carousel } from '@mantine/carousel'; import { Carousel } from '@mantine/carousel';
import { import { Anchor, Button, Paper, Text, Title, rem } from '@mantine/core';
Anchor,
Button,
Paper,
Text,
Title,
createStyles,
rem
} from '@mantine/core';
import { DocumentationLinkItem } from './DocumentationLinks'; import { DocumentationLinkItem } from './DocumentationLinks';
import * as classes from './GettingStartedCarousel.css';
import { PlaceholderPill } from './Placeholder'; 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({ function StartedCard({
title, title,
description, description,
link, link,
placeholder placeholder
}: DocumentationLinkItem) { }: DocumentationLinkItem) {
const { classes } = useStyles();
return ( return (
<Paper shadow="md" p="xl" radius="md" className={classes.card}> <Paper shadow="md" p="xl" radius="md" className={classes.card}>
<div> <div>
@ -81,10 +44,11 @@ export function GettingStartedCarousel({
return ( return (
<Carousel <Carousel
slideSize="50%" slideSize={{ base: '100%', sm: '50%', md: '33.333333%' }}
breakpoints={[{ maxWidth: 'sm', slideSize: '100%', slideGap: rem(2) }]} slideGap={{ base: 0, sm: 'md' }}
slideGap="xl" slidesToScroll={3}
align="start" align="start"
loop
> >
{slides} {slides}
</Carousel> </Carousel>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -9,7 +9,7 @@ export function PlaceholderPill() {
return ( return (
<Tooltip <Tooltip
multiline multiline
width={220} w={220}
withArrow withArrow
label={t`This feature/button/site is a placeholder for a feature that is not implemented, only partial or intended for testing.`} 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.`} title={t`This panel is a placeholder.`}
icon={<IconInfoCircle />} 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> </Alert>
</Stack> </Stack>
); );

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,17 +1,18 @@
import { Trans } from '@lingui/macro'; 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 { IconExternalLink } from '@tabler/icons-react';
import { vars } from '../../theme';
export default function FeedbackWidget() { export default function FeedbackWidget() {
const { colorScheme } = useMantineColorScheme();
return ( return (
<Stack <Stack
sx={(theme) => ({ style={{
backgroundColor: backgroundColor:
theme.colorScheme === 'dark' colorScheme === 'dark' ? vars.colors.gray[9] : vars.colors.gray[1],
? theme.colors.gray[9] borderRadius: vars.radius.md
: theme.colors.gray[1], }}
borderRadius: theme.radius.md
})}
p={15} p={15}
> >
<Title order={5}> <Title order={5}>
@ -26,7 +27,7 @@ export default function FeedbackWidget() {
component="a" component="a"
href="https://github.com/inventree/InvenTree/discussions/5328" href="https://github.com/inventree/InvenTree/discussions/5328"
variant="outline" variant="outline"
leftIcon={<IconExternalLink size="0.9rem" />} leftSection={<IconExternalLink size="0.9rem" />}
> >
<Trans>Provide Feedback</Trans> <Trans>Provide Feedback</Trans>
</Button> </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, Group,
Indicator, Indicator,
Menu, Menu,
Text, Text
createStyles
} from '@mantine/core'; } from '@mantine/core';
import { useDisclosure, useHotkeys } from '@mantine/hooks'; import { useDisclosure, useHotkeys } from '@mantine/hooks';
import { import {
@ -19,6 +18,8 @@ import {
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { Responsive, WidthProvider } from 'react-grid-layout'; import { Responsive, WidthProvider } from 'react-grid-layout';
import * as classes from './WidgetLayout.css';
const ReactGridLayout = WidthProvider(Responsive); const ReactGridLayout = WidthProvider(Responsive);
interface LayoutStorage { interface LayoutStorage {
@ -27,21 +28,6 @@ interface LayoutStorage {
const compactType = 'vertical'; 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 { export interface LayoutItemType {
i: number; i: number;
val: string | JSX.Element | JSX.Element[] | (() => JSX.Element); val: string | JSX.Element | JSX.Element[] | (() => JSX.Element);
@ -66,7 +52,6 @@ export function WidgetLayout({
const [layouts, setLayouts] = useState({}); const [layouts, setLayouts] = useState({});
const [editable, setEditable] = useDisclosure(false); const [editable, setEditable] = useDisclosure(false);
const [boxShown, setBoxShown] = useDisclosure(true); const [boxShown, setBoxShown] = useDisclosure(true);
const { classes } = useItemStyle();
useEffect(() => { useEffect(() => {
let layout = getFromLS('layouts') || []; let layout = getFromLS('layouts') || [];
@ -155,7 +140,7 @@ function WidgetControlBar({
useHotkeys([['mod+E', () => editFnc()]]); useHotkeys([['mod+E', () => editFnc()]]);
return ( return (
<Group position="right"> <Group justify="right">
<Menu <Menu
shadow="md" shadow="md"
width={200} width={200}
@ -181,13 +166,13 @@ function WidgetControlBar({
<Trans>Layout</Trans> <Trans>Layout</Trans>
</Menu.Label> </Menu.Label>
<Menu.Item <Menu.Item
icon={<IconArrowBackUpDouble size={14} />} leftSection={<IconArrowBackUpDouble size={14} />}
onClick={resetLayout} onClick={resetLayout}
> >
<Trans>Reset Layout</Trans> <Trans>Reset Layout</Trans>
</Menu.Item> </Menu.Item>
<Menu.Item <Menu.Item
icon={ leftSection={
<IconLayout2 size={14} color={editable ? 'red' : undefined} /> <IconLayout2 size={14} color={editable ? 'red' : undefined} />
} }
onClick={editFnc} onClick={editFnc}
@ -206,7 +191,7 @@ function WidgetControlBar({
<Trans>Appearance</Trans> <Trans>Appearance</Trans>
</Menu.Label> </Menu.Label>
<Menu.Item <Menu.Item
icon={ leftSection={
boxShown ? ( boxShown ? (
<IconSquareCheck size={14} /> <IconSquareCheck size={14} />
) : ( ) : (

View File

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

View File

@ -1,11 +1,5 @@
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { import { MantineProvider, createTheme } from '@mantine/core';
ColorScheme,
ColorSchemeProvider,
MantineProvider,
MantineThemeOverride
} from '@mantine/core';
import { useColorScheme, useLocalStorage } from '@mantine/hooks';
import { ModalsProvider } from '@mantine/modals'; import { ModalsProvider } from '@mantine/modals';
import { Notifications } from '@mantine/notifications'; import { Notifications } from '@mantine/notifications';
@ -14,36 +8,24 @@ import { LicenseModal } from '../components/modals/LicenseModal';
import { QrCodeModal } from '../components/modals/QrCodeModal'; import { QrCodeModal } from '../components/modals/QrCodeModal';
import { ServerInfoModal } from '../components/modals/ServerInfoModal'; import { ServerInfoModal } from '../components/modals/ServerInfoModal';
import { useLocalState } from '../states/LocalState'; import { useLocalState } from '../states/LocalState';
import { LanguageContext } from './LanguageContext';
import { colorSchema } from './colorSchema';
export function ThemeContext({ children }: { children: JSX.Element }) { export function ThemeContext({ children }: { children: JSX.Element }) {
const [primaryColor, whiteColor, blackColor, radius, loader] = useLocalState( const [primaryColor, whiteColor, blackColor, radius] = useLocalState(
(state) => [ (state) => [
state.primaryColor, state.primaryColor,
state.whiteColor, state.whiteColor,
state.blackColor, state.blackColor,
state.radius, state.radius
state.loader
] ]
); );
// 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 // Theme
const myTheme: MantineThemeOverride = { const myTheme = createTheme({
colorScheme: colorScheme,
primaryColor: primaryColor, primaryColor: primaryColor,
white: whiteColor, white: whiteColor,
black: blackColor, black: blackColor,
loader: loader,
defaultRadius: radius, defaultRadius: radius,
breakpoints: { breakpoints: {
xs: '30em', xs: '30em',
@ -52,15 +34,11 @@ export function ThemeContext({ children }: { children: JSX.Element }) {
lg: '74em', lg: '74em',
xl: '90em' xl: '90em'
} }
}; });
return ( return (
<ColorSchemeProvider <MantineProvider theme={myTheme} colorSchemeManager={colorSchema}>
colorScheme={colorScheme} <LanguageContext>
toggleColorScheme={toggleColorScheme}
>
<MantineProvider theme={myTheme} withGlobalStyles withNormalizeCSS>
<Notifications />
<ModalsProvider <ModalsProvider
labels={{ confirm: t`Submit`, cancel: t`Cancel` }} labels={{ confirm: t`Submit`, cancel: t`Cancel` }}
modals={{ modals={{
@ -70,9 +48,10 @@ export function ThemeContext({ children }: { children: JSX.Element }) {
license: LicenseModal license: LicenseModal
}} }}
> >
<Notifications />
{children} {children}
</ModalsProvider> </ModalsProvider>
</MantineProvider> </LanguageContext>
</ColorSchemeProvider> </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 { 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 { IconHome, IconLink, IconPointer } from '@tabler/icons-react';
import { NavigateFunction } from 'react-router-dom'; import { NavigateFunction } from 'react-router-dom';
@ -10,48 +10,55 @@ import { menuItems } from './menuItems';
export function getActions(navigate: NavigateFunction) { export function getActions(navigate: NavigateFunction) {
const setNavigationOpen = useLocalState((state) => state.setNavigationOpen); 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`, description: `Go to the home page`,
onTrigger: () => navigate(menuItems.home.link), onClick: () => navigate(menuItems.home.link),
icon: <IconHome size="1.2rem" /> leftSection: <IconHome size="1.2rem" />
}, },
{ {
title: t`Dashboard`, id: 'dashboard',
label: t`Dashboard`,
description: t`Go to the InvenTree dashboard`, description: t`Go to the InvenTree dashboard`,
onTrigger: () => navigate(menuItems.dashboard.link), onClick: () => navigate(menuItems.dashboard.link),
icon: <IconLink size="1.2rem" /> leftSection: <IconLink size="1.2rem" />
}, },
{ {
title: t`Documentation`, id: 'documentation',
label: t`Documentation`,
description: t`Visit the documentation to learn more about InvenTree`, description: t`Visit the documentation to learn more about InvenTree`,
onTrigger: () => (window.location.href = docLinks.faq), onClick: () => (window.location.href = docLinks.faq),
icon: <IconLink size="1.2rem" /> leftSection: <IconLink size="1.2rem" />
}, },
{ {
title: t`About InvenTree`, id: 'about',
label: t`About InvenTree`,
description: t`About the InvenTree org`, description: t`About the InvenTree org`,
onTrigger: () => aboutInvenTree(), onClick: () => aboutInvenTree(),
icon: <IconLink size="1.2rem" /> leftSection: <IconLink size="1.2rem" />
}, },
{ {
title: t`Server Information`, id: 'server-info',
label: t`Server Information`,
description: t`About this Inventree instance`, description: t`About this Inventree instance`,
onTrigger: () => serverInfo(), onClick: () => serverInfo(),
icon: <IconLink size="1.2rem" /> leftSection: <IconLink size="1.2rem" />
}, },
{ {
title: t`License Information`, id: 'license-info',
label: t`License Information`,
description: t`Licenses for dependencies of the service`, description: t`Licenses for dependencies of the service`,
onTrigger: () => licenseInfo(), onClick: () => licenseInfo(),
icon: <IconLink size="1.2rem" /> leftSection: <IconLink size="1.2rem" />
}, },
{ {
title: t`Open Navigation`, id: 'navigation',
label: t`Open Navigation`,
description: t`Open the main navigation menu`, description: t`Open the main navigation menu`,
onTrigger: () => setNavigationOpen(true), onClick: () => setNavigationOpen(true),
icon: <IconPointer size="1.2rem" /> leftSection: <IconPointer size="1.2rem" />
} }
]; ];

View File

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

View File

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

View File

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

View File

@ -244,7 +244,7 @@ export function openModalApiForm(props: OpenApiFormProps) {
props.onClose ? props.onClose() : null; props.onClose ? props.onClose() : null;
}, },
children: ( children: (
<Stack spacing={'xs'}> <Stack gap={'xs'}>
<Divider /> <Divider />
<ApiForm id={modalId} props={props} optionsLoading={false} /> <ApiForm id={modalId} props={props} optionsLoading={false} />
</Stack> </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 { Suspense } from 'react';
import { colorSchema } from '../contexts/colorSchema';
import { theme } from '../theme';
function LoadingFallback() { function LoadingFallback() {
return ( return (
<Stack> <MantineProvider theme={theme} colorSchemeManager={colorSchema}>
<Center> <Stack>
<Loader /> <Center>
</Center> <Loader />
</Stack> </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 { 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 { useId } from '@mantine/hooks';
import { useEffect, useMemo, useRef } from 'react'; import { useEffect, useMemo, useRef } from 'react';
import { ApiFormProps, OptionsApiForm } from '../components/forms/ApiForm'; import { ApiFormProps, OptionsApiForm } from '../components/forms/ApiForm';
import { UiSizeType } from '../defaults/formatters';
import { useModal } from './UseModal'; import { useModal } from './UseModal';
/** /**
@ -20,7 +21,7 @@ export interface ApiFormModalProps extends ApiFormProps {
onClose?: () => void; onClose?: () => void;
onOpen?: () => void; onOpen?: () => void;
closeOnClickOutside?: boolean; closeOnClickOutside?: boolean;
size?: MantineNumberSize; size?: UiSizeType;
} }
/** /**
@ -62,7 +63,7 @@ export function useApiFormModal(props: ApiFormModalProps) {
closeOnClickOutside: formProps.closeOnClickOutside, closeOnClickOutside: formProps.closeOnClickOutside,
size: props.size ?? 'xl', size: props.size ?? 'xl',
children: ( children: (
<Stack spacing={'xs'}> <Stack gap={'xs'}>
<Divider /> <Divider />
<OptionsApiForm props={formProps} id={id} /> <OptionsApiForm props={formProps} id={id} />
</Stack> </Stack>

View File

@ -1,13 +1,14 @@
import { MantineNumberSize, Modal } from '@mantine/core'; import { Modal } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks'; import { useDisclosure } from '@mantine/hooks';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import { StylishText } from '../components/items/StylishText'; import { StylishText } from '../components/items/StylishText';
import { UiSizeType } from '../defaults/formatters';
export interface UseModalProps { export interface UseModalProps {
title: string; title: string;
children: React.ReactElement; children: React.ReactElement;
size?: MantineNumberSize; size?: UiSizeType;
onOpen?: () => void; onOpen?: () => void;
onClose?: () => void; onClose?: () => void;
closeOnClickOutside?: boolean; 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 * as Sentry from '@sentry/react';
import 'mantine-datatable/styles.css';
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom/client'; import ReactDOM from 'react-dom/client';
import 'react-grid-layout/css/styles.css'; import 'react-grid-layout/css/styles.css';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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