2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-04-27 19:16:44 +00:00

[PUI] Switch linting to biome (#8317)

* bump pre-commit

* add biome

* autofixes

* use number functions

* fix string usage

* use specific variable definition

* fix missing translations

* reduce alerts

* add missing keys

* fix index creation

* fix more strings

* fix types

* fix function

* add missing keys

* fiy array access

* fix string functions

* do not redefine var

* extend exlcusions

* reduce unnecessary operators

* simplify request

* use number functions

* fix missing translation

* add missing type

* fix filter

* use newer func

* remove unused fragment

* fix confusing assigment

* pass children as elements

* add missing translation

* fix imports

* fix import

* auto-fix problems

* add autfix for unused imports

* fix SAST error

* fix useSelfClosingElements

* fix useTemplate

* add codespell exception

* Update pui_printing.spec.ts

* Update pui_printing.spec.ts

* add vscode defaults
This commit is contained in:
Matthias Mair 2024-11-12 01:03:08 +01:00 committed by GitHub
parent e7cfb4c3c0
commit 0872beaba9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
308 changed files with 2635 additions and 2550 deletions

View File

@ -31,7 +31,8 @@
"ms-python.python", "ms-python.python",
"ms-python.vscode-pylance", "ms-python.vscode-pylance",
"batisteo.vscode-django", "batisteo.vscode-django",
"eamodio.gitlens" "eamodio.gitlens",
"biomejs.biome"
] ]
} }
}, },

View File

@ -69,26 +69,12 @@ repos:
pyproject.toml | pyproject.toml |
src/frontend/vite.config.ts | src/frontend/vite.config.ts |
)$ )$
- repo: https://github.com/pre-commit/mirrors-prettier - repo: https://github.com/biomejs/pre-commit
rev: "v4.0.0-alpha.8" rev: "v0.5.0"
hooks: hooks:
- id: prettier - id: biome-check
files: ^src/frontend/.*\.(js|jsx|ts|tsx)$ additional_dependencies: ["@biomejs/biome@1.9.4"]
additional_dependencies: files: ^src/frontend/.*\.(js|ts|tsx)$
- "prettier@^2.4.1"
- "@trivago/prettier-plugin-sort-imports"
- repo: https://github.com/pre-commit/mirrors-eslint
rev: "v9.12.0"
hooks:
- id: eslint
additional_dependencies:
- eslint@^8.41.0
- eslint-config-google@^0.14.0
- eslint-plugin-react@6.10.3
- babel-eslint@6.1.2
- "@typescript-eslint/eslint-plugin@latest"
- "@typescript-eslint/parser"
files: ^src/frontend/.*\.(js|jsx|ts|tsx)$
- repo: https://github.com/gitleaks/gitleaks - repo: https://github.com/gitleaks/gitleaks
rev: v8.21.0 rev: v8.21.0
hooks: hooks:

5
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,5 @@
{
"recommendations": [
"biomejs.biome"
]
}

8
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,8 @@
{
"[typescript]": {
"editor.defaultFormatter": "biomejs.biome"
},
"editor.codeActionsOnSave": {
"quickfix.biome": "explicit"
}
}

40
biome.json Normal file
View File

@ -0,0 +1,40 @@
{
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
"javascript": {
"formatter": {
"quoteStyle": "single",
"jsxQuoteStyle": "single",
"trailingCommas": "none",
"indentStyle": "space"
}
},
"linter": {
"rules": {
"suspicious" : {
"noExplicitAny": "off",
"noDoubleEquals": "off",
"noArrayIndexKey": "off",
"useDefaultSwitchClauseLast": "off"
},
"style": {
"noUselessElse": "off",
"noNonNullAssertion": "off",
"noParameterAssign": "off"
}, "correctness":{
"useExhaustiveDependencies": "off",
"useJsxKeyInIterable": "off",
"noUnsafeOptionalChaining": "off",
"noSwitchDeclarations": "off",
"noUnusedImports":"error"
}, "complexity": {
"noBannedTypes": "off",
"noExtraBooleanCast": "off",
"noForEach": "off",
"noUselessSwitchCase": "off",
"useLiteralKeys":"off"
}, "performance": {
"noDelete":"off"
}
}
}
}

View File

@ -106,4 +106,4 @@ known_django="django"
sections=["FUTURE","STDLIB","DJANGO","THIRDPARTY","FIRSTPARTY","LOCALFOLDER"] sections=["FUTURE","STDLIB","DJANGO","THIRDPARTY","FIRSTPARTY","LOCALFOLDER"]
[tool.codespell] [tool.codespell]
ignore-words-list = ["assertIn","SME","intoto"] ignore-words-list = ["assertIn","SME","intoto","fitH"]

View File

@ -1,9 +0,0 @@
{
"semi": true,
"trailingComma": "none",
"singleQuote": true,
"printWidth": 80,
"importOrder": ["<THIRD_PARTY_MODULES>", "^[./]"],
"importOrderSeparation": true,
"importOrderSortSpecifiers": true
}

View File

@ -1,7 +0,0 @@
/* eslint-env node */
module.exports = {
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'],
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
root: true,
};

View File

