mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
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:
parent
08b1bdb564
commit
6700a4625d
@ -11,66 +11,78 @@
|
||||
"compile": "lingui compile --typescript"
|
||||
},
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": ">=6.0.0",
|
||||
"@codemirror/lang-liquid": "^6.2.1",
|
||||
"@codemirror/language": ">=6.0.0",
|
||||
"@codemirror/lint": ">=6.0.0",
|
||||
"@codemirror/search": ">=6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/theme-one-dark": ">=6.0.0",
|
||||
"@codemirror/view": ">=6.0.0",
|
||||
"@emotion/react": "^11.11.4",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.5.2",
|
||||
"@fortawesome/free-regular-svg-icons": "^6.5.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.5.2",
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"@lingui/core": "^4.7.1",
|
||||
"@lingui/core": "^4.10.0",
|
||||
"@lingui/react": "^4.10.0",
|
||||
"@mantine/carousel": "<7",
|
||||
"@mantine/core": "<7",
|
||||
"@mantine/dates": "<7",
|
||||
"@mantine/dropzone": "<7",
|
||||
"@mantine/form": "<8",
|
||||
"@mantine/hooks": "<7",
|
||||
"@mantine/modals": "<7",
|
||||
"@mantine/notifications": "<7",
|
||||
"@mantine/spotlight": "<7",
|
||||
"@mantine/carousel": "^7.8.0",
|
||||
"@mantine/core": "^7.8.0",
|
||||
"@mantine/dates": "^7.8.0",
|
||||
"@mantine/dropzone": "^7.8.0",
|
||||
"@mantine/form": "^7.8.0",
|
||||
"@mantine/hooks": "^7.8.0",
|
||||
"@mantine/modals": "^7.8.0",
|
||||
"@mantine/notifications": "^7.8.0",
|
||||
"@mantine/spotlight": "^7.8.0",
|
||||
"@mantine/vanilla-extract": "^7.8.0",
|
||||
"@naisutech/react-tree": "^3.1.0",
|
||||
"@sentry/react": "^7.109.0",
|
||||
"@tabler/icons-react": "^3.1.0",
|
||||
"@tanstack/react-query": "^5.28.14",
|
||||
"@sentry/react": "^7.110.0",
|
||||
"@tabler/icons-react": "^3.2.0",
|
||||
"@tanstack/react-query": "^5.29.2",
|
||||
"@uiw/codemirror-theme-vscode": "^4.21.25",
|
||||
"@uiw/react-codemirror": "^4.21.25",
|
||||
"@uiw/react-split": "^5.9.3",
|
||||
"axios": "^1.6.7",
|
||||
"@vanilla-extract/css": "^1.14.2",
|
||||
"axios": "^1.6.8",
|
||||
"clsx": "^2.1.0",
|
||||
"codemirror": ">=6.0.0",
|
||||
"dayjs": "^1.11.10",
|
||||
"easymde": "^2.18.0",
|
||||
"embla-carousel-react": "^8.0.2",
|
||||
"html5-qrcode": "^2.3.8",
|
||||
"mantine-datatable": "<7",
|
||||
"mantine-datatable": "^7.8.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-grid-layout": "^1.4.4",
|
||||
"react-hook-form": "^7.51.3",
|
||||
"react-is": "^18.2.0",
|
||||
"react-router-dom": "^6.22.1",
|
||||
"react-router-dom": "^6.22.3",
|
||||
"react-select": "^5.8.0",
|
||||
"react-simplemde-editor": "^5.2.0",
|
||||
"recharts": "^2.12.4",
|
||||
"styled-components": "^5.3.6",
|
||||
"zustand": "^4.5.1"
|
||||
"styled-components": "^6.1.8",
|
||||
"zustand": "^4.5.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.23.9",
|
||||
"@babel/preset-react": "^7.23.3",
|
||||
"@babel/preset-typescript": "^7.23.3",
|
||||
"@lingui/cli": "^4.7.2",
|
||||
"@babel/core": "^7.24.4",
|
||||
"@babel/preset-react": "^7.24.1",
|
||||
"@babel/preset-typescript": "^7.24.1",
|
||||
"@lingui/cli": "^4.10.0",
|
||||
"@lingui/macro": "^4.10.0",
|
||||
"@playwright/test": "^1.43.1",
|
||||
"@types/node": "^20.12.3",
|
||||
"@types/react": "^18.2.74",
|
||||
"@types/react-dom": "^18.2.23",
|
||||
"@types/node": "^20.12.7",
|
||||
"@types/react": "^18.2.79",
|
||||
"@types/react-dom": "^18.2.25",
|
||||
"@types/react-grid-layout": "^1.3.5",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"@vanilla-extract/vite-plugin": "^4.0.7",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"babel-plugin-macros": "^3.1.0",
|
||||
"nyc": "^15.1.0",
|
||||
"rollup-plugin-license": "^3.3.1",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^5.2.7",
|
||||
"typescript": "^5.4.5",
|
||||
"vite": "^5.2.8",
|
||||
"vite-plugin-babel-macros": "^1.0.6",
|
||||
"vite-plugin-istanbul": "^6.0.0"
|
||||
}
|
||||
|
@ -5,7 +5,6 @@ export default defineConfig({
|
||||
fullyParallel: true,
|
||||
timeout: 60000,
|
||||
forbidOnly: !!process.env.CI,
|
||||
timeout: 5 * 60 * 1000,
|
||||
retries: process.env.CI ? 1 : 0,
|
||||
workers: process.env.CI ? 2 : undefined,
|
||||
reporter: process.env.CI ? [['html', { open: 'never' }], ['github']] : 'list',
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { ActionIcon, Group, Tooltip } from '@mantine/core';
|
||||
import { FloatingPosition } from '@mantine/core/lib/Floating';
|
||||
import { ActionIcon, FloatingPosition, Group, Tooltip } from '@mantine/core';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
import { notYetImplemented } from '../../functions/notifications';
|
||||
@ -41,7 +40,7 @@ export function ActionButton(props: ActionButtonProps) {
|
||||
onClick={props.onClick ?? notYetImplemented}
|
||||
variant={props.variant ?? 'light'}
|
||||
>
|
||||
<Group spacing="xs" noWrap={true}>
|
||||
<Group gap="xs" wrap="nowrap">
|
||||
{props.icon}
|
||||
</Group>
|
||||
</ActionIcon>
|
||||
|
@ -18,7 +18,7 @@ export function ButtonMenu({
|
||||
return (
|
||||
<Menu shadow="xs">
|
||||
<Menu.Target>
|
||||
<ActionIcon>
|
||||
<ActionIcon variant="default">
|
||||
<Tooltip label={tooltip}>{icon}</Tooltip>
|
||||
</ActionIcon>
|
||||
</Menu.Target>
|
||||
|
@ -17,7 +17,7 @@ export function CopyButton({
|
||||
onClick={copy}
|
||||
title={t`Copy to clipboard`}
|
||||
variant="subtle"
|
||||
compact
|
||||
size="compact-md"
|
||||
>
|
||||
<IconCopy size={10} />
|
||||
{label && <div> </div>}
|
||||
|
@ -14,7 +14,11 @@ export function EditButton({
|
||||
}) {
|
||||
saveIcon = saveIcon || <IconDeviceFloppy />;
|
||||
return (
|
||||
<ActionIcon onClick={() => setEditing()} disabled={disabled}>
|
||||
<ActionIcon
|
||||
onClick={() => setEditing()}
|
||||
disabled={disabled}
|
||||
variant="default"
|
||||
>
|
||||
{editing ? saveIcon : <IconEdit />}
|
||||
</ActionIcon>
|
||||
);
|
||||
|
@ -50,7 +50,7 @@ export function SsoButton({ provider }: { provider: Provider }) {
|
||||
|
||||
return (
|
||||
<Button
|
||||
leftIcon={getBrandIcon(provider)}
|
||||
leftSection={getBrandIcon(provider)}
|
||||
radius="xl"
|
||||
component="a"
|
||||
onClick={login}
|
||||
|
@ -16,6 +16,7 @@ export function ScanButton() {
|
||||
innerProps: {}
|
||||
})
|
||||
}
|
||||
variant="transparent"
|
||||
title={t`Open QR code scanner`}
|
||||
>
|
||||
<IconQrcode />
|
||||
|
19
src/frontend/src/components/buttons/SplitButton.css.ts
Normal file
19
src/frontend/src/components/buttons/SplitButton.css.ts
Normal 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}`
|
||||
});
|
@ -5,13 +5,13 @@ import {
|
||||
Menu,
|
||||
Text,
|
||||
Tooltip,
|
||||
createStyles,
|
||||
useMantineTheme
|
||||
} from '@mantine/core';
|
||||
import { IconChevronDown } from '@tabler/icons-react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { TablerIconType } from '../../functions/icons';
|
||||
import * as classes from './SplitButton.css';
|
||||
|
||||
interface SplitButtonOption {
|
||||
key: string;
|
||||
@ -30,22 +30,6 @@ interface SplitButtonProps {
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
const useStyles = createStyles((theme) => ({
|
||||
button: {
|
||||
borderTopRightRadius: 0,
|
||||
borderBottomRightRadius: 0,
|
||||
'&::before': {
|
||||
borderRadius: '0 !important'
|
||||
}
|
||||
},
|
||||
icon: {
|
||||
borderTopLeftRadius: 0,
|
||||
borderBottomLeftRadius: 0,
|
||||
border: 0,
|
||||
borderLeft: `1px solid ${theme.primaryShade}`
|
||||
}
|
||||
}));
|
||||
|
||||
export function SplitButton({
|
||||
options,
|
||||
defaultSelected,
|
||||
@ -54,7 +38,6 @@ export function SplitButton({
|
||||
loading
|
||||
}: Readonly<SplitButtonProps>) {
|
||||
const [current, setCurrent] = useState<string>(defaultSelected);
|
||||
const { classes } = useStyles();
|
||||
|
||||
useEffect(() => {
|
||||
setSelected?.(current);
|
||||
@ -72,7 +55,7 @@ export function SplitButton({
|
||||
const theme = useMantineTheme();
|
||||
|
||||
return (
|
||||
<Group noWrap style={{ gap: 0 }}>
|
||||
<Group wrap="nowrap" style={{ gap: 0 }}>
|
||||
<Button
|
||||
onClick={currentOption?.onClick}
|
||||
disabled={loading ? false : currentOption?.disabled}
|
||||
@ -106,7 +89,7 @@ export function SplitButton({
|
||||
option.onClick();
|
||||
}}
|
||||
disabled={option.disabled}
|
||||
icon={<option.icon />}
|
||||
leftSection={<option.icon />}
|
||||
>
|
||||
<Tooltip label={option.tooltip} position="right">
|
||||
<Text>{option.name}</Text>
|
||||
|
@ -1,14 +1,19 @@
|
||||
import { t } from '@lingui/macro';
|
||||
import { ActionIcon } from '@mantine/core';
|
||||
import { spotlight } from '@mantine/spotlight';
|
||||
import { IconCommand } from '@tabler/icons-react';
|
||||
|
||||
import { firstSpotlight } from '../nav/Layout';
|
||||
|
||||
/**
|
||||
* A button which opens the quick command modal
|
||||
*/
|
||||
export function SpotlightButton() {
|
||||
return (
|
||||
<ActionIcon onClick={() => spotlight.open()} title={t`Open spotlight`}>
|
||||
<ActionIcon
|
||||
onClick={() => firstSpotlight.open()}
|
||||
title={t`Open spotlight`}
|
||||
variant="transparent"
|
||||
>
|
||||
<IconCommand />
|
||||
</ActionIcon>
|
||||
);
|
||||
|
@ -324,7 +324,12 @@ function CopyField({ value }: { value: string }) {
|
||||
<CopyButton value={value}>
|
||||
{({ copied, copy }) => (
|
||||
<Tooltip label={copied ? t`Copied` : t`Copy`} withArrow>
|
||||
<ActionIcon color={copied ? 'teal' : 'gray'} onClick={copy}>
|
||||
<ActionIcon
|
||||
color={copied ? 'teal' : 'gray'}
|
||||
onClick={copy}
|
||||
variant="transparent"
|
||||
size="sm"
|
||||
>
|
||||
{copied ? (
|
||||
<InvenTreeIcon icon="check" />
|
||||
) : (
|
||||
@ -398,9 +403,14 @@ export function DetailsTable({
|
||||
}) {
|
||||
return (
|
||||
<Paper p="xs" withBorder radius="xs">
|
||||
<Stack spacing="xs">
|
||||
<Stack gap="xs">
|
||||
{title && <StylishText size="lg">{title}</StylishText>}
|
||||
<Table striped>
|
||||
<Table
|
||||
striped
|
||||
verticalSpacing="sm"
|
||||
horizontalSpacing="md"
|
||||
withColumnBorders
|
||||
>
|
||||
<tbody>
|
||||
{fields
|
||||
.filter((field: DetailsField) => !field.hidden)
|
||||
|
@ -8,7 +8,7 @@ import {
|
||||
Paper,
|
||||
Text,
|
||||
rem,
|
||||
useMantineTheme
|
||||
useMantineColorScheme
|
||||
} from '@mantine/core';
|
||||
import { Dropzone, FileWithPath, IMAGE_MIME_TYPE } from '@mantine/dropzone';
|
||||
import { useHover } from '@mantine/hooks';
|
||||
@ -21,6 +21,7 @@ import { cancelEvent } from '../../functions/events';
|
||||
import { InvenTreeIcon } from '../../functions/icons';
|
||||
import { useUserState } from '../../states/UserState';
|
||||
import { PartThumbTable } from '../../tables/part/PartThumbTable';
|
||||
import { vars } from '../../theme';
|
||||
import { ActionButton } from '../buttons/ActionButton';
|
||||
import { ApiImage } from '../images/ApiImage';
|
||||
import { StylishText } from '../items/StylishText';
|
||||
@ -87,8 +88,6 @@ function UploadModal({
|
||||
const [file1, setFile] = useState<FileWithPath | null>(null);
|
||||
let uploading = false;
|
||||
|
||||
const theme = useMantineTheme();
|
||||
|
||||
// Components to show in the Dropzone when no file is selected
|
||||
const noFileIdle = (
|
||||
<Group>
|
||||
@ -122,7 +121,7 @@ function UploadModal({
|
||||
>
|
||||
<Image
|
||||
src={imageUrl}
|
||||
imageProps={{ onLoad: () => URL.revokeObjectURL(imageUrl) }}
|
||||
onLoad={() => URL.revokeObjectURL(imageUrl)}
|
||||
radius="sm"
|
||||
height={75}
|
||||
fit="contain"
|
||||
@ -160,12 +159,14 @@ function UploadModal({
|
||||
}
|
||||
};
|
||||
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
|
||||
const primaryColor =
|
||||
theme.colors[theme.primaryColor][theme.colorScheme === 'dark' ? 4 : 6];
|
||||
const redColor = theme.colors.red[theme.colorScheme === 'dark' ? 4 : 6];
|
||||
vars.colors.primaryColors[colorScheme === 'dark' ? 4 : 6];
|
||||
const redColor = vars.colors.red[colorScheme === 'dark' ? 4 : 6];
|
||||
|
||||
return (
|
||||
<Paper sx={{ height: '220px' }}>
|
||||
<Paper style={{ height: '220px' }}>
|
||||
<Dropzone
|
||||
onDrop={(files) => setFile(files[0])}
|
||||
maxFiles={1}
|
||||
@ -173,8 +174,8 @@ function UploadModal({
|
||||
loading={uploading}
|
||||
>
|
||||
<Group
|
||||
position="center"
|
||||
spacing="xl"
|
||||
justify="center"
|
||||
gap="xl"
|
||||
style={{ minHeight: rem(140), pointerEvents: 'none' }}
|
||||
>
|
||||
<Dropzone.Accept>
|
||||
@ -252,7 +253,7 @@ function ImageActionButtons({
|
||||
<>
|
||||
{visible && (
|
||||
<Group
|
||||
spacing="xs"
|
||||
gap="xs"
|
||||
style={{ zIndex: 2, position: 'absolute', top: '10px', left: '10px' }}
|
||||
>
|
||||
{actions.selectExisting && (
|
||||
@ -358,8 +359,8 @@ export function DetailsImage(props: Readonly<DetailImageProps>) {
|
||||
<>
|
||||
<ApiImage
|
||||
src={img}
|
||||
height={IMAGE_DIMENSION}
|
||||
width={IMAGE_DIMENSION}
|
||||
mah={IMAGE_DIMENSION}
|
||||
maw={IMAGE_DIMENSION}
|
||||
onClick={expandImage}
|
||||
/>
|
||||
{permissions.hasChangeRole(props.appRole) &&
|
||||
|
@ -222,7 +222,7 @@ export function TemplateEditor(props: Readonly<TemplateEditorProps>) {
|
||||
<Split style={{ gap: '10px' }}>
|
||||
<Tabs
|
||||
value={editorValue}
|
||||
onTabChange={async (v) => {
|
||||
onChange={async (v) => {
|
||||
codeRef.current = await getCodeFromEditor();
|
||||
setEditorValue(v);
|
||||
}}
|
||||
@ -239,13 +239,13 @@ export function TemplateEditor(props: Readonly<TemplateEditorProps>) {
|
||||
<Tabs.Tab
|
||||
key={Editor.key}
|
||||
value={Editor.key}
|
||||
icon={<Editor.icon size="0.8rem" />}
|
||||
leftSection={<Editor.icon size="0.8rem" />}
|
||||
>
|
||||
{Editor.name}
|
||||
</Tabs.Tab>
|
||||
))}
|
||||
|
||||
<Group position="right" style={{ flex: '1' }} noWrap>
|
||||
<Group justify="right" style={{ flex: '1' }} wrap="nowrap">
|
||||
<SplitButton
|
||||
loading={isPreviewLoading}
|
||||
defaultSelected="preview_save"
|
||||
@ -288,7 +288,7 @@ export function TemplateEditor(props: Readonly<TemplateEditorProps>) {
|
||||
|
||||
<Tabs
|
||||
value={previewValue}
|
||||
onTabChange={setPreviewValue}
|
||||
onChange={setPreviewValue}
|
||||
style={{
|
||||
minWidth: '200px',
|
||||
display: 'flex',
|
||||
@ -300,7 +300,7 @@ export function TemplateEditor(props: Readonly<TemplateEditorProps>) {
|
||||
<Tabs.Tab
|
||||
key={PreviewArea.key}
|
||||
value={PreviewArea.key}
|
||||
icon={<PreviewArea.icon size="0.8rem" />}
|
||||
leftSection={<PreviewArea.icon size="0.8rem" />}
|
||||
>
|
||||
{PreviewArea.name}
|
||||
</Tabs.Tab>
|
||||
|
@ -482,11 +482,11 @@ export function ApiForm({
|
||||
<Paper mah={'65vh'} style={{ overflowY: 'auto' }}>
|
||||
<div>
|
||||
{/* Form Fields */}
|
||||
<Stack spacing="sm">
|
||||
<Stack gap="sm">
|
||||
{(!isValid || nonFieldErrors.length > 0) && (
|
||||
<Alert radius="sm" color="red" title={t`Form Errors Exist`}>
|
||||
{nonFieldErrors.length > 0 && (
|
||||
<Stack spacing="xs">
|
||||
<Stack gap="xs">
|
||||
{nonFieldErrors.map((message) => (
|
||||
<Text key={message}>{message}</Text>
|
||||
))}
|
||||
@ -509,7 +509,7 @@ export function ApiForm({
|
||||
</Boundary>
|
||||
<Boundary label={`ApiForm-${id}-FormContent`}>
|
||||
<FormProvider {...form}>
|
||||
<Stack spacing="xs">
|
||||
<Stack gap="xs">
|
||||
{!optionsLoading &&
|
||||
Object.entries(fields).map(([fieldName, field]) => (
|
||||
<ApiFormField
|
||||
@ -532,7 +532,7 @@ export function ApiForm({
|
||||
{/* Footer with Action Buttons */}
|
||||
<Divider />
|
||||
<div>
|
||||
<Group position="right">
|
||||
<Group justify="right">
|
||||
{props.actions?.map((action, i) => (
|
||||
<Button
|
||||
key={i}
|
||||
|
@ -100,7 +100,7 @@ export function AuthenticationForm() {
|
||||
) : null}
|
||||
<form onSubmit={classicForm.onSubmit(() => {})}>
|
||||
{classicLoginMode ? (
|
||||
<Stack spacing={0}>
|
||||
<Stack gap={0}>
|
||||
<TextInput
|
||||
required
|
||||
label={t`Username`}
|
||||
@ -114,7 +114,7 @@ export function AuthenticationForm() {
|
||||
{...classicForm.getInputProps('password')}
|
||||
/>
|
||||
{auth_settings?.password_forgotten_enabled === true && (
|
||||
<Group position="apart" mt="0">
|
||||
<Group justify="space-between" mt="0">
|
||||
<Anchor
|
||||
component="button"
|
||||
type="button"
|
||||
@ -139,7 +139,7 @@ export function AuthenticationForm() {
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
<Group position="apart" mt="xl">
|
||||
<Group justify="space-between" mt="xl">
|
||||
<Anchor
|
||||
component="button"
|
||||
type="button"
|
||||
@ -221,7 +221,7 @@ export function RegistrationForm() {
|
||||
<>
|
||||
{auth_settings?.registration_enabled && (
|
||||
<form onSubmit={registrationForm.onSubmit(() => {})}>
|
||||
<Stack spacing={0}>
|
||||
<Stack gap={0}>
|
||||
<TextInput
|
||||
required
|
||||
label={t`Username`}
|
||||
@ -249,7 +249,7 @@ export function RegistrationForm() {
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<Group position="apart" mt="xl">
|
||||
<Group justify="space-between" mt="xl">
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isRegistering}
|
||||
|
@ -35,13 +35,13 @@ export function HostOptionsForm({
|
||||
<TextInput
|
||||
placeholder={t`Host`}
|
||||
withAsterisk
|
||||
sx={{ flex: 1 }}
|
||||
style={{ flex: 1 }}
|
||||
{...form.getInputProps(`${key}.host`)}
|
||||
/>
|
||||
<TextInput
|
||||
placeholder={t`Name`}
|
||||
withAsterisk
|
||||
sx={{ flex: 1 }}
|
||||
style={{ flex: 1 }}
|
||||
{...form.getInputProps(`${key}.name`)}
|
||||
/>
|
||||
<ActionIcon
|
||||
@ -49,6 +49,7 @@ export function HostOptionsForm({
|
||||
onClick={() => {
|
||||
deleteItem(key);
|
||||
}}
|
||||
variant="default"
|
||||
>
|
||||
<IconTrash />
|
||||
</ActionIcon>
|
||||
@ -59,18 +60,18 @@ export function HostOptionsForm({
|
||||
|
||||
return (
|
||||
<form onSubmit={form.onSubmit(saveOptions)}>
|
||||
<Box sx={{ maxWidth: 500 }} mx="auto">
|
||||
<Box style={{ maxWidth: 500 }} mx="auto">
|
||||
{fields.length > 0 ? (
|
||||
<Group mb="xs">
|
||||
<Text weight={500} size="sm" sx={{ flex: 1 }}>
|
||||
<Text fw={500} size="sm" style={{ flex: 1 }}>
|
||||
<Trans>Host</Trans>
|
||||
</Text>
|
||||
<Text weight={500} size="sm" sx={{ flex: 1 }}>
|
||||
<Text fw={500} size="sm" style={{ flex: 1 }}>
|
||||
<Trans>Name</Trans>
|
||||
</Text>
|
||||
</Group>
|
||||
) : (
|
||||
<Text color="dimmed" align="center">
|
||||
<Text c="dimmed" ta="center">
|
||||
<Trans>No one here...</Trans>
|
||||
</Text>
|
||||
)}
|
||||
@ -84,7 +85,7 @@ export function HostOptionsForm({
|
||||
<IconSquarePlus />
|
||||
<Trans>Add Host</Trans>
|
||||
</Button>
|
||||
<Space sx={{ flex: 1 }} />
|
||||
<Space style={{ flex: 1 }} />
|
||||
<Button type="submit">
|
||||
<Trans>Save</Trans>
|
||||
</Button>
|
||||
|
@ -15,7 +15,7 @@ export function InstanceOptions({
|
||||
setHostEdit
|
||||
}: {
|
||||
hostKey: string;
|
||||
ChangeHost: (newHost: string) => void;
|
||||
ChangeHost: (newHost: string | null) => void;
|
||||
setHostEdit: () => void;
|
||||
}) {
|
||||
const [HostListEdit, setHostListEdit] = useToggle([false, true] as const);
|
||||
|
@ -230,9 +230,8 @@ export function ApiFormField({
|
||||
id={fieldId}
|
||||
value={numericalValue}
|
||||
error={error?.message}
|
||||
precision={definition.field_type == 'integer' ? 0 : 10}
|
||||
onChange={(value: number) => onChange(value)}
|
||||
removeTrailingZeros
|
||||
decimalScale={definition.field_type == 'integer' ? 0 : 10}
|
||||
onChange={(value: number | string | null) => onChange(value)}
|
||||
step={1}
|
||||
/>
|
||||
);
|
||||
|
@ -31,8 +31,8 @@ export function ChoiceField({
|
||||
|
||||
return choices.map((choice) => {
|
||||
return {
|
||||
value: choice.value,
|
||||
label: choice.display_name
|
||||
value: choice.value.toString(),
|
||||
label: choice.display_name.toString()
|
||||
};
|
||||
});
|
||||
}, [definition.choices]);
|
||||
@ -62,8 +62,8 @@ export function ChoiceField({
|
||||
placeholder={definition.placeholder}
|
||||
required={definition.required}
|
||||
disabled={definition.disabled}
|
||||
icon={definition.icon}
|
||||
withinPortal={true}
|
||||
leftSection={definition.icon}
|
||||
comboboxProps={{ withinPortal: true }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -61,7 +61,7 @@ export default function DateField({
|
||||
label={definition.label}
|
||||
description={definition.description}
|
||||
placeholder={definition.placeholder}
|
||||
icon={definition.icon}
|
||||
leftSection={definition.icon}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -19,8 +19,8 @@ export function NestedObjectField({
|
||||
<Text>{definition.label}</Text>
|
||||
</Accordion.Control>
|
||||
<Accordion.Panel>
|
||||
<Divider sx={{ marginTop: '-10px', marginBottom: '10px' }} />
|
||||
<Stack spacing="xs">
|
||||
<Divider style={{ marginTop: '-10px', marginBottom: '10px' }} />
|
||||
<Stack gap="xs">
|
||||
{Object.entries(definition.children ?? {}).map(
|
||||
([childFieldName, field]) => (
|
||||
<ApiFormField
|
||||
|
@ -1,5 +1,10 @@
|
||||
import { t } from '@lingui/macro';
|
||||
import { Input, useMantineTheme } from '@mantine/core';
|
||||
import {
|
||||
Input,
|
||||
darken,
|
||||
useMantineColorScheme,
|
||||
useMantineTheme
|
||||
} from '@mantine/core';
|
||||
import { useDebouncedValue, useId } from '@mantine/hooks';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
@ -11,6 +16,7 @@ import {
|
||||
import Select from 'react-select';
|
||||
|
||||
import { api } from '../../../App';
|
||||
import { vars } from '../../../theme';
|
||||
import { RenderInstance } from '../../render/Instance';
|
||||
import { ApiFormFieldType } from './ApiFormField';
|
||||
|
||||
@ -217,43 +223,46 @@ export function RelatedModelField({
|
||||
// Define color theme to pass to field based on Mantine theme
|
||||
const theme = useMantineTheme();
|
||||
|
||||
const colorschema = vars.colors.primaryColors;
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
|
||||
const colors = useMemo(() => {
|
||||
let colors: any;
|
||||
if (theme.colorScheme === 'dark') {
|
||||
if (colorScheme === 'dark') {
|
||||
colors = {
|
||||
neutral0: theme.colors[theme.colorScheme][6],
|
||||
neutral5: theme.colors[theme.colorScheme][4],
|
||||
neutral10: theme.colors[theme.colorScheme][4],
|
||||
neutral20: theme.colors[theme.colorScheme][4],
|
||||
neutral30: theme.colors[theme.colorScheme][3],
|
||||
neutral40: theme.colors[theme.colorScheme][2],
|
||||
neutral50: theme.colors[theme.colorScheme][1],
|
||||
neutral60: theme.colors[theme.colorScheme][0],
|
||||
neutral70: theme.colors[theme.colorScheme][0],
|
||||
neutral80: theme.colors[theme.colorScheme][0],
|
||||
neutral90: theme.colors[theme.colorScheme][0],
|
||||
primary: theme.colors[theme.primaryColor][7],
|
||||
primary25: theme.colors[theme.primaryColor][6],
|
||||
primary50: theme.colors[theme.primaryColor][5],
|
||||
primary75: theme.colors[theme.primaryColor][4]
|
||||
neutral0: colorschema[6],
|
||||
neutral5: colorschema[4],
|
||||
neutral10: colorschema[4],
|
||||
neutral20: colorschema[4],
|
||||
neutral30: colorschema[3],
|
||||
neutral40: colorschema[2],
|
||||
neutral50: colorschema[1],
|
||||
neutral60: colorschema[0],
|
||||
neutral70: colorschema[0],
|
||||
neutral80: colorschema[0],
|
||||
neutral90: colorschema[0],
|
||||
primary: vars.colors.primaryColors[7],
|
||||
primary25: vars.colors.primaryColors[6],
|
||||
primary50: vars.colors.primaryColors[5],
|
||||
primary75: vars.colors.primaryColors[4]
|
||||
};
|
||||
} else {
|
||||
colors = {
|
||||
neutral0: theme.white,
|
||||
neutral5: theme.fn.darken(theme.white, 0.05),
|
||||
neutral10: theme.fn.darken(theme.white, 0.1),
|
||||
neutral20: theme.fn.darken(theme.white, 0.2),
|
||||
neutral30: theme.fn.darken(theme.white, 0.3),
|
||||
neutral40: theme.fn.darken(theme.white, 0.4),
|
||||
neutral50: theme.fn.darken(theme.white, 0.5),
|
||||
neutral60: theme.fn.darken(theme.white, 0.6),
|
||||
neutral70: theme.fn.darken(theme.white, 0.7),
|
||||
neutral80: theme.fn.darken(theme.white, 0.8),
|
||||
neutral90: theme.fn.darken(theme.white, 0.9),
|
||||
primary: theme.colors[theme.primaryColor][7],
|
||||
primary25: theme.colors[theme.primaryColor][4],
|
||||
primary50: theme.colors[theme.primaryColor][5],
|
||||
primary75: theme.colors[theme.primaryColor][6]
|
||||
neutral0: vars.colors.white,
|
||||
neutral5: darken(vars.colors.white, 0.05),
|
||||
neutral10: darken(vars.colors.white, 0.1),
|
||||
neutral20: darken(vars.colors.white, 0.2),
|
||||
neutral30: darken(vars.colors.white, 0.3),
|
||||
neutral40: darken(vars.colors.white, 0.4),
|
||||
neutral50: darken(vars.colors.white, 0.5),
|
||||
neutral60: darken(vars.colors.white, 0.6),
|
||||
neutral70: darken(vars.colors.white, 0.7),
|
||||
neutral80: darken(vars.colors.white, 0.8),
|
||||
neutral90: darken(vars.colors.white, 0.9),
|
||||
primary: vars.colors.primaryColors[7],
|
||||
primary25: vars.colors.primaryColors[4],
|
||||
primary50: vars.colors.primaryColors[5],
|
||||
primary75: vars.colors.primaryColors[6]
|
||||
};
|
||||
}
|
||||
return colors;
|
||||
|
@ -8,10 +8,14 @@ import { useMemo } from 'react';
|
||||
|
||||
import { useLocalState } from '../../states/LocalState';
|
||||
|
||||
interface ApiImageProps extends ImageProps {
|
||||
onClick?: (event: any) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct an image container which will load and display the image
|
||||
*/
|
||||
export function ApiImage(props: ImageProps) {
|
||||
export function ApiImage(props: Readonly<ApiImageProps>) {
|
||||
const { host } = useLocalState.getState();
|
||||
|
||||
const imageUrl = useMemo(() => {
|
||||
@ -21,12 +25,9 @@ export function ApiImage(props: ImageProps) {
|
||||
return (
|
||||
<Stack>
|
||||
{imageUrl ? (
|
||||
<Image {...props} src={imageUrl} withPlaceholder fit="contain" />
|
||||
<Image {...props} src={imageUrl} fit="contain" />
|
||||
) : (
|
||||
<Skeleton
|
||||
height={props?.height ?? props.width}
|
||||
width={props?.width ?? props.height}
|
||||
/>
|
||||
<Skeleton h={props?.h ?? props.w} w={props?.w ?? props.h} />
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
|
@ -37,19 +37,14 @@ export function Thumbnail({
|
||||
}, [link, text]);
|
||||
|
||||
return (
|
||||
<Group align={align ?? 'left'} spacing="xs" noWrap={true}>
|
||||
<Group align={align ?? 'left'} gap="xs" wrap="nowrap">
|
||||
<ApiImage
|
||||
src={src || backup_image}
|
||||
alt={alt}
|
||||
width={size}
|
||||
aria-label={alt}
|
||||
w={size}
|
||||
fit="contain"
|
||||
radius="xs"
|
||||
withPlaceholder
|
||||
imageProps={{
|
||||
style: {
|
||||
maxHeight: size
|
||||
}
|
||||
}}
|
||||
style={{ maxHeight: size }}
|
||||
/>
|
||||
{inner}
|
||||
</Group>
|
||||
@ -71,7 +66,7 @@ export function ThumbnailHoverCard({
|
||||
}) {
|
||||
const card = useMemo(() => {
|
||||
return (
|
||||
<Group position="left" spacing={10} noWrap={true}>
|
||||
<Group justify="left" gap={10} wrap="nowrap">
|
||||
<Thumbnail src={src} alt={alt} size={size} />
|
||||
<Text>{text}</Text>
|
||||
</Group>
|
||||
|
@ -79,7 +79,7 @@ export function ActionDropdown({
|
||||
>
|
||||
<Tooltip label={action.tooltip}>
|
||||
<Menu.Item
|
||||
icon={action.icon}
|
||||
leftSection={action.icon}
|
||||
onClick={() => {
|
||||
if (action.onClick != undefined) {
|
||||
action.onClick();
|
||||
|
@ -72,7 +72,7 @@ export function AttachmentLink({
|
||||
}, [host, attachment, external]);
|
||||
|
||||
return (
|
||||
<Group position="left" spacing="sm">
|
||||
<Group justify="left" gap="sm">
|
||||
{external ? <IconLink /> : attachmentIcon(attachment)}
|
||||
<Anchor href={url} target="_blank" rel="noopener noreferrer">
|
||||
{text}
|
||||
|
@ -1,20 +1,21 @@
|
||||
import { ActionIcon, Group, useMantineColorScheme } from '@mantine/core';
|
||||
import { IconMoonStars, IconSun } from '@tabler/icons-react';
|
||||
|
||||
import { vars } from '../../theme';
|
||||
|
||||
export function ColorToggle() {
|
||||
const { colorScheme, toggleColorScheme } = useMantineColorScheme();
|
||||
|
||||
return (
|
||||
<Group position="center">
|
||||
<Group justify="center">
|
||||
<ActionIcon
|
||||
onClick={() => toggleColorScheme()}
|
||||
onClick={toggleColorScheme}
|
||||
size="lg"
|
||||
sx={(theme) => ({
|
||||
style={{
|
||||
color:
|
||||
theme.colorScheme === 'dark'
|
||||
? theme.colors.yellow[4]
|
||||
: theme.colors.blue[6]
|
||||
})}
|
||||
colorScheme === 'dark' ? vars.colors.yellow[4] : vars.colors.blue[6]
|
||||
}}
|
||||
variant="transparent"
|
||||
>
|
||||
{colorScheme === 'dark' ? <IconSun /> : <IconMoonStars />}
|
||||
</ActionIcon>
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Group, LoadingOverlay, Paper, Text } from '@mantine/core';
|
||||
|
||||
import { InvenTreeStyle } from '../../globalStyle';
|
||||
import * as classes from '../../main.css';
|
||||
|
||||
export interface StatisticItemProps {
|
||||
title: string;
|
||||
@ -16,18 +16,16 @@ export function StatisticItem({
|
||||
data: StatisticItemProps;
|
||||
isLoading: boolean;
|
||||
}) {
|
||||
const { classes } = InvenTreeStyle();
|
||||
|
||||
return (
|
||||
<Paper withBorder p="xs" key={id} pos="relative">
|
||||
<LoadingOverlay visible={isLoading} overlayBlur={2} />
|
||||
<Group position="apart">
|
||||
<LoadingOverlay visible={isLoading} overlayProps={{ blur: 2 }} />
|
||||
<Group justify="space-between">
|
||||
<Text size="xs" color="dimmed" className={classes.dashboardItemTitle}>
|
||||
{data.title}
|
||||
</Text>
|
||||
</Group>
|
||||
|
||||
<Group align="flex-end" spacing="xs" mt={25}>
|
||||
<Group align="flex-end" gap="xs" mt={25}>
|
||||
<Text className={classes.dashboardItemValue}>{data.value}</Text>
|
||||
</Group>
|
||||
</Paper>
|
||||
|
@ -2,7 +2,7 @@ import { Trans } from '@lingui/macro';
|
||||
import { Anchor, Container, HoverCard, ScrollArea, Text } from '@mantine/core';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { InvenTreeStyle } from '../../globalStyle';
|
||||
import * as classes from '../../main.css';
|
||||
|
||||
export interface BaseDocProps {
|
||||
text: string | JSX.Element;
|
||||
@ -22,8 +22,6 @@ export function DocTooltip({
|
||||
link,
|
||||
docchildren
|
||||
}: Readonly<DocTooltipProps>) {
|
||||
const { classes } = InvenTreeStyle();
|
||||
|
||||
return (
|
||||
<HoverCard
|
||||
shadow="md"
|
||||
|
@ -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] }
|
||||
});
|
@ -1,54 +1,17 @@
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { Carousel } from '@mantine/carousel';
|
||||
import {
|
||||
Anchor,
|
||||
Button,
|
||||
Paper,
|
||||
Text,
|
||||
Title,
|
||||
createStyles,
|
||||
rem
|
||||
} from '@mantine/core';
|
||||
import { Anchor, Button, Paper, Text, Title, rem } from '@mantine/core';
|
||||
|
||||
import { DocumentationLinkItem } from './DocumentationLinks';
|
||||
import * as classes from './GettingStartedCarousel.css';
|
||||
import { PlaceholderPill } from './Placeholder';
|
||||
|
||||
const useStyles = createStyles((theme) => ({
|
||||
card: {
|
||||
height: rem(170),
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'flex-start',
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center'
|
||||
},
|
||||
|
||||
title: {
|
||||
fontWeight: 900,
|
||||
color:
|
||||
theme.colorScheme === 'dark' ? theme.colors.white : theme.colors.dark,
|
||||
lineHeight: 1.2,
|
||||
fontSize: rem(32),
|
||||
marginTop: 0
|
||||
},
|
||||
|
||||
category: {
|
||||
color:
|
||||
theme.colorScheme === 'dark' ? theme.colors.white : theme.colors.dark,
|
||||
opacity: 0.7,
|
||||
fontWeight: 700
|
||||
}
|
||||
}));
|
||||
|
||||
function StartedCard({
|
||||
title,
|
||||
description,
|
||||
link,
|
||||
placeholder
|
||||
}: DocumentationLinkItem) {
|
||||
const { classes } = useStyles();
|
||||
|
||||
return (
|
||||
<Paper shadow="md" p="xl" radius="md" className={classes.card}>
|
||||
<div>
|
||||
@ -81,10 +44,11 @@ export function GettingStartedCarousel({
|
||||
|
||||
return (
|
||||
<Carousel
|
||||
slideSize="50%"
|
||||
breakpoints={[{ maxWidth: 'sm', slideSize: '100%', slideGap: rem(2) }]}
|
||||
slideGap="xl"
|
||||
slideSize={{ base: '100%', sm: '50%', md: '33.333333%' }}
|
||||
slideGap={{ base: 0, sm: 'md' }}
|
||||
slidesToScroll={3}
|
||||
align="start"
|
||||
loop
|
||||
>
|
||||
{slides}
|
||||
</Carousel>
|
||||
|
@ -43,7 +43,7 @@ export function InfoItem({
|
||||
}
|
||||
|
||||
return (
|
||||
<Group position="apart">
|
||||
<Group justify="space-between">
|
||||
<Text fz="sm" fw={700}>
|
||||
{name}:
|
||||
</Text>
|
||||
|
@ -10,7 +10,7 @@ export const InvenTreeLogoHomeButton = forwardRef<HTMLDivElement>(
|
||||
return (
|
||||
<div ref={ref} {...props}>
|
||||
<NavLink to={'/'}>
|
||||
<ActionIcon size={28}>
|
||||
<ActionIcon size={28} variant="transparent">
|
||||
<InvenTreeLogo />
|
||||
</ActionIcon>
|
||||
</NavLink>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Select, SelectItem } from '@mantine/core';
|
||||
import { Select } from '@mantine/core';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { getSupportedLanguages } from '../../contexts/LanguageContext';
|
||||
@ -10,7 +10,7 @@ export function LanguageSelect({ width = 80 }: { width?: number }) {
|
||||
state.language,
|
||||
state.setLanguage
|
||||
]);
|
||||
const [langOptions, setLangOptions] = useState<SelectItem[]>([]);
|
||||
const [langOptions, setLangOptions] = useState<any[]>([]);
|
||||
|
||||
// change global language on change
|
||||
useEffect(() => {
|
||||
|
@ -9,14 +9,18 @@ export function LanguageToggle() {
|
||||
|
||||
return (
|
||||
<Group
|
||||
position="center"
|
||||
justify="center"
|
||||
style={{
|
||||
border: open === true ? `1px dashed ` : ``,
|
||||
margin: open === true ? 2 : 12,
|
||||
padding: open === true ? 8 : 0
|
||||
}}
|
||||
>
|
||||
<ActionIcon onClick={() => toggle.toggle()} size="lg">
|
||||
<ActionIcon
|
||||
onClick={() => toggle.toggle()}
|
||||
size="lg"
|
||||
variant="transparent"
|
||||
>
|
||||
<IconLanguage />
|
||||
</ActionIcon>
|
||||
{open && (
|
||||
|
@ -2,7 +2,7 @@ import { SimpleGrid, Text, UnstyledButton } from '@mantine/core';
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { InvenTreeStyle } from '../../globalStyle';
|
||||
import * as classes from '../../main.css';
|
||||
import { DocTooltip } from './DocTooltip';
|
||||
|
||||
export interface MenuLinkItem {
|
||||
@ -50,8 +50,6 @@ export function MenuLinks({
|
||||
links: MenuLinkItem[];
|
||||
highlighted?: boolean;
|
||||
}) {
|
||||
const { classes } = InvenTreeStyle();
|
||||
|
||||
const filteredLinks = links.filter(
|
||||
(item) => !highlighted || item.highlight === true
|
||||
);
|
||||
|
@ -9,7 +9,7 @@ export function PlaceholderPill() {
|
||||
return (
|
||||
<Tooltip
|
||||
multiline
|
||||
width={220}
|
||||
w={220}
|
||||
withArrow
|
||||
label={t`This feature/button/site is a placeholder for a feature that is not implemented, only partial or intended for testing.`}
|
||||
>
|
||||
@ -31,7 +31,7 @@ export function PlaceholderPanel() {
|
||||
title={t`This panel is a placeholder.`}
|
||||
icon={<IconInfoCircle />}
|
||||
>
|
||||
<Text color="gray">This panel has not yet been implemented</Text>
|
||||
<Text c="gray">This panel has not yet been implemented</Text>
|
||||
</Alert>
|
||||
</Stack>
|
||||
);
|
||||
|
@ -23,9 +23,9 @@ export function ProgressBar(props: Readonly<ProgressBarProps>) {
|
||||
}, [props]);
|
||||
|
||||
return (
|
||||
<Stack spacing={2} style={{ flexGrow: 1, minWidth: '100px' }}>
|
||||
<Stack gap={2} style={{ flexGrow: 1, minWidth: '100px' }}>
|
||||
{props.progressLabel && (
|
||||
<Text align="center" size="xs">
|
||||
<Text ta="center" size="xs">
|
||||
{props.value} / {props.maximum}
|
||||
</Text>
|
||||
)}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Text } from '@mantine/core';
|
||||
|
||||
import { InvenTreeStyle } from '../../globalStyle';
|
||||
import * as classes from '../../main.css';
|
||||
|
||||
export function StylishText({
|
||||
children,
|
||||
@ -9,7 +9,6 @@ export function StylishText({
|
||||
children: JSX.Element | string;
|
||||
size?: string;
|
||||
}) {
|
||||
const { classes } = InvenTreeStyle();
|
||||
return (
|
||||
<Text size={size} className={classes.signText} variant="gradient">
|
||||
{children}
|
||||
|
@ -59,7 +59,7 @@ export function AboutInvenTreeModal({
|
||||
<tr key={idx}>
|
||||
<td>{map.title}</td>
|
||||
<td>
|
||||
<Group position="apart" spacing="xs">
|
||||
<Group justify="space-between" gap="xs">
|
||||
{alwaysLink ? (
|
||||
<Anchor href={data[map.ref]} target="_blank">
|
||||
{data[map.ref]}
|
||||
@ -177,7 +177,7 @@ export function AboutInvenTreeModal({
|
||||
</tbody>
|
||||
</Table>
|
||||
<Divider />
|
||||
<Group position="apart">
|
||||
<Group justify="space-between">
|
||||
<CopyButton
|
||||
value={copyval}
|
||||
label={<Trans>Copy version information</Trans>}
|
||||
|
@ -18,7 +18,7 @@ import { apiUrl } from '../../states/ApiState';
|
||||
|
||||
export function LicenceView(entries: Readonly<any[]>) {
|
||||
return (
|
||||
<Stack spacing="xs">
|
||||
<Stack gap="xs">
|
||||
<Divider />
|
||||
{entries?.length > 0 ? (
|
||||
<Accordion variant="contained" defaultValue="-">
|
||||
@ -28,7 +28,7 @@ export function LicenceView(entries: Readonly<any[]>) {
|
||||
value={`entry-${index}`}
|
||||
>
|
||||
<Accordion.Control>
|
||||
<Group position="apart" grow>
|
||||
<Group justify="space-between" grow>
|
||||
<Text>{entry.name}</Text>
|
||||
<Text>{entry.license}</Text>
|
||||
<Space />
|
||||
@ -63,7 +63,7 @@ export function LicenseModal() {
|
||||
const rspdata = !data ? [] : Object.keys(data ?? {});
|
||||
|
||||
return (
|
||||
<Stack spacing="xs">
|
||||
<Stack gap="xs">
|
||||
<Divider />
|
||||
<LoadingOverlay visible={isFetching} />
|
||||
{isFetching && (
|
||||
|
@ -150,7 +150,7 @@ export function QrCodeModal({
|
||||
<Stack>
|
||||
<Group>
|
||||
<Text size="sm">{camId?.label}</Text>
|
||||
<Space sx={{ flex: 1 }} />
|
||||
<Space style={{ flex: 1 }} />
|
||||
<Badge>{ScanningEnabled ? t`Scanning` : t`Not scanning`}</Badge>
|
||||
</Group>
|
||||
<Container px={0} id="reader" w={'100%'} mih="300px" />
|
||||
@ -162,14 +162,14 @@ export function QrCodeModal({
|
||||
<>
|
||||
<Group>
|
||||
<Button
|
||||
sx={{ flex: 1 }}
|
||||
style={{ flex: 1 }}
|
||||
onClick={() => startScanning()}
|
||||
disabled={camId != undefined && ScanningEnabled}
|
||||
>
|
||||
<Trans>Start scanning</Trans>
|
||||
</Button>
|
||||
<Button
|
||||
sx={{ flex: 1 }}
|
||||
style={{ flex: 1 }}
|
||||
onClick={() => stopScanning()}
|
||||
disabled={!ScanningEnabled}
|
||||
>
|
||||
@ -177,11 +177,11 @@ export function QrCodeModal({
|
||||
</Button>
|
||||
</Group>
|
||||
{values.length == 0 ? (
|
||||
<Text color={'grey'}>
|
||||
<Text c={'grey'}>
|
||||
<Trans>No scans yet!</Trans>
|
||||
</Text>
|
||||
) : (
|
||||
<ScrollArea sx={{ height: 200 }} type="auto" offsetScrollbars>
|
||||
<ScrollArea style={{ height: 200 }} type="auto" offsetScrollbars>
|
||||
{values.map((value, index) => (
|
||||
<div key={index}>{value}</div>
|
||||
))}
|
||||
|
@ -137,7 +137,7 @@ export function ServerInfoModal({
|
||||
</tbody>
|
||||
</Table>
|
||||
<Divider />
|
||||
<Group position="right">
|
||||
<Group justify="right">
|
||||
<Button
|
||||
color="red"
|
||||
onClick={() => {
|
||||
|
@ -41,10 +41,14 @@ export function BreadcrumbList({
|
||||
}, [breadcrumbs]);
|
||||
|
||||
return (
|
||||
<Paper p="3" radius="xs">
|
||||
<Group spacing="xs">
|
||||
<Paper p="7" radius="xs" shadow="xs">
|
||||
<Group gap="xs">
|
||||
{navCallback && (
|
||||
<ActionIcon key="nav-action" onClick={navCallback}>
|
||||
<ActionIcon
|
||||
key="nav-action"
|
||||
onClick={navCallback}
|
||||
variant="transparent"
|
||||
>
|
||||
<IconMenu2 />
|
||||
</ActionIcon>
|
||||
)}
|
||||
|
6
src/frontend/src/components/nav/DetailDrawer.css.ts
Normal file
6
src/frontend/src/components/nav/DetailDrawer.css.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const flex = style({
|
||||
display: 'flex',
|
||||
flex: 1
|
||||
});
|
@ -1,19 +1,12 @@
|
||||
import {
|
||||
ActionIcon,
|
||||
Divider,
|
||||
Drawer,
|
||||
Group,
|
||||
MantineNumberSize,
|
||||
Stack,
|
||||
Text,
|
||||
createStyles
|
||||
} from '@mantine/core';
|
||||
import { ActionIcon, Divider, Drawer, Group, Stack, Text } from '@mantine/core';
|
||||
import { IconChevronLeft } from '@tabler/icons-react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { Link, Route, Routes, useNavigate, useParams } from 'react-router-dom';
|
||||
import type { To } from 'react-router-dom';
|
||||
|
||||
import { UiSizeType } from '../../defaults/formatters';
|
||||
import { useLocalState } from '../../states/LocalState';
|
||||
import * as classes from './DetailDrawer.css';
|
||||
|
||||
/**
|
||||
* @param title - drawer title
|
||||
@ -26,17 +19,10 @@ export interface DrawerProps {
|
||||
position?: 'right' | 'left';
|
||||
renderContent: (id?: string) => React.ReactNode;
|
||||
urlPrefix?: string;
|
||||
size?: MantineNumberSize;
|
||||
size?: UiSizeType;
|
||||
closeOnEscape?: boolean;
|
||||
}
|
||||
|
||||
const useStyles = createStyles(() => ({
|
||||
flex: {
|
||||
display: 'flex',
|
||||
flex: 1
|
||||
}
|
||||
}));
|
||||
|
||||
function DetailDrawerComponent({
|
||||
title,
|
||||
position = 'right',
|
||||
@ -46,7 +32,6 @@ function DetailDrawerComponent({
|
||||
}: Readonly<DrawerProps>) {
|
||||
const navigate = useNavigate();
|
||||
const { id } = useParams();
|
||||
const { classes } = useStyles();
|
||||
|
||||
const content = renderContent(id);
|
||||
const opened = useMemo(() => !!id && !!content, [id, content]);
|
||||
@ -87,7 +72,7 @@ function DetailDrawerComponent({
|
||||
</Group>
|
||||
}
|
||||
>
|
||||
<Stack spacing={'xs'} className={classes.flex}>
|
||||
<Stack gap={'xs'} className={classes.flex}>
|
||||
<Divider />
|
||||
{content}
|
||||
</Stack>
|
||||
|
@ -1,14 +1,13 @@
|
||||
import { Anchor, Container, Group } from '@mantine/core';
|
||||
|
||||
import { footerLinks } from '../../defaults/links';
|
||||
import { InvenTreeStyle } from '../../globalStyle';
|
||||
import * as classes from '../../main.css';
|
||||
import { InvenTreeLogoHomeButton } from '../items/InvenTreeLogo';
|
||||
|
||||
export function Footer() {
|
||||
const { classes } = InvenTreeStyle();
|
||||
const items = footerLinks.map((link) => (
|
||||
<Anchor<'a'>
|
||||
color="dimmed"
|
||||
c="dimmed"
|
||||
key={link.key}
|
||||
href={link.link}
|
||||
onClick={(event) => event.preventDefault()}
|
||||
|
@ -8,7 +8,7 @@ import { useMatch, useNavigate } from 'react-router-dom';
|
||||
import { api } from '../../App';
|
||||
import { navTabs as mainNavTabs } from '../../defaults/links';
|
||||
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
||||
import { InvenTreeStyle } from '../../globalStyle';
|
||||
import * as classes from '../../main.css';
|
||||
import { apiUrl } from '../../states/ApiState';
|
||||
import { useLocalState } from '../../states/LocalState';
|
||||
import { useUserState } from '../../states/UserState';
|
||||
@ -21,7 +21,6 @@ import { NotificationDrawer } from './NotificationDrawer';
|
||||
import { SearchDrawer } from './SearchDrawer';
|
||||
|
||||
export function Header() {
|
||||
const { classes } = InvenTreeStyle();
|
||||
const [setNavigationOpen, navigationOpen] = useLocalState((state) => [
|
||||
state.setNavigationOpen,
|
||||
state.navigationOpen
|
||||
@ -54,12 +53,13 @@ export function Header() {
|
||||
limit: 1
|
||||
}
|
||||
};
|
||||
let response = await api.get(
|
||||
apiUrl(ApiEndpoints.notifications_list),
|
||||
params
|
||||
);
|
||||
setNotificationCount(response.data.count);
|
||||
return response.data;
|
||||
let response = await api
|
||||
.get(apiUrl(ApiEndpoints.notifications_list), params)
|
||||
.catch(() => {
|
||||
return null;
|
||||
});
|
||||
setNotificationCount(response?.data?.count ?? 0);
|
||||
return response?.data;
|
||||
} catch (error) {
|
||||
return error;
|
||||
}
|
||||
@ -93,28 +93,32 @@ export function Header() {
|
||||
}}
|
||||
/>
|
||||
<Container className={classes.layoutHeaderSection} size="100%">
|
||||
<Group position="apart">
|
||||
<Group justify="space-between">
|
||||
<Group>
|
||||
<NavHoverMenu openDrawer={openNavDrawer} />
|
||||
<NavTabs />
|
||||
</Group>
|
||||
<Group>
|
||||
<ActionIcon onClick={openSearchDrawer}>
|
||||
<ActionIcon onClick={openSearchDrawer} variant="transparent">
|
||||
<IconSearch />
|
||||
</ActionIcon>
|
||||
<SpotlightButton />
|
||||
<ScanButton />
|
||||
<ActionIcon onClick={openNotificationDrawer}>
|
||||
<Indicator
|
||||
radius="lg"
|
||||
size="18"
|
||||
label={notificationCount}
|
||||
color="red"
|
||||
disabled={notificationCount <= 0}
|
||||
<Indicator
|
||||
radius="lg"
|
||||
size="18"
|
||||
label={notificationCount}
|
||||
color="red"
|
||||
disabled={notificationCount <= 0}
|
||||
inline
|
||||
>
|
||||
<ActionIcon
|
||||
onClick={openNotificationDrawer}
|
||||
variant="transparent"
|
||||
>
|
||||
<IconBell />
|
||||
</Indicator>
|
||||
</ActionIcon>
|
||||
</ActionIcon>
|
||||
</Indicator>
|
||||
<MainMenu />
|
||||
</Group>
|
||||
</Group>
|
||||
@ -124,7 +128,6 @@ export function Header() {
|
||||
}
|
||||
|
||||
function NavTabs() {
|
||||
const { classes } = InvenTreeStyle();
|
||||
const navigate = useNavigate();
|
||||
const match = useMatch(':tabName/*');
|
||||
const tabValue = match?.params.tabName;
|
||||
@ -134,11 +137,11 @@ function NavTabs() {
|
||||
defaultValue="home"
|
||||
classNames={{
|
||||
root: classes.tabs,
|
||||
tabsList: classes.tabsList,
|
||||
list: classes.tabsList,
|
||||
tab: classes.tab
|
||||
}}
|
||||
value={tabValue}
|
||||
onTabChange={(value) =>
|
||||
onChange={(value) =>
|
||||
value == '/' ? navigate('/') : navigate(`/${value}`)
|
||||
}
|
||||
>
|
||||
|
@ -1,12 +1,12 @@
|
||||
import { t } from '@lingui/macro';
|
||||
import { Container, Flex, Space } from '@mantine/core';
|
||||
import { SpotlightProvider } from '@mantine/spotlight';
|
||||
import { Spotlight, createSpotlight } from '@mantine/spotlight';
|
||||
import { IconSearch } from '@tabler/icons-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Navigate, Outlet, useLocation, useNavigate } from 'react-router-dom';
|
||||
|
||||
import { getActions } from '../../defaults/actions';
|
||||
import { InvenTreeStyle } from '../../globalStyle';
|
||||
import * as classes from '../../main.css';
|
||||
import { useUserState } from '../../states/UserState';
|
||||
import { Boundary } from '../Boundary';
|
||||
import { Footer } from './Footer';
|
||||
@ -25,8 +25,9 @@ export const ProtectedRoute = ({ children }: { children: JSX.Element }) => {
|
||||
return children;
|
||||
};
|
||||
|
||||
export const [firstStore, firstSpotlight] = createSpotlight();
|
||||
|
||||
export default function LayoutComponent() {
|
||||
const { classes } = InvenTreeStyle();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
@ -38,6 +39,9 @@ export default function LayoutComponent() {
|
||||
if (change.length > defaultActions.length) setCustomActions(true);
|
||||
setActions(change);
|
||||
}
|
||||
// firstStore.subscribe(actionsAreChanging);
|
||||
|
||||
// clear additional actions on location change
|
||||
useEffect(() => {
|
||||
if (customActions) {
|
||||
setActions(defaultActions);
|
||||
@ -47,26 +51,28 @@ export default function LayoutComponent() {
|
||||
|
||||
return (
|
||||
<ProtectedRoute>
|
||||
<SpotlightProvider
|
||||
actions={actions}
|
||||
onActionsChange={actionsAreChanging}
|
||||
searchIcon={<IconSearch size="1.2rem" />}
|
||||
searchPlaceholder={t`Search...`}
|
||||
shortcut={['mod + K', '/']}
|
||||
nothingFoundMessage={t`Nothing found...`}
|
||||
>
|
||||
<Flex direction="column" mih="100vh">
|
||||
<Header />
|
||||
<Container className={classes.layoutContent} size="100%">
|
||||
<Boundary label={'layout'}>
|
||||
<Outlet />
|
||||
</Boundary>
|
||||
{/* </ErrorBoundary> */}
|
||||
</Container>
|
||||
<Space h="xl" />
|
||||
<Footer />
|
||||
</Flex>
|
||||
</SpotlightProvider>
|
||||
<Flex direction="column" mih="100vh">
|
||||
<Header />
|
||||
<Container className={classes.layoutContent} size="100%">
|
||||
<Boundary label={'layout'}>
|
||||
<Outlet />
|
||||
</Boundary>
|
||||
{/* </ErrorBoundary> */}
|
||||
</Container>
|
||||
<Space h="xl" />
|
||||
<Footer />
|
||||
<Spotlight
|
||||
actions={actions}
|
||||
store={firstStore}
|
||||
highlightQuery
|
||||
searchProps={{
|
||||
leftSection: <IconSearch size="1.2rem" />,
|
||||
placeholder: t`Search...`
|
||||
}}
|
||||
shortcut={['mod + K', '/']}
|
||||
nothingFound={t`Nothing found...`}
|
||||
/>
|
||||
</Flex>
|
||||
</ProtectedRoute>
|
||||
);
|
||||
}
|
||||
|
@ -10,26 +10,29 @@ import {
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
|
||||
import { doLogout } from '../../functions/auth';
|
||||
import { InvenTreeStyle } from '../../globalStyle';
|
||||
import * as classes from '../../main.css';
|
||||
import { useUserState } from '../../states/UserState';
|
||||
import { vars } from '../../theme';
|
||||
|
||||
export function MainMenu() {
|
||||
const navigate = useNavigate();
|
||||
const { classes, theme } = InvenTreeStyle();
|
||||
const userState = useUserState();
|
||||
const [user, username] = useUserState((state) => [
|
||||
state.user,
|
||||
state.username
|
||||
]);
|
||||
|
||||
return (
|
||||
<Menu width={260} position="bottom-end">
|
||||
<Menu.Target>
|
||||
<UnstyledButton className={classes.layoutHeaderUser}>
|
||||
<Group spacing={7}>
|
||||
<Text weight={500} size="sm" sx={{ lineHeight: 1 }} mr={3}>
|
||||
{userState.username() ? (
|
||||
userState.username()
|
||||
) : (
|
||||
<Skeleton height={20} width={40} radius={theme.defaultRadius} />
|
||||
)}
|
||||
</Text>
|
||||
<Group gap={7}>
|
||||
{username() ? (
|
||||
<Text fw={500} size="sm" style={{ lineHeight: 1 }} mr={3}>
|
||||
{username()}
|
||||
</Text>
|
||||
) : (
|
||||
<Skeleton height={20} width={40} radius={vars.radiusDefault} />
|
||||
)}
|
||||
<IconChevronDown />
|
||||
</Group>
|
||||
</UnstyledButton>
|
||||
@ -38,22 +41,26 @@ export function MainMenu() {
|
||||
<Menu.Label>
|
||||
<Trans>Settings</Trans>
|
||||
</Menu.Label>
|
||||
<Menu.Item icon={<IconUserCog />} component={Link} to="/settings/user">
|
||||
<Menu.Item
|
||||
leftSection={<IconUserCog />}
|
||||
component={Link}
|
||||
to="/settings/user"
|
||||
>
|
||||
<Trans>Account settings</Trans>
|
||||
</Menu.Item>
|
||||
{userState.user?.is_staff && (
|
||||
{user?.is_staff && (
|
||||
<Menu.Item
|
||||
icon={<IconSettings />}
|
||||
leftSection={<IconSettings />}
|
||||
component={Link}
|
||||
to="/settings/system"
|
||||
>
|
||||
<Trans>System Settings</Trans>
|
||||
</Menu.Item>
|
||||
)}
|
||||
{userState.user?.is_staff && <Menu.Divider />}
|
||||
{userState.user?.is_staff && (
|
||||
{user?.is_staff && <Menu.Divider />}
|
||||
{user?.is_staff && (
|
||||
<Menu.Item
|
||||
icon={<IconUserBolt />}
|
||||
leftSection={<IconUserBolt />}
|
||||
component={Link}
|
||||
to="/settings/admin"
|
||||
>
|
||||
@ -62,7 +69,7 @@ export function MainMenu() {
|
||||
)}
|
||||
<Menu.Divider />
|
||||
<Menu.Item
|
||||
icon={<IconLogout />}
|
||||
leftSection={<IconLogout />}
|
||||
onClick={() => {
|
||||
doLogout(navigate);
|
||||
}}
|
||||
|
@ -8,15 +8,17 @@ import {
|
||||
HoverCard,
|
||||
Skeleton,
|
||||
Text,
|
||||
UnstyledButton
|
||||
UnstyledButton,
|
||||
useMantineColorScheme
|
||||
} from '@mantine/core';
|
||||
import { IconLayoutSidebar } from '@tabler/icons-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { menuItems } from '../../defaults/menuItems';
|
||||
import { InvenTreeStyle } from '../../globalStyle';
|
||||
import * as classes from '../../main.css';
|
||||
import { useServerApiState } from '../../states/ApiState';
|
||||
import { useLocalState } from '../../states/LocalState';
|
||||
import { vars } from '../../theme';
|
||||
import { InvenTreeLogo } from '../items/InvenTreeLogo';
|
||||
import { MenuLinks } from '../items/MenuLinks';
|
||||
|
||||
@ -27,13 +29,13 @@ export function NavHoverMenu({
|
||||
}: {
|
||||
openDrawer: () => void;
|
||||
}) {
|
||||
const { classes, theme } = InvenTreeStyle();
|
||||
const [hostKey, hostList] = useLocalState((state) => [
|
||||
state.hostKey,
|
||||
state.hostList
|
||||
]);
|
||||
const [servername] = useServerApiState((state) => [state.server.instance]);
|
||||
const [instanceName, setInstanceName] = useState<string>();
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
|
||||
useEffect(() => {
|
||||
if (hostKey && hostList[hostKey]) {
|
||||
@ -55,26 +57,27 @@ export function NavHoverMenu({
|
||||
</UnstyledButton>
|
||||
</HoverCard.Target>
|
||||
|
||||
<HoverCard.Dropdown sx={{ overflow: 'hidden' }}>
|
||||
<Group position="apart" px="md">
|
||||
<HoverCard.Dropdown style={{ overflow: 'hidden' }}>
|
||||
<Group justify="space-between" px="md">
|
||||
<ActionIcon
|
||||
onClick={openDrawer}
|
||||
onMouseOver={openDrawer}
|
||||
title={t`Open Navigation`}
|
||||
variant="default"
|
||||
>
|
||||
<IconLayoutSidebar />
|
||||
</ActionIcon>
|
||||
<Group spacing={'xs'}>
|
||||
<Group gap={'xs'}>
|
||||
{instanceName ? (
|
||||
instanceName
|
||||
) : (
|
||||
<Skeleton height={20} width={40} radius={theme.defaultRadius} />
|
||||
<Skeleton height={20} width={40} radius={vars.radiusDefault} />
|
||||
)}{' '}
|
||||
|{' '}
|
||||
{servername ? (
|
||||
servername
|
||||
) : (
|
||||
<Skeleton height={20} width={40} radius={theme.defaultRadius} />
|
||||
<Skeleton height={20} width={40} radius={vars.radiusDefault} />
|
||||
)}
|
||||
</Group>
|
||||
<Anchor href="#" fz="xs" onClick={openDrawer}>
|
||||
@ -85,11 +88,13 @@ export function NavHoverMenu({
|
||||
<Divider
|
||||
my="sm"
|
||||
mx="-md"
|
||||
color={theme.colorScheme === 'dark' ? 'dark.5' : 'gray.1'}
|
||||
color={
|
||||
colorScheme === 'dark' ? vars.colors.dark[5] : vars.colors.gray[1]
|
||||
}
|
||||
/>
|
||||
<MenuLinks links={onlyItems} highlighted={true} />
|
||||
<div className={classes.headerDropdownFooter}>
|
||||
<Group position="apart">
|
||||
<Group justify="space-between">
|
||||
<div>
|
||||
<Text fw={500} fz="sm">
|
||||
<Trans>Get started</Trans>
|
||||
|
@ -12,7 +12,7 @@ import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { aboutLinks, navDocLinks } from '../../defaults/links';
|
||||
import { menuItems } from '../../defaults/menuItems';
|
||||
import { InvenTreeStyle } from '../../globalStyle';
|
||||
import * as classes from '../../main.css';
|
||||
import { DocumentationLinks } from '../items/DocumentationLinks';
|
||||
import { MenuLinkItem, MenuLinks } from '../items/MenuLinks';
|
||||
|
||||
@ -27,8 +27,6 @@ export function NavigationDrawer({
|
||||
opened: boolean;
|
||||
close: () => void;
|
||||
}) {
|
||||
const { classes } = InvenTreeStyle();
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
opened={opened}
|
||||
@ -44,7 +42,6 @@ export function NavigationDrawer({
|
||||
);
|
||||
}
|
||||
function DrawerContent() {
|
||||
const { classes } = InvenTreeStyle();
|
||||
const [scrollHeight, setScrollHeight] = useState(0);
|
||||
const ref = useRef(null);
|
||||
const { height } = useViewportSize();
|
||||
|
@ -2,9 +2,11 @@ import { t } from '@lingui/macro';
|
||||
import {
|
||||
ActionIcon,
|
||||
Alert,
|
||||
Center,
|
||||
Divider,
|
||||
Drawer,
|
||||
Group,
|
||||
Loader,
|
||||
LoadingOverlay,
|
||||
Space,
|
||||
Stack,
|
||||
@ -13,6 +15,7 @@ import {
|
||||
} from '@mantine/core';
|
||||
import { IconBellCheck, IconBellPlus } from '@tabler/icons-react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useMemo } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
|
||||
import { api } from '../../App';
|
||||
@ -51,6 +54,10 @@ export function NotificationDrawer({
|
||||
refetchOnWindowFocus: false
|
||||
});
|
||||
|
||||
const hasNotifications: boolean = useMemo(() => {
|
||||
return (notificationQuery.data?.results?.length ?? 0) > 0;
|
||||
}, [notificationQuery.data]);
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
opened={opened}
|
||||
@ -67,74 +74,80 @@ export function NotificationDrawer({
|
||||
}
|
||||
}}
|
||||
title={
|
||||
<Group position="apart" noWrap={true}>
|
||||
<Group justify="space-between" wrap="nowrap">
|
||||
<StylishText size="lg">{t`Notifications`}</StylishText>
|
||||
<ActionIcon
|
||||
onClick={() => {
|
||||
onClose();
|
||||
navigate('/notifications/unread');
|
||||
}}
|
||||
variant="transparent"
|
||||
>
|
||||
<IconBellPlus />
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
}
|
||||
>
|
||||
<Stack spacing="xs">
|
||||
<Stack gap="xs">
|
||||
<Divider />
|
||||
<LoadingOverlay visible={notificationQuery.isFetching} />
|
||||
{(notificationQuery.data?.results?.length ?? 0) == 0 && (
|
||||
{!hasNotifications && (
|
||||
<Alert color="green">
|
||||
<Text size="sm">{t`You have no unread notifications.`}</Text>
|
||||
</Alert>
|
||||
)}
|
||||
{notificationQuery.data?.results?.map((notification: any) => (
|
||||
<Group position="apart" key={notification.pk}>
|
||||
<Stack spacing="3">
|
||||
{notification?.target?.link ? (
|
||||
<Text
|
||||
size="sm"
|
||||
component={Link}
|
||||
to={notification?.target?.link}
|
||||
target="_blank"
|
||||
>
|
||||
{notification.target?.name ??
|
||||
notification.name ??
|
||||
t`Notification`}
|
||||
</Text>
|
||||
) : (
|
||||
<Text size="sm">
|
||||
{notification.target?.name ??
|
||||
notification.name ??
|
||||
t`Notification`}
|
||||
</Text>
|
||||
)}
|
||||
<Text size="xs">{notification.age_human ?? ''}</Text>
|
||||
</Stack>
|
||||
<Space />
|
||||
<ActionIcon
|
||||
color="gray"
|
||||
variant="hover"
|
||||
onClick={() => {
|
||||
let url = apiUrl(
|
||||
ApiEndpoints.notifications_list,
|
||||
notification.pk
|
||||
);
|
||||
api
|
||||
.patch(url, {
|
||||
read: true
|
||||
})
|
||||
.then((response) => {
|
||||
notificationQuery.refetch();
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Tooltip label={t`Mark as read`}>
|
||||
<IconBellCheck />
|
||||
</Tooltip>
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
))}
|
||||
{hasNotifications &&
|
||||
notificationQuery.data?.results?.map((notification: any) => (
|
||||
<Group justify="space-between" key={notification.pk}>
|
||||
<Stack gap="3">
|
||||
{notification?.target?.link ? (
|
||||
<Text
|
||||
size="sm"
|
||||
component={Link}
|
||||
to={notification?.target?.link}
|
||||
target="_blank"
|
||||
>
|
||||
{notification.target?.name ??
|
||||
notification.name ??
|
||||
t`Notification`}
|
||||
</Text>
|
||||
) : (
|
||||
<Text size="sm">
|
||||
{notification.target?.name ??
|
||||
notification.name ??
|
||||
t`Notification`}
|
||||
</Text>
|
||||
)}
|
||||
<Text size="xs">{notification.age_human ?? ''}</Text>
|
||||
</Stack>
|
||||
<Space />
|
||||
<ActionIcon
|
||||
color="gray"
|
||||
variant="hover"
|
||||
onClick={() => {
|
||||
let url = apiUrl(
|
||||
ApiEndpoints.notifications_list,
|
||||
notification.pk
|
||||
);
|
||||
api
|
||||
.patch(url, {
|
||||
read: true
|
||||
})
|
||||
.then((response) => {
|
||||
notificationQuery.refetch();
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Tooltip label={t`Mark as read`}>
|
||||
<IconBellCheck />
|
||||
</Tooltip>
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
))}
|
||||
{notificationQuery.isFetching && (
|
||||
<Center>
|
||||
<Loader size="sm" />
|
||||
</Center>
|
||||
)}
|
||||
</Stack>
|
||||
</Drawer>
|
||||
);
|
||||
|
@ -33,23 +33,21 @@ export function PageDetail({
|
||||
actions
|
||||
}: Readonly<PageDetailInterface>) {
|
||||
return (
|
||||
<Stack spacing="xs">
|
||||
<Stack gap="xs">
|
||||
{breadcrumbs && breadcrumbs.length > 0 && (
|
||||
<Paper p="xs" radius="xs" shadow="xs">
|
||||
<BreadcrumbList
|
||||
navCallback={breadcrumbAction}
|
||||
breadcrumbs={breadcrumbs}
|
||||
/>
|
||||
</Paper>
|
||||
<BreadcrumbList
|
||||
navCallback={breadcrumbAction}
|
||||
breadcrumbs={breadcrumbs}
|
||||
/>
|
||||
)}
|
||||
<Paper p="xs" radius="xs" shadow="xs">
|
||||
<Stack spacing="xs">
|
||||
<Group position="apart" noWrap={true}>
|
||||
<Group position="left" noWrap={true}>
|
||||
<Stack gap="xs">
|
||||
<Group justify="space-between" wrap="nowrap">
|
||||
<Group justify="left" wrap="nowrap">
|
||||
{imageUrl && (
|
||||
<ApiImage src={imageUrl} radius="sm" height={64} width={64} />
|
||||
<ApiImage src={imageUrl} radius="sm" h={64} w={64} />
|
||||
)}
|
||||
<Stack spacing="xs">
|
||||
<Stack gap="xs">
|
||||
{title && <StylishText size="lg">{title}</StylishText>}
|
||||
{subtitle && (
|
||||
<Text size="md" truncate>
|
||||
@ -60,14 +58,14 @@ export function PageDetail({
|
||||
</Group>
|
||||
<Space />
|
||||
{detail}
|
||||
<Group position="right" spacing="xs" noWrap>
|
||||
<Group justify="right" gap="xs" wrap="nowrap">
|
||||
{badges?.map((badge, idx) => (
|
||||
<Fragment key={idx}>{badge}</Fragment>
|
||||
))}
|
||||
</Group>
|
||||
<Space />
|
||||
{actions && (
|
||||
<Group spacing={5} position="right">
|
||||
<Group gap={5} justify="right">
|
||||
{actions.map((action, idx) => (
|
||||
<Fragment key={idx}>{action}</Fragment>
|
||||
))}
|
||||
|
@ -72,7 +72,7 @@ function BasePanelGroup({
|
||||
}, [setLastUsedPanel]);
|
||||
|
||||
// Callback when the active panel changes
|
||||
function handlePanelChange(panel: string) {
|
||||
function handlePanelChange(panel: string | null) {
|
||||
if (activePanels.findIndex((p) => p.name === panel) === -1) {
|
||||
setLastUsedPanel('');
|
||||
return navigate('../');
|
||||
@ -81,7 +81,7 @@ function BasePanelGroup({
|
||||
navigate(`../${panel}`);
|
||||
|
||||
// Optionally call external callback hook
|
||||
if (onPanelChange) {
|
||||
if (panel && onPanelChange) {
|
||||
onPanelChange(panel);
|
||||
}
|
||||
}
|
||||
@ -109,10 +109,10 @@ function BasePanelGroup({
|
||||
<Tabs
|
||||
value={panel}
|
||||
orientation="vertical"
|
||||
onTabChange={handlePanelChange}
|
||||
onChange={handlePanelChange}
|
||||
keepMounted={false}
|
||||
>
|
||||
<Tabs.List position="left">
|
||||
<Tabs.List justify="left">
|
||||
{panels.map(
|
||||
(panel) =>
|
||||
!panel.hidden && (
|
||||
@ -125,7 +125,7 @@ function BasePanelGroup({
|
||||
p="xs"
|
||||
value={panel.name}
|
||||
// icon={(<InvenTreeIcon icon={panel.name}/>)} // Enable when implementing Icon manager everywhere
|
||||
icon={panel.icon}
|
||||
leftSection={panel.icon}
|
||||
hidden={panel.hidden}
|
||||
disabled={panel.disabled}
|
||||
style={{ cursor: panel.disabled ? 'unset' : 'pointer' }}
|
||||
@ -141,6 +141,8 @@ function BasePanelGroup({
|
||||
paddingLeft: '10px'
|
||||
}}
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
variant="transparent"
|
||||
size="md"
|
||||
>
|
||||
{expanded ? (
|
||||
<IconLayoutSidebarLeftCollapse opacity={0.5} />
|
||||
@ -162,7 +164,7 @@ function BasePanelGroup({
|
||||
width: '100%'
|
||||
}}
|
||||
>
|
||||
<Stack spacing="md">
|
||||
<Stack gap="md">
|
||||
{panel.showHeadline !== false && (
|
||||
<>
|
||||
<StylishText size="xl">{panel.label}</StylishText>
|
||||
|
@ -5,7 +5,7 @@ import {
|
||||
LoadingOverlay,
|
||||
Stack,
|
||||
Text,
|
||||
useMantineTheme
|
||||
useMantineColorScheme
|
||||
} from '@mantine/core';
|
||||
import { ReactTree, ThemeSettings } from '@naisutech/react-tree';
|
||||
import {
|
||||
@ -20,6 +20,7 @@ import { useNavigate } from 'react-router-dom';
|
||||
import { api } from '../../App';
|
||||
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
||||
import { apiUrl } from '../../states/ApiState';
|
||||
import { theme, vars } from '../../theme';
|
||||
import { StylishText } from '../items/StylishText';
|
||||
|
||||
export function PartCategoryTree({
|
||||
@ -59,9 +60,9 @@ export function PartCategoryTree({
|
||||
function renderNode({ node }: { node: any }) {
|
||||
return (
|
||||
<Group
|
||||
position="apart"
|
||||
justify="space-between"
|
||||
key={node.id}
|
||||
noWrap={true}
|
||||
wrap="nowrap"
|
||||
onClick={() => {
|
||||
onClose();
|
||||
navigate(`/part/category/${node.id}`);
|
||||
@ -80,57 +81,61 @@ export function PartCategoryTree({
|
||||
return open ? <IconChevronDown /> : <IconChevronRight />;
|
||||
}
|
||||
|
||||
const mantineTheme = useMantineTheme();
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
|
||||
const themes: ThemeSettings = useMemo(() => {
|
||||
const currentTheme =
|
||||
mantineTheme.colorScheme === 'dark'
|
||||
? mantineTheme.colorScheme
|
||||
: mantineTheme.primaryColor;
|
||||
colorScheme === 'dark'
|
||||
? vars.colors.defaultColor
|
||||
: vars.colors.primaryColors;
|
||||
|
||||
return {
|
||||
dark: {
|
||||
text: {
|
||||
...mantineTheme.fn.fontStyles()
|
||||
fontFamily: vars.fontFamily,
|
||||
//fontSize: vars.fontSizes.md,
|
||||
color: vars.colors.text
|
||||
},
|
||||
nodes: {
|
||||
height: '2.5rem',
|
||||
folder: {
|
||||
selectedBgColor: mantineTheme.colors[currentTheme][4],
|
||||
hoverBgColor: mantineTheme.colors[currentTheme][6]
|
||||
selectedBgColor: currentTheme[4],
|
||||
hoverBgColor: currentTheme[6]
|
||||
},
|
||||
leaf: {
|
||||
selectedBgColor: mantineTheme.colors[currentTheme][4],
|
||||
hoverBgColor: mantineTheme.colors[currentTheme][6]
|
||||
selectedBgColor: currentTheme[4],
|
||||
hoverBgColor: currentTheme[6]
|
||||
},
|
||||
icons: {
|
||||
folderColor: mantineTheme.colors[currentTheme][3],
|
||||
leafColor: mantineTheme.colors[currentTheme][3]
|
||||
folderColor: currentTheme[3],
|
||||
leafColor: currentTheme[3]
|
||||
}
|
||||
}
|
||||
},
|
||||
light: {
|
||||
text: {
|
||||
...mantineTheme.fn.fontStyles()
|
||||
fontFamily: vars.fontFamily,
|
||||
//fontSize: vars.fontSizes.md,
|
||||
color: vars.colors.text
|
||||
},
|
||||
nodes: {
|
||||
height: '2.5rem',
|
||||
folder: {
|
||||
selectedBgColor: mantineTheme.colors[currentTheme][4],
|
||||
hoverBgColor: mantineTheme.colors[currentTheme][2]
|
||||
selectedBgColor: currentTheme[4],
|
||||
hoverBgColor: currentTheme[2]
|
||||
},
|
||||
leaf: {
|
||||
selectedBgColor: mantineTheme.colors[currentTheme][4],
|
||||
hoverBgColor: mantineTheme.colors[currentTheme][2]
|
||||
selectedBgColor: currentTheme[4],
|
||||
hoverBgColor: currentTheme[2]
|
||||
},
|
||||
icons: {
|
||||
folderColor: mantineTheme.colors[currentTheme][8],
|
||||
leafColor: mantineTheme.colors[currentTheme][6]
|
||||
folderColor: currentTheme[8],
|
||||
leafColor: currentTheme[6]
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}, [mantineTheme]);
|
||||
}, [theme]);
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
@ -148,13 +153,13 @@ export function PartCategoryTree({
|
||||
}
|
||||
}}
|
||||
title={
|
||||
<Group position="left" p="ms" spacing="md" noWrap={true}>
|
||||
<Group justify="left" p="ms" gap="md" wrap="nowrap">
|
||||
<IconSitemap />
|
||||
<StylishText size="lg">{t`Part Categories`}</StylishText>
|
||||
</Group>
|
||||
}
|
||||
>
|
||||
<Stack spacing="xs">
|
||||
<Stack gap="xs">
|
||||
<LoadingOverlay visible={treeQuery.isFetching} />
|
||||
<ReactTree
|
||||
nodes={treeQuery.data ?? []}
|
||||
@ -162,7 +167,7 @@ export function PartCategoryTree({
|
||||
RenderIcon={renderIcon}
|
||||
defaultSelectedNodes={selectedCategory ? [selectedCategory] : []}
|
||||
showEmptyItems={false}
|
||||
theme={mantineTheme.colorScheme}
|
||||
theme={colorScheme}
|
||||
themes={themes}
|
||||
/>
|
||||
</Stack>
|
||||
|
@ -69,10 +69,10 @@ function QueryResultGroup({
|
||||
return (
|
||||
<Paper shadow="sm" radius="xs" p="md" key={`paper-${query.model}`}>
|
||||
<Stack key={`stack-${query.model}`}>
|
||||
<Group position="apart" noWrap={true}>
|
||||
<Group position="left" spacing={5} noWrap={true}>
|
||||
<Group justify="space-between" wrap="nowrap">
|
||||
<Group justify="left" gap={5} wrap="nowrap">
|
||||
<Text size="lg">{model.label_multiple}</Text>
|
||||
<Text size="sm" italic>
|
||||
<Text size="sm" style={{ fontStyle: 'italic' }}>
|
||||
{' '}
|
||||
- {query.results.count} <Trans>results</Trans>
|
||||
</Text>
|
||||
@ -332,13 +332,13 @@ export function SearchDrawer({
|
||||
withCloseButton={false}
|
||||
styles={{ header: { width: '100%' }, title: { width: '100%' } }}
|
||||
title={
|
||||
<Group position="apart" spacing={1} noWrap={true}>
|
||||
<Group justify="space-between" gap={1} wrap="nowrap">
|
||||
<TextInput
|
||||
placeholder={t`Enter search text`}
|
||||
radius="xs"
|
||||
value={value}
|
||||
onChange={(event) => setValue(event.currentTarget.value)}
|
||||
icon={<IconSearch size="0.8rem" />}
|
||||
leftSection={<IconSearch size="0.8rem" />}
|
||||
rightSection={
|
||||
value && (
|
||||
<IconBackspace color="red" onClick={() => setValue('')} />
|
||||
@ -394,7 +394,7 @@ export function SearchDrawer({
|
||||
</Center>
|
||||
)}
|
||||
{!searchQuery.isFetching && !searchQuery.isError && (
|
||||
<Stack spacing="md">
|
||||
<Stack gap="md">
|
||||
{queryResults.map((query, idx) => (
|
||||
<QueryResultGroup
|
||||
key={idx}
|
||||
|
@ -24,7 +24,7 @@ export function SettingsHeader({
|
||||
switch_link
|
||||
}: Readonly<SettingsHeaderInterface>) {
|
||||
return (
|
||||
<Stack spacing="0" ml={'sm'}>
|
||||
<Stack gap="0" ml={'sm'}>
|
||||
<Group>
|
||||
<Title order={3}>{title}</Title>
|
||||
{shorthand && <Text c="dimmed">({shorthand})</Text>}
|
||||
|
@ -51,9 +51,9 @@ export function StockLocationTree({
|
||||
function renderNode({ node }: { node: any }) {
|
||||
return (
|
||||
<Group
|
||||
position="apart"
|
||||
justify="space-between"
|
||||
key={node.id}
|
||||
noWrap={true}
|
||||
wrap="nowrap"
|
||||
onClick={() => {
|
||||
onClose();
|
||||
navigate(`/stock/location/${node.id}`);
|
||||
@ -88,13 +88,13 @@ export function StockLocationTree({
|
||||
}
|
||||
}}
|
||||
title={
|
||||
<Group position="left" noWrap={true} spacing="md" p="md">
|
||||
<Group justify="left" wrap="nowrap" gap="md" p="md">
|
||||
<IconSitemap />
|
||||
<StylishText size="lg">{t`Stock Locations`}</StylishText>
|
||||
</Group>
|
||||
}
|
||||
>
|
||||
<Stack spacing="xs">
|
||||
<Stack gap="xs">
|
||||
<LoadingOverlay visible={treeQuery.isFetching} />
|
||||
<ReactTree
|
||||
nodes={treeQuery.data ?? []}
|
||||
|
@ -112,8 +112,8 @@ export function RenderInlineModel({
|
||||
// TODO: Handle URL
|
||||
|
||||
return (
|
||||
<Group spacing="xs" position="apart" noWrap={true}>
|
||||
<Group spacing="xs" position="left" noWrap={true}>
|
||||
<Group gap="xs" justify="space-between" wrap="nowrap">
|
||||
<Group gap="xs" justify="left" wrap="nowrap">
|
||||
{image && Thumbnail({ src: image, size: 18 })}
|
||||
<Text size="sm">{primary}</Text>
|
||||
{secondary && <Text size="xs">{secondary}</Text>}
|
||||
@ -121,7 +121,7 @@ export function RenderInlineModel({
|
||||
{suffix && (
|
||||
<>
|
||||
<Space />
|
||||
<Text size="xs">{suffix}</Text>
|
||||
<div style={{ fontSize: 'xs', lineHeight: 'xs' }}>{suffix}</div>
|
||||
</>
|
||||
)}
|
||||
</Group>
|
||||
|
@ -7,7 +7,7 @@ import {
|
||||
Stack,
|
||||
Switch,
|
||||
Text,
|
||||
useMantineTheme
|
||||
useMantineColorScheme
|
||||
} from '@mantine/core';
|
||||
import { showNotification } from '@mantine/notifications';
|
||||
import { IconEdit } from '@tabler/icons-react';
|
||||
@ -19,6 +19,7 @@ import { openModalApiForm } from '../../functions/forms';
|
||||
import { apiUrl } from '../../states/ApiState';
|
||||
import { SettingsStateProps } from '../../states/SettingsState';
|
||||
import { Setting, SettingType } from '../../states/states';
|
||||
import { vars } from '../../theme';
|
||||
import { ApiFormFieldType } from '../forms/fields/ApiFormField';
|
||||
|
||||
/**
|
||||
@ -137,7 +138,7 @@ function SettingValue({
|
||||
);
|
||||
default:
|
||||
return valueText ? (
|
||||
<Group spacing="xs" position="right">
|
||||
<Group gap="xs" justify="right">
|
||||
<Space />
|
||||
<Button variant="subtle" onClick={onEditButton}>
|
||||
{valueText}
|
||||
@ -165,20 +166,18 @@ export function SettingItem({
|
||||
shaded: boolean;
|
||||
onChange?: () => void;
|
||||
}) {
|
||||
const theme = useMantineTheme();
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
|
||||
const style: Record<string, string> = { paddingLeft: '8px' };
|
||||
if (shaded) {
|
||||
style['backgroundColor'] =
|
||||
theme.colorScheme === 'light'
|
||||
? theme.colors.gray[1]
|
||||
: theme.colors.gray[9];
|
||||
colorScheme === 'light' ? vars.colors.gray[1] : vars.colors.gray[9];
|
||||
}
|
||||
|
||||
return (
|
||||
<Paper style={style}>
|
||||
<Group position="apart" p="3">
|
||||
<Stack spacing="2" p="4px">
|
||||
<Group justify="space-between" p="3">
|
||||
<Stack gap="2" p="4px">
|
||||
<Text>
|
||||
{setting.name}
|
||||
{setting.required ? ' *' : ''}
|
||||
|
@ -35,7 +35,7 @@ export function SettingList({
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack spacing="xs">
|
||||
<Stack gap="xs">
|
||||
{(keys || allKeys).map((key, i) => {
|
||||
const setting = settingsState?.settings?.find(
|
||||
(s: any) => s.key === key
|
||||
@ -51,7 +51,7 @@ export function SettingList({
|
||||
onChange={onChange}
|
||||
/>
|
||||
) : (
|
||||
<Text size="sm" italic color="red">
|
||||
<Text size="sm" style={{ fontStyle: 'italic' }} color="red">
|
||||
Setting {key} not found
|
||||
</Text>
|
||||
)}
|
||||
@ -59,7 +59,7 @@ export function SettingList({
|
||||
);
|
||||
})}
|
||||
{(keys || allKeys).length === 0 && (
|
||||
<Text italic>
|
||||
<Text style={{ fontStyle: 'italic' }}>
|
||||
<Trans>No settings specified</Trans>
|
||||
</Text>
|
||||
)}
|
||||
|
@ -1,17 +1,18 @@
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { Button, Stack, Title } from '@mantine/core';
|
||||
import { Button, Stack, Title, useMantineColorScheme } from '@mantine/core';
|
||||
import { IconExternalLink } from '@tabler/icons-react';
|
||||
|
||||
import { vars } from '../../theme';
|
||||
|
||||
export default function FeedbackWidget() {
|
||||
const { colorScheme } = useMantineColorScheme();
|
||||
return (
|
||||
<Stack
|
||||
sx={(theme) => ({
|
||||
style={{
|
||||
backgroundColor:
|
||||
theme.colorScheme === 'dark'
|
||||
? theme.colors.gray[9]
|
||||
: theme.colors.gray[1],
|
||||
borderRadius: theme.radius.md
|
||||
})}
|
||||
colorScheme === 'dark' ? vars.colors.gray[9] : vars.colors.gray[1],
|
||||
borderRadius: vars.radius.md
|
||||
}}
|
||||
p={15}
|
||||
>
|
||||
<Title order={5}>
|
||||
@ -26,7 +27,7 @@ export default function FeedbackWidget() {
|
||||
component="a"
|
||||
href="https://github.com/inventree/InvenTree/discussions/5328"
|
||||
variant="outline"
|
||||
leftIcon={<IconExternalLink size="0.9rem" />}
|
||||
leftSection={<IconExternalLink size="0.9rem" />}
|
||||
>
|
||||
<Trans>Provide Feedback</Trans>
|
||||
</Button>
|
||||
|
16
src/frontend/src/components/widgets/WidgetLayout.css.ts
Normal file
16
src/frontend/src/components/widgets/WidgetLayout.css.ts
Normal 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'
|
||||
});
|
@ -5,8 +5,7 @@ import {
|
||||
Group,
|
||||
Indicator,
|
||||
Menu,
|
||||
Text,
|
||||
createStyles
|
||||
Text
|
||||
} from '@mantine/core';
|
||||
import { useDisclosure, useHotkeys } from '@mantine/hooks';
|
||||
import {
|
||||
@ -19,6 +18,8 @@ import {
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Responsive, WidthProvider } from 'react-grid-layout';
|
||||
|
||||
import * as classes from './WidgetLayout.css';
|
||||
|
||||
const ReactGridLayout = WidthProvider(Responsive);
|
||||
|
||||
interface LayoutStorage {
|
||||
@ -27,21 +28,6 @@ interface LayoutStorage {
|
||||
|
||||
const compactType = 'vertical';
|
||||
|
||||
const useItemStyle = createStyles((theme) => ({
|
||||
backgroundItem: {
|
||||
backgroundColor:
|
||||
theme.colorScheme === 'dark' ? theme.colors.dark[5] : theme.white,
|
||||
maxWidth: '100%',
|
||||
padding: '8px',
|
||||
boxShadow: theme.shadows.md
|
||||
},
|
||||
|
||||
baseItem: {
|
||||
maxWidth: '100%',
|
||||
padding: '8px'
|
||||
}
|
||||
}));
|
||||
|
||||
export interface LayoutItemType {
|
||||
i: number;
|
||||
val: string | JSX.Element | JSX.Element[] | (() => JSX.Element);
|
||||
@ -66,7 +52,6 @@ export function WidgetLayout({
|
||||
const [layouts, setLayouts] = useState({});
|
||||
const [editable, setEditable] = useDisclosure(false);
|
||||
const [boxShown, setBoxShown] = useDisclosure(true);
|
||||
const { classes } = useItemStyle();
|
||||
|
||||
useEffect(() => {
|
||||
let layout = getFromLS('layouts') || [];
|
||||
@ -155,7 +140,7 @@ function WidgetControlBar({
|
||||
useHotkeys([['mod+E', () => editFnc()]]);
|
||||
|
||||
return (
|
||||
<Group position="right">
|
||||
<Group justify="right">
|
||||
<Menu
|
||||
shadow="md"
|
||||
width={200}
|
||||
@ -181,13 +166,13 @@ function WidgetControlBar({
|
||||
<Trans>Layout</Trans>
|
||||
</Menu.Label>
|
||||
<Menu.Item
|
||||
icon={<IconArrowBackUpDouble size={14} />}
|
||||
leftSection={<IconArrowBackUpDouble size={14} />}
|
||||
onClick={resetLayout}
|
||||
>
|
||||
<Trans>Reset Layout</Trans>
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
icon={
|
||||
leftSection={
|
||||
<IconLayout2 size={14} color={editable ? 'red' : undefined} />
|
||||
}
|
||||
onClick={editFnc}
|
||||
@ -206,7 +191,7 @@ function WidgetControlBar({
|
||||
<Trans>Appearance</Trans>
|
||||
</Menu.Label>
|
||||
<Menu.Item
|
||||
icon={
|
||||
leftSection={
|
||||
boxShown ? (
|
||||
<IconSquareCheck size={14} />
|
||||
) : (
|
||||
|
@ -1,15 +1,12 @@
|
||||
import { QueryClientProvider } from '@tanstack/react-query';
|
||||
|
||||
import { queryClient } from '../App';
|
||||
import { LanguageContext } from './LanguageContext';
|
||||
import { ThemeContext } from './ThemeContext';
|
||||
|
||||
export const BaseContext = ({ children }: { children: any }) => {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<LanguageContext>
|
||||
<ThemeContext>{children}</ThemeContext>
|
||||
</LanguageContext>
|
||||
<ThemeContext>{children}</ThemeContext>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
};
|
||||
|
@ -1,11 +1,5 @@
|
||||
import { t } from '@lingui/macro';
|
||||
import {
|
||||
ColorScheme,
|
||||
ColorSchemeProvider,
|
||||
MantineProvider,
|
||||
MantineThemeOverride
|
||||
} from '@mantine/core';
|
||||
import { useColorScheme, useLocalStorage } from '@mantine/hooks';
|
||||
import { MantineProvider, createTheme } from '@mantine/core';
|
||||
import { ModalsProvider } from '@mantine/modals';
|
||||
import { Notifications } from '@mantine/notifications';
|
||||
|
||||
@ -14,36 +8,24 @@ import { LicenseModal } from '../components/modals/LicenseModal';
|
||||
import { QrCodeModal } from '../components/modals/QrCodeModal';
|
||||
import { ServerInfoModal } from '../components/modals/ServerInfoModal';
|
||||
import { useLocalState } from '../states/LocalState';
|
||||
import { LanguageContext } from './LanguageContext';
|
||||
import { colorSchema } from './colorSchema';
|
||||
|
||||
export function ThemeContext({ children }: { children: JSX.Element }) {
|
||||
const [primaryColor, whiteColor, blackColor, radius, loader] = useLocalState(
|
||||
const [primaryColor, whiteColor, blackColor, radius] = useLocalState(
|
||||
(state) => [
|
||||
state.primaryColor,
|
||||
state.whiteColor,
|
||||
state.blackColor,
|
||||
state.radius,
|
||||
state.loader
|
||||
state.radius
|
||||
]
|
||||
);
|
||||
|
||||
// Color Scheme
|
||||
const preferredColorScheme = useColorScheme();
|
||||
const [colorScheme, setColorScheme] = useLocalStorage<ColorScheme>({
|
||||
key: 'scheme',
|
||||
defaultValue: preferredColorScheme
|
||||
});
|
||||
const toggleColorScheme = (value?: ColorScheme) => {
|
||||
setColorScheme(value || (colorScheme === 'dark' ? 'light' : 'dark'));
|
||||
myTheme.colorScheme = colorScheme;
|
||||
};
|
||||
|
||||
// Theme
|
||||
const myTheme: MantineThemeOverride = {
|
||||
colorScheme: colorScheme,
|
||||
const myTheme = createTheme({
|
||||
primaryColor: primaryColor,
|
||||
white: whiteColor,
|
||||
black: blackColor,
|
||||
loader: loader,
|
||||
defaultRadius: radius,
|
||||
breakpoints: {
|
||||
xs: '30em',
|
||||
@ -52,15 +34,11 @@ export function ThemeContext({ children }: { children: JSX.Element }) {
|
||||
lg: '74em',
|
||||
xl: '90em'
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<ColorSchemeProvider
|
||||
colorScheme={colorScheme}
|
||||
toggleColorScheme={toggleColorScheme}
|
||||
>
|
||||
<MantineProvider theme={myTheme} withGlobalStyles withNormalizeCSS>
|
||||
<Notifications />
|
||||
<MantineProvider theme={myTheme} colorSchemeManager={colorSchema}>
|
||||
<LanguageContext>
|
||||
<ModalsProvider
|
||||
labels={{ confirm: t`Submit`, cancel: t`Cancel` }}
|
||||
modals={{
|
||||
@ -70,9 +48,10 @@ export function ThemeContext({ children }: { children: JSX.Element }) {
|
||||
license: LicenseModal
|
||||
}}
|
||||
>
|
||||
<Notifications />
|
||||
{children}
|
||||
</ModalsProvider>
|
||||
</MantineProvider>
|
||||
</ColorSchemeProvider>
|
||||
</LanguageContext>
|
||||
</MantineProvider>
|
||||
);
|
||||
}
|
||||
|
67
src/frontend/src/contexts/colorSchema.tsx
Normal file
67
src/frontend/src/contexts/colorSchema.tsx
Normal 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'
|
||||
});
|
@ -1,5 +1,5 @@
|
||||
import { t } from '@lingui/macro';
|
||||
import type { SpotlightAction } from '@mantine/spotlight';
|
||||
import type { SpotlightActionData } from '@mantine/spotlight';
|
||||
import { IconHome, IconLink, IconPointer } from '@tabler/icons-react';
|
||||
import { NavigateFunction } from 'react-router-dom';
|
||||
|
||||
@ -10,48 +10,55 @@ import { menuItems } from './menuItems';
|
||||
export function getActions(navigate: NavigateFunction) {
|
||||
const setNavigationOpen = useLocalState((state) => state.setNavigationOpen);
|
||||
|
||||
const actions: SpotlightAction[] = [
|
||||
const actions: SpotlightActionData[] = [
|
||||
{
|
||||
title: t`Home`,
|
||||
id: 'home',
|
||||
label: t`Home`,
|
||||
description: `Go to the home page`,
|
||||
onTrigger: () => navigate(menuItems.home.link),
|
||||
icon: <IconHome size="1.2rem" />
|
||||
onClick: () => navigate(menuItems.home.link),
|
||||
leftSection: <IconHome size="1.2rem" />
|
||||
},
|
||||
{
|
||||
title: t`Dashboard`,
|
||||
id: 'dashboard',
|
||||
label: t`Dashboard`,
|
||||
description: t`Go to the InvenTree dashboard`,
|
||||
onTrigger: () => navigate(menuItems.dashboard.link),
|
||||
icon: <IconLink size="1.2rem" />
|
||||
onClick: () => navigate(menuItems.dashboard.link),
|
||||
leftSection: <IconLink size="1.2rem" />
|
||||
},
|
||||
{
|
||||
title: t`Documentation`,
|
||||
id: 'documentation',
|
||||
label: t`Documentation`,
|
||||
description: t`Visit the documentation to learn more about InvenTree`,
|
||||
onTrigger: () => (window.location.href = docLinks.faq),
|
||||
icon: <IconLink size="1.2rem" />
|
||||
onClick: () => (window.location.href = docLinks.faq),
|
||||
leftSection: <IconLink size="1.2rem" />
|
||||
},
|
||||
{
|
||||
title: t`About InvenTree`,
|
||||
id: 'about',
|
||||
label: t`About InvenTree`,
|
||||
description: t`About the InvenTree org`,
|
||||
onTrigger: () => aboutInvenTree(),
|
||||
icon: <IconLink size="1.2rem" />
|
||||
onClick: () => aboutInvenTree(),
|
||||
leftSection: <IconLink size="1.2rem" />
|
||||
},
|
||||
{
|
||||
title: t`Server Information`,
|
||||
id: 'server-info',
|
||||
label: t`Server Information`,
|
||||
description: t`About this Inventree instance`,
|
||||
onTrigger: () => serverInfo(),
|
||||
icon: <IconLink size="1.2rem" />
|
||||
onClick: () => serverInfo(),
|
||||
leftSection: <IconLink size="1.2rem" />
|
||||
},
|
||||
{
|
||||
title: t`License Information`,
|
||||
id: 'license-info',
|
||||
label: t`License Information`,
|
||||
description: t`Licenses for dependencies of the service`,
|
||||
onTrigger: () => licenseInfo(),
|
||||
icon: <IconLink size="1.2rem" />
|
||||
onClick: () => licenseInfo(),
|
||||
leftSection: <IconLink size="1.2rem" />
|
||||
},
|
||||
{
|
||||
title: t`Open Navigation`,
|
||||
id: 'navigation',
|
||||
label: t`Open Navigation`,
|
||||
description: t`Open the main navigation menu`,
|
||||
onTrigger: () => setNavigationOpen(true),
|
||||
icon: <IconPointer size="1.2rem" />
|
||||
onClick: () => setNavigationOpen(true),
|
||||
leftSection: <IconPointer size="1.2rem" />
|
||||
}
|
||||
];
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { MantineSize } from '@mantine/core';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import {
|
||||
@ -154,3 +155,5 @@ export function renderDate(
|
||||
return date;
|
||||
}
|
||||
}
|
||||
|
||||
export type UiSizeType = MantineSize | string | number;
|
||||
|
@ -392,7 +392,7 @@ export function useCancelBuildOutputsForm({
|
||||
|
||||
const preFormContent = useMemo(() => {
|
||||
return (
|
||||
<Stack spacing="xs">
|
||||
<Stack gap="xs">
|
||||
<Alert color="red" title={t`Cancel Build Outputs`}>
|
||||
<Text>{t`Selected build outputs will be deleted`}</Text>
|
||||
</Alert>
|
||||
|
@ -311,7 +311,7 @@ function StockOperationsRow({
|
||||
<td>{record.location ? record.location_detail?.pathstring : '-'}</td>
|
||||
<td>
|
||||
<Flex align="center" gap="xs">
|
||||
<Group position="apart">
|
||||
<Group justify="space-between">
|
||||
<Text>{stockString}</Text>
|
||||
<StatusRenderer status={record.status} type={ModelType.stockitem} />
|
||||
</Group>
|
||||
|
@ -244,7 +244,7 @@ export function openModalApiForm(props: OpenApiFormProps) {
|
||||
props.onClose ? props.onClose() : null;
|
||||
},
|
||||
children: (
|
||||
<Stack spacing={'xs'}>
|
||||
<Stack gap={'xs'}>
|
||||
<Divider />
|
||||
<ApiForm id={modalId} props={props} optionsLoading={false} />
|
||||
</Stack>
|
||||
|
@ -1,13 +1,18 @@
|
||||
import { Center, Loader, Stack } from '@mantine/core';
|
||||
import { Center, Loader, MantineProvider, Stack } from '@mantine/core';
|
||||
import { Suspense } from 'react';
|
||||
|
||||
import { colorSchema } from '../contexts/colorSchema';
|
||||
import { theme } from '../theme';
|
||||
|
||||
function LoadingFallback() {
|
||||
return (
|
||||
<Stack>
|
||||
<Center>
|
||||
<Loader />
|
||||
</Center>
|
||||
</Stack>
|
||||
<MantineProvider theme={theme} colorSchemeManager={colorSchema}>
|
||||
<Stack>
|
||||
<Center>
|
||||
<Loader />
|
||||
</Center>
|
||||
</Stack>
|
||||
</MantineProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}));
|
@ -1,9 +1,10 @@
|
||||
import { t } from '@lingui/macro';
|
||||
import { Alert, Divider, MantineNumberSize, Stack } from '@mantine/core';
|
||||
import { Alert, Divider, Stack } from '@mantine/core';
|
||||
import { useId } from '@mantine/hooks';
|
||||
import { useEffect, useMemo, useRef } from 'react';
|
||||
|
||||
import { ApiFormProps, OptionsApiForm } from '../components/forms/ApiForm';
|
||||
import { UiSizeType } from '../defaults/formatters';
|
||||
import { useModal } from './UseModal';
|
||||
|
||||
/**
|
||||
@ -20,7 +21,7 @@ export interface ApiFormModalProps extends ApiFormProps {
|
||||
onClose?: () => void;
|
||||
onOpen?: () => void;
|
||||
closeOnClickOutside?: boolean;
|
||||
size?: MantineNumberSize;
|
||||
size?: UiSizeType;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -62,7 +63,7 @@ export function useApiFormModal(props: ApiFormModalProps) {
|
||||
closeOnClickOutside: formProps.closeOnClickOutside,
|
||||
size: props.size ?? 'xl',
|
||||
children: (
|
||||
<Stack spacing={'xs'}>
|
||||
<Stack gap={'xs'}>
|
||||
<Divider />
|
||||
<OptionsApiForm props={formProps} id={id} />
|
||||
</Stack>
|
||||
|
@ -1,13 +1,14 @@
|
||||
import { MantineNumberSize, Modal } from '@mantine/core';
|
||||
import { Modal } from '@mantine/core';
|
||||
import { useDisclosure } from '@mantine/hooks';
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import { StylishText } from '../components/items/StylishText';
|
||||
import { UiSizeType } from '../defaults/formatters';
|
||||
|
||||
export interface UseModalProps {
|
||||
title: string;
|
||||
children: React.ReactElement;
|
||||
size?: MantineNumberSize;
|
||||
size?: UiSizeType;
|
||||
onOpen?: () => void;
|
||||
onClose?: () => void;
|
||||
closeOnClickOutside?: boolean;
|
||||
|
180
src/frontend/src/main.css.ts
Normal file
180
src/frontend/src/main.css.ts
Normal 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
|
||||
});
|
@ -1,4 +1,9 @@
|
||||
import '@mantine/carousel/styles.css';
|
||||
import '@mantine/core/styles.css';
|
||||
import '@mantine/notifications/styles.css';
|
||||
import '@mantine/spotlight/styles.css';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import 'mantine-datatable/styles.css';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import 'react-grid-layout/css/styles.css';
|
||||
|
@ -22,7 +22,7 @@ export default function Logged_In() {
|
||||
<Text size="lg">
|
||||
<Trans>Checking if you are already logged in</Trans>
|
||||
</Text>
|
||||
<Group position="center">
|
||||
<Group justify="center">
|
||||
<Loader />
|
||||
</Group>
|
||||
</Stack>
|
||||
|
@ -36,7 +36,8 @@ export default function Login() {
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
// Data manipulation functions
|
||||
function ChangeHost(newHost: string): void {
|
||||
function ChangeHost(newHost: string | null): void {
|
||||
if (newHost === null) return;
|
||||
setHost(hostList[newHost]?.host, newHost);
|
||||
setApiDefaults();
|
||||
fetchServerApiState();
|
||||
@ -81,7 +82,7 @@ export default function Login() {
|
||||
) : (
|
||||
<>
|
||||
<Paper radius="md" p="xl" withBorder>
|
||||
<Text size="lg" weight={500}>
|
||||
<Text size="lg" fw={500}>
|
||||
{loginMode ? (
|
||||
<Trans>Welcome, log in below</Trans>
|
||||
) : (
|
||||
|
@ -22,7 +22,7 @@ export default function Logout() {
|
||||
<Text size="lg">
|
||||
<Trans>Logging out</Trans>
|
||||
</Text>
|
||||
<Group position="center">
|
||||
<Group justify="center">
|
||||
<Loader />
|
||||
</Group>
|
||||
</Stack>
|
||||
|
@ -8,13 +8,14 @@ import {
|
||||
Text,
|
||||
TextInput
|
||||
} from '@mantine/core';
|
||||
import { spotlight } from '@mantine/spotlight';
|
||||
import { SpotlightActionData } from '@mantine/spotlight';
|
||||
import { IconAlien } from '@tabler/icons-react';
|
||||
import { ReactNode, useMemo, useState } from 'react';
|
||||
|
||||
import { OptionsApiForm } from '../../components/forms/ApiForm';
|
||||
import { PlaceholderPill } from '../../components/items/Placeholder';
|
||||
import { StylishText } from '../../components/items/StylishText';
|
||||
import { firstSpotlight } from '../../components/nav/Layout';
|
||||
import { StatusRenderer } from '../../components/render/StatusRenderer';
|
||||
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
||||
import { ModelType } from '../../enums/ModelType';
|
||||
@ -135,7 +136,7 @@ function ApiFormsPlayground() {
|
||||
<Button onClick={() => openCreatePart()}>Create Part new Modal</Button>
|
||||
{createPartModal}
|
||||
</Group>
|
||||
<Card sx={{ padding: '30px' }}>
|
||||
<Card style={{ padding: '30px' }}>
|
||||
<OptionsApiForm
|
||||
props={{
|
||||
url: ApiEndpoints.part_list,
|
||||
@ -181,25 +182,28 @@ function SpotlighPlayground() {
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
spotlight.registerActions([
|
||||
const setAdditionalActions = (value: SpotlightActionData[]) => {
|
||||
console.log('would add', value);
|
||||
};
|
||||
setAdditionalActions([
|
||||
{
|
||||
id: 'secret-action-1',
|
||||
title: 'Secret action',
|
||||
description: 'It was registered with a button click',
|
||||
icon: <IconAlien size="1.2rem" />,
|
||||
onTrigger: () => console.log('Secret')
|
||||
leftSection: <IconAlien size="1.2rem" />,
|
||||
onClick: () => console.log('Secret')
|
||||
},
|
||||
{
|
||||
id: 'secret-action-2',
|
||||
title: 'Another secret action',
|
||||
description:
|
||||
'You can register multiple actions with just one command',
|
||||
icon: <IconAlien size="1.2rem" />,
|
||||
onTrigger: () => console.log('Secret')
|
||||
leftSection: <IconAlien size="1.2rem" />,
|
||||
onClick: () => console.log('Secret')
|
||||
}
|
||||
]);
|
||||
console.log('registed');
|
||||
spotlight.open();
|
||||
firstSpotlight.open();
|
||||
}}
|
||||
>
|
||||
Register extra actions
|
||||
|
@ -4,7 +4,6 @@ import {
|
||||
Badge,
|
||||
Button,
|
||||
Checkbox,
|
||||
Col,
|
||||
Container,
|
||||
Grid,
|
||||
Group,
|
||||
@ -42,7 +41,7 @@ import {
|
||||
} from '@tabler/icons-react';
|
||||
import { Html5Qrcode } from 'html5-qrcode';
|
||||
import { CameraDevice } from 'html5-qrcode/camera/core';
|
||||
import { ReactNode, useEffect, useState } from 'react';
|
||||
import { ReactNode, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { api } from '../../App';
|
||||
import { DocInfo } from '../../components/items/DocInfo';
|
||||
@ -243,14 +242,14 @@ export default function Scan() {
|
||||
|
||||
if (uniqueObjectTypes.length === 0) {
|
||||
return (
|
||||
<Group spacing={0}>
|
||||
<Group gap={0}>
|
||||
<IconQuestionMark color="orange" />
|
||||
<Trans>Selected elements are not known</Trans>
|
||||
</Group>
|
||||
);
|
||||
} else if (uniqueObjectTypes.length > 1) {
|
||||
return (
|
||||
<Group spacing={0}>
|
||||
<Group gap={0}>
|
||||
<IconAlertCircle color="orange" />
|
||||
<Trans>Multiple object types selected</Trans>
|
||||
</Group>
|
||||
@ -262,7 +261,11 @@ export default function Scan() {
|
||||
<Trans>Actions for {uniqueObjectTypes[0]} </Trans>
|
||||
</Text>
|
||||
<Group>
|
||||
<ActionIcon onClick={notYetImplemented} title={t`Count`}>
|
||||
<ActionIcon
|
||||
onClick={notYetImplemented}
|
||||
title={t`Count`}
|
||||
variant="default"
|
||||
>
|
||||
<IconNumber />
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
@ -273,8 +276,8 @@ export default function Scan() {
|
||||
// rendering
|
||||
return (
|
||||
<>
|
||||
<Group position="apart">
|
||||
<Group position="left">
|
||||
<Group justify="space-between">
|
||||
<Group justify="left">
|
||||
<StylishText>
|
||||
<Trans>Scan Page</Trans>
|
||||
</StylishText>
|
||||
@ -293,10 +296,10 @@ export default function Scan() {
|
||||
</Group>
|
||||
<Space h={'md'} />
|
||||
<Grid maw={'100%'}>
|
||||
<Col span={4}>
|
||||
<Grid.Col span={4}>
|
||||
<Stack>
|
||||
<Stack spacing="xs">
|
||||
<Group position="apart">
|
||||
<Stack gap="xs">
|
||||
<Group justify="space-between">
|
||||
<TitleWithDoc
|
||||
order={3}
|
||||
text={t`Select the input method you want to use to scan items.`}
|
||||
@ -309,12 +312,12 @@ export default function Scan() {
|
||||
data={inputOptions}
|
||||
searchable
|
||||
placeholder={t`Select input method`}
|
||||
nothingFound={t`Nothing found`}
|
||||
nothingFoundMessage={t`Nothing found`}
|
||||
/>
|
||||
</Group>
|
||||
{inp}
|
||||
</Stack>
|
||||
<Stack spacing={0}>
|
||||
<Stack gap={0}>
|
||||
<TitleWithDoc
|
||||
order={3}
|
||||
text={t`Depending on the selected parts actions will be shown here. Not all barcode types are supported currently.`}
|
||||
@ -338,6 +341,7 @@ export default function Scan() {
|
||||
color="red"
|
||||
onClick={btnDeleteHistory}
|
||||
title={t`Delete`}
|
||||
variant="default"
|
||||
>
|
||||
<IconTrash />
|
||||
</ActionIcon>
|
||||
@ -345,6 +349,7 @@ export default function Scan() {
|
||||
onClick={btnRunSelectedBarcode}
|
||||
disabled={selection.length > 1}
|
||||
title={t`Lookup part`}
|
||||
variant="default"
|
||||
>
|
||||
<IconSearch />
|
||||
</ActionIcon>
|
||||
@ -352,6 +357,7 @@ export default function Scan() {
|
||||
onClick={btnOpenSelectedLink}
|
||||
disabled={!selectionLinked}
|
||||
title={t`Open Link`}
|
||||
variant="default"
|
||||
>
|
||||
<IconLink />
|
||||
</ActionIcon>
|
||||
@ -361,9 +367,9 @@ export default function Scan() {
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Group position="apart">
|
||||
</Grid.Col>
|
||||
<Grid.Col span={8}>
|
||||
<Group justify="space-between">
|
||||
<TitleWithDoc
|
||||
order={3}
|
||||
text={t`History is locally kept in this browser.`}
|
||||
@ -374,6 +380,7 @@ export default function Scan() {
|
||||
<ActionIcon
|
||||
color="red"
|
||||
onClick={btnDeleteFullHistory}
|
||||
variant="default"
|
||||
title={t`Delete History`}
|
||||
>
|
||||
<IconTrash />
|
||||
@ -384,7 +391,7 @@ export default function Scan() {
|
||||
selection={selection}
|
||||
setSelection={setSelection}
|
||||
/>
|
||||
</Col>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
</>
|
||||
);
|
||||
@ -409,34 +416,30 @@ function HistoryTable({
|
||||
setSelection((current) =>
|
||||
current.length === data.length ? [] : data.map((item) => item.id)
|
||||
);
|
||||
const [rows, setRows] = useState<ReactNode>();
|
||||
|
||||
useEffect(() => {
|
||||
setRows(
|
||||
data.map((item) => {
|
||||
return (
|
||||
<tr key={item.id}>
|
||||
<td>
|
||||
<Checkbox
|
||||
checked={selection.includes(item.id)}
|
||||
onChange={() => toggleRow(item.id)}
|
||||
transitionDuration={0}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
{item.pk && item.model && item.instance ? (
|
||||
<RenderInstance model={item.model} instance={item.instance} />
|
||||
) : (
|
||||
item.ref
|
||||
)}
|
||||
</td>
|
||||
<td>{item.model}</td>
|
||||
<td>{item.source}</td>
|
||||
<td>{item.timestamp?.toString()}</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
);
|
||||
const rows = useMemo(() => {
|
||||
return data.map((item) => {
|
||||
return (
|
||||
<tr key={item.id}>
|
||||
<td>
|
||||
<Checkbox
|
||||
checked={selection.includes(item.id)}
|
||||
onChange={() => toggleRow(item.id)}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
{item.pk && item.model && item.instance ? (
|
||||
<RenderInstance model={item.model} instance={item.instance} />
|
||||
) : (
|
||||
item.ref
|
||||
)}
|
||||
</td>
|
||||
<td>{item.model}</td>
|
||||
<td>{item.source}</td>
|
||||
<td>{item.timestamp?.toString()}</td>
|
||||
</tr>
|
||||
);
|
||||
});
|
||||
}, [data, selection]);
|
||||
|
||||
// rendering
|
||||
@ -458,7 +461,6 @@ function HistoryTable({
|
||||
indeterminate={
|
||||
selection.length > 0 && selection.length !== data.length
|
||||
}
|
||||
transitionDuration={0}
|
||||
/>
|
||||
</th>
|
||||
<th>
|
||||
@ -528,7 +530,7 @@ function InputManual({ action }: Readonly<ScanInputInterface>) {
|
||||
onChange={(event) => setValue(event.currentTarget.value)}
|
||||
onKeyDown={getHotkeyHandler([['Enter', btnAddItem]])}
|
||||
/>
|
||||
<ActionIcon onClick={btnAddItem} w={16}>
|
||||
<ActionIcon onClick={btnAddItem} w={16} variant="default">
|
||||
<IconPlus />
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
@ -712,8 +714,8 @@ function InputImageBarcode({ action }: Readonly<ScanInputInterface>) {
|
||||
}, [cameraValue]);
|
||||
|
||||
return (
|
||||
<Stack spacing="xs">
|
||||
<Group spacing="xs">
|
||||
<Stack gap="xs">
|
||||
<Group gap="xs">
|
||||
<Select
|
||||
value={cameraValue}
|
||||
onChange={setCameraValue}
|
||||
@ -723,7 +725,11 @@ function InputImageBarcode({ action }: Readonly<ScanInputInterface>) {
|
||||
size="sm"
|
||||
/>
|
||||
{ScanningEnabled ? (
|
||||
<ActionIcon onClick={btnStopScanning} title={t`Stop scanning`}>
|
||||
<ActionIcon
|
||||
onClick={btnStopScanning}
|
||||
title={t`Stop scanning`}
|
||||
variant="default"
|
||||
>
|
||||
<IconPlayerStopFilled />
|
||||
</ActionIcon>
|
||||
) : (
|
||||
@ -731,11 +737,12 @@ function InputImageBarcode({ action }: Readonly<ScanInputInterface>) {
|
||||
onClick={btnStartScanning}
|
||||
title={t`Start scanning`}
|
||||
disabled={!camId}
|
||||
variant="default"
|
||||
>
|
||||
<IconPlayerPlayFilled />
|
||||
</ActionIcon>
|
||||
)}
|
||||
<Space sx={{ flex: 1 }} />
|
||||
<Space style={{ flex: 1 }} />
|
||||
<Badge color={ScanningEnabled ? 'green' : 'orange'}>
|
||||
{ScanningEnabled ? t`Scanning` : t`Not scanning`}
|
||||
</Badge>
|
||||
|
@ -40,7 +40,7 @@ export function AccountDetailPanel() {
|
||||
</Group>
|
||||
<Group>
|
||||
{editing ? (
|
||||
<Stack spacing="xs">
|
||||
<Stack gap="xs">
|
||||
<TextInput
|
||||
label="first name"
|
||||
placeholder={t`First name`}
|
||||
@ -51,14 +51,14 @@ export function AccountDetailPanel() {
|
||||
placeholder={t`Last name`}
|
||||
{...form.getInputProps('last_name')}
|
||||
/>
|
||||
<Group position="right" mt="md">
|
||||
<Group justify="right" mt="md">
|
||||
<Button type="submit">
|
||||
<Trans>Submit</Trans>
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
) : (
|
||||
<Stack spacing="0">
|
||||
<Stack gap="0">
|
||||
<Text>
|
||||
<Trans>First name: </Trans>
|
||||
{form.values.first_name}
|
||||
|
@ -137,7 +137,7 @@ function EmailContent({}: {}) {
|
||||
key={link.id}
|
||||
value={String(link.id)}
|
||||
label={
|
||||
<Group position="apart">
|
||||
<Group justify="space-between">
|
||||
{link.email}
|
||||
{link.primary && (
|
||||
<Badge color="blue">
|
||||
@ -168,7 +168,7 @@ function EmailContent({}: {}) {
|
||||
<TextInput
|
||||
label={t`E-Mail`}
|
||||
placeholder={t`E-Mail address`}
|
||||
icon={<IconAt />}
|
||||
leftSection={<IconAt />}
|
||||
value={newEmailValue}
|
||||
onChange={(event) => setNewEmailValue(event.currentTarget.value)}
|
||||
/>
|
||||
@ -251,7 +251,7 @@ function SsoContent({ dataProvider }: { dataProvider: any | undefined }) {
|
||||
variant="outline"
|
||||
disabled={!provider.configured}
|
||||
>
|
||||
<Group position="apart">
|
||||
<Group justify="space-between">
|
||||
{provider.display_name}
|
||||
{provider.configured == false && <IconAlertCircle />}
|
||||
</Group>
|
||||
@ -308,7 +308,7 @@ function SsoContent({ dataProvider }: { dataProvider: any | undefined }) {
|
||||
{currentProviders === undefined ? (
|
||||
<Trans>Loading</Trans>
|
||||
) : (
|
||||
<Stack spacing="xs">
|
||||
<Stack gap="xs">
|
||||
{currentProviders.map((provider: any) => (
|
||||
<ProviderButton key={provider.id} provider={provider} />
|
||||
))}
|
||||
|
@ -11,12 +11,11 @@ import {
|
||||
Table,
|
||||
Title
|
||||
} from '@mantine/core';
|
||||
import { LoaderType } from '@mantine/styles/lib/theme/types/MantineTheme';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { SizeMarks } from '../../../../defaults/defaults';
|
||||
import { InvenTreeStyle } from '../../../../globalStyle';
|
||||
import { useLocalState } from '../../../../states/LocalState';
|
||||
import { theme } from '../../../../theme';
|
||||
|
||||
function getLkp(color: string) {
|
||||
return { [DEFAULT_THEME.colors[color][6]]: color };
|
||||
@ -27,8 +26,6 @@ const LOOKUP = Object.assign(
|
||||
);
|
||||
|
||||
export function UserTheme({ height }: { height: number }) {
|
||||
const { theme } = InvenTreeStyle();
|
||||
|
||||
// primary color
|
||||
function changePrimary(color: string) {
|
||||
useLocalState.setState({ primaryColor: LOOKUP[color] });
|
||||
@ -69,10 +66,13 @@ export function UserTheme({ height }: { height: number }) {
|
||||
{ value: 'oval', label: t`oval` },
|
||||
{ value: 'dots', label: t`dots` }
|
||||
];
|
||||
const [loader, setLoader] = useState<LoaderType>(theme.loader);
|
||||
function changeLoader(value: LoaderType) {
|
||||
setLoader(value);
|
||||
useLocalState.setState({ loader: value });
|
||||
const [themeLoader, setThemeLoader] = useLocalState((state) => [
|
||||
state.loader,
|
||||
state.setLoader
|
||||
]);
|
||||
function changeLoader(value: string | null) {
|
||||
if (value === null) return;
|
||||
setThemeLoader(value);
|
||||
}
|
||||
|
||||
return (
|
||||
@ -135,10 +135,10 @@ export function UserTheme({ height }: { height: number }) {
|
||||
<Group align="center">
|
||||
<Select
|
||||
data={loaderDate}
|
||||
value={loader}
|
||||
value={themeLoader}
|
||||
onChange={changeLoader}
|
||||
/>
|
||||
<Loader type={loader} mah={18} />
|
||||
<Loader type={themeLoader} mah={18} />
|
||||
</Group>
|
||||
</td>
|
||||
</tr>
|
||||
|
@ -97,7 +97,7 @@ export default function AdminCenter() {
|
||||
label: t`Project Codes`,
|
||||
icon: <IconListDetails />,
|
||||
content: (
|
||||
<Stack spacing="xs">
|
||||
<Stack gap="xs">
|
||||
<GlobalSettingList keys={['PROJECT_CODES_ENABLED']} />
|
||||
<Divider />
|
||||
<ProjectCodeTable />
|
||||
@ -144,7 +144,7 @@ export default function AdminCenter() {
|
||||
}, []);
|
||||
|
||||
const QuickAction = () => (
|
||||
<Stack spacing={'xs'} ml={'sm'}>
|
||||
<Stack gap={'xs'} ml={'sm'}>
|
||||
<Title order={5}>
|
||||
<Trans>Quick Actions</Trans>
|
||||
</Title>
|
||||
@ -167,7 +167,7 @@ export default function AdminCenter() {
|
||||
);
|
||||
|
||||
return (
|
||||
<Stack spacing="xs">
|
||||
<Stack gap="xs">
|
||||
<SettingsHeader
|
||||
title={t`Admin Center`}
|
||||
subtitle={t`Advanced Options`}
|
||||
|
@ -38,7 +38,7 @@ export default function MachineManagementPanel() {
|
||||
|
||||
<Space h="10px" />
|
||||
|
||||
<Stack spacing={'xs'}>
|
||||
<Stack gap={'xs'}>
|
||||
<Title order={5}>
|
||||
<Trans>Machine types</Trans>
|
||||
</Title>
|
||||
@ -47,7 +47,7 @@ export default function MachineManagementPanel() {
|
||||
|
||||
<Space h="10px" />
|
||||
|
||||
<Stack spacing={'xs'}>
|
||||
<Stack gap={'xs'}>
|
||||
<Group>
|
||||
<Title order={5}>
|
||||
<Trans>Machine Error Stack</Trans>
|
||||
@ -58,7 +58,7 @@ export default function MachineManagementPanel() {
|
||||
</Group>
|
||||
{registryStatus?.registry_errors &&
|
||||
registryStatus.registry_errors.length === 0 ? (
|
||||
<Text italic>
|
||||
<Text style={{ fontStyle: 'italic' }}>
|
||||
<Trans>There are no machine registry errors.</Trans>
|
||||
</Text>
|
||||
) : (
|
||||
|
@ -8,7 +8,7 @@ import { UserTable } from '../../../../tables/settings/UserTable';
|
||||
|
||||
export default function UserManagementPanel() {
|
||||
return (
|
||||
<Stack spacing="xs">
|
||||
<Stack gap="xs">
|
||||
<Title order={5}>
|
||||
<Trans>Users</Trans>
|
||||
</Title>
|
||||
@ -21,7 +21,7 @@ export default function UserManagementPanel() {
|
||||
|
||||
<Divider />
|
||||
|
||||
<Stack spacing={0}>
|
||||
<Stack gap={0}>
|
||||
<Text>
|
||||
<Trans>Settings</Trans>
|
||||
</Text>
|
||||
|
@ -287,7 +287,7 @@ export default function SystemSettings() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack spacing="xs">
|
||||
<Stack gap="xs">
|
||||
<SettingsHeader
|
||||
title={t`System Settings`}
|
||||
subtitle={server.instance || ''}
|
||||
|
@ -110,7 +110,7 @@ export default function UserSettings() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack spacing="xs">
|
||||
<Stack gap="xs">
|
||||
<SettingsHeader
|
||||
title={t`Account Settings`}
|
||||
subtitle={`${user?.first_name} ${user?.last_name}`}
|
||||
|
@ -415,7 +415,7 @@ export default function BuildDetail() {
|
||||
{editBuild.modal}
|
||||
{duplicateBuild.modal}
|
||||
{cancelBuild.modal}
|
||||
<Stack spacing="xs">
|
||||
<Stack gap="xs">
|
||||
<LoadingOverlay visible={instanceQuery.isFetching} />
|
||||
<PageDetail
|
||||
title={build.reference}
|
||||
|
@ -81,7 +81,8 @@ export default function CompanyDetail(props: Readonly<CompanyDetailProps>) {
|
||||
{
|
||||
type: 'text',
|
||||
name: 'description',
|
||||
label: t`Description`
|
||||
label: t`Description`,
|
||||
copy: true
|
||||
},
|
||||
{
|
||||
type: 'link',
|
||||
@ -314,7 +315,7 @@ export default function CompanyDetail(props: Readonly<CompanyDetailProps>) {
|
||||
return (
|
||||
<>
|
||||
{editCompany.modal}
|
||||
<Stack spacing="xs">
|
||||
<Stack gap="xs">
|
||||
<LoadingOverlay visible={instanceQuery.isFetching} />
|
||||
<PageDetail
|
||||
title={t`Company` + `: ${company.name}`}
|
||||
|
@ -241,8 +241,7 @@ export default function ManufacturerPartDetail() {
|
||||
return (
|
||||
<>
|
||||
{editManufacturerPart.modal}
|
||||
{duplicateManufacturerPart.modal}
|
||||
<Stack spacing="xs">
|
||||
<Stack gap="xs">
|
||||
<LoadingOverlay visible={instanceQuery.isFetching} />
|
||||
<PageDetail
|
||||
title={t`ManufacturerPart`}
|
||||
|
@ -311,8 +311,7 @@ export default function SupplierPartDetail() {
|
||||
return (
|
||||
<>
|
||||
{editSuppliertPart.modal}
|
||||
{duplicateSupplierPart.modal}
|
||||
<Stack spacing="xs">
|
||||
<Stack gap="xs">
|
||||
<LoadingOverlay visible={instanceQuery.isFetching} />
|
||||
<PageDetail
|
||||
title={t`Supplier Part`}
|
||||
|
@ -222,7 +222,7 @@ export default function CategoryDetail({}: {}) {
|
||||
return (
|
||||
<>
|
||||
{editCategory.modal}
|
||||
<Stack spacing="xs">
|
||||
<Stack gap="xs">
|
||||
<LoadingOverlay visible={instanceQuery.isFetching} />
|
||||
<PartCategoryTree
|
||||
opened={treeOpen}
|
||||
|
@ -442,7 +442,7 @@ export default function PartDetail() {
|
||||
/>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={8}>
|
||||
<Stack spacing="xs">
|
||||
<Stack gap="xs">
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
@ -782,7 +782,7 @@ export default function PartDetail() {
|
||||
<>
|
||||
{duplicatePart.modal}
|
||||
{editPart.modal}
|
||||
<Stack spacing="xs">
|
||||
<Stack gap="xs">
|
||||
<LoadingOverlay visible={instanceQuery.isFetching} />
|
||||
<PartCategoryTree
|
||||
opened={treeOpen}
|
||||
|
@ -65,7 +65,7 @@ export default function PartPricingPanel({ part }: { part: any }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack spacing="xs">
|
||||
<Stack gap="xs">
|
||||
<LoadingOverlay visible={instanceQuery.isLoading} />
|
||||
{!pricing && !instanceQuery.isLoading && (
|
||||
<Alert color="ref" title={t`Error`}>
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user