@ -23,7 +23,7 @@ export function setApiDefaults() {
api.defaults.xsrfHeaderName = 'X-CSRFToken'; api.defaults.xsrfHeaderName = 'X-CSRFToken';
if (token) { if (token) {
api.defaults.headers['Authorization'] = `Token ${token}`; api.defaults.headers.Authorization = `Token ${token}`;
} else { } else {
delete api.defaults.headers['Authorization']; delete api.defaults.headers['Authorization'];
} }

View File

@ -1,15 +1,15 @@
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { Alert } from '@mantine/core'; import { Alert } from '@mantine/core';
import { ErrorBoundary, FallbackRender } from '@sentry/react'; import { ErrorBoundary, type FallbackRender } from '@sentry/react';
import { IconExclamationCircle } from '@tabler/icons-react'; import { IconExclamationCircle } from '@tabler/icons-react';
import { ReactNode, useCallback } from 'react'; import { type ReactNode, useCallback } from 'react';
function DefaultFallback({ title }: Readonly<{ title: string }>): ReactNode { function DefaultFallback({ title }: Readonly<{ title: string }>): ReactNode {
return ( return (
<Alert <Alert
color="red" color='red'
icon={<IconExclamationCircle />} icon={<IconExclamationCircle />}
title={t`Error rendering component` + `: ${title}`} title={`${t`Error rendering component`}: ${title}`}
> >
{t`An error occurred while rendering this component. Refer to the console for more information.`} {t`An error occurred while rendering this component. Refer to the console for more information.`}
</Alert> </Alert>

View File

@ -1,5 +1,10 @@
import { ActionIcon, FloatingPosition, Group, Tooltip } from '@mantine/core'; import {
import { ReactNode } from 'react'; ActionIcon,
type FloatingPosition,
Group,
Tooltip
} from '@mantine/core';
import type { ReactNode } from 'react';
import { identifierString } from '../../functions/conversion'; import { identifierString } from '../../functions/conversion';
@ -46,7 +51,7 @@ export function ActionButton(props: ActionButtonProps) {
}} }}
variant={props.variant ?? 'transparent'} variant={props.variant ?? 'transparent'}
> >
<Group gap="xs" wrap="nowrap"> <Group gap='xs' wrap='nowrap'>
{props.icon} {props.icon}
</Group> </Group>
</ActionIcon> </ActionIcon>

View File

@ -1,10 +1,10 @@
import { IconPlus } from '@tabler/icons-react'; import { IconPlus } from '@tabler/icons-react';
import { ActionButton, ActionButtonProps } from './ActionButton'; import { ActionButton, type ActionButtonProps } from './ActionButton';
/** /**
* A generic icon button which is used to add or create a new item * A generic icon button which is used to add or create a new item
*/ */
export function AddItemButton(props: Readonly<ActionButtonProps>) { export function AddItemButton(props: Readonly<ActionButtonProps>) {
return <ActionButton {...props} color="green" icon={<IconPlus />} />; return <ActionButton {...props} color='green' icon={<IconPlus />} />;
} }

View File

@ -2,7 +2,7 @@ import { t } from '@lingui/macro';
import { IconUserStar } from '@tabler/icons-react'; import { IconUserStar } from '@tabler/icons-react';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import { ModelType } from '../../enums/ModelType'; import type { ModelType } from '../../enums/ModelType';
import { useServerApiState } from '../../states/ApiState'; import { useServerApiState } from '../../states/ApiState';
import { useLocalState } from '../../states/LocalState'; import { useLocalState } from '../../states/LocalState';
import { useUserState } from '../../states/UserState'; import { useUserState } from '../../states/UserState';
@ -78,14 +78,14 @@ export default function AdminButton(props: Readonly<AdminButtonProps>) {
return ( return (
<ActionButton <ActionButton
icon={<IconUserStar />} icon={<IconUserStar />}
color="blue" color='blue'
size="lg" size='lg'
radius="sm" radius='sm'
variant="filled" variant='filled'
tooltip={t`Open in admin interface`} tooltip={t`Open in admin interface`}
hidden={!enabled} hidden={!enabled}
onClick={openAdmin} onClick={openAdmin}
tooltipAlignment="bottom" tooltipAlignment='bottom'
/> />
); );
} }

View File

@ -16,16 +16,16 @@ export function ButtonMenu({
tooltip?: string; tooltip?: string;
}>) { }>) {
return ( return (
<Menu shadow="xs"> <Menu shadow='xs'>
<Menu.Target> <Menu.Target>
<ActionIcon variant="default"> <ActionIcon variant='default'>
<Tooltip label={tooltip}>{icon}</Tooltip> <Tooltip label={tooltip}>{icon}</Tooltip>
</ActionIcon> </ActionIcon>
</Menu.Target> </Menu.Target>
<Menu.Dropdown> <Menu.Dropdown>
{label && <Menu.Label>{label}</Menu.Label>} {label && <Menu.Label>{label}</Menu.Label>}
{actions.map((action, i) => ( {actions.map((action, i) => (
<Menu.Item key={i}>{action}</Menu.Item> <Menu.Item key={`${i}-${action}`}>{action}</Menu.Item>
))} ))}
</Menu.Dropdown> </Menu.Dropdown>
</Menu> </Menu>

View File

@ -3,7 +3,7 @@ import {
ActionIcon, ActionIcon,
Button, Button,
CopyButton as MantineCopyButton, CopyButton as MantineCopyButton,
MantineSize, type MantineSize,
Text, Text,
Tooltip Tooltip
} from '@mantine/core'; } from '@mantine/core';
@ -30,13 +30,13 @@ export function CopyButton({
<ButtonComponent <ButtonComponent
color={copied ? 'teal' : 'gray'} color={copied ? 'teal' : 'gray'}
onClick={copy} onClick={copy}
variant="transparent" variant='transparent'
size={size ?? 'sm'} size={size ?? 'sm'}
> >
{copied ? ( {copied ? (
<InvenTreeIcon icon="check" /> <InvenTreeIcon icon='check' />
) : ( ) : (
<InvenTreeIcon icon="copy" /> <InvenTreeIcon icon='copy' />
)} )}
{content} {content}
{label && ( {label && (

View File

@ -17,7 +17,7 @@ export function EditButton({
<ActionIcon <ActionIcon
onClick={() => setEditing()} onClick={() => setEditing()}
disabled={disabled} disabled={disabled}
variant="default" variant='default'
> >
{editing ? saveIcon : <IconEdit />} {editing ? saveIcon : <IconEdit />}
</ActionIcon> </ActionIcon>

View File

@ -1,6 +1,6 @@
import { Button, Tooltip } from '@mantine/core'; import { Button, Tooltip } from '@mantine/core';
import { InvenTreeIcon, InvenTreeIconType } from '../../functions/icons'; import { InvenTreeIcon, type InvenTreeIconType } from '../../functions/icons';
/** /**
* A "primary action" button for display on a page detail, (for example) * A "primary action" button for display on a page detail, (for example)
@ -25,12 +25,12 @@ export default function PrimaryActionButton({
} }
return ( return (
<Tooltip label={tooltip ?? title} position="bottom" hidden={!tooltip}> <Tooltip label={tooltip ?? title} position='bottom' hidden={!tooltip}>
<Button <Button
leftSection={icon && <InvenTreeIcon icon={icon} />} leftSection={icon && <InvenTreeIcon icon={icon} />}
color={color} color={color}
radius="sm" radius='sm'
p="xs" p='xs'
onClick={onClick} onClick={onClick}
> >
{title} {title}

View File

@ -6,13 +6,13 @@ import { useMemo, useState } from 'react';
import { api } from '../../App'; import { api } from '../../App';
import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType'; import type { ModelType } from '../../enums/ModelType';
import { extractAvailableFields } from '../../functions/forms'; import { extractAvailableFields } from '../../functions/forms';
import { useCreateApiFormModal } from '../../hooks/UseForm'; import { useCreateApiFormModal } from '../../hooks/UseForm';
import { apiUrl } from '../../states/ApiState'; import { apiUrl } from '../../states/ApiState';
import { useLocalState } from '../../states/LocalState'; import { useLocalState } from '../../states/LocalState';
import { useUserSettingsState } from '../../states/SettingsState'; import { useUserSettingsState } from '../../states/SettingsState';
import { ApiFormFieldSet } from '../forms/fields/ApiFormField'; import type { ApiFormFieldSet } from '../forms/fields/ApiFormField';
import { ActionDropdown } from '../items/ActionDropdown'; import { ActionDropdown } from '../items/ActionDropdown';
export function PrintingActions({ export function PrintingActions({
@ -57,11 +57,11 @@ export function PrintingActions({
}); });
const labelFields: ApiFormFieldSet = useMemo(() => { const labelFields: ApiFormFieldSet = useMemo(() => {
let fields: ApiFormFieldSet = printingFields.data || {}; const fields: ApiFormFieldSet = printingFields.data || {};
// Override field values // Override field values
fields['template'] = { fields.template = {
...fields['template'], ...fields.template,
filters: { filters: {
enabled: true, enabled: true,
model_type: modelType, model_type: modelType,
@ -69,8 +69,8 @@ export function PrintingActions({
} }
}; };
fields['items'] = { fields.items = {
...fields['items'], ...fields.items,
value: items, value: items,
hidden: true hidden: true
}; };

View File

@ -13,10 +13,10 @@ export default function RemoveRowButton({
return ( return (
<ActionButton <ActionButton
onClick={onClick} onClick={onClick}
icon={<InvenTreeIcon icon="square_x" />} icon={<InvenTreeIcon icon='square_x' />}
tooltip={tooltip} tooltip={tooltip}
tooltipAlignment="top" tooltipAlignment='top'
color="red" color='red'
/> />
); );
} }

View File

@ -17,7 +17,7 @@ import {
import { api } from '../../App'; import { api } from '../../App';
import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { apiUrl } from '../../states/ApiState'; import { apiUrl } from '../../states/ApiState';
import { Provider } from '../../states/states'; import type { Provider } from '../../states/states';
const brandIcons: { [key: string]: JSX.Element } = { const brandIcons: { [key: string]: JSX.Element } = {
google: <IconBrandGoogle />, google: <IconBrandGoogle />,
@ -51,8 +51,8 @@ export function SsoButton({ provider }: Readonly<{ provider: Provider }>) {
return ( return (
<Button <Button
leftSection={getBrandIcon(provider)} leftSection={getBrandIcon(provider)}
radius="xl" radius='xl'
component="a" component='a'
onClick={login} onClick={login}
> >
{provider.display_name}{' '} {provider.display_name}{' '}

View File

@ -16,7 +16,7 @@ export function ScanButton() {
innerProps: {} innerProps: {}
}) })
} }
variant="transparent" variant='transparent'
title={t`Open Barcode Scanner`} title={t`Open Barcode Scanner`}
> >
<IconQrcode /> <IconQrcode />

View File

@ -11,7 +11,7 @@ import { IconChevronDown } from '@tabler/icons-react';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { identifierString } from '../../functions/conversion'; import { identifierString } from '../../functions/conversion';
import { TablerIconType } from '../../functions/icons'; import type { TablerIconType } from '../../functions/icons';
import * as classes from './SplitButton.css'; import * as classes from './SplitButton.css';
interface SplitButtonOption { interface SplitButtonOption {
@ -58,7 +58,7 @@ export function SplitButton({
const theme = useMantineTheme(); const theme = useMantineTheme();
return ( return (
<Group wrap="nowrap" style={{ gap: 0 }}> <Group wrap='nowrap' style={{ gap: 0 }}>
<Button <Button
onClick={currentOption?.onClick} onClick={currentOption?.onClick}
disabled={loading ? false : currentOption?.disabled} disabled={loading ? false : currentOption?.disabled}
@ -70,12 +70,12 @@ export function SplitButton({
</Button> </Button>
<Menu <Menu
transitionProps={{ transition: 'pop' }} transitionProps={{ transition: 'pop' }}
position="bottom-end" position='bottom-end'
withinPortal withinPortal
> >
<Menu.Target> <Menu.Target>
<ActionIcon <ActionIcon
variant="filled" variant='filled'
color={theme.primaryColor} color={theme.primaryColor}
size={36} size={36}
className={classes.icon} className={classes.icon}
@ -99,7 +99,7 @@ export function SplitButton({
disabled={option.disabled} disabled={option.disabled}
leftSection={<option.icon />} leftSection={<option.icon />}
> >
<Tooltip label={option.tooltip} position="right"> <Tooltip label={option.tooltip} position='right'>
<Text>{option.name}</Text> <Text>{option.name}</Text>
</Tooltip> </Tooltip>
</Menu.Item> </Menu.Item>

View File

@ -12,8 +12,8 @@ export function SpotlightButton() {
<ActionIcon <ActionIcon
onClick={() => firstSpotlight.open()} onClick={() => firstSpotlight.open()}
title={t`Open spotlight`} title={t`Open spotlight`}
variant="transparent" variant='transparent'
aria-label="open-spotlight" aria-label='open-spotlight'
> >
<IconCommand /> <IconCommand />
</ActionIcon> </ActionIcon>

View File

@ -19,9 +19,9 @@ export function PassFailButton({
return ( return (
<Badge <Badge
color={v ? 'lime.5' : 'red.6'} color={v ? 'lime.5' : 'red.6'}
variant="filled" variant='filled'
radius="lg" radius='lg'
size="sm" size='sm'
style={{ maxWidth: '50px' }} style={{ maxWidth: '50px' }}
> >
{v ? pass : fail} {v ? pass : fail}

View File

@ -3,12 +3,12 @@ import { Alert, Card, Center, Divider, Loader, Text } from '@mantine/core';
import { useDisclosure, useHotkeys } from '@mantine/hooks'; import { useDisclosure, useHotkeys } from '@mantine/hooks';
import { IconInfoCircle } from '@tabler/icons-react'; import { IconInfoCircle } from '@tabler/icons-react';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import { Layout, Responsive, WidthProvider } from 'react-grid-layout'; import { type Layout, Responsive, WidthProvider } from 'react-grid-layout';
import { useDashboardItems } from '../../hooks/UseDashboardItems'; import { useDashboardItems } from '../../hooks/UseDashboardItems';
import { useUserState } from '../../states/UserState'; import { useUserState } from '../../states/UserState';
import DashboardMenu from './DashboardMenu'; import DashboardMenu from './DashboardMenu';
import DashboardWidget, { DashboardWidgetProps } from './DashboardWidget'; import DashboardWidget, { type DashboardWidgetProps } from './DashboardWidget';
import DashboardWidgetDrawer from './DashboardWidgetDrawer'; import DashboardWidgetDrawer from './DashboardWidgetDrawer';
const ReactGridLayout = WidthProvider(Responsive); const ReactGridLayout = WidthProvider(Responsive);
@ -17,7 +17,7 @@ const ReactGridLayout = WidthProvider(Responsive);
* Save the dashboard layout to local storage * Save the dashboard layout to local storage
*/ */
function saveDashboardLayout(layouts: any, userId: number | undefined): void { function saveDashboardLayout(layouts: any, userId: number | undefined): void {
let reducedLayouts: any = {}; const reducedLayouts: any = {};
// Reduce the layouts to exclude default attributes from the dataset // Reduce the layouts to exclude default attributes from the dataset
Object.keys(layouts).forEach((key) => { Object.keys(layouts).forEach((key) => {
@ -93,7 +93,7 @@ function loadDashboardWidgets(userId: number | undefined): string[] {
} }
} }
export default function DashboardLayout({}: {}) { export default function DashboardLayout() {
const user = useUserState(); const user = useUserState();
// Dashboard layout definition // Dashboard layout definition
@ -141,7 +141,7 @@ export default function DashboardLayout({}: {}) {
*/ */
const addWidget = useCallback( const addWidget = useCallback(
(widget: string) => { (widget: string) => {
let newWidget = availableWidgets.items.find( const newWidget = availableWidgets.items.find(
(wid) => wid.label === widget (wid) => wid.label === widget
); );
@ -150,7 +150,7 @@ export default function DashboardLayout({}: {}) {
} }
// Update the layouts to include the new widget (and enforce initial size) // Update the layouts to include the new widget (and enforce initial size)
let _layouts: any = { ...layouts }; const _layouts: any = { ...layouts };
Object.keys(_layouts).forEach((key) => { Object.keys(_layouts).forEach((key) => {
_layouts[key] = updateLayoutForWidget(_layouts[key], widgets, true); _layouts[key] = updateLayoutForWidget(_layouts[key], widgets, true);
@ -170,7 +170,7 @@ export default function DashboardLayout({}: {}) {
setWidgets(widgets.filter((item) => item.label !== widget)); setWidgets(widgets.filter((item) => item.label !== widget));
// Remove the widget from the layout // Remove the widget from the layout
let _layouts: any = { ...layouts }; const _layouts: any = { ...layouts };
Object.keys(_layouts).forEach((key) => { Object.keys(_layouts).forEach((key) => {
_layouts[key] = _layouts[key].filter( _layouts[key] = _layouts[key].filter(
@ -188,7 +188,7 @@ export default function DashboardLayout({}: {}) {
(layout: any[], widgets: any[], overrideSize: boolean) => { (layout: any[], widgets: any[], overrideSize: boolean) => {
return layout.map((item: Layout): Layout => { return layout.map((item: Layout): Layout => {
// Find the matching widget // Find the matching widget
let widget = widgets.find( const widget = widgets.find(
(widget: DashboardWidgetProps) => widget.label === item.i (widget: DashboardWidgetProps) => widget.label === item.i
); );
@ -275,14 +275,14 @@ export default function DashboardLayout({}: {}) {
editing={editing} editing={editing}
removing={removing} removing={removing}
/> />
<Divider p="xs" /> <Divider p='xs' />
{layouts && loaded && availableWidgets.loaded ? ( {layouts && loaded && availableWidgets.loaded ? (
<> <>
{widgetLabels.length == 0 ? ( {widgetLabels.length == 0 ? (
<Center> <Center>
<Card shadow="xs" padding="xl" style={{ width: '100%' }}> <Card shadow='xs' padding='xl' style={{ width: '100%' }}>
<Alert <Alert
color="blue" color='blue'
title={t`No Widgets Selected`} title={t`No Widgets Selected`}
icon={<IconInfoCircle />} icon={<IconInfoCircle />}
> >
@ -292,7 +292,7 @@ export default function DashboardLayout({}: {}) {
</Center> </Center>
) : ( ) : (
<ReactGridLayout <ReactGridLayout
className="dashboard-layout" className='dashboard-layout'
breakpoints={{ lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }} breakpoints={{ lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }}
cols={{ lg: 12, md: 10, sm: 6, xs: 4, xxs: 2 }} cols={{ lg: 12, md: 10, sm: 6, xs: 4, xxs: 2 }}
rowHeight={64} rowHeight={64}
@ -320,7 +320,7 @@ export default function DashboardLayout({}: {}) {
</> </>
) : ( ) : (
<Center> <Center>
<Loader size="xl" /> <Loader size='xl' />
</Center> </Center>
)} )}
</> </>

View File

@ -45,42 +45,42 @@ export default function DashboardMenu({
const username = user.username(); const username = user.username();
return ( return (
<StylishText size="lg">{`${instanceName} - ${username}`}</StylishText> <StylishText size='lg'>{`${instanceName} - ${username}`}</StylishText>
); );
}, [user, instanceName]); }, [user, instanceName]);
return ( return (
<Paper p="sm" shadow="xs"> <Paper p='sm' shadow='xs'>
<Group justify="space-between" wrap="nowrap"> <Group justify='space-between' wrap='nowrap'>
{title} {title}
<Group justify="right" wrap="nowrap"> <Group justify='right' wrap='nowrap'>
{(editing || removing) && ( {(editing || removing) && (
<Tooltip label={t`Accept Layout`} onClick={onAcceptLayout}> <Tooltip label={t`Accept Layout`} onClick={onAcceptLayout}>
<ActionIcon <ActionIcon
aria-label={'dashboard-accept-layout'} aria-label={'dashboard-accept-layout'}
color="green" color='green'
variant="transparent" variant='transparent'
> >
<IconCircleCheck /> <IconCircleCheck />
</ActionIcon> </ActionIcon>
</Tooltip> </Tooltip>
)} )}
<Menu <Menu
shadow="md" shadow='md'
width={200} width={200}
openDelay={100} openDelay={100}
closeDelay={400} closeDelay={400}
position="bottom-end" position='bottom-end'
> >
<Menu.Target> <Menu.Target>
<Indicator <Indicator
color="red" color='red'
position="bottom-start" position='bottom-start'
processing processing
disabled={!editing} disabled={!editing}
> >
<ActionIcon variant="transparent" aria-label="dashboard-menu"> <ActionIcon variant='transparent' aria-label='dashboard-menu'>
<IconDotsVertical /> <IconDotsVertical />
</ActionIcon> </ActionIcon>
</Indicator> </Indicator>
@ -93,7 +93,7 @@ export default function DashboardMenu({
{!editing && !removing && ( {!editing && !removing && (
<Menu.Item <Menu.Item
leftSection={<IconLayout2 color="blue" size={14} />} leftSection={<IconLayout2 color='blue' size={14} />}
onClick={onStartEdit} onClick={onStartEdit}
> >
<Trans>Edit Layout</Trans> <Trans>Edit Layout</Trans>
@ -102,7 +102,7 @@ export default function DashboardMenu({
{!editing && !removing && ( {!editing && !removing && (
<Menu.Item <Menu.Item
leftSection={<IconLayoutGridAdd color="green" size={14} />} leftSection={<IconLayoutGridAdd color='green' size={14} />}
onClick={onAddWidget} onClick={onAddWidget}
> >
<Trans>Add Widget</Trans> <Trans>Add Widget</Trans>
@ -111,7 +111,7 @@ export default function DashboardMenu({
{!editing && !removing && ( {!editing && !removing && (
<Menu.Item <Menu.Item
leftSection={<IconLayoutGridRemove color="red" size={14} />} leftSection={<IconLayoutGridRemove color='red' size={14} />}
onClick={onStartRemove} onClick={onStartRemove}
> >
<Trans>Remove Widgets</Trans> <Trans>Remove Widgets</Trans>
@ -120,7 +120,7 @@ export default function DashboardMenu({
{(editing || removing) && ( {(editing || removing) && (
<Menu.Item <Menu.Item
leftSection={<IconCircleCheck color="green" size={14} />} leftSection={<IconCircleCheck color='green' size={14} />}
onClick={onAcceptLayout} onClick={onAcceptLayout}
> >
<Trans>Accept Layout</Trans> <Trans>Accept Layout</Trans>

View File

@ -43,7 +43,7 @@ export default function DashboardWidget({
// TODO: Add button to remove widget (if "editing") // TODO: Add button to remove widget (if "editing")
return ( return (
<Paper withBorder key={item.label} shadow="sm" p="xs"> <Paper withBorder key={item.label} shadow='sm' p='xs'>
<Boundary label={`dashboard-widget-${item.label}`}> <Boundary label={`dashboard-widget-${item.label}`}>
<Box <Box
key={`dashboard-widget-${item.label}`} key={`dashboard-widget-${item.label}`}
@ -58,17 +58,17 @@ export default function DashboardWidget({
{item.render()} {item.render()}
</Box> </Box>
{removing && ( {removing && (
<Overlay color="black" opacity={0.7} zIndex={1000}> <Overlay color='black' opacity={0.7} zIndex={1000}>
{removing && ( {removing && (
<Group justify="right"> <Group justify='right'>
<Tooltip <Tooltip
label={t`Remove this widget from the dashboard`} label={t`Remove this widget from the dashboard`}
position="bottom" position='bottom'
> >
<ActionIcon <ActionIcon
aria-label={`remove-dashboard-item-${item.label}`} aria-label={`remove-dashboard-item-${item.label}`}
variant="filled" variant='filled'
color="red" color='red'
onClick={onRemove} onClick={onRemove}
> >
<IconX /> <IconX />

View File

@ -49,7 +49,7 @@ export default function DashboardWidgetDrawer({
// Filter widgets based on search text // Filter widgets based on search text
const filteredWidgets = useMemo(() => { const filteredWidgets = useMemo(() => {
let words = filterText.trim().toLowerCase().split(' '); const words = filterText.trim().toLowerCase().split(' ');
return unusedWidgets.filter((widget) => { return unusedWidgets.filter((widget) => {
return words.every((word) => return words.every((word) =>
@ -60,28 +60,28 @@ export default function DashboardWidgetDrawer({
return ( return (
<Drawer <Drawer
position="right" position='right'
size="50%" size='50%'
opened={opened} opened={opened}
onClose={onClose} onClose={onClose}
title={ title={
<Group justify="space-between" wrap="nowrap"> <Group justify='space-between' wrap='nowrap'>
<StylishText size="lg">Add Dashboard Widgets</StylishText> <StylishText size='lg'>Add Dashboard Widgets</StylishText>
</Group> </Group>
} }
> >
<Stack gap="xs"> <Stack gap='xs'>
<Divider /> <Divider />
<TextInput <TextInput
aria-label="dashboard-widgets-filter-input" aria-label='dashboard-widgets-filter-input'
placeholder={t`Filter dashboard widgets`} placeholder={t`Filter dashboard widgets`}
value={filter} value={filter}
onChange={(event) => setFilter(event.currentTarget.value)} onChange={(event) => setFilter(event.currentTarget.value)}
rightSection={ rightSection={
filter && ( filter && (
<IconBackspace <IconBackspace
aria-label="dashboard-widgets-filter-clear" aria-label='dashboard-widgets-filter-clear'
color="red" color='red'
onClick={() => setFilter('')} onClick={() => setFilter('')}
/> />
) )
@ -94,18 +94,18 @@ export default function DashboardWidgetDrawer({
<Table.Tr key={widget.label}> <Table.Tr key={widget.label}>
<Table.Td> <Table.Td>
<Tooltip <Tooltip
position="left" position='left'
label={t`Add this widget to the dashboard`} label={t`Add this widget to the dashboard`}
> >
<ActionIcon <ActionIcon
aria-label={`add-widget-${widget.label}`} aria-label={`add-widget-${widget.label}`}
variant="transparent" variant='transparent'
color="green" color='green'
onClick={() => { onClick={() => {
onAddWidget(widget.label); onAddWidget(widget.label);
}} }}
> >
<IconLayoutGridAdd></IconLayoutGridAdd> <IconLayoutGridAdd />
</ActionIcon> </ActionIcon>
</Tooltip> </Tooltip>
</Table.Td> </Table.Td>
@ -113,14 +113,14 @@ export default function DashboardWidgetDrawer({
<Text>{widget.title}</Text> <Text>{widget.title}</Text>
</Table.Td> </Table.Td>
<Table.Td> <Table.Td>
<Text size="sm">{widget.description}</Text> <Text size='sm'>{widget.description}</Text>
</Table.Td> </Table.Td>
</Table.Tr> </Table.Tr>
))} ))}
</Table.Tbody> </Table.Tbody>
</Table> </Table>
{unusedWidgets.length === 0 && ( {unusedWidgets.length === 0 && (
<Alert color="blue" title={t`No Widgets Available`}> <Alert color='blue' title={t`No Widgets Available`}>
<Text>{t`There are no more widgets available for the dashboard`}</Text> <Text>{t`There are no more widgets available for the dashboard`}</Text>
</Alert> </Alert>
)} )}

View File

@ -1,7 +1,7 @@
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { ModelType } from '../../enums/ModelType'; import { ModelType } from '../../enums/ModelType';
import { DashboardWidgetProps } from './DashboardWidget'; import type { DashboardWidgetProps } from './DashboardWidget';
import ColorToggleDashboardWidget from './widgets/ColorToggleWidget'; import ColorToggleDashboardWidget from './widgets/ColorToggleWidget';
import GetStartedWidget from './widgets/GetStartedWidget'; import GetStartedWidget from './widgets/GetStartedWidget';
import LanguageSelectDashboardWidget from './widgets/LanguageSelectWidget'; import LanguageSelectDashboardWidget from './widgets/LanguageSelectWidget';

View File

@ -3,12 +3,12 @@ import { Group } from '@mantine/core';
import { ColorToggle } from '../../items/ColorToggle'; import { ColorToggle } from '../../items/ColorToggle';
import { StylishText } from '../../items/StylishText'; import { StylishText } from '../../items/StylishText';
import { DashboardWidgetProps } from '../DashboardWidget'; import type { DashboardWidgetProps } from '../DashboardWidget';
function ColorToggleWidget(title: string) { function ColorToggleWidget(title: string) {
return ( return (
<Group justify="space-between" wrap="nowrap"> <Group justify='space-between' wrap='nowrap'>
<StylishText size="lg">{title}</StylishText> <StylishText size='lg'>{title}</StylishText>
<ColorToggle /> <ColorToggle />
</Group> </Group>
); );

View File

@ -4,7 +4,7 @@ import { useMemo } from 'react';
import { DocumentationLinks } from '../../../defaults/links'; import { DocumentationLinks } from '../../../defaults/links';
import { GettingStartedCarousel } from '../../items/GettingStartedCarousel'; import { GettingStartedCarousel } from '../../items/GettingStartedCarousel';
import { MenuLinkItem } from '../../items/MenuLinks'; import type { MenuLinkItem } from '../../items/MenuLinks';
import { StylishText } from '../../items/StylishText'; import { StylishText } from '../../items/StylishText';
export default function GetStartedWidget() { export default function GetStartedWidget() {
@ -12,7 +12,7 @@ export default function GetStartedWidget() {
return ( return (
<Stack> <Stack>
<StylishText size="xl">{t`Getting Started`}</StylishText> <StylishText size='xl'>{t`Getting Started`}</StylishText>
<GettingStartedCarousel items={docLinks} /> <GettingStartedCarousel items={docLinks} />
</Stack> </Stack>
); );

View File

@ -3,12 +3,12 @@ import { Stack } from '@mantine/core';
import { LanguageSelect } from '../../items/LanguageSelect'; import { LanguageSelect } from '../../items/LanguageSelect';
import { StylishText } from '../../items/StylishText'; import { StylishText } from '../../items/StylishText';
import { DashboardWidgetProps } from '../DashboardWidget'; import type { DashboardWidgetProps } from '../DashboardWidget';
function LanguageSelectWidget(title: string) { function LanguageSelectWidget(title: string) {
return ( return (
<Stack gap="xs"> <Stack gap='xs'>
<StylishText size="lg">{title}</StylishText> <StylishText size='lg'>{title}</StylishText>
<LanguageSelect width={140} /> <LanguageSelect width={140} />
</Stack> </Stack>
); );

View File

@ -4,7 +4,6 @@ import {
Alert, Alert,
Anchor, Anchor,
Container, Container,
Group,
ScrollArea, ScrollArea,
Stack, Stack,
Table, Table,
@ -28,13 +27,13 @@ import { StylishText } from '../../items/StylishText';
function NewsLink({ item }: { item: any }) { function NewsLink({ item }: { item: any }) {
let link: string = item.link; let link: string = item.link;
if (link && link.startsWith('/')) { if (link?.startsWith('/')) {
link = 'https://inventree.org' + link; link = `https://inventree.org${link}`;
} }
if (link) { if (link) {
return ( return (
<Anchor href={link} target="_blank"> <Anchor href={link} target='_blank'>
{item.title} {item.title}
</Anchor> </Anchor>
); );
@ -60,9 +59,9 @@ function NewsItem({
<Table.Td> <Table.Td>
<Tooltip label={t`Mark as read`}> <Tooltip label={t`Mark as read`}>
<ActionIcon <ActionIcon
size="sm" size='sm'
color="green" color='green'
variant="transparent" variant='transparent'
onClick={() => onMarkRead(item.pk)} onClick={() => onMarkRead(item.pk)}
> >
<IconMailCheck /> <IconMailCheck />
@ -112,7 +111,7 @@ export default function NewsWidget() {
if (!user.isSuperuser()) { if (!user.isSuperuser()) {
return ( return (
<Alert color="red" title={t`Requires Superuser`}> <Alert color='red' title={t`Requires Superuser`}>
<Text>{t`This widget requires superuser permissions`}</Text> <Text>{t`This widget requires superuser permissions`}</Text>
</Alert> </Alert>
); );
@ -120,7 +119,7 @@ export default function NewsWidget() {
return ( return (
<Stack> <Stack>
<StylishText size="xl">{t`News Updates`}</StylishText> <StylishText size='xl'>{t`News Updates`}</StylishText>
<ScrollArea h={400}> <ScrollArea h={400}>
<Container> <Container>
<Table> <Table>
@ -130,7 +129,7 @@ export default function NewsWidget() {
<NewsItem key={item.pk} item={item} onMarkRead={markRead} /> <NewsItem key={item.pk} item={item} onMarkRead={markRead} />
)) ))
) : ( ) : (
<Alert color="green" title={t`No News`}> <Alert color='green' title={t`No News`}>
<Text>{t`There are no unread news items`}</Text> <Text>{t`There are no unread news items`}</Text>
</Alert> </Alert>
)} )}

View File

@ -1,29 +1,21 @@
import { import { ActionIcon, Group, Loader } from '@mantine/core';
ActionIcon,
Card,
Group,
Loader,
Skeleton,
Space,
Stack,
Text
} from '@mantine/core';
import { IconExternalLink } from '@tabler/icons-react'; import { IconExternalLink } from '@tabler/icons-react';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { on } from 'events'; import { type ReactNode, useCallback } from 'react';
import { ReactNode, useCallback } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { api } from '../../../App'; import { api } from '../../../App';
import { ModelType } from '../../../enums/ModelType'; import type { ModelType } from '../../../enums/ModelType';
import { identifierString } from '../../../functions/conversion'; import {
import { InvenTreeIcon, InvenTreeIconType } from '../../../functions/icons'; InvenTreeIcon,
type InvenTreeIconType
} from '../../../functions/icons';
import { navigateToLink } from '../../../functions/navigation'; import { navigateToLink } from '../../../functions/navigation';
import { apiUrl } from '../../../states/ApiState'; import { apiUrl } from '../../../states/ApiState';
import { useUserState } from '../../../states/UserState'; import { useUserState } from '../../../states/UserState';
import { StylishText } from '../../items/StylishText'; import { StylishText } from '../../items/StylishText';
import { ModelInformationDict } from '../../render/ModelType'; import { ModelInformationDict } from '../../render/ModelType';
import { DashboardWidgetProps } from '../DashboardWidget'; import type { DashboardWidgetProps } from '../DashboardWidget';
/** /**
* A simple dashboard widget for displaying the number of results for a particular query * A simple dashboard widget for displaying the number of results for a particular query
@ -81,18 +73,18 @@ function QueryCountWidget({
// TODO: Improve visual styling // TODO: Improve visual styling
return ( return (
<Group gap="xs" wrap="nowrap"> <Group gap='xs' wrap='nowrap'>
<InvenTreeIcon icon={icon ?? modelProperties.icon} /> <InvenTreeIcon icon={icon ?? modelProperties.icon} />
<Group gap="xs" wrap="nowrap" justify="space-between"> <Group gap='xs' wrap='nowrap' justify='space-between'>
<StylishText size="md">{title}</StylishText> <StylishText size='md'>{title}</StylishText>
<Group gap="xs" wrap="nowrap" justify="right"> <Group gap='xs' wrap='nowrap' justify='right'>
{query.isFetching ? ( {query.isFetching ? (
<Loader size="sm" /> <Loader size='sm' />
) : ( ) : (
<StylishText size="sm">{query.data?.count ?? '-'}</StylishText> <StylishText size='sm'>{query.data?.count ?? '-'}</StylishText>
)} )}
{modelProperties?.url_overview && ( {modelProperties?.url_overview && (
<ActionIcon size="sm" variant="transparent" onClick={onFollowLink}> <ActionIcon size='sm' variant='transparent' onClick={onFollowLink}>
<IconExternalLink /> <IconExternalLink />
</ActionIcon> </ActionIcon>
)} )}

View File

@ -11,14 +11,14 @@ import {
} from '@mantine/core'; } from '@mantine/core';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { getValueAtPath } from 'mantine-datatable'; import { getValueAtPath } from 'mantine-datatable';
import { ReactNode, useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { api } from '../../App'; import { api } from '../../App';
import { formatDate } from '../../defaults/formatters'; import { formatDate } from '../../defaults/formatters';
import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType'; import type { ModelType } from '../../enums/ModelType';
import { InvenTreeIcon, InvenTreeIconType } from '../../functions/icons'; import { InvenTreeIcon, type InvenTreeIconType } from '../../functions/icons';
import { navigateToLink } from '../../functions/navigation'; import { navigateToLink } from '../../functions/navigation';
import { getDetailUrl } from '../../functions/urls'; import { getDetailUrl } from '../../functions/urls';
import { apiUrl } from '../../states/ApiState'; import { apiUrl } from '../../states/ApiState';
@ -30,8 +30,7 @@ import { StylishText } from '../items/StylishText';
import { getModelInfo } from '../render/ModelType'; import { getModelInfo } from '../render/ModelType';
import { StatusRenderer } from '../render/StatusRenderer'; import { StatusRenderer } from '../render/StatusRenderer';
export type DetailsField = export type DetailsField = {
| {
hidden?: boolean; hidden?: boolean;
icon?: InvenTreeIconType; icon?: InvenTreeIconType;
name: string; name: string;
@ -104,7 +103,7 @@ function NameBadge({
const { data } = useQuery({ const { data } = useQuery({
queryKey: ['badge', type, pk], queryKey: ['badge', type, pk],
queryFn: async () => { queryFn: async () => {
let path: string = ''; let path = '';
switch (type) { switch (type) {
case 'owner': case 'owner':
@ -141,7 +140,7 @@ function NameBadge({
const settings = useGlobalSettingsState(); const settings = useGlobalSettingsState();
if (!data || data.isLoading || data.isFetching) { if (!data || data.isLoading || data.isFetching) {
return <Skeleton height={12} radius="md" />; return <Skeleton height={12} radius='md' />;
} }
// Rendering a user's rame for the badge // Rendering a user's rame for the badge
@ -162,10 +161,10 @@ function NameBadge({
} }
return ( return (
<Group wrap="nowrap" gap="sm" justify="right"> <Group wrap='nowrap' gap='sm' justify='right'>
<Badge <Badge
color="dark" color='dark'
variant="filled" variant='filled'
style={{ display: 'flex', alignItems: 'center' }} style={{ display: 'flex', alignItems: 'center' }}
> >
{data?.name ?? _render_name()} {data?.name ?? _render_name()}
@ -176,7 +175,7 @@ function NameBadge({
} }
function DateValue(props: Readonly<FieldProps>) { function DateValue(props: Readonly<FieldProps>) {
return <Text size="sm">{formatDate(props.field_value?.toString())}</Text>; return <Text size='sm'>{formatDate(props.field_value?.toString())}</Text>;
} }
/** /**
@ -185,7 +184,7 @@ function DateValue(props: Readonly<FieldProps>) {
* If user is defined, a badge is rendered in addition to main value * If user is defined, a badge is rendered in addition to main value
*/ */
function TableStringValue(props: Readonly<FieldProps>) { function TableStringValue(props: Readonly<FieldProps>) {
let value = props?.field_value; const value = props?.field_value;
let renderedValue = null; let renderedValue = null;
@ -194,19 +193,19 @@ function TableStringValue(props: Readonly<FieldProps>) {
} else if (props?.field_data?.value_formatter) { } else if (props?.field_data?.value_formatter) {
renderedValue = props.field_data.value_formatter(); renderedValue = props.field_data.value_formatter();
} else if (value === null || value === undefined) { } else if (value === null || value === undefined) {
renderedValue = <Text size="sm">'---'</Text>; renderedValue = <Text size='sm'>'---'</Text>;
} else { } else {
renderedValue = <Text size="sm">{value.toString()}</Text>; renderedValue = <Text size='sm'>{value.toString()}</Text>;
} }
return ( return (
<Group wrap="nowrap" gap="xs" justify="space-apart"> <Group wrap='nowrap' gap='xs' justify='space-apart'>
<Group wrap="nowrap" gap="xs" justify="left"> <Group wrap='nowrap' gap='xs' justify='left'>
{renderedValue} {renderedValue}
{props.field_data.unit == true && <Text size="xs">{props.unit}</Text>} {props.field_data.unit && <Text size='xs'>{props.unit}</Text>}
</Group> </Group>
{props.field_data.user && ( {props.field_data.user && (
<NameBadge pk={props.field_data?.user} type="user" /> <NameBadge pk={props.field_data?.user} type='user' />
)} )}
</Group> </Group>
); );
@ -265,7 +264,7 @@ function TableAnchorValue(props: Readonly<FieldProps>) {
); );
if (!data || data.isLoading || data.isFetching) { if (!data || data.isLoading || data.isFetching) {
return <Skeleton height={12} radius="md" />; return <Skeleton height={12} radius='md' />;
} }
if (props.field_data.external) { if (props.field_data.external) {
@ -277,7 +276,7 @@ function TableAnchorValue(props: Readonly<FieldProps>) {
> >
<span style={{ display: 'flex', alignItems: 'center', gap: '3px' }}> <span style={{ display: 'flex', alignItems: 'center', gap: '3px' }}>
<Text>{props.field_value}</Text> <Text>{props.field_value}</Text>
<InvenTreeIcon icon="external" iconProps={{ size: 15 }} /> <InvenTreeIcon icon='external' iconProps={{ size: 15 }} />
</span> </span>
</Anchor> </Anchor>
); );
@ -304,7 +303,7 @@ function TableAnchorValue(props: Readonly<FieldProps>) {
return ( return (
<> <>
{make_link ? ( {make_link ? (
<Anchor href="#" onClick={handleLinkClick}> <Anchor href='#' onClick={handleLinkClick}>
<Text>{value}</Text> <Text>{value}</Text>
</Anchor> </Anchor>
) : ( ) : (
@ -316,7 +315,7 @@ function TableAnchorValue(props: Readonly<FieldProps>) {
function ProgressBarValue(props: Readonly<FieldProps>) { function ProgressBarValue(props: Readonly<FieldProps>) {
if (props.field_data.total <= 0) { if (props.field_data.total <= 0) {
return <Text size="sm">{props.field_data.progress}</Text>; return <Text size='sm'>{props.field_data.progress}</Text>;
} }
return ( return (
@ -404,10 +403,10 @@ export function DetailsTable({
title?: string; title?: string;
}>) { }>) {
return ( return (
<Paper p="xs" withBorder radius="xs"> <Paper p='xs' withBorder radius='xs'>
<Stack gap="xs"> <Stack gap='xs'>
{title && <StylishText size="lg">{title}</StylishText>} {title && <StylishText size='lg'>{title}</StylishText>}
<Table striped verticalSpacing={5} horizontalSpacing="sm"> <Table striped verticalSpacing={5} horizontalSpacing='sm'>
<Table.Tbody> <Table.Tbody>
{fields {fields
.filter((field: DetailsField) => !field.hidden) .filter((field: DetailsField) => !field.hidden)

View File

@ -13,7 +13,7 @@ export default function DetailsBadge(props: Readonly<DetailsBadgeProps>) {
} }
return ( return (
<Badge color={props.color} variant="filled" size={props.size ?? 'lg'}> <Badge color={props.color} variant='filled' size={props.size ?? 'lg'}>
{props.label} {props.label}
</Badge> </Badge>
); );

View File

@ -10,13 +10,17 @@ import {
rem, rem,
useMantineColorScheme useMantineColorScheme
} from '@mantine/core'; } from '@mantine/core';
import { Dropzone, FileWithPath, IMAGE_MIME_TYPE } from '@mantine/dropzone'; import {
Dropzone,
type FileWithPath,
IMAGE_MIME_TYPE
} from '@mantine/dropzone';
import { useHover } from '@mantine/hooks'; import { useHover } from '@mantine/hooks';
import { modals } from '@mantine/modals'; import { modals } from '@mantine/modals';
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import { api } from '../../App'; import { api } from '../../App';
import { UserRoles } from '../../enums/Roles'; import type { UserRoles } from '../../enums/Roles';
import { cancelEvent } from '../../functions/events'; import { cancelEvent } from '../../functions/events';
import { InvenTreeIcon } from '../../functions/icons'; import { InvenTreeIcon } from '../../functions/icons';
import { useEditApiFormModal } from '../../hooks/UseForm'; import { useEditApiFormModal } from '../../hooks/UseForm';
@ -66,7 +70,7 @@ const backup_image = '/static/img/blank_image.png';
*/ */
const removeModal = (apiPath: string, setImage: (image: string) => void) => const removeModal = (apiPath: string, setImage: (image: string) => void) =>
modals.openConfirmModal({ modals.openConfirmModal({
title: <StylishText size="xl">{t`Remove Image`}</StylishText>, title: <StylishText size='xl'>{t`Remove Image`}</StylishText>,
children: ( children: (
<Text> <Text>
<Trans>Remove the associated image from this item?</Trans> <Trans>Remove the associated image from this item?</Trans>
@ -95,12 +99,12 @@ function UploadModal({
// Components to show in the Dropzone when no file is selected // Components to show in the Dropzone when no file is selected
const noFileIdle = ( const noFileIdle = (
<Group> <Group>
<InvenTreeIcon icon="photo" iconProps={{ size: '3.2rem', stroke: 1.5 }} /> <InvenTreeIcon icon='photo' iconProps={{ size: '3.2rem', stroke: 1.5 }} />
<div> <div>
<Text size="xl" inline> <Text size='xl' inline>
<Trans>Drag and drop to upload</Trans> <Trans>Drag and drop to upload</Trans>
</Text> </Text>
<Text size="sm" c="dimmed" inline mt={7}> <Text size='sm' c='dimmed' inline mt={7}>
<Trans>Click to select file(s)</Trans> <Trans>Click to select file(s)</Trans>
</Text> </Text>
</div> </div>
@ -126,16 +130,16 @@ function UploadModal({
<Image <Image
src={imageUrl} src={imageUrl}
onLoad={() => URL.revokeObjectURL(imageUrl)} onLoad={() => URL.revokeObjectURL(imageUrl)}
radius="sm" radius='sm'
height={75} height={75}
fit="contain" fit='contain'
style={{ flexBasis: '40%' }} style={{ flexBasis: '40%' }}
/> />
<div style={{ flexBasis: '60%' }}> <div style={{ flexBasis: '60%' }}>
<Text size="xl" inline style={{ wordBreak: 'break-all' }}> <Text size='xl' inline style={{ wordBreak: 'break-all' }}>
{file.name} {file.name}
</Text> </Text>
<Text size="sm" c="dimmed" inline mt={7}> <Text size='sm' c='dimmed' inline mt={7}>
{size.toFixed(2)} MB {size.toFixed(2)} MB
</Text> </Text>
</div> </div>
@ -178,13 +182,13 @@ function UploadModal({
loading={uploading} loading={uploading}
> >
<Group <Group
justify="center" justify='center'
gap="xl" gap='xl'
style={{ minHeight: rem(140), pointerEvents: 'none' }} style={{ minHeight: rem(140), pointerEvents: 'none' }}
> >
<Dropzone.Accept> <Dropzone.Accept>
<InvenTreeIcon <InvenTreeIcon
icon="upload" icon='upload'
iconProps={{ iconProps={{
size: '3.2rem', size: '3.2rem',
stroke: 1.5, stroke: 1.5,
@ -194,7 +198,7 @@ function UploadModal({
</Dropzone.Accept> </Dropzone.Accept>
<Dropzone.Reject> <Dropzone.Reject>
<InvenTreeIcon <InvenTreeIcon
icon="reject" icon='reject'
iconProps={{ iconProps={{
size: '3.2rem', size: '3.2rem',
stroke: 1.5, stroke: 1.5,
@ -223,7 +227,7 @@ function UploadModal({
}} }}
> >
<Button <Button
variant="outline" variant='outline'
disabled={!currentFile} disabled={!currentFile}
onClick={() => setCurrentFile(null)} onClick={() => setCurrentFile(null)}
> >
@ -266,26 +270,26 @@ function ImageActionButtons({
<> <>
{visible && ( {visible && (
<Group <Group
gap="xs" gap='xs'
style={{ zIndex: 2, position: 'absolute', top: '10px', left: '10px' }} style={{ zIndex: 2, position: 'absolute', top: '10px', left: '10px' }}
> >
{actions.selectExisting && ( {actions.selectExisting && (
<ActionButton <ActionButton
icon={ icon={
<InvenTreeIcon <InvenTreeIcon
icon="select_image" icon='select_image'
iconProps={{ color: 'white' }} iconProps={{ color: 'white' }}
/> />
} }
tooltip={t`Select from existing images`} tooltip={t`Select from existing images`}
variant="outline" variant='outline'
size="lg" size='lg'
tooltipAlignment="top" tooltipAlignment='top'
onClick={(event: any) => { onClick={(event: any) => {
cancelEvent(event); cancelEvent(event);
modals.open({ modals.open({
title: <StylishText size="xl">{t`Select Image`}</StylishText>, title: <StylishText size='xl'>{t`Select Image`}</StylishText>,
size: 'xxl', size: 'xxl',
children: <PartThumbTable pk={pk} setImage={setImage} /> children: <PartThumbTable pk={pk} setImage={setImage} />
}); });
@ -297,14 +301,14 @@ function ImageActionButtons({
<ActionButton <ActionButton
icon={ icon={
<InvenTreeIcon <InvenTreeIcon
icon="download" icon='download'
iconProps={{ color: 'white' }} iconProps={{ color: 'white' }}
/> />
} }
tooltip={t`Download remote image`} tooltip={t`Download remote image`}
variant="outline" variant='outline'
size="lg" size='lg'
tooltipAlignment="top" tooltipAlignment='top'
onClick={(event: any) => { onClick={(event: any) => {
cancelEvent(event); cancelEvent(event);
downloadImage(); downloadImage();
@ -314,16 +318,16 @@ function ImageActionButtons({
{actions.uploadFile && ( {actions.uploadFile && (
<ActionButton <ActionButton
icon={ icon={
<InvenTreeIcon icon="upload" iconProps={{ color: 'white' }} /> <InvenTreeIcon icon='upload' iconProps={{ color: 'white' }} />
} }
tooltip={t`Upload new image`} tooltip={t`Upload new image`}
variant="outline" variant='outline'
size="lg" size='lg'
tooltipAlignment="top" tooltipAlignment='top'
onClick={(event: any) => { onClick={(event: any) => {
cancelEvent(event); cancelEvent(event);
modals.open({ modals.open({
title: <StylishText size="xl">{t`Upload Image`}</StylishText>, title: <StylishText size='xl'>{t`Upload Image`}</StylishText>,
children: ( children: (
<UploadModal apiPath={apiPath} setImage={setImage} /> <UploadModal apiPath={apiPath} setImage={setImage} />
) )
@ -334,12 +338,12 @@ function ImageActionButtons({
{actions.deleteFile && hasImage && ( {actions.deleteFile && hasImage && (
<ActionButton <ActionButton
icon={ icon={
<InvenTreeIcon icon="delete" iconProps={{ color: 'red' }} /> <InvenTreeIcon icon='delete' iconProps={{ color: 'red' }} />
} }
tooltip={t`Delete image`} tooltip={t`Delete image`}
variant="outline" variant='outline'
size="lg" size='lg'
tooltipAlignment="top" tooltipAlignment='top'
onClick={(event: any) => { onClick={(event: any) => {
cancelEvent(event); cancelEvent(event);
removeModal(apiPath, setImage); removeModal(apiPath, setImage);
@ -363,7 +367,7 @@ export function DetailsImage(props: Readonly<DetailImageProps>) {
// Sets a new image, and triggers upstream instance refresh // Sets a new image, and triggers upstream instance refresh
const setAndRefresh = (image: string) => { const setAndRefresh = (image: string) => {
setImg(image); setImg(image);
props.refresh && props.refresh(); props.refresh?.();
}; };
const permissions = useUserState(); const permissions = useUserState();
@ -403,7 +407,7 @@ export function DetailsImage(props: Readonly<DetailImageProps>) {
return ( return (
<> <>
{downloadImage.modal} {downloadImage.modal}
<AspectRatio ref={ref} maw={IMAGE_DIMENSION} ratio={1} pos="relative"> <AspectRatio ref={ref} maw={IMAGE_DIMENSION} ratio={1} pos='relative'>
<> <>
<ApiImage <ApiImage
src={img} src={img}
@ -414,12 +418,12 @@ export function DetailsImage(props: Readonly<DetailImageProps>) {
{permissions.hasChangeRole(props.appRole) && {permissions.hasChangeRole(props.appRole) &&
hasOverlay && hasOverlay &&
hovered && ( hovered && (
<Overlay color="black" opacity={0.8} onClick={expandImage}> <Overlay color='black' opacity={0.8} onClick={expandImage}>
<ImageActionButtons <ImageActionButtons
visible={hovered} visible={hovered}
actions={props.imageActions} actions={props.imageActions}
apiPath={props.apiPath} apiPath={props.apiPath}
hasImage={props.src ? true : false} hasImage={!!props.src}
pk={props.pk} pk={props.pk}
setImage={setAndRefresh} setImage={setAndRefresh}
downloadImage={downloadImage.open} downloadImage={downloadImage.open}

View File

@ -1,10 +1,10 @@
import { Paper, SimpleGrid } from '@mantine/core'; import { Paper, SimpleGrid } from '@mantine/core';
import React from 'react'; import type React from 'react';
export function ItemDetailsGrid(props: React.PropsWithChildren<{}>) { export function ItemDetailsGrid(props: React.PropsWithChildren<{}>) {
return ( return (
<Paper p="xs"> <Paper p='xs'>
<SimpleGrid cols={2} spacing="xs" verticalSpacing="xs"> <SimpleGrid cols={2} spacing='xs' verticalSpacing='xs'>
{props.children} {props.children}
</SimpleGrid> </SimpleGrid>
</Paper> </Paper>

View File

@ -2,14 +2,14 @@ import { t } from '@lingui/macro';
import { notifications } from '@mantine/notifications'; import { notifications } from '@mantine/notifications';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import DOMPurify from 'dompurify'; import DOMPurify from 'dompurify';
import EasyMDE, { default as SimpleMde } from 'easymde'; import EasyMDE, { type default as SimpleMde } from 'easymde';
import 'easymde/dist/easymde.min.css'; import 'easymde/dist/easymde.min.css';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import SimpleMDE from 'react-simplemde-editor'; import SimpleMDE from 'react-simplemde-editor';
import { api } from '../../App'; import { api } from '../../App';
import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType'; import type { ModelType } from '../../enums/ModelType';
import { apiUrl } from '../../states/ApiState'; import { apiUrl } from '../../states/ApiState';
import { ModelInformationDict } from '../render/ModelType'; import { ModelInformationDict } from '../render/ModelType';
@ -126,7 +126,7 @@ export default function NotesEditor({
.catch((error) => { .catch((error) => {
notifications.hide('notes'); notifications.hide('notes');
let msg = const msg =
error?.response?.data?.non_field_errors[0] ?? error?.response?.data?.non_field_errors[0] ??
t`Failed to save notes`; t`Failed to save notes`;
@ -142,7 +142,7 @@ export default function NotesEditor({
); );
const editorOptions: SimpleMde.Options = useMemo(() => { const editorOptions: SimpleMde.Options = useMemo(() => {
let icons: any[] = []; const icons: any[] = [];
if (editing) { if (editing) {
icons.push({ icons.push({
@ -201,13 +201,14 @@ export default function NotesEditor({
useEffect(() => { useEffect(() => {
if (mdeInstance) { if (mdeInstance) {
let previewMode = !(editable && editing); const previewMode = !(editable && editing);
mdeInstance.codemirror?.setOption('readOnly', previewMode); mdeInstance.codemirror?.setOption('readOnly', previewMode);
// Ensure the preview mode is toggled if required // Ensure the preview mode is toggled if required
if (mdeInstance.isPreviewActive() != previewMode) { if (mdeInstance.isPreviewActive() != previewMode) {
let sibling = mdeInstance?.codemirror.getWrapperElement()?.nextSibling; const sibling =
mdeInstance?.codemirror.getWrapperElement()?.nextSibling;
if (sibling != null) { if (sibling != null) {
EasyMDE.togglePreview(mdeInstance); EasyMDE.togglePreview(mdeInstance);

View File

@ -9,7 +9,7 @@ import {
useState useState
} from 'react'; } from 'react';
import { EditorComponent } from '../TemplateEditor'; import type { EditorComponent } from '../TemplateEditor';
type Tag = { type Tag = {
label: string; label: string;
@ -88,8 +88,8 @@ Returns: <small>${tag.returns}</small>`;
const tooltips = hoverTooltip((view, pos, side) => { const tooltips = hoverTooltip((view, pos, side) => {
// extract the word at the current hover position into the variable text // extract the word at the current hover position into the variable text
let { from, to, text } = view.state.doc.lineAt(pos); let { from, to, text } = view.state.doc.lineAt(pos);
let start = pos, let start = pos;
end = pos; let end = pos;
while (start > from && /\w/.test(text[start - from - 1])) start--; while (start > from && /\w/.test(text[start - from - 1])) start--;
while (end < to && /\w/.test(text[end - from])) end++; while (end < to && /\w/.test(text[end - from])) end++;
if ((start == pos && side < 0) || (end == pos && side > 0)) return null; if ((start == pos && side < 0) || (end == pos && side > 0)) return null;
@ -152,7 +152,7 @@ export const CodeEditorComponent: EditorComponent = forwardRef((props, ref) => {
<div <div
style={{ position: 'absolute', top: 0, bottom: 0, left: 0, right: 0 }} style={{ position: 'absolute', top: 0, bottom: 0, left: 0, right: 0 }}
ref={editor} ref={editor}
></div> />
</div> </div>
); );
}); });

View File

@ -1,7 +1,7 @@
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { IconCode } from '@tabler/icons-react'; import { IconCode } from '@tabler/icons-react';
import { Editor } from '../TemplateEditor'; import type { Editor } from '../TemplateEditor';
import { CodeEditorComponent } from './CodeEditor'; import { CodeEditorComponent } from './CodeEditor';
export const CodeEditor: Editor = { export const CodeEditor: Editor = {

View File

@ -2,7 +2,7 @@ import { Trans } from '@lingui/macro';
import { forwardRef, useImperativeHandle, useState } from 'react'; import { forwardRef, useImperativeHandle, useState } from 'react';
import { api } from '../../../../App'; import { api } from '../../../../App';
import { PreviewAreaComponent } from '../TemplateEditor'; import type { PreviewAreaComponent } from '../TemplateEditor';
export const PdfPreviewComponent: PreviewAreaComponent = forwardRef( export const PdfPreviewComponent: PreviewAreaComponent = forwardRef(
(props, ref) => { (props, ref) => {
@ -56,13 +56,13 @@ export const PdfPreviewComponent: PreviewAreaComponent = forwardRef(
}); });
} }
let pdf = new Blob([preview.data], { const pdf = new Blob([preview.data], {
type: preview.headers['content-type'] type: preview.headers['content-type']
}); });
let srcUrl = URL.createObjectURL(pdf); const srcUrl = URL.createObjectURL(pdf);
setPdfUrl(srcUrl + '#view=fitH'); setPdfUrl(`${srcUrl}#view=fitH`);
} }
})); }));
@ -82,7 +82,7 @@ export const PdfPreviewComponent: PreviewAreaComponent = forwardRef(
</div> </div>
)} )}
{pdfUrl && ( {pdfUrl && (
<iframe src={pdfUrl} width="100%" height="100%" title="PDF Preview" /> <iframe src={pdfUrl} width='100%' height='100%' title='PDF Preview' />
)} )}
</> </>
); );

View File

@ -1,7 +1,7 @@
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { IconFileTypePdf } from '@tabler/icons-react'; import { IconFileTypePdf } from '@tabler/icons-react';
import { PreviewArea } from '../TemplateEditor'; import type { PreviewArea } from '../TemplateEditor';
import { PdfPreviewComponent } from './PdfPreview'; import { PdfPreviewComponent } from './PdfPreview';
export const PdfPreview: PreviewArea = { export const PdfPreview: PreviewArea = {

View File

@ -21,19 +21,14 @@ import {
IconRefresh IconRefresh
} from '@tabler/icons-react'; } from '@tabler/icons-react';
import Split from '@uiw/react-split'; import Split from '@uiw/react-split';
import React, { import type React from 'react';
useCallback, import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
useEffect,
useMemo,
useRef,
useState
} from 'react';
import { api } from '../../../App'; import { api } from '../../../App';
import { ModelType } from '../../../enums/ModelType'; import { ModelType } from '../../../enums/ModelType';
import { TablerIconType } from '../../../functions/icons'; import type { TablerIconType } from '../../../functions/icons';
import { apiUrl } from '../../../states/ApiState'; import { apiUrl } from '../../../states/ApiState';
import { TemplateI } from '../../../tables/settings/TemplateTable'; import type { TemplateI } from '../../../tables/settings/TemplateTable';
import { Boundary } from '../../Boundary'; import { Boundary } from '../../Boundary';
import { SplitButton } from '../../buttons/SplitButton'; import { SplitButton } from '../../buttons/SplitButton';
import { StandaloneField } from '../../forms/StandaloneField'; import { StandaloneField } from '../../forms/StandaloneField';
@ -166,13 +161,13 @@ export function TemplateEditor(props: Readonly<TemplateEditorProps>) {
}, [editorValue]); }, [editorValue]);
const updatePreview = useCallback( const updatePreview = useCallback(
async (confirmed: boolean, saveTemplate: boolean = true) => { async (confirmed: boolean, saveTemplate = true) => {
if (!confirmed) { if (!confirmed) {
openConfirmModal({ openConfirmModal({
title: t`Save & Reload Preview`, title: t`Save & Reload Preview`,
children: ( children: (
<Alert <Alert
color="yellow" color='yellow'
icon={<IconAlertTriangle />} icon={<IconAlertTriangle />}
title={t`Are you sure you want to Save & Reload the preview?`} title={t`Are you sure you want to Save & Reload the preview?`}
> >
@ -254,7 +249,7 @@ export function TemplateEditor(props: Readonly<TemplateEditorProps>) {
}, [previewApiUrl, templateFilters]); }, [previewApiUrl, templateFilters]);
return ( return (
<Boundary label="TemplateEditor"> <Boundary label='TemplateEditor'>
<Stack style={{ height: '100%', flex: '1' }}> <Stack style={{ height: '100%', flex: '1' }}>
<Split style={{ gap: '10px' }}> <Split style={{ gap: '10px' }}>
<Tabs <Tabs
@ -277,18 +272,18 @@ export function TemplateEditor(props: Readonly<TemplateEditorProps>) {
<Tabs.Tab <Tabs.Tab
key={Editor.key} key={Editor.key}
value={Editor.key} value={Editor.key}
leftSection={Editor.icon && <Editor.icon size="0.8rem" />} leftSection={Editor.icon && <Editor.icon size='0.8rem' />}
> >
{Editor.name} {Editor.name}
</Tabs.Tab> </Tabs.Tab>
); );
})} })}
<Group justify="right" style={{ flex: '1' }} wrap="nowrap"> <Group justify='right' style={{ flex: '1' }} wrap='nowrap'>
<SplitButton <SplitButton
loading={isPreviewLoading} loading={isPreviewLoading}
defaultSelected="preview_save" defaultSelected='preview_save'
name="preview-options" name='preview-options'
options={[ options={[
{ {
key: 'preview', key: 'preview',
@ -342,7 +337,7 @@ export function TemplateEditor(props: Readonly<TemplateEditorProps>) {
key={PreviewArea.key} key={PreviewArea.key}
value={PreviewArea.key} value={PreviewArea.key}
leftSection={ leftSection={
PreviewArea.icon && <PreviewArea.icon size="0.8rem" /> PreviewArea.icon && <PreviewArea.icon size='0.8rem' />
} }
> >
{PreviewArea.name} {PreviewArea.name}
@ -392,7 +387,7 @@ export function TemplateEditor(props: Readonly<TemplateEditorProps>) {
<PreviewArea.component ref={previewRef} /> <PreviewArea.component ref={previewRef} />
{errorOverlay && ( {errorOverlay && (
<Overlay color="red" center blur={0.2}> <Overlay color='red' center blur={0.2}>
<CloseButton <CloseButton
onClick={() => setErrorOverlay(null)} onClick={() => setErrorOverlay(null)}
style={{ style={{
@ -401,13 +396,13 @@ export function TemplateEditor(props: Readonly<TemplateEditorProps>) {
right: '10px', right: '10px',
color: '#fff' color: '#fff'
}} }}
variant="filled" variant='filled'
/> />
<Alert <Alert
color="red" color='red'
icon={<IconExclamationCircle />} icon={<IconExclamationCircle />}
title={t`Error rendering template`} title={t`Error rendering template`}
mx="10px" mx='10px'
> >
<Code>{errorOverlay}</Code> <Code>{errorOverlay}</Code>
</Alert> </Alert>

View File

@ -31,20 +31,20 @@ export default function ErrorPage({
return ( return (
<LanguageContext> <LanguageContext>
<Center> <Center>
<Container w="md" miw={400}> <Container w='md' miw={400}>
<Card withBorder shadow="xs" padding="xl" radius="sm"> <Card withBorder shadow='xs' padding='xl' radius='sm'>
<Card.Section p="lg"> <Card.Section p='lg'>
<Group gap="xs"> <Group gap='xs'>
<ActionIcon color="red" variant="transparent" size="xl"> <ActionIcon color='red' variant='transparent' size='xl'>
<IconExclamationCircle /> <IconExclamationCircle />
</ActionIcon> </ActionIcon>
<Text size="xl">{title}</Text> <Text size='xl'>{title}</Text>
</Group> </Group>
</Card.Section> </Card.Section>
<Divider /> <Divider />
<Card.Section p="lg"> <Card.Section p='lg'>
<Stack gap="md"> <Stack gap='md'>
<Text size="lg">{message}</Text> <Text size='lg'>{message}</Text>
{status && ( {status && (
<Text> <Text>
<Trans>Status Code</Trans>: {status} <Trans>Status Code</Trans>: {status}
@ -53,11 +53,11 @@ export default function ErrorPage({
</Stack> </Stack>
</Card.Section> </Card.Section>
<Divider /> <Divider />
<Card.Section p="lg"> <Card.Section p='lg'>
<Center> <Center>
<Button <Button
variant="outline" variant='outline'
color="green" color='green'
onClick={() => navigate('/')} onClick={() => navigate('/')}
> >
<Trans>Return to the index page</Trans> <Trans>Return to the index page</Trans>

View File

@ -2,7 +2,7 @@ import { t } from '@lingui/macro';
import { import {
Alert, Alert,
Button, Button,
DefaultMantineColor, type DefaultMantineColor,
Divider, Divider,
Group, Group,
LoadingOverlay, LoadingOverlay,
@ -15,19 +15,19 @@ import { notifications } from '@mantine/notifications';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import { import {
FieldValues, type FieldValues,
FormProvider, FormProvider,
SubmitErrorHandler, type SubmitErrorHandler,
SubmitHandler, type SubmitHandler,
useForm useForm
} from 'react-hook-form'; } from 'react-hook-form';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { api, queryClient } from '../../App'; import { api, queryClient } from '../../App';
import { ApiEndpoints } from '../../enums/ApiEndpoints'; import type { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType'; import type { ModelType } from '../../enums/ModelType';
import { import {
NestedDict, type NestedDict,
constructField, constructField,
constructFormUrl, constructFormUrl,
extractAvailableFields, extractAvailableFields,
@ -38,13 +38,13 @@ import {
showTimeoutNotification showTimeoutNotification
} from '../../functions/notifications'; } from '../../functions/notifications';
import { getDetailUrl } from '../../functions/urls'; import { getDetailUrl } from '../../functions/urls';
import { TableState } from '../../hooks/UseTable'; import type { TableState } from '../../hooks/UseTable';
import { PathParams } from '../../states/ApiState'; import type { PathParams } from '../../states/ApiState';
import { Boundary } from '../Boundary'; import { Boundary } from '../Boundary';
import { import {
ApiFormField, ApiFormField,
ApiFormFieldSet, type ApiFormFieldSet,
ApiFormFieldType type ApiFormFieldType
} from './fields/ApiFormField'; } from './fields/ApiFormField';
export interface ApiFormAction { export interface ApiFormAction {
@ -137,7 +137,7 @@ export function OptionsApiForm({
props.pathParams props.pathParams
], ],
queryFn: async () => { queryFn: async () => {
let response = await api.options(url); const response = await api.options(url);
let fields: Record<string, ApiFormFieldType> | null = {}; let fields: Record<string, ApiFormFieldType> | null = {};
if (!props.ignorePermissionCheck) { if (!props.ignorePermissionCheck) {
fields = extractAvailableFields(response, props.method); fields = extractAvailableFields(response, props.method);
@ -173,7 +173,7 @@ export function OptionsApiForm({
}); });
// If the user has specified initial data, use that value here // If the user has specified initial data, use that value here
let value = _props?.initialData?.[k]; const value = _props?.initialData?.[k];
if (value) { if (value) {
_props.fields[k].value = value; _props.fields[k].value = value;
@ -212,7 +212,7 @@ export function ApiForm({
); );
const defaultValues: FieldValues = useMemo(() => { const defaultValues: FieldValues = useMemo(() => {
let defaultValuesMap = mapFields(fields ?? {}, (_path, field) => { const defaultValuesMap = mapFields(fields ?? {}, (_path, field) => {
return field.value ?? field.default ?? undefined; return field.value ?? field.default ?? undefined;
}); });
@ -306,9 +306,9 @@ export function ApiForm({
}); });
useEffect(() => { useEffect(() => {
let _fields: any = props.fields || {}; const _fields: any = props.fields || {};
let _initialData: any = props.initialData || {}; const _initialData: any = props.initialData || {};
let _fetchedData: any = initialDataQuery.data || {}; const _fetchedData: any = initialDataQuery.data || {};
for (const k of Object.keys(_fields)) { for (const k of Object.keys(_fields)) {
// Ensure default values override initial field spec // Ensure default values override initial field spec
@ -391,7 +391,7 @@ export function ApiForm({
const submitForm: SubmitHandler<FieldValues> = async (data) => { const submitForm: SubmitHandler<FieldValues> = async (data) => {
setNonFieldErrors([]); setNonFieldErrors([]);
let method = props.method?.toLowerCase() ?? 'get'; const method = props.method?.toLowerCase() ?? 'get';
let hasFiles = false; let hasFiles = false;
@ -400,13 +400,13 @@ export function ApiForm({
data = props.processFormData(data); data = props.processFormData(data);
} }
let jsonData = { ...data }; const jsonData = { ...data };
let formData = new FormData(); const formData = new FormData();
Object.keys(data).forEach((key: string) => { Object.keys(data).forEach((key: string) => {
let value: any = data[key]; let value: any = data[key];
let field_type = fields[key]?.field_type; const field_type = fields[key]?.field_type;
let exclude = fields[key]?.exclude; const exclude = fields[key]?.exclude;
if (field_type == 'file upload' && !!value) { if (field_type == 'file upload' && !!value) {
hasFiles = true; hasFiles = true;
@ -457,7 +457,7 @@ export function ApiForm({
navigate(getDetailUrl(props.modelType, response.data?.pk)); navigate(getDetailUrl(props.modelType, response.data?.pk));
} else if (props.table) { } else if (props.table) {
// If we want to automatically update or reload a linked table // If we want to automatically update or reload a linked table
let pk_field = props.pk_field ?? 'pk'; const pk_field = props.pk_field ?? 'pk';
if (props.pk && response?.data[pk_field]) { if (props.pk && response?.data[pk_field]) {
props.table.updateRecord(response.data); props.table.updateRecord(response.data);
@ -499,8 +499,8 @@ export function ApiForm({
const path = _path ? `${_path}.${k}` : k; const path = _path ? `${_path}.${k}` : k;
// Determine if field "k" is valid (exists and is visible) // Determine if field "k" is valid (exists and is visible)
let field = fields[k]; const field = fields[k];
let valid = field && !field.hidden; const valid = field && !field.hidden;
if (!valid || k === 'non_field_errors' || k === '__all__') { if (!valid || k === 'non_field_errors' || k === '__all__') {
if (Array.isArray(v)) { if (Array.isArray(v)) {
@ -574,11 +574,11 @@ export function ApiForm({
<Paper mah={'65vh'} style={{ overflowY: 'auto' }}> <Paper mah={'65vh'} style={{ overflowY: 'auto' }}>
<div> <div>
{/* Form Fields */} {/* Form Fields */}
<Stack gap="sm"> <Stack gap='sm'>
{(!isValid || nonFieldErrors.length > 0) && ( {(!isValid || nonFieldErrors.length > 0) && (
<Alert radius="sm" color="red" title={t`Form Error`}> <Alert radius='sm' color='red' title={t`Form Error`}>
{nonFieldErrors.length > 0 ? ( {nonFieldErrors.length > 0 ? (
<Stack gap="xs"> <Stack gap='xs'>
{nonFieldErrors.map((message) => ( {nonFieldErrors.map((message) => (
<Text key={message}>{message}</Text> <Text key={message}>{message}</Text>
))} ))}
@ -591,19 +591,19 @@ export function ApiForm({
<Boundary label={`ApiForm-${id}-PreFormContent`}> <Boundary label={`ApiForm-${id}-PreFormContent`}>
{props.preFormContent} {props.preFormContent}
{props.preFormSuccess && ( {props.preFormSuccess && (
<Alert color="green" radius="sm"> <Alert color='green' radius='sm'>
{props.preFormSuccess} {props.preFormSuccess}
</Alert> </Alert>
)} )}
{props.preFormWarning && ( {props.preFormWarning && (
<Alert color="orange" radius="sm"> <Alert color='orange' radius='sm'>
{props.preFormWarning} {props.preFormWarning}
</Alert> </Alert>
)} )}
</Boundary> </Boundary>
<Boundary label={`ApiForm-${id}-FormContent`}> <Boundary label={`ApiForm-${id}-FormContent`}>
<FormProvider {...form}> <FormProvider {...form}>
<Stack gap="xs"> <Stack gap='xs'>
{Object.entries(fields).map(([fieldName, field]) => { {Object.entries(fields).map(([fieldName, field]) => {
return ( return (
<ApiFormField <ApiFormField
@ -638,13 +638,13 @@ export function ApiForm({
{/* Footer with Action Buttons */} {/* Footer with Action Buttons */}
<Divider /> <Divider />
<div> <div>
<Group justify="right"> <Group justify='right'>
{props.actions?.map((action, i) => ( {props.actions?.map((action, i) => (
<Button <Button
key={i} key={`${i}-${action.text}`}
onClick={action.onClick} onClick={action.onClick}
variant={action.variant ?? 'outline'} variant={action.variant ?? 'outline'}
radius="sm" radius='sm'
color={action.color} color={action.color}
> >
{action.text} {action.text}
@ -652,8 +652,8 @@ export function ApiForm({
))} ))}
<Button <Button
onClick={form.handleSubmit(submitForm, onFormError)} onClick={form.handleSubmit(submitForm, onFormError)}
variant="filled" variant='filled'
radius="sm" radius='sm'
color={props.submitColor ?? 'green'} color={props.submitColor ?? 'green'}
disabled={isLoading || (props.fetchInitialData && !isDirty)} disabled={isLoading || (props.fetchInitialData && !isDirty)}
> >

View File

@ -88,7 +88,7 @@ export function AuthenticationForm() {
<> <>
{auth_settings?.sso_enabled === true ? ( {auth_settings?.sso_enabled === true ? (
<> <>
<Group grow mb="md" mt="md"> <Group grow mb='md' mt='md'>
{auth_settings.providers.map((provider) => ( {auth_settings.providers.map((provider) => (
<SsoButton provider={provider} key={provider.id} /> <SsoButton provider={provider} key={provider.id} />
))} ))}
@ -96,8 +96,8 @@ export function AuthenticationForm() {
<Divider <Divider
label={t`Or continue with other methods`} label={t`Or continue with other methods`}
labelPosition="center" labelPosition='center'
my="lg" my='lg'
/> />
</> </>
) : null} ) : null}
@ -117,12 +117,12 @@ export function AuthenticationForm() {
{...classicForm.getInputProps('password')} {...classicForm.getInputProps('password')}
/> />
{auth_settings?.password_forgotten_enabled === true && ( {auth_settings?.password_forgotten_enabled === true && (
<Group justify="space-between" mt="0"> <Group justify='space-between' mt='0'>
<Anchor <Anchor
component="button" component='button'
type="button" type='button'
c="dimmed" c='dimmed'
size="xs" size='xs'
onClick={() => navigate('/reset-password')} onClick={() => navigate('/reset-password')}
> >
<Trans>Reset password</Trans> <Trans>Reset password</Trans>
@ -136,18 +136,18 @@ export function AuthenticationForm() {
required required
label={t`Email`} label={t`Email`}
description={t`We will send you a link to login - if you are registered`} description={t`We will send you a link to login - if you are registered`}
placeholder="email@example.org" placeholder='email@example.org'
{...simpleForm.getInputProps('email')} {...simpleForm.getInputProps('email')}
/> />
</Stack> </Stack>
)} )}
<Group justify="space-between" mt="xl"> <Group justify='space-between' mt='xl'>
<Anchor <Anchor
component="button" component='button'
type="button" type='button'
c="dimmed" c='dimmed'
size="xs" size='xs'
onClick={() => setMode.toggle()} onClick={() => setMode.toggle()}
> >
{classicLoginMode ? ( {classicLoginMode ? (
@ -156,9 +156,9 @@ export function AuthenticationForm() {
<Trans>Use username and password</Trans> <Trans>Use username and password</Trans>
)} )}
</Anchor> </Anchor>
<Button type="submit" disabled={isLoggingIn} onClick={handleLogin}> <Button type='submit' disabled={isLoggingIn} onClick={handleLogin}>
{isLoggingIn ? ( {isLoggingIn ? (
<Loader size="sm" /> <Loader size='sm' />
) : ( ) : (
<> <>
{classicLoginMode ? ( {classicLoginMode ? (
@ -235,7 +235,7 @@ export function RegistrationForm() {
required required
label={t`Email`} label={t`Email`}
description={t`This will be used for a confirmation`} description={t`This will be used for a confirmation`}
placeholder="email@example.org" placeholder='email@example.org'
{...registrationForm.getInputProps('email')} {...registrationForm.getInputProps('email')}
/> />
<PasswordInput <PasswordInput
@ -252,9 +252,9 @@ export function RegistrationForm() {
/> />
</Stack> </Stack>
<Group justify="space-between" mt="xl"> <Group justify='space-between' mt='xl'>
<Button <Button
type="submit" type='submit'
disabled={isRegistering} disabled={isRegistering}
onClick={handleRegistration} onClick={handleRegistration}
fullWidth fullWidth
@ -265,10 +265,10 @@ export function RegistrationForm() {
</form> </form>
)} )}
{both_reg_enabled && ( {both_reg_enabled && (
<Divider label={t`Or use SSO`} labelPosition="center" my="lg" /> <Divider label={t`Or use SSO`} labelPosition='center' my='lg' />
)} )}
{auth_settings?.sso_registration === true && ( {auth_settings?.sso_registration === true && (
<Group grow mb="md" mt="md"> <Group grow mb='md' mt='md'>
{auth_settings.providers.map((provider) => ( {auth_settings.providers.map((provider) => (
<SsoButton provider={provider} key={provider.id} /> <SsoButton provider={provider} key={provider.id} />
))} ))}
@ -293,15 +293,15 @@ export function ModeSelector({
if (registration_enabled === false) return null; if (registration_enabled === false) return null;
return ( return (
<Text ta="center" size={'xs'} mt={'md'}> <Text ta='center' size={'xs'} mt={'md'}>
{loginMode ? ( {loginMode ? (
<> <>
<Trans>Don&apos;t have an account?</Trans>{' '} <Trans>Don&apos;t have an account?</Trans>{' '}
<Anchor <Anchor
component="button" component='button'
type="button" type='button'
c="dimmed" c='dimmed'
size="xs" size='xs'
onClick={() => setMode.close()} onClick={() => setMode.close()}
> >
<Trans>Register</Trans> <Trans>Register</Trans>
@ -309,10 +309,10 @@ export function ModeSelector({
</> </>
) : ( ) : (
<Anchor <Anchor
component="button" component='button'
type="button" type='button'
c="dimmed" c='dimmed'
size="xs" size='xs'
onClick={() => setMode.open()} onClick={() => setMode.open()}
> >
<Trans>Go back to login</Trans> <Trans>Go back to login</Trans>

View File

@ -12,7 +12,7 @@ import { useForm } from '@mantine/form';
import { randomId } from '@mantine/hooks'; import { randomId } from '@mantine/hooks';
import { IconSquarePlus, IconTrash } from '@tabler/icons-react'; import { IconSquarePlus, IconTrash } from '@tabler/icons-react';
import { HostList } from '../../states/states'; import type { HostList } from '../../states/states';
export function HostOptionsForm({ export function HostOptionsForm({
data, data,
@ -29,7 +29,7 @@ export function HostOptionsForm({
} }
const fields = Object.entries(form.values).map(([key]) => ( const fields = Object.entries(form.values).map(([key]) => (
<Group key={key} mt="xs"> <Group key={key} mt='xs'>
{form.values[key] !== undefined && ( {form.values[key] !== undefined && (
<> <>
<TextInput <TextInput
@ -45,11 +45,11 @@ export function HostOptionsForm({
{...form.getInputProps(`${key}.name`)} {...form.getInputProps(`${key}.name`)}
/> />
<ActionIcon <ActionIcon
color="red" color='red'
onClick={() => { onClick={() => {
deleteItem(key); deleteItem(key);
}} }}
variant="default" variant='default'
> >
<IconTrash /> <IconTrash />
</ActionIcon> </ActionIcon>
@ -60,23 +60,23 @@ export function HostOptionsForm({
return ( return (
<form onSubmit={form.onSubmit(saveOptions)}> <form onSubmit={form.onSubmit(saveOptions)}>
<Box style={{ maxWidth: 500 }} mx="auto"> <Box style={{ maxWidth: 500 }} mx='auto'>
{fields.length > 0 ? ( {fields.length > 0 ? (
<Group mb="xs"> <Group mb='xs'>
<Text fw={500} size="sm" style={{ flex: 1 }}> <Text fw={500} size='sm' style={{ flex: 1 }}>
<Trans>Host</Trans> <Trans>Host</Trans>
</Text> </Text>
<Text fw={500} size="sm" style={{ flex: 1 }}> <Text fw={500} size='sm' style={{ flex: 1 }}>
<Trans>Name</Trans> <Trans>Name</Trans>
</Text> </Text>
</Group> </Group>
) : ( ) : (
<Text c="dimmed" ta="center"> <Text c='dimmed' ta='center'>
<Trans>No one here...</Trans> <Trans>No one here...</Trans>
</Text> </Text>
)} )}
{fields} {fields}
<Group mt="md"> <Group mt='md'>
<Button <Button
onClick={() => onClick={() =>
form.setFieldValue(`${randomId()}`, { name: '', host: '' }) form.setFieldValue(`${randomId()}`, { name: '', host: '' })
@ -86,7 +86,7 @@ export function HostOptionsForm({
<Trans>Add Host</Trans> <Trans>Add Host</Trans>
</Button> </Button>
<Space style={{ flex: 1 }} /> <Space style={{ flex: 1 }} />
<Button type="submit"> <Button type='submit'>
<Trans>Save</Trans> <Trans>Save</Trans>
</Button> </Button>
</Group> </Group>

View File

@ -5,7 +5,7 @@ import { IconCheck } from '@tabler/icons-react';
import { useServerApiState } from '../../states/ApiState'; import { useServerApiState } from '../../states/ApiState';
import { useLocalState } from '../../states/LocalState'; import { useLocalState } from '../../states/LocalState';
import { HostList } from '../../states/states'; import type { HostList } from '../../states/states';
import { EditButton } from '../buttons/EditButton'; import { EditButton } from '../buttons/EditButton';
import { HostOptionsForm } from './HostOptionsForm'; import { HostOptionsForm } from './HostOptionsForm';

View File

@ -1,7 +1,7 @@
import { useEffect, useMemo } from 'react'; import { useEffect, useMemo } from 'react';
import { FormProvider, useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
import { ApiFormField, ApiFormFieldType } from './fields/ApiFormField'; import { ApiFormField, type ApiFormFieldType } from './fields/ApiFormField';
export function StandaloneField({ export function StandaloneField({
fieldDefinition, fieldDefinition,

View File

@ -1,11 +1,11 @@
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { Alert, FileInput, NumberInput, Stack, Switch } from '@mantine/core'; import { Alert, FileInput, NumberInput, Stack, Switch } from '@mantine/core';
import { UseFormReturnType } from '@mantine/form'; import type { UseFormReturnType } from '@mantine/form';
import { useId } from '@mantine/hooks'; import { useId } from '@mantine/hooks';
import { ReactNode, useCallback, useEffect, useMemo } from 'react'; import { type ReactNode, useCallback, useEffect, useMemo } from 'react';
import { Control, FieldValues, useController } from 'react-hook-form'; import { type Control, type FieldValues, useController } from 'react-hook-form';
import { ModelType } from '../../../enums/ModelType'; import type { ModelType } from '../../../enums/ModelType';
import { isTrue } from '../../../functions/conversion'; import { isTrue } from '../../../functions/conversion';
import { ChoiceField } from './ChoiceField'; import { ChoiceField } from './ChoiceField';
import DateField from './DateField'; import DateField from './DateField';
@ -167,15 +167,16 @@ export function ApiFormField({
// Callback helper when form value changes // Callback helper when form value changes
const onChange = useCallback( const onChange = useCallback(
(value: any) => { (value: any) => {
let rtnValue = value;
// Allow for custom value adjustments (per field) // Allow for custom value adjustments (per field)
if (definition.adjustValue) { if (definition.adjustValue) {
value = definition.adjustValue(value); rtnValue = definition.adjustValue(value);
} }
field.onChange(value); field.onChange(rtnValue);
// Run custom callback for this field // Run custom callback for this field
definition.onValueChange?.(value); definition.onValueChange?.(rtnValue);
}, },
[fieldName, definition] [fieldName, definition]
); );
@ -186,18 +187,18 @@ export function ApiFormField({
switch (definition.field_type) { switch (definition.field_type) {
case 'integer': case 'integer':
val = parseInt(value) ?? ''; val = Number.parseInt(value) ?? '';
break; break;
case 'decimal': case 'decimal':
case 'float': case 'float':
case 'number': case 'number':
val = parseFloat(value) ?? ''; val = Number.parseFloat(value) ?? '';
break; break;
default: default:
break; break;
} }
if (isNaN(val) || !isFinite(val)) { if (Number.isNaN(val) || !Number.isFinite(val)) {
val = ''; val = '';
} }
@ -246,8 +247,8 @@ export function ApiFormField({
ref={ref} ref={ref}
id={fieldId} id={fieldId}
aria-label={`boolean-field-${fieldName}`} aria-label={`boolean-field-${fieldName}`}
radius="lg" radius='lg'
size="sm" size='sm'
error={error?.message} error={error?.message}
onChange={(event) => onChange(event.currentTarget.checked)} onChange={(event) => onChange(event.currentTarget.checked)}
/> />
@ -264,7 +265,7 @@ export function ApiFormField({
return ( return (
<NumberInput <NumberInput
{...reducedDefinition} {...reducedDefinition}
radius="sm" radius='sm'
ref={field.ref} ref={field.ref}
id={fieldId} id={fieldId}
aria-label={`number-field-${field.name}`} aria-label={`number-field-${field.name}`}
@ -289,7 +290,7 @@ export function ApiFormField({
{...reducedDefinition} {...reducedDefinition}
id={fieldId} id={fieldId}
ref={field.ref} ref={field.ref}
radius="sm" radius='sm'
value={value} value={value}
error={error?.message} error={error?.message}
onChange={(payload: File | null) => onChange(payload)} onChange={(payload: File | null) => onChange(payload)}
@ -325,7 +326,7 @@ export function ApiFormField({
); );
default: default:
return ( return (
<Alert color="red" title={t`Error`}> <Alert color='red' title={t`Error`}>
Invalid field type for field '{fieldName}': ' Invalid field type for field '{fieldName}': '
{fieldDefinition.field_type}' {fieldDefinition.field_type}'
</Alert> </Alert>

View File

@ -1,9 +1,9 @@
import { Select } from '@mantine/core'; import { Select } from '@mantine/core';
import { useId } from '@mantine/hooks'; import { useId } from '@mantine/hooks';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import { FieldValues, UseControllerReturn } from 'react-hook-form'; import type { FieldValues, UseControllerReturn } from 'react-hook-form';
import { ApiFormFieldType } from './ApiFormField'; import type { ApiFormFieldType } from './ApiFormField';
/** /**
* Render a 'select' field for selecting from a list of choices * Render a 'select' field for selecting from a list of choices
@ -28,7 +28,7 @@ export function ChoiceField({
// Build a set of choices for the field // Build a set of choices for the field
const choices: any[] = useMemo(() => { const choices: any[] = useMemo(() => {
let choices = definition.choices ?? []; const choices = definition.choices ?? [];
// TODO: Allow provision of custom render function also // TODO: Allow provision of custom render function also
@ -64,7 +64,7 @@ export function ChoiceField({
id={fieldId} id={fieldId}
aria-label={`choice-field-${field.name}`} aria-label={`choice-field-${field.name}`}
error={error?.message} error={error?.message}
radius="sm" radius='sm'
{...field} {...field}
onChange={onChange} onChange={onChange}
data={choices} data={choices}

View File

@ -2,9 +2,9 @@ import { DateInput } from '@mantine/dates';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import customParseFormat from 'dayjs/plugin/customParseFormat'; import customParseFormat from 'dayjs/plugin/customParseFormat';
import { useCallback, useId, useMemo } from 'react'; import { useCallback, useId, useMemo } from 'react';
import { FieldValues, UseControllerReturn } from 'react-hook-form'; import type { FieldValues, UseControllerReturn } from 'react-hook-form';
import { ApiFormFieldType } from './ApiFormField'; import type { ApiFormFieldType } from './ApiFormField';
dayjs.extend(customParseFormat); dayjs.extend(customParseFormat);
@ -47,7 +47,7 @@ export default function DateField({
} }
// Ensure that the date is valid // Ensure that the date is valid
if (dv instanceof Date && !isNaN(dv.getTime())) { if (dv instanceof Date && !Number.isNaN(dv.getTime())) {
return dv; return dv;
} else { } else {
return null; return null;
@ -58,7 +58,7 @@ export default function DateField({
<DateInput <DateInput
id={fieldId} id={fieldId}
aria-label={`date-field-${field.name}`} aria-label={`date-field-${field.name}`}
radius="sm" radius='sm'
ref={field.ref} ref={field.ref}
type={undefined} type={undefined}
error={error?.message} error={error?.message}

View File

@ -1,5 +1,9 @@
import { useEffect, useMemo } from 'react'; import { useEffect, useMemo } from 'react';
import { Control, FieldValues, useFormContext } from 'react-hook-form'; import {
type Control,
type FieldValues,
useFormContext
} from 'react-hook-form';
import { api } from '../../../App'; import { api } from '../../../App';
import { import {
@ -8,8 +12,8 @@ import {
} from '../../../functions/forms'; } from '../../../functions/forms';
import { import {
ApiFormField, ApiFormField,
ApiFormFieldSet, type ApiFormFieldSet,
ApiFormFieldType type ApiFormFieldType
} from './ApiFormField'; } from './ApiFormField';
export function DependentField({ export function DependentField({

View File

@ -3,7 +3,7 @@ import {
Box, Box,
CloseButton, CloseButton,
Combobox, Combobox,
ComboboxStore, type ComboboxStore,
Group, Group,
Input, Input,
InputBase, InputBase,
@ -17,12 +17,12 @@ import { useDebouncedValue, useElementSize } from '@mantine/hooks';
import { IconX } from '@tabler/icons-react'; import { IconX } from '@tabler/icons-react';
import Fuse from 'fuse.js'; import Fuse from 'fuse.js';
import { startTransition, useEffect, useMemo, useRef, useState } from 'react'; import { startTransition, useEffect, useMemo, useRef, useState } from 'react';
import { FieldValues, UseControllerReturn } from 'react-hook-form'; import type { FieldValues, UseControllerReturn } from 'react-hook-form';
import { FixedSizeGrid as Grid } from 'react-window'; import { FixedSizeGrid as Grid } from 'react-window';
import { useIconState } from '../../../states/IconState'; import { useIconState } from '../../../states/IconState';
import { ApiIcon } from '../../items/ApiIcon'; import { ApiIcon } from '../../items/ApiIcon';
import { ApiFormFieldType } from './ApiFormField'; import type { ApiFormFieldType } from './ApiFormField';
export default function IconField({ export default function IconField({
controller, controller,
@ -52,13 +52,13 @@ export default function IconField({
required={definition.required} required={definition.required}
error={error?.message} error={error?.message}
ref={field.ref} ref={field.ref}
component="button" component='button'
type="button" type='button'
pointer pointer
rightSection={ rightSection={
value !== null && !definition.required ? ( value !== null && !definition.required ? (
<CloseButton <CloseButton
size="sm" size='sm'
onMouseDown={(e) => e.preventDefault()} onMouseDown={(e) => e.preventDefault()}
onClick={() => field.onChange(null)} onClick={() => field.onChange(null)}
/> />
@ -70,9 +70,9 @@ export default function IconField({
rightSectionPointerEvents={value === null ? 'none' : 'all'} rightSectionPointerEvents={value === null ? 'none' : 'all'}
> >
{field.value ? ( {field.value ? (
<Group gap="xs"> <Group gap='xs'>
<ApiIcon name={field.value} /> <ApiIcon name={field.value} />
<Text size="sm" c="dimmed"> <Text size='sm' c='dimmed'>
{field.value} {field.value}
</Text> </Text>
</Group> </Group>
@ -209,7 +209,7 @@ function ComboboxDropdown({
placeholder={t`Search...`} placeholder={t`Search...`}
rightSection={ rightSection={
searchValue && !definition.required ? ( searchValue && !definition.required ? (
<IconX size="1rem" onClick={() => setSearchValue('')} /> <IconX size='1rem' onClick={() => setSearchValue('')} />
) : null ) : null
} }
flex={1} flex={1}
@ -233,7 +233,7 @@ function ComboboxDropdown({
/> />
</Group> </Group>
<Text size="sm" c="dimmed" ta="center" mt={-4}> <Text size='sm' c='dimmed' ta='center' mt={-4}>
<Trans>{filteredIcons.length} icons</Trans> <Trans>{filteredIcons.length} icons</Trans>
</Text> </Text>

View File

@ -1,10 +1,10 @@
import { Accordion, Divider, Stack, Text } from '@mantine/core'; import { Accordion, Divider, Stack, Text } from '@mantine/core';
import { Control, FieldValues } from 'react-hook-form'; import type { Control, FieldValues } from 'react-hook-form';
import { import {
ApiFormField, ApiFormField,
ApiFormFieldSet, type ApiFormFieldSet,
ApiFormFieldType type ApiFormFieldType
} from './ApiFormField'; } from './ApiFormField';
export function NestedObjectField({ export function NestedObjectField({
@ -21,14 +21,14 @@ export function NestedObjectField({
setFields?: React.Dispatch<React.SetStateAction<ApiFormFieldSet>>; setFields?: React.Dispatch<React.SetStateAction<ApiFormFieldSet>>;
}>) { }>) {
return ( return (
<Accordion defaultValue={'OpenByDefault'} variant="contained"> <Accordion defaultValue={'OpenByDefault'} variant='contained'>
<Accordion.Item value={'OpenByDefault'}> <Accordion.Item value={'OpenByDefault'}>
<Accordion.Control icon={definition.icon}> <Accordion.Control icon={definition.icon}>
<Text>{definition.label}</Text> <Text>{definition.label}</Text>
</Accordion.Control> </Accordion.Control>
<Accordion.Panel> <Accordion.Panel>
<Divider style={{ marginTop: '-10px', marginBottom: '10px' }} /> <Divider style={{ marginTop: '-10px', marginBottom: '10px' }} />
<Stack gap="xs"> <Stack gap='xs'>
{Object.entries(definition.children ?? {}).map( {Object.entries(definition.children ?? {}).map(
([childFieldName, field]) => ( ([childFieldName, field]) => (
<ApiFormField <ApiFormField

View File

@ -9,8 +9,8 @@ import { useDebouncedValue, useId } from '@mantine/hooks';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { import {
FieldValues, type FieldValues,
UseControllerReturn, type UseControllerReturn,
useFormContext useFormContext
} from 'react-hook-form'; } from 'react-hook-form';
import Select from 'react-select'; import Select from 'react-select';
@ -18,7 +18,7 @@ import Select from 'react-select';
import { api } from '../../../App'; import { api } from '../../../App';
import { vars } from '../../../theme'; import { vars } from '../../../theme';
import { RenderInstance } from '../../render/Instance'; import { RenderInstance } from '../../render/Instance';
import { ApiFormFieldType } from './ApiFormField'; import type { ApiFormFieldType } from './ApiFormField';
/** /**
* Render a 'select' field for searching the database against a particular model type * Render a 'select' field for searching the database against a particular model type
@ -71,8 +71,8 @@ export function RelatedModelField({
} }
api.get(url).then((response) => { api.get(url).then((response) => {
let pk_field = definition.pk_field ?? 'pk'; const pk_field = definition.pk_field ?? 'pk';
if (response.data && response.data[pk_field]) { if (response.data?.[pk_field]) {
const value = { const value = {
value: response.data[pk_field], value: response.data[pk_field],
data: response.data data: response.data
@ -138,7 +138,7 @@ export function RelatedModelField({
setFilters(_filters); setFilters(_filters);
} }
let params = { const params = {
..._filters, ..._filters,
search: searchText, search: searchText,
offset: offset, offset: offset,
@ -158,8 +158,8 @@ export function RelatedModelField({
const results = response.data?.results ?? response.data ?? []; const results = response.data?.results ?? response.data ?? [];
results.forEach((item: any) => { results.forEach((item: any) => {
let pk_field = definition.pk_field ?? 'pk'; const pk_field = definition.pk_field ?? 'pk';
let pk = item[pk_field]; const pk = item[pk_field];
if (pk && !alreadyPresentPks.includes(pk)) { if (pk && !alreadyPresentPks.includes(pk)) {
values.push({ values.push({
@ -201,7 +201,7 @@ export function RelatedModelField({
// Update form values when the selected value changes // Update form values when the selected value changes
const onChange = useCallback( const onChange = useCallback(
(value: any) => { (value: any) => {
let _pk = value?.value ?? null; const _pk = value?.value ?? null;
field.onChange(_pk); field.onChange(_pk);
setPk(_pk); setPk(_pk);
@ -230,7 +230,7 @@ export function RelatedModelField({
return null; return null;
} }
let _data = [...data, initialData]; const _data = [...data, initialData];
return _data.find((item) => item.value === pk); return _data.find((item) => item.value === pk);
}, [pk, data]); }, [pk, data]);
@ -316,11 +316,11 @@ export function RelatedModelField({
isClearable={!definition.required} isClearable={!definition.required}
isDisabled={definition.disabled} isDisabled={definition.disabled}
isSearchable={true} isSearchable={true}
placeholder={definition.placeholder || t`Search` + `...`} placeholder={definition.placeholder || `${t`Search`}...`}
loadingMessage={() => t`Loading` + `...`} loadingMessage={() => `${t`Loading`}...`}
menuPortalTarget={document.body} menuPortalTarget={document.body}
noOptionsMessage={() => t`No results found`} noOptionsMessage={() => t`No results found`}
menuPosition="fixed" menuPosition='fixed'
styles={{ menuPortal: (base: any) => ({ ...base, zIndex: 9999 }) }} styles={{ menuPortal: (base: any) => ({ ...base, zIndex: 9999 }) }}
formatOptionLabel={(option: any) => formatOption(option)} formatOptionLabel={(option: any) => formatOption(option)}
theme={(theme) => { theme={(theme) => {

View File

@ -1,13 +1,13 @@
import { Trans, t } from '@lingui/macro'; import { Trans, t } from '@lingui/macro';
import { Alert, Container, Group, Stack, Table, Text } from '@mantine/core'; import { Alert, Container, Group, Stack, Table, Text } from '@mantine/core';
import { IconExclamationCircle } from '@tabler/icons-react'; import { IconExclamationCircle } from '@tabler/icons-react';
import { ReactNode, useCallback, useEffect, useMemo } from 'react'; import { type ReactNode, useCallback, useEffect, useMemo } from 'react';
import { FieldValues, UseControllerReturn } from 'react-hook-form'; import type { FieldValues, UseControllerReturn } from 'react-hook-form';
import { identifierString } from '../../../functions/conversion'; import { identifierString } from '../../../functions/conversion';
import { InvenTreeIcon } from '../../../functions/icons'; import { InvenTreeIcon } from '../../../functions/icons';
import { StandaloneField } from '../StandaloneField'; import { StandaloneField } from '../StandaloneField';
import { ApiFormFieldType } from './ApiFormField'; import type { ApiFormFieldType } from './ApiFormField';
export interface TableFieldRowProps { export interface TableFieldRowProps {
item: any; item: any;
@ -38,10 +38,10 @@ function TableFieldRow({
// Table fields require render function // Table fields require render function
if (!definition.modelRenderer) { if (!definition.modelRenderer) {
return ( return (
<Table.Tr key="table-row-no-renderer"> <Table.Tr key='table-row-no-renderer'>
<Table.Td colSpan={definition.headers?.length}> <Table.Td colSpan={definition.headers?.length}>
<Alert color="red" title={t`Error`} icon={<IconExclamationCircle />}> <Alert color='red' title={t`Error`} icon={<IconExclamationCircle />}>
{`modelRenderer entry required for tables`} {t`modelRenderer entry required for tables`}
</Alert> </Alert>
</Table.Td> </Table.Td>
</Table.Tr> </Table.Tr>
@ -67,13 +67,13 @@ export function TableFieldErrorWrapper({
errorKey: string; errorKey: string;
children: ReactNode; children: ReactNode;
}) { }) {
const msg = props?.rowErrors && props.rowErrors[errorKey]; const msg = props?.rowErrors?.[errorKey];
return ( return (
<Stack gap="xs"> <Stack gap='xs'>
{children} {children}
{msg && ( {msg && (
<Text size="xs" c="red"> <Text size='xs' c='red'>
{msg.message} {msg.message}
</Text> </Text>
)} )}
@ -151,7 +151,7 @@ export function TableField({
); );
}) })
) : ( ) : (
<Table.Tr key="table-row-no-entries"> <Table.Tr key='table-row-no-entries'>
<Table.Td <Table.Td
style={{ textAlign: 'center' }} style={{ textAlign: 'center' }}
colSpan={definition.headers?.length} colSpan={definition.headers?.length}
@ -163,7 +163,7 @@ export function TableField({
gap: '5px' gap: '5px'
}} }}
> >
<InvenTreeIcon icon="info" /> <InvenTreeIcon icon='info' />
<Trans>No entries available</Trans> <Trans>No entries available</Trans>
</span> </span>
</Table.Td> </Table.Td>
@ -215,9 +215,9 @@ export function TableFieldExtraRow({
visible && ( visible && (
<Table.Tr> <Table.Tr>
<Table.Td colSpan={10}> <Table.Td colSpan={10}>
<Group grow preventGrowOverflow={false} justify="flex-apart" p="xs"> <Group grow preventGrowOverflow={false} justify='flex-apart' p='xs'>
<Container flex={0} p="xs"> <Container flex={0} p='xs'>
<InvenTreeIcon icon="downright" /> <InvenTreeIcon icon='downright' />
</Container> </Container>
<StandaloneField <StandaloneField
fieldDefinition={field} fieldDefinition={field}

View File

@ -2,7 +2,7 @@ import { TextInput } from '@mantine/core';
import { useDebouncedValue } from '@mantine/hooks'; import { useDebouncedValue } from '@mantine/hooks';
import { IconX } from '@tabler/icons-react'; import { IconX } from '@tabler/icons-react';
import { useCallback, useEffect, useId, useState } from 'react'; import { useCallback, useEffect, useId, useState } from 'react';
import { FieldValues, UseControllerReturn } from 'react-hook-form'; import type { FieldValues, UseControllerReturn } from 'react-hook-form';
/* /*
* Custom implementation of the mantine <TextInput> component, * Custom implementation of the mantine <TextInput> component,
@ -57,7 +57,7 @@ export default function TextField({
type={definition.field_type} type={definition.field_type}
value={rawText || ''} value={rawText || ''}
error={error?.message} error={error?.message}
radius="sm" radius='sm'
onChange={(event) => onTextChange(event.currentTarget.value)} onChange={(event) => onTextChange(event.currentTarget.value)}
onBlur={(event) => { onBlur={(event) => {
if (event.currentTarget.value != value) { if (event.currentTarget.value != value) {
@ -67,7 +67,7 @@ export default function TextField({
onKeyDown={(event) => onKeyDown(event.code)} onKeyDown={(event) => onKeyDown(event.code)}
rightSection={ rightSection={
value && !definition.required ? ( value && !definition.required ? (
<IconX size="1rem" color="red" onClick={() => onTextChange('')} /> <IconX size='1rem' color='red' onClick={() => onTextChange('')} />
) : null ) : null
} }
/> />

View File

@ -3,7 +3,7 @@
* *
* Image caching is handled automagically by the browsers cache * Image caching is handled automagically by the browsers cache
*/ */
import { Image, ImageProps, Skeleton, Stack } from '@mantine/core'; import { Image, type ImageProps, Skeleton, Stack } from '@mantine/core';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useLocalState } from '../../states/LocalState'; import { useLocalState } from '../../states/LocalState';
@ -25,7 +25,7 @@ export function ApiImage(props: Readonly<ApiImageProps>) {
return ( return (
<Stack> <Stack>
{imageUrl ? ( {imageUrl ? (
<Image {...props} src={imageUrl} fit="contain" /> <Image {...props} src={imageUrl} fit='contain' />
) : ( ) : (
<Skeleton h={props?.h ?? props.w} w={props?.w ?? props.h} /> <Skeleton h={props?.h ?? props.w} w={props?.w ?? props.h} />
)} )}

View File

@ -1,6 +1,6 @@
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { Anchor, Group } from '@mantine/core'; import { Anchor, Group } from '@mantine/core';
import { ReactNode, useMemo } from 'react'; import { type ReactNode, useMemo } from 'react';
import { ApiImage } from './ApiImage'; import { ApiImage } from './ApiImage';
@ -27,7 +27,7 @@ export function Thumbnail({
const inner = useMemo(() => { const inner = useMemo(() => {
if (link) { if (link) {
return ( return (
<Anchor href={link} target="_blank"> <Anchor href={link} target='_blank'>
{text} {text}
</Anchor> </Anchor>
); );
@ -37,13 +37,13 @@ export function Thumbnail({
}, [link, text]); }, [link, text]);
return ( return (
<Group align={align ?? 'left'} gap="xs" wrap="nowrap"> <Group align={align ?? 'left'} gap='xs' wrap='nowrap'>
<ApiImage <ApiImage
src={src || backup_image} src={src || backup_image}
aria-label={alt} aria-label={alt}
w={size} w={size}
fit="contain" fit='contain'
radius="xs" radius='xs'
style={{ maxHeight: size }} style={{ maxHeight: size }}
/> />
{inner} {inner}

View File

@ -7,7 +7,7 @@ import {
IconCircleDashedCheck, IconCircleDashedCheck,
IconExclamationCircle IconExclamationCircle
} from '@tabler/icons-react'; } from '@tabler/icons-react';
import { ReactNode, useCallback, useMemo, useState } from 'react'; import { type ReactNode, useCallback, useMemo, useState } from 'react';
import { api } from '../../App'; import { api } from '../../App';
import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { ApiEndpoints } from '../../enums/ApiEndpoints';
@ -16,20 +16,20 @@ import {
useDeleteApiFormModal, useDeleteApiFormModal,
useEditApiFormModal useEditApiFormModal
} from '../../hooks/UseForm'; } from '../../hooks/UseForm';
import { ImportSessionState } from '../../hooks/UseImportSession'; import type { ImportSessionState } from '../../hooks/UseImportSession';
import { useTable } from '../../hooks/UseTable'; import { useTable } from '../../hooks/UseTable';
import { apiUrl } from '../../states/ApiState'; import { apiUrl } from '../../states/ApiState';
import { TableColumn } from '../../tables/Column'; import type { TableColumn } from '../../tables/Column';
import { TableFilter } from '../../tables/Filter'; import type { TableFilter } from '../../tables/Filter';
import { InvenTreeTable } from '../../tables/InvenTreeTable'; import { InvenTreeTable } from '../../tables/InvenTreeTable';
import { import {
RowAction, type RowAction,
RowDeleteAction, RowDeleteAction,
RowEditAction RowEditAction
} from '../../tables/RowActions'; } from '../../tables/RowActions';
import { ActionButton } from '../buttons/ActionButton'; import { ActionButton } from '../buttons/ActionButton';
import { YesNoButton } from '../buttons/YesNoButton'; import { YesNoButton } from '../buttons/YesNoButton';
import { ApiFormFieldSet } from '../forms/fields/ApiFormField'; import type { ApiFormFieldSet } from '../forms/fields/ApiFormField';
import { ProgressBar } from '../items/ProgressBar'; import { ProgressBar } from '../items/ProgressBar';
import { RenderRemoteInstance } from '../render/Instance'; import { RenderRemoteInstance } from '../render/Instance';
@ -63,7 +63,7 @@ function ImporterDataCell({
}, [row.errors, column.field]); }, [row.errors, column.field]);
const cellValue: ReactNode = useMemo(() => { const cellValue: ReactNode = useMemo(() => {
let field_def = session.availableFields[column.field]; const field_def = session.availableFields[column.field];
if (!row?.data) { if (!row?.data) {
return '-'; return '-';
@ -88,7 +88,7 @@ function ImporterDataCell({
break; break;
} }
let value = row.data ? row.data[column.field] ?? '' : ''; let value = row.data ? (row.data[column.field] ?? '') : '';
if (!value) { if (!value) {
value = '-'; value = '-';
@ -105,18 +105,18 @@ function ImporterDataCell({
return ( return (
<HoverCard disabled={cellValid} openDelay={100} closeDelay={100}> <HoverCard disabled={cellValid} openDelay={100} closeDelay={100}>
<HoverCard.Target> <HoverCard.Target>
<Group grow justify="apart" onClick={onRowEdit}> <Group grow justify='apart' onClick={onRowEdit}>
<Group grow style={{ flex: 1 }}> <Group grow style={{ flex: 1 }}>
<Text size="xs" c={cellValid ? undefined : 'red'}> <Text size='xs' c={cellValid ? undefined : 'red'}>
{cellValue} {cellValue}
</Text> </Text>
</Group> </Group>
</Group> </Group>
</HoverCard.Target> </HoverCard.Target>
<HoverCard.Dropdown> <HoverCard.Dropdown>
<Stack gap="xs"> <Stack gap='xs'>
{cellErrors.map((error: string) => ( {cellErrors.map((error: string) => (
<Text size="xs" c="red" key={error}> <Text size='xs' c='red' key={error}>
{error} {error}
</Text> </Text>
))} ))}
@ -136,11 +136,11 @@ export default function ImporterDataSelector({
const [selectedFieldNames, setSelectedFieldNames] = useState<string[]>([]); const [selectedFieldNames, setSelectedFieldNames] = useState<string[]>([]);
const selectedFields: ApiFormFieldSet = useMemo(() => { const selectedFields: ApiFormFieldSet = useMemo(() => {
let fields: ApiFormFieldSet = {}; const fields: ApiFormFieldSet = {};
for (let field of selectedFieldNames) { for (const field of selectedFieldNames) {
// Find the field definition in session.availableFields // Find the field definition in session.availableFields
let fieldDef = session.availableFields[field]; const fieldDef = session.availableFields[field];
if (fieldDef) { if (fieldDef) {
// Construct field filters based on session field filters // Construct field filters based on session field filters
let filters = fieldDef.filters ?? {}; let filters = fieldDef.filters ?? {};
@ -243,7 +243,7 @@ export default function ImporterDataSelector({
return []; return [];
} }
let errors: string[] = []; const errors: string[] = [];
for (const k of Object.keys(row.errors)) { for (const k of Object.keys(row.errors)) {
if (row.errors[k]) { if (row.errors[k]) {
@ -261,7 +261,7 @@ export default function ImporterDataSelector({
}, []); }, []);
const columns: TableColumn[] = useMemo(() => { const columns: TableColumn[] = useMemo(() => {
let columns: TableColumn[] = [ const columns: TableColumn[] = [
{ {
accessor: 'row_index', accessor: 'row_index',
title: t`Row`, title: t`Row`,
@ -269,22 +269,22 @@ export default function ImporterDataSelector({
switchable: false, switchable: false,
render: (row: any) => { render: (row: any) => {
return ( return (
<Group justify="left" gap="xs"> <Group justify='left' gap='xs'>
<Text size="sm">{row.row_index}</Text> <Text size='sm'>{row.row_index}</Text>
{row.complete && <IconCircleCheck color="green" size={16} />} {row.complete && <IconCircleCheck color='green' size={16} />}
{!row.complete && row.valid && ( {!row.complete && row.valid && (
<IconCircleDashedCheck color="blue" size={16} /> <IconCircleDashedCheck color='blue' size={16} />
)} )}
{!row.complete && !row.valid && ( {!row.complete && !row.valid && (
<HoverCard openDelay={50} closeDelay={100}> <HoverCard openDelay={50} closeDelay={100}>
<HoverCard.Target> <HoverCard.Target>
<IconExclamationCircle color="red" size={16} /> <IconExclamationCircle color='red' size={16} />
</HoverCard.Target> </HoverCard.Target>
<HoverCard.Dropdown> <HoverCard.Dropdown>
<Stack gap="xs"> <Stack gap='xs'>
<Text>{t`Row contains errors`}:</Text> <Text>{t`Row contains errors`}:</Text>
{rowErrors(row).map((error: string) => ( {rowErrors(row).map((error: string) => (
<Text size="sm" c="red" key={error}> <Text size='sm' c='red' key={error}>
{error} {error}
</Text> </Text>
))} ))}
@ -377,10 +377,10 @@ export default function ImporterDataSelector({
return [ return [
<ActionButton <ActionButton
key="import-selected-rows" key='import-selected-rows'
disabled={!canImport} disabled={!canImport}
icon={<IconArrowRight />} icon={<IconArrowRight />}
color="green" color='green'
tooltip={t`Import selected rows`} tooltip={t`Import selected rows`}
onClick={() => { onClick={() => {
importData(table.selectedRecords.map((row: any) => row.pk)); importData(table.selectedRecords.map((row: any) => row.pk));
@ -393,10 +393,10 @@ export default function ImporterDataSelector({
<> <>
{editRow.modal} {editRow.modal}
{deleteRow.modal} {deleteRow.modal}
<Stack gap="xs"> <Stack gap='xs'>
<Paper shadow="xs" p="xs"> <Paper shadow='xs' p='xs'>
<Group grow justify="apart"> <Group grow justify='apart'>
<Text size="lg">{t`Processing Data`}</Text> <Text size='lg'>{t`Processing Data`}</Text>
<Space /> <Space />
<ProgressBar <ProgressBar
maximum={session.rowCount} maximum={session.rowCount}

View File

@ -15,10 +15,10 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
import { api } from '../../App'; import { api } from '../../App';
import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ImportSessionState } from '../../hooks/UseImportSession'; import type { ImportSessionState } from '../../hooks/UseImportSession';
import { apiUrl } from '../../states/ApiState'; import { apiUrl } from '../../states/ApiState';
import { StandaloneField } from '../forms/StandaloneField'; import { StandaloneField } from '../forms/StandaloneField';
import { ApiFormFieldType } from '../forms/fields/ApiFormField'; import type { ApiFormFieldType } from '../forms/fields/ApiFormField';
function ImporterColumn({ function ImporterColumn({
column, column,
@ -81,7 +81,7 @@ function ImporterDefaultField({
const onChange = useCallback( const onChange = useCallback(
(value: any) => { (value: any) => {
// Update the default value for the field // Update the default value for the field
let defaults = { const defaults = {
...session.fieldDefaults, ...session.fieldDefaults,
[fieldName]: value [fieldName]: value
}; };
@ -133,19 +133,19 @@ function ImporterColumnTableRow({
return ( return (
<Table.Tr key={column.label ?? column.field}> <Table.Tr key={column.label ?? column.field}>
<Table.Td> <Table.Td>
<Group gap="xs"> <Group gap='xs'>
<Text fw={column.required ? 700 : undefined}> <Text fw={column.required ? 700 : undefined}>
{column.label ?? column.field} {column.label ?? column.field}
</Text> </Text>
{column.required && ( {column.required && (
<Text c="red" fw={700}> <Text c='red' fw={700}>
* *
</Text> </Text>
)} )}
</Group> </Group>
</Table.Td> </Table.Td>
<Table.Td> <Table.Td>
<Text size="sm">{column.description}</Text> <Text size='sm'>{column.description}</Text>
</Table.Td> </Table.Td>
<Table.Td> <Table.Td>
<ImporterColumn column={column} options={options} /> <ImporterColumn column={column} options={options} />
@ -193,12 +193,12 @@ export default function ImporterColumnSelector({
}, [session.availableColumns]); }, [session.availableColumns]);
return ( return (
<Stack gap="xs"> <Stack gap='xs'>
<Paper shadow="xs" p="xs"> <Paper shadow='xs' p='xs'>
<Group grow justify="apart"> <Group grow justify='apart'>
<Text size="lg">{t`Mapping data columns to database fields`}</Text> <Text size='lg'>{t`Mapping data columns to database fields`}</Text>
<Space /> <Space />
<Button color="green" variant="filled" onClick={acceptMapping}> <Button color='green' variant='filled' onClick={acceptMapping}>
<Group> <Group>
<IconCheck /> <IconCheck />
{t`Accept Column Mapping`} {t`Accept Column Mapping`}
@ -207,7 +207,7 @@ export default function ImporterColumnSelector({
</Group> </Group>
</Paper> </Paper>
{errorMessage && ( {errorMessage && (
<Alert color="red" title={t`Error`}> <Alert color='red' title={t`Error`}>
<Text>{errorMessage}</Text> <Text>{errorMessage}</Text>
</Alert> </Alert>
)} )}

View File

@ -14,7 +14,7 @@ import {
Text Text
} from '@mantine/core'; } from '@mantine/core';
import { IconCheck } from '@tabler/icons-react'; import { IconCheck } from '@tabler/icons-react';
import { ReactNode, useMemo } from 'react'; import { type ReactNode, useMemo } from 'react';
import { ModelType } from '../../enums/ModelType'; import { ModelType } from '../../enums/ModelType';
import { useImportSession } from '../../hooks/UseImportSession'; import { useImportSession } from '../../hooks/UseImportSession';
@ -41,7 +41,7 @@ function ImportDrawerStepper({
onStepClick={undefined} onStepClick={undefined}
allowNextStepsSelect={false} allowNextStepsSelect={false}
iconSize={20} iconSize={20}
size="xs" size='xs'
> >
<Stepper.Step label={t`Upload File`} /> <Stepper.Step label={t`Upload File`} />
<Stepper.Step label={t`Map Columns`} /> <Stepper.Step label={t`Map Columns`} />
@ -100,24 +100,24 @@ export default function ImporterDrawer({
return <ImporterDataSelector session={session} />; return <ImporterDataSelector session={session} />;
case importSessionStatus.COMPLETE: case importSessionStatus.COMPLETE:
return ( return (
<Stack gap="xs"> <Stack gap='xs'>
<Alert <Alert
color="green" color='green'
title={t`Import Complete`} title={t`Import Complete`}
icon={<IconCheck />} icon={<IconCheck />}
> >
{t`Data has been imported successfully`} {t`Data has been imported successfully`}
</Alert> </Alert>
<Button color="blue" onClick={onClose}>{t`Close`}</Button> <Button color='blue' onClick={onClose}>{t`Close`}</Button>
</Stack> </Stack>
); );
default: default:
return ( return (
<Stack gap="xs"> <Stack gap='xs'>
<Alert color="red" title={t`Unknown Status`} icon={<IconCheck />}> <Alert color='red' title={t`Unknown Status`} icon={<IconCheck />}>
{t`Import session has unknown status`}: {session.status} {t`Import session has unknown status`}: {session.status}
</Alert> </Alert>
<Button color="red" onClick={onClose}>{t`Close`}</Button> <Button color='red' onClick={onClose}>{t`Close`}</Button>
</Stack> </Stack>
); );
} }
@ -125,15 +125,15 @@ export default function ImporterDrawer({
const title: ReactNode = useMemo(() => { const title: ReactNode = useMemo(() => {
return ( return (
<Stack gap="xs" style={{ width: '100%' }}> <Stack gap='xs' style={{ width: '100%' }}>
<Group <Group
gap="xs" gap='xs'
wrap="nowrap" wrap='nowrap'
justify="space-apart" justify='space-apart'
grow grow
preventGrowOverflow={false} preventGrowOverflow={false}
> >
<StylishText size="lg"> <StylishText size='lg'>
{session.sessionData?.statusText ?? t`Importing Data`} {session.sessionData?.statusText ?? t`Importing Data`}
</StylishText> </StylishText>
<ImportDrawerStepper currentStep={currentStep} /> <ImportDrawerStepper currentStep={currentStep} />
@ -146,8 +146,8 @@ export default function ImporterDrawer({
return ( return (
<Drawer <Drawer
position="bottom" position='bottom'
size="80%" size='80%'
title={title} title={title}
opened={opened} opened={opened}
onClose={onClose} onClose={onClose}
@ -163,9 +163,9 @@ export default function ImporterDrawer({
} }
}} }}
> >
<Stack gap="xs"> <Stack gap='xs'>
<LoadingOverlay visible={session.sessionQuery.isFetching} /> <LoadingOverlay visible={session.sessionQuery.isFetching} />
<Paper p="md">{session.sessionQuery.isFetching || widget}</Paper> <Paper p='md'>{session.sessionQuery.isFetching || widget}</Paper>
</Stack> </Stack>
</Drawer> </Drawer>
); );

View File

@ -4,7 +4,7 @@ import { useInterval } from '@mantine/hooks';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { ModelType } from '../../enums/ModelType'; import { ModelType } from '../../enums/ModelType';
import { ImportSessionState } from '../../hooks/UseImportSession'; import type { ImportSessionState } from '../../hooks/UseImportSession';
import useStatusCodes from '../../hooks/UseStatusCodes'; import useStatusCodes from '../../hooks/UseStatusCodes';
import { StylishText } from '../items/StylishText'; import { StylishText } from '../items/StylishText';
@ -32,10 +32,10 @@ export default function ImporterImportProgress({
return ( return (
<Center> <Center>
<Container> <Container>
<Stack gap="xs"> <Stack gap='xs'>
<StylishText size="lg">{t`Importing Records`}</StylishText> <StylishText size='lg'>{t`Importing Records`}</StylishText>
<Loader /> <Loader />
<Text size="lg"> <Text size='lg'>
{t`Imported Rows`}: {session.sessionData.row_count} {t`Imported Rows`}: {session.sessionData.row_count}
</Text> </Text>
</Stack> </Stack>

View File

@ -2,7 +2,7 @@ import { t } from '@lingui/macro';
import { import {
Button, Button,
Indicator, Indicator,
IndicatorProps, type IndicatorProps,
Menu, Menu,
Tooltip Tooltip
} from '@mantine/core'; } from '@mantine/core';
@ -17,9 +17,9 @@ import {
IconTrash, IconTrash,
IconUnlink IconUnlink
} from '@tabler/icons-react'; } from '@tabler/icons-react';
import { ReactNode, useMemo } from 'react'; import { type ReactNode, useMemo } from 'react';
import { ModelType } from '../../enums/ModelType'; import type { ModelType } from '../../enums/ModelType';
import { identifierString } from '../../functions/conversion'; import { identifierString } from '../../functions/conversion';
import { InvenTreeIcon } from '../../functions/icons'; import { InvenTreeIcon } from '../../functions/icons';
import { InvenTreeQRCode, QRCodeLink, QRCodeUnlink } from './QRCode'; import { InvenTreeQRCode, QRCodeLink, QRCodeUnlink } from './QRCode';
@ -67,17 +67,17 @@ export function ActionDropdown({
}, [tooltip]); }, [tooltip]);
return !hidden && hasActions ? ( return !hidden && hasActions ? (
<Menu position="bottom-end" key={menuName}> <Menu position='bottom-end' key={menuName}>
<Indicator disabled={!indicatorProps} {...indicatorProps?.indicator}> <Indicator disabled={!indicatorProps} {...indicatorProps?.indicator}>
<Menu.Target> <Menu.Target>
<Tooltip label={tooltip} hidden={!tooltip} position="bottom"> <Tooltip label={tooltip} hidden={!tooltip} position='bottom'>
<Button <Button
radius="sm" radius='sm'
variant={noindicator ? 'transparent' : 'light'} variant={noindicator ? 'transparent' : 'light'}
disabled={disabled} disabled={disabled}
aria-label={menuName} aria-label={menuName}
p="0" p='0'
size="sm" size='sm'
rightSection={ rightSection={
noindicator ? null : <IconChevronDown stroke={1.5} /> noindicator ? null : <IconChevronDown stroke={1.5} />
} }
@ -102,7 +102,7 @@ export function ActionDropdown({
<Tooltip <Tooltip
label={action.tooltip} label={action.tooltip}
hidden={!action.tooltip} hidden={!action.tooltip}
position="left" position='left'
> >
<Menu.Item <Menu.Item
aria-label={id} aria-label={id}
@ -232,7 +232,7 @@ function GeneralBarcodeAction({
export function EditItemAction(props: ActionDropdownItem): ActionDropdownItem { export function EditItemAction(props: ActionDropdownItem): ActionDropdownItem {
return { return {
...props, ...props,
icon: <IconEdit color="blue" />, icon: <IconEdit color='blue' />,
name: t`Edit`, name: t`Edit`,
tooltip: props.tooltip ?? t`Edit item` tooltip: props.tooltip ?? t`Edit item`
}; };
@ -244,7 +244,7 @@ export function DeleteItemAction(
): ActionDropdownItem { ): ActionDropdownItem {
return { return {
...props, ...props,
icon: <IconTrash color="red" />, icon: <IconTrash color='red' />,
name: t`Delete`, name: t`Delete`,
tooltip: props.tooltip ?? t`Delete item` tooltip: props.tooltip ?? t`Delete item`
}; };
@ -253,7 +253,7 @@ export function DeleteItemAction(
export function HoldItemAction(props: ActionDropdownItem): ActionDropdownItem { export function HoldItemAction(props: ActionDropdownItem): ActionDropdownItem {
return { return {
...props, ...props,
icon: <InvenTreeIcon icon="hold" iconProps={{ color: 'orange' }} />, icon: <InvenTreeIcon icon='hold' iconProps={{ color: 'orange' }} />,
name: t`Hold`, name: t`Hold`,
tooltip: props.tooltip ?? t`Hold` tooltip: props.tooltip ?? t`Hold`
}; };
@ -264,7 +264,7 @@ export function CancelItemAction(
): ActionDropdownItem { ): ActionDropdownItem {
return { return {
...props, ...props,
icon: <InvenTreeIcon icon="cancel" iconProps={{ color: 'red' }} />, icon: <InvenTreeIcon icon='cancel' iconProps={{ color: 'red' }} />,
name: t`Cancel`, name: t`Cancel`,
tooltip: props.tooltip ?? t`Cancel` tooltip: props.tooltip ?? t`Cancel`
}; };
@ -276,7 +276,7 @@ export function DuplicateItemAction(
): ActionDropdownItem { ): ActionDropdownItem {
return { return {
...props, ...props,
icon: <IconCopy color="green" />, icon: <IconCopy color='green' />,
name: t`Duplicate`, name: t`Duplicate`,
tooltip: props.tooltip ?? t`Duplicate item` tooltip: props.tooltip ?? t`Duplicate item`
}; };

View File

@ -9,9 +9,9 @@ type ApiIconProps = {
export const ApiIcon = ({ name: _name, size = 22 }: ApiIconProps) => { export const ApiIcon = ({ name: _name, size = 22 }: ApiIconProps) => {
const [iconPackage, name, variant] = _name.split(':'); const [iconPackage, name, variant] = _name.split(':');
const icon = useIconState( const icon = useIconState(
(s) => s.packagesMap[iconPackage]?.['icons'][name]?.['variants'][variant] (s) => s.packagesMap[iconPackage]?.icons[name]?.variants[variant]
); );
const unicode = icon ? String.fromCodePoint(parseInt(icon, 16)) : ''; const unicode = icon ? String.fromCodePoint(Number.parseInt(icon, 16)) : '';
return ( return (
<i <i

View File

@ -9,7 +9,7 @@ import {
IconLink, IconLink,
IconPhoto IconPhoto
} from '@tabler/icons-react'; } from '@tabler/icons-react';
import { ReactNode, useMemo } from 'react'; import { type ReactNode, useMemo } from 'react';
import { useLocalState } from '../../states/LocalState'; import { useLocalState } from '../../states/LocalState';
@ -18,7 +18,7 @@ import { useLocalState } from '../../states/LocalState';
*/ */
export function attachmentIcon(attachment: string): ReactNode { export function attachmentIcon(attachment: string): ReactNode {
const sz = 18; const sz = 18;
let suffix = attachment.split('.').pop()?.toLowerCase() ?? ''; const suffix = attachment.split('.').pop()?.toLowerCase() ?? '';
switch (suffix) { switch (suffix) {
case 'pdf': case 'pdf':
return <IconFileTypePdf size={sz} />; return <IconFileTypePdf size={sz} />;
@ -59,7 +59,7 @@ export function AttachmentLink({
attachment: string; attachment: string;
external?: boolean; external?: boolean;
}>): ReactNode { }>): ReactNode {
let text = external ? attachment : attachment.split('/').pop(); const text = external ? attachment : attachment.split('/').pop();
const host = useLocalState((s) => s.host); const host = useLocalState((s) => s.host);
@ -72,9 +72,9 @@ export function AttachmentLink({
}, [host, attachment, external]); }, [host, attachment, external]);
return ( return (
<Group justify="left" gap="sm" wrap="nowrap"> <Group justify='left' gap='sm' wrap='nowrap'>
{external ? <IconLink /> : attachmentIcon(attachment)} {external ? <IconLink /> : attachmentIcon(attachment)}
<Anchor href={url} target="_blank" rel="noopener noreferrer"> <Anchor href={url} target='_blank' rel='noopener noreferrer'>
{text} {text}
</Anchor> </Anchor>
</Group> </Group>

View File

@ -1,7 +1,8 @@
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { ActionIcon, Box, Button, Divider, TextInput } from '@mantine/core'; import { ActionIcon, Box, Button, Divider, TextInput } from '@mantine/core';
import { IconQrcode } from '@tabler/icons-react'; import { IconQrcode } from '@tabler/icons-react';
import React, { useState } from 'react'; import type React from 'react';
import { useState } from 'react';
import { InputImageBarcode } from '../../pages/Index/Scan'; import { InputImageBarcode } from '../../pages/Index/Scan';
@ -47,10 +48,10 @@ export function BarcodeInput({
<IconQrcode /> <IconQrcode />
</ActionIcon> </ActionIcon>
} }
w="100%" w='100%'
/> />
{onAction ? ( {onAction ? (
<Button color="green" onClick={onAction} mt="lg" fullWidth> <Button color='green' onClick={onAction} mt='lg' fullWidth>
{actionText} {actionText}
</Button> </Button>
) : null} ) : null}

View File

@ -7,15 +7,15 @@ export function ColorToggle() {
const { colorScheme, toggleColorScheme } = useMantineColorScheme(); const { colorScheme, toggleColorScheme } = useMantineColorScheme();
return ( return (
<Group justify="center"> <Group justify='center'>
<ActionIcon <ActionIcon
onClick={toggleColorScheme} onClick={toggleColorScheme}
size="lg" size='lg'
style={{ style={{
color: color:
colorScheme === 'dark' ? vars.colors.yellow[4] : vars.colors.blue[6] colorScheme === 'dark' ? vars.colors.yellow[4] : vars.colors.blue[6]
}} }}
variant="transparent" variant='transparent'
> >
{colorScheme === 'dark' ? <IconSun /> : <IconMoonStars />} {colorScheme === 'dark' ? <IconSun /> : <IconMoonStars />}
</ActionIcon> </ActionIcon>

View File

@ -17,15 +17,15 @@ export function StatisticItem({
isLoading: boolean; isLoading: boolean;
}>) { }>) {
return ( return (
<Paper withBorder p="xs" key={id} pos="relative"> <Paper withBorder p='xs' key={id} pos='relative'>
<LoadingOverlay visible={isLoading} overlayProps={{ blur: 2 }} /> <LoadingOverlay visible={isLoading} overlayProps={{ blur: 2 }} />
<Group justify="space-between"> <Group justify='space-between'>
<Text size="xs" c="dimmed" className={classes.dashboardItemTitle}> <Text size='xs' c='dimmed' className={classes.dashboardItemTitle}>
{data.title} {data.title}
</Text> </Text>
</Group> </Group>
<Group align="flex-end" gap="xs" mt={25}> <Group align='flex-end' gap='xs' mt={25}>
<Text className={classes.dashboardItemValue}>{data.value}</Text> <Text className={classes.dashboardItemValue}>{data.value}</Text>
</Group> </Group>
</Paper> </Paper>

View File

@ -1,6 +1,6 @@
import { IconInfoCircle } from '@tabler/icons-react'; import { IconInfoCircle } from '@tabler/icons-react';
import { BaseDocProps, DocTooltip } from './DocTooltip'; import { type BaseDocProps, DocTooltip } from './DocTooltip';
interface DocInfoProps extends BaseDocProps { interface DocInfoProps extends BaseDocProps {
size?: number; size?: number;

View File

@ -24,7 +24,7 @@ export function DocTooltip({
}: Readonly<DocTooltipProps>) { }: Readonly<DocTooltipProps>) {
return ( return (
<HoverCard <HoverCard
shadow="md" shadow='md'
openDelay={200} openDelay={200}
closeDelay={200} closeDelay={200}
withinPortal={true} withinPortal={true}
@ -63,7 +63,7 @@ function ConstBody({
useEffect(() => { useEffect(() => {
if (ref.current == null) return; if (ref.current == null) return;
let height = ref.current['clientHeight']; const height = ref.current['clientHeight'];
if (height > 250) { if (height > 250) {
setHeight(250); setHeight(250);
} else { } else {
@ -78,7 +78,7 @@ function ConstBody({
<ScrollArea h={height} mah={250}> <ScrollArea h={height} mah={250}>
<div ref={ref}> <div ref={ref}>
{detail && ( {detail && (
<Text size="xs" c="dimmed"> <Text size='xs' c='dimmed'>
{detail} {detail}
</Text> </Text>
)} )}
@ -87,7 +87,7 @@ function ConstBody({
</ScrollArea> </ScrollArea>
)} )}
{link && ( {link && (
<Anchor href={link} target="_blank"> <Anchor href={link} target='_blank'>
<Text size={'sm'}> <Text size={'sm'}>
<Trans>Read More</Trans> <Trans>Read More</Trans>
</Text> </Text>

View File

@ -9,7 +9,7 @@ export function ErrorItem({
return ( return (
<> <>
<Alert color="red" title={t`An error occurred`}> <Alert color='red' title={t`An error occurred`}>
<Text>{error_message}</Text> <Text>{error_message}</Text>
</Alert> </Alert>
</> </>

View File

@ -3,19 +3,19 @@ import { Carousel } from '@mantine/carousel';
import { Anchor, Button, Paper, Text } from '@mantine/core'; import { Anchor, Button, Paper, Text } from '@mantine/core';
import * as classes from './GettingStartedCarousel.css'; import * as classes from './GettingStartedCarousel.css';
import { MenuLinkItem } from './MenuLinks'; import type { MenuLinkItem } from './MenuLinks';
import { StylishText } from './StylishText'; import { StylishText } from './StylishText';
function StartedCard({ title, description, link }: MenuLinkItem) { function StartedCard({ title, description, link }: MenuLinkItem) {
return ( return (
<Paper shadow="md" p="xl" radius="md" className={classes.card}> <Paper shadow='md' p='xl' radius='md' className={classes.card}>
<div> <div>
<StylishText size="md">{title}</StylishText> <StylishText size='md'>{title}</StylishText>
<Text size="sm" className={classes.category} lineClamp={2}> <Text size='sm' className={classes.category} lineClamp={2}>
{description} {description}
</Text> </Text>
</div> </div>
<Anchor href={link} target="_blank"> <Anchor href={link} target='_blank'>
<Button> <Button>
<Trans>Read More</Trans> <Trans>Read More</Trans>
</Button> </Button>
@ -40,7 +40,7 @@ export function GettingStartedCarousel({
slideSize={{ base: '100%', sm: '50%', md: '33.333333%' }} slideSize={{ base: '100%', sm: '50%', md: '33.333333%' }}
slideGap={{ base: 0, sm: 'md' }} slideGap={{ base: 0, sm: 'md' }}
slidesToScroll={3} slidesToScroll={3}
align="start" align='start'
loop loop
> >
{slides} {slides}

View File

@ -1,6 +1,6 @@
import { Trans } from '@lingui/macro'; import { Trans } from '@lingui/macro';
import { Code, Flex, Group, Text } from '@mantine/core'; import { Code, Flex, Group, Text } from '@mantine/core';
import { Link, To } from 'react-router-dom'; import { Link, type To } from 'react-router-dom';
import { YesNoButton } from '../buttons/YesNoButton'; import { YesNoButton } from '../buttons/YesNoButton';
import { DetailDrawerLink } from '../nav/DetailDrawer'; import { DetailDrawerLink } from '../nav/DetailDrawer';
@ -43,8 +43,8 @@ export function InfoItem({
} }
return ( return (
<Group justify="space-between"> <Group justify='space-between'>
<Text fz="sm" fw={700}> <Text fz='sm' fw={700}>
{name}: {name}:
</Text> </Text>
<Flex> <Flex>

View File

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

View File

@ -37,7 +37,7 @@ export function LanguageSelect({ width = 80 }: Readonly<{ width?: number }>) {
value={value} value={value}
onChange={setValue} onChange={setValue}
searchable searchable
aria-label="Select language" aria-label='Select language'
/> />
); );
} }

View File

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

View File

@ -8,11 +8,10 @@ import {
Tooltip, Tooltip,
UnstyledButton UnstyledButton
} from '@mantine/core'; } from '@mantine/core';
import { IconLink } from '@tabler/icons-react';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { InvenTreeIcon, InvenTreeIconType } from '../../functions/icons'; import { InvenTreeIcon, type InvenTreeIconType } from '../../functions/icons';
import { navigateToLink } from '../../functions/navigation'; import { navigateToLink } from '../../functions/navigation';
import { StylishText } from './StylishText'; import { StylishText } from './StylishText';
@ -50,9 +49,9 @@ export function MenuLinks({
return ( return (
<> <>
<Stack gap="xs"> <Stack gap='xs'>
<Divider /> <Divider />
<StylishText size="md">{title}</StylishText> <StylishText size='md'>{title}</StylishText>
<Divider /> <Divider />
<SimpleGrid cols={2} spacing={0} p={3}> <SimpleGrid cols={2} spacing={0} p={3}>
{visibleLinks.map((item) => ( {visibleLinks.map((item) => (
@ -63,7 +62,7 @@ export function MenuLinks({
> >
{item.link && item.external ? ( {item.link && item.external ? (
<Anchor href={item.link}> <Anchor href={item.link}>
<Group wrap="nowrap"> <Group wrap='nowrap'>
{item.external && ( {item.external && (
<InvenTreeIcon <InvenTreeIcon
icon={item.icon ?? 'link'} icon={item.icon ?? 'link'}
@ -87,7 +86,7 @@ export function MenuLinks({
} }
}} }}
> >
<Group wrap="nowrap"> <Group wrap='nowrap'>
{item.icon && ( {item.icon && (
<InvenTreeIcon <InvenTreeIcon
icon={item.icon} icon={item.icon}

View File

@ -13,7 +13,7 @@ export function PlaceholderPill() {
withArrow withArrow
label={t`This feature/button/site is a placeholder for a feature that is not implemented, only partial or intended for testing.`} label={t`This feature/button/site is a placeholder for a feature that is not implemented, only partial or intended for testing.`}
> >
<Badge color="teal" variant="outline"> <Badge color='teal' variant='outline'>
<Trans>PLH</Trans> <Trans>PLH</Trans>
</Badge> </Badge>
</Tooltip> </Tooltip>
@ -27,11 +27,11 @@ export function PlaceholderPanel() {
return ( return (
<Stack> <Stack>
<Alert <Alert
color="teal" color='teal'
title={t`This panel is a placeholder.`} title={t`This panel is a placeholder.`}
icon={<IconInfoCircle />} icon={<IconInfoCircle />}
> >
<Text c="gray">This panel has not yet been implemented</Text> <Text c='gray'>This panel has not yet been implemented</Text>
</Alert> </Alert>
</Stack> </Stack>
); );

View File

@ -15,8 +15,8 @@ export type ProgressBarProps = {
*/ */
export function ProgressBar(props: Readonly<ProgressBarProps>) { export function ProgressBar(props: Readonly<ProgressBarProps>) {
const progress = useMemo(() => { const progress = useMemo(() => {
let maximum = props.maximum ?? 100; const maximum = props.maximum ?? 100;
let value = Math.max(props.value, 0); const value = Math.max(props.value, 0);
if (maximum == 0) { if (maximum == 0) {
return 0; return 0;
@ -28,7 +28,7 @@ export function ProgressBar(props: Readonly<ProgressBarProps>) {
return ( return (
<Stack gap={2} style={{ flexGrow: 1, minWidth: '100px' }}> <Stack gap={2} style={{ flexGrow: 1, minWidth: '100px' }}>
{props.progressLabel && ( {props.progressLabel && (
<Text ta="center" size="xs"> <Text ta='center' size='xs'>
{props.value} / {props.maximum} {props.value} / {props.maximum}
</Text> </Text>
)} )}
@ -36,7 +36,7 @@ export function ProgressBar(props: Readonly<ProgressBarProps>) {
value={progress} value={progress}
color={progress < 100 ? 'orange' : progress > 100 ? 'blue' : 'green'} color={progress < 100 ? 'orange' : progress > 100 ? 'blue' : 'green'}
size={props.size ?? 'md'} size={props.size ?? 'md'}
radius="sm" radius='sm'
/> />
</Stack> </Stack>
); );

View File

@ -21,7 +21,7 @@ import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { apiUrl } from '../../states/ApiState'; import { apiUrl } from '../../states/ApiState';
import { useGlobalSettingsState } from '../../states/SettingsState'; import { useGlobalSettingsState } from '../../states/SettingsState';
import { CopyButton } from '../buttons/CopyButton'; import { CopyButton } from '../buttons/CopyButton';
import { QrCodeType } from './ActionDropdown'; import type { QrCodeType } from './ActionDropdown';
import { BarcodeInput } from './BarcodeInput'; import { BarcodeInput } from './BarcodeInput';
type QRCodeProps = { type QRCodeProps = {
@ -46,7 +46,7 @@ export const QRCode = ({ data, ecl = 'Q', margin = 1 }: QRCodeProps) => {
return ( return (
<Box> <Box>
{qrCode ? ( {qrCode ? (
<Image src={qrCode} alt="QR Code" /> <Image src={qrCode} alt='QR Code' />
) : ( ) : (
<Skeleton height={500} /> <Skeleton height={500} />
)} )}
@ -97,7 +97,7 @@ export const InvenTreeQRCode = ({
return ( return (
<Stack> <Stack>
{mdl_prop.hash ? ( {mdl_prop.hash ? (
<Alert variant="outline" color="red" title={t`Custom barcode`}> <Alert variant='outline' color='red' title={t`Custom barcode`}>
<Trans> <Trans>
A custom barcode is registered for this item. The shown code is not A custom barcode is registered for this item. The shown code is not
that custom barcode. that custom barcode.
@ -110,11 +110,11 @@ export const InvenTreeQRCode = ({
{data && settings.getSetting('BARCODE_SHOW_TEXT', 'false') && ( {data && settings.getSetting('BARCODE_SHOW_TEXT', 'false') && (
<Group <Group
justify={showEclSelector ? 'space-between' : 'center'} justify={showEclSelector ? 'space-between' : 'center'}
align="flex-start" align='flex-start'
px={16} px={16}
> >
<Stack gap={4} pt={2}> <Stack gap={4} pt={2}>
<Text size="sm" fw={500}> <Text size='sm' fw={500}>
<Trans>Barcode Data:</Trans> <Trans>Barcode Data:</Trans>
</Text> </Text>
<Group> <Group>
@ -189,7 +189,7 @@ export const QRCodeUnlink = ({ mdl_prop }: { mdl_prop: QrCodeType }) => {
<Text> <Text>
<Trans>This will remove the link to the associated barcode</Trans> <Trans>This will remove the link to the associated barcode</Trans>
</Text> </Text>
<Button color="red" onClick={unlinkBarcode}> <Button color='red' onClick={unlinkBarcode}>
<Trans>Unlink Barcode</Trans> <Trans>Unlink Barcode</Trans>
</Button> </Button>
</Box> </Box>

View File

@ -10,7 +10,7 @@ export function StylishText({
size?: string; size?: string;
}>) { }>) {
return ( return (
<Text size={size} className={classes.signText} variant="gradient"> <Text size={size} className={classes.signText} variant='gradient'>
{children} {children}
</Text> </Text>
); );

View File

@ -1,4 +1,4 @@
import { Group, Title, TitleProps } from '@mantine/core'; import { Group, Title, type TitleProps } from '@mantine/core';
import { DocInfo } from './DocInfo'; import { DocInfo } from './DocInfo';

View File

@ -1,5 +1,5 @@
import { IconAlertCircle } from '@tabler/icons-react'; import { IconAlertCircle } from '@tabler/icons-react';
export function UnavailableIndicator() { export function UnavailableIndicator() {
return <IconAlertCircle size={18} color="red" />; return <IconAlertCircle size={18} color='red' />;
} }

View File

@ -8,10 +8,9 @@ import {
Space, Space,
Stack, Stack,
Table, Table,
Text, Text
Title
} from '@mantine/core'; } from '@mantine/core';
import { ContextModalProps } from '@mantine/modals'; import type { ContextModalProps } from '@mantine/modals';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { api } from '../../App'; import { api } from '../../App';
@ -51,22 +50,18 @@ export function AboutInvenTreeModal({
queryFn: () => api.get(apiUrl(ApiEndpoints.version)).then((res) => res.data) queryFn: () => api.get(apiUrl(ApiEndpoints.version)).then((res) => res.data)
}); });
function fillTable( function fillTable(lookup: AboutLookupRef[], data: any, alwaysLink = false) {
lookup: AboutLookupRef[],
data: any,
alwaysLink: boolean = false
) {
return lookup.map((map: AboutLookupRef, idx) => ( return lookup.map((map: AboutLookupRef, idx) => (
<Table.Tr key={idx}> <Table.Tr key={idx}>
<Table.Td>{map.title}</Table.Td> <Table.Td>{map.title}</Table.Td>
<Table.Td> <Table.Td>
<Group justify="space-between" gap="xs"> <Group justify='space-between' gap='xs'>
{alwaysLink ? ( {alwaysLink ? (
<Anchor href={data[map.ref]} target="_blank"> <Anchor href={data[map.ref]} target='_blank'>
{data[map.ref]} {data[map.ref]}
</Anchor> </Anchor>
) : map.link ? ( ) : map.link ? (
<Anchor href={map.link} target="_blank"> <Anchor href={map.link} target='_blank'>
{data[map.ref]} {data[map.ref]}
</Anchor> </Anchor>
) : ( ) : (
@ -96,20 +91,20 @@ export function AboutInvenTreeModal({
return ( return (
<Stack> <Stack>
<Divider /> <Divider />
<Group justify="space-between" wrap="nowrap"> <Group justify='space-between' wrap='nowrap'>
<StylishText size="lg"> <StylishText size='lg'>
<Trans>Version Information</Trans> <Trans>Version Information</Trans>
</StylishText> </StylishText>
{data.dev ? ( {data.dev ? (
<Badge color="blue"> <Badge color='blue'>
<Trans>Development Version</Trans> <Trans>Development Version</Trans>
</Badge> </Badge>
) : data.up_to_date ? ( ) : data.up_to_date ? (
<Badge color="green"> <Badge color='green'>
<Trans>Up to Date</Trans> <Trans>Up to Date</Trans>
</Badge> </Badge>
) : ( ) : (
<Badge color="teal"> <Badge color='teal'>
<Trans>Update Available</Trans> <Trans>Update Available</Trans>
</Badge> </Badge>
)} )}
@ -162,7 +157,7 @@ export function AboutInvenTreeModal({
</Table.Tbody> </Table.Tbody>
</Table> </Table>
<Divider /> <Divider />
<StylishText size="lg"> <StylishText size='lg'>
<Trans>Links</Trans> <Trans>Links</Trans>
</StylishText> </StylishText>
<Table striped> <Table striped>
@ -181,7 +176,7 @@ export function AboutInvenTreeModal({
</Table.Tbody> </Table.Tbody>
</Table> </Table>
<Divider /> <Divider />
<Group justify="space-between"> <Group justify='space-between'>
<CopyButton value={copyval} label={t`Copy version information`} /> <CopyButton value={copyval} label={t`Copy version information`} />
<Space /> <Space />
<Button <Button

View File

@ -19,17 +19,17 @@ import { apiUrl } from '../../states/ApiState';
export function LicenceView(entries: any[]) { export function LicenceView(entries: any[]) {
return ( return (
<Stack gap="xs"> <Stack gap='xs'>
<Divider /> <Divider />
{entries?.length > 0 ? ( {entries?.length > 0 ? (
<Accordion variant="contained" defaultValue="-"> <Accordion variant='contained' defaultValue='-'>
{entries?.map((entry: any, index: number) => ( {entries?.map((entry: any, index: number) => (
<Accordion.Item <Accordion.Item
key={entry.name + entry.license + entry.version} key={entry.name + entry.license + entry.version}
value={`entry-${index}`} value={`entry-${index}`}
> >
<Accordion.Control> <Accordion.Control>
<Group justify="space-between" grow> <Group justify='space-between' grow>
<Text>{entry.name}</Text> <Text>{entry.name}</Text>
<Text>{entry.license}</Text> <Text>{entry.license}</Text>
<Space /> <Space />
@ -75,7 +75,7 @@ export function LicenseModal() {
}, [packageKeys]); }, [packageKeys]);
return ( return (
<Stack gap="xs"> <Stack gap='xs'>
<Divider /> <Divider />
<LoadingOverlay visible={isFetching} /> <LoadingOverlay visible={isFetching} />
{isFetching && ( {isFetching && (
@ -84,7 +84,7 @@ export function LicenseModal() {
</Text> </Text>
)} )}
{isError ? ( {isError ? (
<Alert color="red" title={t`Error`}> <Alert color='red' title={t`Error`}>
<Text> <Text>
<Trans>Failed to fetch license information</Trans> <Trans>Failed to fetch license information</Trans>
</Text> </Text>

View File

@ -1,7 +1,7 @@
import { Trans, t } from '@lingui/macro'; import { Trans, t } from '@lingui/macro';
import { Button, ScrollArea, Stack, Text } from '@mantine/core'; import { Button, ScrollArea, Stack, Text } from '@mantine/core';
import { useListState } from '@mantine/hooks'; import { useListState } from '@mantine/hooks';
import { ContextModalProps } from '@mantine/modals'; import type { ContextModalProps } from '@mantine/modals';
import { showNotification } from '@mantine/notifications'; import { showNotification } from '@mantine/notifications';
import { api } from '../../App'; import { api } from '../../App';
@ -32,14 +32,14 @@ export function QrCodeModal({
} }
return ( return (
<Stack gap="xs"> <Stack gap='xs'>
<BarcodeInput onScan={onScanAction} /> <BarcodeInput onScan={onScanAction} />
{values.length == 0 ? ( {values.length == 0 ? (
<Text c={'grey'}> <Text c={'grey'}>
<Trans>No scans yet!</Trans> <Trans>No scans yet!</Trans>
</Text> </Text>
) : ( ) : (
<ScrollArea style={{ height: 200 }} type="auto" offsetScrollbars> <ScrollArea style={{ height: 200 }} type='auto' offsetScrollbars>
{values.map((value, index) => ( {values.map((value, index) => (
<div key={`${index}-${value}`}>{value}</div> <div key={`${index}-${value}`}>{value}</div>
))} ))}
@ -47,8 +47,8 @@ export function QrCodeModal({
)} )}
<Button <Button
fullWidth fullWidth
mt="md" mt='md'
color="red" color='red'
onClick={() => { onClick={() => {
// stopScanning(); // stopScanning();
context.closeModal(id); context.closeModal(id);

View File

@ -1,14 +1,6 @@
import { Trans } from '@lingui/macro'; import { Trans } from '@lingui/macro';
import { import { Badge, Button, Divider, Group, Stack, Table } from '@mantine/core';
Badge, import type { ContextModalProps } from '@mantine/modals';
Button,
Divider,
Group,
Stack,
Table,
Title
} from '@mantine/core';
import { ContextModalProps } from '@mantine/modals';
import { useServerApiState } from '../../states/ApiState'; import { useServerApiState } from '../../states/ApiState';
import { OnlyStaff } from '../items/OnlyStaff'; import { OnlyStaff } from '../items/OnlyStaff';
@ -23,7 +15,7 @@ export function ServerInfoModal({
return ( return (
<Stack> <Stack>
<Divider /> <Divider />
<StylishText size="lg"> <StylishText size='lg'>
<Trans>Server</Trans> <Trans>Server</Trans>
</StylishText> </StylishText>
<Table striped> <Table striped>
@ -110,7 +102,7 @@ export function ServerInfoModal({
<Trans>Background Worker</Trans> <Trans>Background Worker</Trans>
</Table.Td> </Table.Td>
<Table.Td> <Table.Td>
<Badge color="red"> <Badge color='red'>
<Trans>Background worker not running</Trans> <Trans>Background worker not running</Trans>
</Badge> </Badge>
</Table.Td> </Table.Td>
@ -122,7 +114,7 @@ export function ServerInfoModal({
<Trans>Email Settings</Trans> <Trans>Email Settings</Trans>
</Table.Td> </Table.Td>
<Table.Td> <Table.Td>
<Badge color="red"> <Badge color='red'>
<Trans>Email settings not configured</Trans> <Trans>Email settings not configured</Trans>
</Badge> </Badge>
</Table.Td> </Table.Td>
@ -131,7 +123,7 @@ export function ServerInfoModal({
</Table.Tbody> </Table.Tbody>
</Table> </Table>
<Divider /> <Divider />
<Group justify="right"> <Group justify='right'>
<Button <Button
onClick={() => { onClick={() => {
context.closeModal(id); context.closeModal(id);

View File

@ -45,23 +45,23 @@ export function BreadcrumbList({
}, [breadcrumbs]); }, [breadcrumbs]);
return ( return (
<Paper p="7" radius="xs" shadow="xs"> <Paper p='7' radius='xs' shadow='xs'>
<Group gap="xs"> <Group gap='xs'>
{navCallback && ( {navCallback && (
<ActionIcon <ActionIcon
key="nav-breadcrumb-action" key='nav-breadcrumb-action'
aria-label="nav-breadcrumb-action" aria-label='nav-breadcrumb-action'
onClick={navCallback} onClick={navCallback}
variant="transparent" variant='transparent'
> >
<IconMenu2 /> <IconMenu2 />
</ActionIcon> </ActionIcon>
)} )}
<Breadcrumbs key="breadcrumbs" separator=">"> <Breadcrumbs key='breadcrumbs' separator='>'>
{elements.map((breadcrumb, index) => { {elements.map((breadcrumb, index) => {
return ( return (
<Anchor <Anchor
key={index} key={`${index}-${breadcrumb.name}`}
aria-label={`breadcrumb-${index}-${identifierString( aria-label={`breadcrumb-${index}-${identifierString(
breadcrumb.name breadcrumb.name
)}`} )}`}
@ -72,7 +72,7 @@ export function BreadcrumbList({
> >
<Group gap={4}> <Group gap={4}>
{breadcrumb.icon} {breadcrumb.icon}
<Text size="sm">{breadcrumb.name}</Text> <Text size='sm'>{breadcrumb.name}</Text>
</Group> </Group>
</Anchor> </Anchor>
); );

View File

@ -4,7 +4,7 @@ import { useCallback, useMemo } from 'react';
import { Link, Route, Routes, useNavigate, useParams } from 'react-router-dom'; import { Link, Route, Routes, useNavigate, useParams } from 'react-router-dom';
import type { To } from 'react-router-dom'; import type { To } from 'react-router-dom';
import { UiSizeType } from '../../defaults/formatters'; import type { UiSizeType } from '../../defaults/formatters';
import { useLocalState } from '../../states/LocalState'; import { useLocalState } from '../../states/LocalState';
import * as classes from './DetailDrawer.css'; import * as classes from './DetailDrawer.css';
@ -57,7 +57,7 @@ function DetailDrawerComponent({
<Group> <Group>
{detailDrawerStack > 0 && ( {detailDrawerStack > 0 && (
<ActionIcon <ActionIcon
variant="outline" variant='outline'
onClick={() => { onClick={() => {
navigate(-1); navigate(-1);
addDetailDrawer(-1); addDetailDrawer(-1);
@ -66,7 +66,7 @@ function DetailDrawerComponent({
<IconChevronLeft /> <IconChevronLeft />
</ActionIcon> </ActionIcon>
)} )}
<Text size="xl" fw={600} variant="gradient"> <Text size='xl' fw={600} variant='gradient'>
{title} {title}
</Text> </Text>
</Group> </Group>
@ -83,7 +83,7 @@ function DetailDrawerComponent({
export function DetailDrawer(props: Readonly<DrawerProps>) { export function DetailDrawer(props: Readonly<DrawerProps>) {
return ( return (
<Routes> <Routes>
<Route path=":id?/" element={<DetailDrawerComponent {...props} />} /> <Route path=':id?/' element={<DetailDrawerComponent {...props} />} />
</Routes> </Routes>
); );
} }

View File

@ -2,7 +2,7 @@ import { ActionIcon, Container, Group, Indicator, Tabs } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks'; import { useDisclosure } from '@mantine/hooks';
import { IconBell, IconSearch } from '@tabler/icons-react'; import { IconBell, IconSearch } from '@tabler/icons-react';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { ReactNode, useEffect, useMemo, useState } from 'react'; import { type ReactNode, useEffect, useMemo, useState } from 'react';
import { useMatch, useNavigate } from 'react-router-dom'; import { useMatch, useNavigate } from 'react-router-dom';
import { api } from '../../App'; import { api } from '../../App';
@ -61,7 +61,7 @@ export function Header() {
limit: 1 limit: 1
} }
}; };
let response = await api const response = await api
.get(apiUrl(ApiEndpoints.notifications_list), params) .get(apiUrl(ApiEndpoints.notifications_list), params)
.catch(() => { .catch(() => {
return null; return null;
@ -99,8 +99,8 @@ export function Header() {
closeNotificationDrawer(); closeNotificationDrawer();
}} }}
/> />
<Container className={classes.layoutHeaderSection} size="100%"> <Container className={classes.layoutHeaderSection} size='100%'>
<Group justify="space-between"> <Group justify='space-between'>
<Group> <Group>
<NavHoverMenu openDrawer={openNavDrawer} /> <NavHoverMenu openDrawer={openNavDrawer} />
<NavTabs /> <NavTabs />
@ -108,25 +108,25 @@ export function Header() {
<Group> <Group>
<ActionIcon <ActionIcon
onClick={openSearchDrawer} onClick={openSearchDrawer}
variant="transparent" variant='transparent'
aria-label="open-search" aria-label='open-search'
> >
<IconSearch /> <IconSearch />
</ActionIcon> </ActionIcon>
<SpotlightButton /> <SpotlightButton />
{globalSettings.isSet('BARCODE_ENABLE') && <ScanButton />} {globalSettings.isSet('BARCODE_ENABLE') && <ScanButton />}
<Indicator <Indicator
radius="lg" radius='lg'
size="18" size='18'
label={notificationCount} label={notificationCount}
color="red" color='red'
disabled={notificationCount <= 0} disabled={notificationCount <= 0}
inline inline
> >
<ActionIcon <ActionIcon
onClick={openNotificationDrawer} onClick={openNotificationDrawer}
variant="transparent" variant='transparent'
aria-label="open-notifications" aria-label='open-notifications'
> >
<IconBell /> <IconBell />
</ActionIcon> </ActionIcon>
@ -146,7 +146,7 @@ function NavTabs() {
const tabValue = match?.params.tabName; const tabValue = match?.params.tabName;
const tabs: ReactNode[] = useMemo(() => { const tabs: ReactNode[] = useMemo(() => {
let _tabs: ReactNode[] = []; const _tabs: ReactNode[] = [];
mainNavTabs.forEach((tab) => { mainNavTabs.forEach((tab) => {
if (tab.role && !user.hasViewRole(tab.role)) { if (tab.role && !user.hasViewRole(tab.role)) {
@ -171,7 +171,7 @@ function NavTabs() {
return ( return (
<Tabs <Tabs
defaultValue="home" defaultValue='home'
classNames={{ classNames={{
root: classes.tabs, root: classes.tabs,
list: classes.tabsList, list: classes.tabsList,

View File

@ -19,7 +19,7 @@ export const ProtectedRoute = ({ children }: { children: JSX.Element }) => {
if (!isLoggedIn()) { if (!isLoggedIn()) {
return ( return (
<Navigate <Navigate
to="/logged-in" to='/logged-in'
state={{ state={{
redirectUrl: location.pathname, redirectUrl: location.pathname,
queryParams: location.search, queryParams: location.search,
@ -58,22 +58,22 @@ export default function LayoutComponent() {
return ( return (
<ProtectedRoute> <ProtectedRoute>
<Flex direction="column" mih="100vh"> <Flex direction='column' mih='100vh'>
<Header /> <Header />
<Container className={classes.layoutContent} size="100%"> <Container className={classes.layoutContent} size='100%'>
<Boundary label={'layout'}> <Boundary label={'layout'}>
<Outlet /> <Outlet />
</Boundary> </Boundary>
{/* </ErrorBoundary> */} {/* </ErrorBoundary> */}
</Container> </Container>
<Space h="xl" /> <Space h='xl' />
<Footer /> <Footer />
<Spotlight <Spotlight
actions={actions} actions={actions}
store={firstStore} store={firstStore}
highlightQuery highlightQuery
searchProps={{ searchProps={{
leftSection: <IconSearch size="1.2rem" />, leftSection: <IconSearch size='1.2rem' />,
placeholder: t`Search...` placeholder: t`Search...`
}} }}
shortcut={['mod + K', '/']} shortcut={['mod + K', '/']}

View File

@ -32,12 +32,12 @@ export function MainMenu() {
const { colorScheme, toggleColorScheme } = useMantineColorScheme(); const { colorScheme, toggleColorScheme } = useMantineColorScheme();
return ( return (
<Menu width={260} position="bottom-end"> <Menu width={260} position='bottom-end'>
<Menu.Target> <Menu.Target>
<UnstyledButton className={classes.layoutHeaderUser}> <UnstyledButton className={classes.layoutHeaderUser}>
<Group gap={7}> <Group gap={7}>
{username() ? ( {username() ? (
<Text fw={500} size="sm" style={{ lineHeight: 1 }} mr={3}> <Text fw={500} size='sm' style={{ lineHeight: 1 }} mr={3}>
{username()} {username()}
</Text> </Text>
) : ( ) : (
@ -54,7 +54,7 @@ export function MainMenu() {
<Menu.Item <Menu.Item
leftSection={<IconUserCog />} leftSection={<IconUserCog />}
component={Link} component={Link}
to="/settings/user" to='/settings/user'
> >
<Trans>Account Settings</Trans> <Trans>Account Settings</Trans>
</Menu.Item> </Menu.Item>
@ -62,7 +62,7 @@ export function MainMenu() {
<Menu.Item <Menu.Item
leftSection={<IconSettings />} leftSection={<IconSettings />}
component={Link} component={Link}
to="/settings/system" to='/settings/system'
> >
<Trans>System Settings</Trans> <Trans>System Settings</Trans>
</Menu.Item> </Menu.Item>
@ -81,7 +81,7 @@ export function MainMenu() {
<Menu.Item <Menu.Item
leftSection={<IconUserBolt />} leftSection={<IconUserBolt />}
component={Link} component={Link}
to="/settings/admin" to='/settings/admin'
> >
<Trans>Admin Center</Trans> <Trans>Admin Center</Trans>
</Menu.Item> </Menu.Item>

View File

@ -3,12 +3,12 @@ import { UnstyledButton } from '@mantine/core';
import { InvenTreeLogo } from '../items/InvenTreeLogo'; import { InvenTreeLogo } from '../items/InvenTreeLogo';
export function NavHoverMenu({ export function NavHoverMenu({
openDrawer: openDrawer openDrawer
}: Readonly<{ }: Readonly<{
openDrawer: () => void; openDrawer: () => void;
}>) { }>) {
return ( return (
<UnstyledButton onClick={() => openDrawer()} aria-label="navigation-menu"> <UnstyledButton onClick={() => openDrawer()} aria-label='navigation-menu'>
<InvenTreeLogo /> <InvenTreeLogo />
</UnstyledButton> </UnstyledButton>
); );

View File

@ -18,7 +18,7 @@ import * as classes from '../../main.css';
import { useGlobalSettingsState } from '../../states/SettingsState'; import { useGlobalSettingsState } from '../../states/SettingsState';
import { useUserState } from '../../states/UserState'; import { useUserState } from '../../states/UserState';
import { InvenTreeLogo } from '../items/InvenTreeLogo'; import { InvenTreeLogo } from '../items/InvenTreeLogo';
import { MenuLinkItem, MenuLinks } from '../items/MenuLinks'; import { type MenuLinkItem, MenuLinks } from '../items/MenuLinks';
import { StylishText } from '../items/StylishText'; import { StylishText } from '../items/StylishText';
// TODO @matmair #1: implement plugin loading and menu item generation see #5269 // TODO @matmair #1: implement plugin loading and menu item generation see #5269
@ -35,7 +35,7 @@ export function NavigationDrawer({
<Drawer <Drawer
opened={opened} opened={opened}
onClose={close} onClose={close}
size="lg" size='lg'
withCloseButton={false} withCloseButton={false}
classNames={{ classNames={{
body: classes.navigationDrawer body: classes.navigationDrawer
@ -161,14 +161,14 @@ function DrawerContent({ closeFunc }: { closeFunc?: () => void }) {
const menuItemsAbout: MenuLinkItem[] = useMemo(() => AboutLinks(), []); const menuItemsAbout: MenuLinkItem[] = useMemo(() => AboutLinks(), []);
return ( return (
<Flex direction="column" mih="100vh" p={16}> <Flex direction='column' mih='100vh' p={16}>
<Group wrap="nowrap"> <Group wrap='nowrap'>
<InvenTreeLogo /> <InvenTreeLogo />
<StylishText size="xl">{title}</StylishText> <StylishText size='xl'>{title}</StylishText>
</Group> </Group>
<Space h="xs" /> <Space h='xs' />
<Container className={classes.layoutContent} p={0}> <Container className={classes.layoutContent} p={0}>
<ScrollArea h={scrollHeight} type="always" offsetScrollbars> <ScrollArea h={scrollHeight} type='always' offsetScrollbars>
<MenuLinks <MenuLinks
title={t`Navigation`} title={t`Navigation`}
links={menuItemsNavigate} links={menuItemsNavigate}
@ -184,7 +184,7 @@ function DrawerContent({ closeFunc }: { closeFunc?: () => void }) {
links={menuItemsAction} links={menuItemsAction}
beforeClick={closeFunc} beforeClick={closeFunc}
/> />
<Space h="md" /> <Space h='md' />
{plugins.length > 0 ? ( {plugins.length > 0 ? (
<> <>
<MenuLinks <MenuLinks
@ -199,13 +199,13 @@ function DrawerContent({ closeFunc }: { closeFunc?: () => void }) {
</ScrollArea> </ScrollArea>
</Container> </Container>
<div ref={ref}> <div ref={ref}>
<Space h="md" /> <Space h='md' />
<MenuLinks <MenuLinks
title={t`Documentation`} title={t`Documentation`}
links={menuItemsDocumentation} links={menuItemsDocumentation}
beforeClick={closeFunc} beforeClick={closeFunc}
/> />
<Space h="md" /> <Space h='md' />
<MenuLinks <MenuLinks
title={t`About`} title={t`About`}
links={menuItemsAbout} links={menuItemsAbout}

View File

@ -5,11 +5,11 @@ import {
Drawer, Drawer,
Group, Group,
LoadingOverlay, LoadingOverlay,
RenderTreeNodePayload, type RenderTreeNodePayload,
Space, Space,
Stack, Stack,
Tree, Tree,
TreeNodeData, type TreeNodeData,
useTree useTree
} from '@mantine/core'; } from '@mantine/core';
import { import {
@ -22,8 +22,8 @@ import { useCallback, useMemo } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { api } from '../../App'; import { api } from '../../App';
import { ApiEndpoints } from '../../enums/ApiEndpoints'; import type { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType'; import type { ModelType } from '../../enums/ModelType';
import { navigateToLink } from '../../functions/navigation'; import { navigateToLink } from '../../functions/navigation';
import { getDetailUrl } from '../../functions/urls'; import { getDetailUrl } from '../../functions/urls';
import { apiUrl } from '../../states/ApiState'; import { apiUrl } from '../../states/ApiState';
@ -89,19 +89,19 @@ export default function NavigationTree({
* It is required (and assumed) that the data is first sorted by level. * It is required (and assumed) that the data is first sorted by level.
*/ */
let nodes: Record<number, any> = {}; const nodes: Record<number, any> = {};
let tree: TreeNodeData[] = []; const tree: TreeNodeData[] = [];
if (!query?.data?.length) { if (!query?.data?.length) {
return []; return [];
} }
for (let ii = 0; ii < query.data.length; ii++) { for (let ii = 0; ii < query.data.length; ii++) {
let node = { const node = {
...query.data[ii], ...query.data[ii],
children: [], children: [],
label: ( label: (
<Group gap="xs"> <Group gap='xs'>
<ApiIcon name={query.data[ii].icon} /> <ApiIcon name={query.data[ii].icon} />
{query.data[ii].name} {query.data[ii].name}
</Group> </Group>
@ -141,9 +141,9 @@ export default function NavigationTree({
(payload: RenderTreeNodePayload) => { (payload: RenderTreeNodePayload) => {
return ( return (
<Group <Group
justify="left" justify='left'
key={payload.node.value} key={payload.node.value}
wrap="nowrap" wrap='nowrap'
onClick={() => { onClick={() => {
if (payload.hasChildren) { if (payload.hasChildren) {
treeState.toggleExpanded(payload.node.value); treeState.toggleExpanded(payload.node.value);
@ -152,8 +152,8 @@ export default function NavigationTree({
> >
<Space w={5 * payload.level} /> <Space w={5 * payload.level} />
<ActionIcon <ActionIcon
size="sm" size='sm'
variant="transparent" variant='transparent'
aria-label={`nav-tree-toggle-${payload.node.value}}`} aria-label={`nav-tree-toggle-${payload.node.value}}`}
> >
{payload.hasChildren ? ( {payload.hasChildren ? (
@ -179,8 +179,8 @@ export default function NavigationTree({
return ( return (
<Drawer <Drawer
opened={opened} opened={opened}
size="md" size='md'
position="left" position='left'
onClose={onClose} onClose={onClose}
withCloseButton={true} withCloseButton={true}
styles={{ styles={{
@ -192,13 +192,13 @@ export default function NavigationTree({
} }
}} }}
title={ title={
<Group justify="left" p="ms" gap="md" wrap="nowrap"> <Group justify='left' p='ms' gap='md' wrap='nowrap'>
<IconSitemap /> <IconSitemap />
<StylishText size="lg">{title}</StylishText> <StylishText size='lg'>{title}</StylishText>
</Group> </Group>
} }
> >
<Stack gap="xs"> <Stack gap='xs'>
<Divider /> <Divider />
<LoadingOverlay visible={query.isFetching || query.isLoading} /> <LoadingOverlay visible={query.isFetching || query.isLoading} />
<Tree data={data} tree={treeState} renderNode={renderNode} /> <Tree data={data} tree={treeState} renderNode={renderNode} />

View File

@ -20,7 +20,7 @@ import { useNavigate } from 'react-router-dom';
import { api } from '../../App'; import { api } from '../../App';
import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType'; import type { ModelType } from '../../enums/ModelType';
import { navigateToLink } from '../../functions/navigation'; import { navigateToLink } from '../../functions/navigation';
import { getDetailUrl } from '../../functions/urls'; import { getDetailUrl } from '../../functions/urls';
import { base_url } from '../../main'; import { base_url } from '../../main';
@ -44,12 +44,12 @@ function NotificationEntry({
let link = notification.target?.link; let link = notification.target?.link;
let model_type = notification.target?.model_type; const model_type = notification.target?.model_type;
let model_id = notification.target?.model_id; const model_id = notification.target?.model_id;
// If a valid model type is provided, that overrides the specified link // If a valid model type is provided, that overrides the specified link
if (model_type as ModelType) { if (model_type as ModelType) {
let model_info = ModelInformationDict[model_type as ModelType]; const model_info = ModelInformationDict[model_type as ModelType];
if (model_info?.url_detail && model_id) { if (model_info?.url_detail && model_id) {
link = getDetailUrl(model_type as ModelType, model_id); link = getDetailUrl(model_type as ModelType, model_id);
} else if (model_info?.url_overview) { } else if (model_info?.url_overview) {
@ -58,18 +58,18 @@ function NotificationEntry({
} }
return ( return (
<Paper p="xs" shadow="xs"> <Paper p='xs' shadow='xs'>
<Group justify="space-between" wrap="nowrap"> <Group justify='space-between' wrap='nowrap'>
<Tooltip <Tooltip
label={notification.message} label={notification.message}
position="bottom-end" position='bottom-end'
hidden={!notification.message} hidden={!notification.message}
> >
<Stack gap={2}> <Stack gap={2}>
<Anchor <Anchor
href={link ? `/${base_url}${link}` : '#'} href={link ? `/${base_url}${link}` : '#'}
underline="hover" underline='hover'
target="_blank" target='_blank'
onClick={(event: any) => { onClick={(event: any) => {
if (link) { if (link) {
// Mark the notification as read // Mark the notification as read
@ -81,13 +81,13 @@ function NotificationEntry({
} }
}} }}
> >
<Text size="sm">{notification.name}</Text> <Text size='sm'>{notification.name}</Text>
</Anchor> </Anchor>
<Text size="xs">{notification.age_human}</Text> <Text size='xs'>{notification.age_human}</Text>
</Stack> </Stack>
</Tooltip> </Tooltip>
<Tooltip label={t`Mark as read`} position="bottom-end"> <Tooltip label={t`Mark as read`} position='bottom-end'>
<ActionIcon variant="transparent" onClick={onRead}> <ActionIcon variant='transparent' onClick={onRead}>
<IconBellCheck /> <IconBellCheck />
</ActionIcon> </ActionIcon>
</Tooltip> </Tooltip>
@ -162,8 +162,8 @@ export function NotificationDrawer({
return ( return (
<Drawer <Drawer
opened={opened} opened={opened}
size="md" size='md'
position="right" position='right'
onClose={onClose} onClose={onClose}
withCloseButton={false} withCloseButton={false}
styles={{ styles={{
@ -175,12 +175,12 @@ export function NotificationDrawer({
} }
}} }}
title={ title={
<Group justify="space-between" wrap="nowrap"> <Group justify='space-between' wrap='nowrap'>
<StylishText size="lg">{t`Notifications`}</StylishText> <StylishText size='lg'>{t`Notifications`}</StylishText>
<Group justify="end" wrap="nowrap"> <Group justify='end' wrap='nowrap'>
<Tooltip label={t`Mark all as read`}> <Tooltip label={t`Mark all as read`}>
<ActionIcon <ActionIcon
variant="transparent" variant='transparent'
onClick={() => { onClick={() => {
markAllAsRead(); markAllAsRead();
}} }}
@ -194,7 +194,7 @@ export function NotificationDrawer({
onClose(); onClose();
navigateToLink('/notifications/unread', navigate, event); navigateToLink('/notifications/unread', navigate, event);
}} }}
variant="transparent" variant='transparent'
> >
<IconArrowRight /> <IconArrowRight />
</ActionIcon> </ActionIcon>
@ -203,12 +203,12 @@ export function NotificationDrawer({
</Group> </Group>
} }
> >
<Boundary label="NotificationDrawer"> <Boundary label='NotificationDrawer'>
<Stack gap="xs"> <Stack gap='xs'>
<Divider /> <Divider />
{!hasNotifications && ( {!hasNotifications && (
<Alert color="green"> <Alert color='green'>
<Text size="sm">{t`You have no unread notifications.`}</Text> <Text size='sm'>{t`You have no unread notifications.`}</Text>
</Alert> </Alert>
)} )}
{hasNotifications && {hasNotifications &&
@ -221,7 +221,7 @@ export function NotificationDrawer({
))} ))}
{notificationQuery.isFetching && ( {notificationQuery.isFetching && (
<Center> <Center>
<Loader size="sm" /> <Loader size='sm' />
</Center> </Center>
)} )}
</Stack> </Stack>

View File

@ -1,10 +1,10 @@
import { Group, Paper, Space, Stack, Text } from '@mantine/core'; import { Group, Paper, Space, Stack, Text } from '@mantine/core';
import { useHotkeys } from '@mantine/hooks'; import { useHotkeys } from '@mantine/hooks';
import { Fragment, ReactNode } from 'react'; import { Fragment, type ReactNode } from 'react';
import { ApiImage } from '../images/ApiImage'; import { ApiImage } from '../images/ApiImage';
import { StylishText } from '../items/StylishText'; import { StylishText } from '../items/StylishText';
import { Breadcrumb, BreadcrumbList } from './BreadcrumbList'; import { type Breadcrumb, BreadcrumbList } from './BreadcrumbList';
interface PageDetailInterface { interface PageDetailInterface {
title?: string; title?: string;
@ -51,26 +51,26 @@ export function PageDetail({
]); ]);
return ( return (
<Stack gap="xs"> <Stack gap='xs'>
{breadcrumbs && breadcrumbs.length > 0 && ( {breadcrumbs && breadcrumbs.length > 0 && (
<BreadcrumbList <BreadcrumbList
navCallback={breadcrumbAction} navCallback={breadcrumbAction}
breadcrumbs={breadcrumbs} breadcrumbs={breadcrumbs}
/> />
)} )}
<Paper p="xs" radius="xs" shadow="xs"> <Paper p='xs' radius='xs' shadow='xs'>
<Stack gap="xs"> <Stack gap='xs'>
<Group justify="space-between" wrap="nowrap"> <Group justify='space-between' wrap='nowrap'>
<Group justify="left" wrap="nowrap"> <Group justify='left' wrap='nowrap'>
{imageUrl && ( {imageUrl && (
<ApiImage src={imageUrl} radius="sm" mah={42} maw={42} /> <ApiImage src={imageUrl} radius='sm' mah={42} maw={42} />
)} )}
<Stack gap="xs"> <Stack gap='xs'>
{title && <StylishText size="lg">{title}</StylishText>} {title && <StylishText size='lg'>{title}</StylishText>}
{subtitle && ( {subtitle && (
<Group gap="xs"> <Group gap='xs'>
{icon} {icon}
<Text size="md" truncate> <Text size='md' truncate>
{subtitle} {subtitle}
</Text> </Text>
</Group> </Group>
@ -79,14 +79,14 @@ export function PageDetail({
</Group> </Group>
<Space /> <Space />
{detail} {detail}
<Group justify="right" gap="xs" wrap="nowrap"> <Group justify='right' gap='xs' wrap='nowrap'>
{badges?.map((badge, idx) => ( {badges?.map((badge, idx) => (
<Fragment key={idx}>{badge}</Fragment> <Fragment key={idx}>{badge}</Fragment>
))} ))}
</Group> </Group>
<Space /> <Space />
{actions && ( {actions && (
<Group gap={5} justify="right"> <Group gap={5} justify='right'>
{actions.map((action, idx) => ( {actions.map((action, idx) => (
<Fragment key={idx}>{action}</Fragment> <Fragment key={idx}>{action}</Fragment>
))} ))}

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