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:
parent
e7cfb4c3c0
commit
0872beaba9
@ -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"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -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
5
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"biomejs.biome"
|
||||||
|
]
|
||||||
|
}
|
8
.vscode/settings.json
vendored
Normal file
8
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"[typescript]": {
|
||||||
|
"editor.defaultFormatter": "biomejs.biome"
|
||||||
|
},
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"quickfix.biome": "explicit"
|
||||||
|
}
|
||||||
|
}
|
40
biome.json
Normal file
40
biome.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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"]
|
||||||
|
@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"semi": true,
|
|
||||||
"trailingComma": "none",
|
|
||||||
"singleQuote": true,
|
|
||||||
"printWidth": 80,
|
|
||||||
"importOrder": ["<THIRD_PARTY_MODULES>", "^[./]"],
|
|
||||||
"importOrderSeparation": true,
|
|
||||||
"importOrderSortSpecifiers": true
|
|
||||||
}
|
|
@ -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,
|
|
||||||
};
|
|
@ -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'];
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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 />} />;
|
||||||
}
|
}
|
||||||
|
@ -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'
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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 && (
|
||||||
|
@ -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>
|
||||||
|
@ -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}
|
||||||
|
@ -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
|
||||||
};
|
};
|
||||||
|
@ -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'
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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}{' '}
|
||||||
|
@ -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 />
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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}
|
||||||
|
@ -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>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
@ -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>
|
||||||
|
@ -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 />
|
||||||
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
@ -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';
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
@ -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,22 +30,21 @@ 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;
|
label?: string;
|
||||||
label?: string;
|
badge?: BadgeType;
|
||||||
badge?: BadgeType;
|
copy?: boolean;
|
||||||
copy?: boolean;
|
value_formatter?: () => ValueFormatterReturn;
|
||||||
value_formatter?: () => ValueFormatterReturn;
|
} & (
|
||||||
} & (
|
| StringDetailField
|
||||||
| StringDetailField
|
| BooleanField
|
||||||
| BooleanField
|
| LinkDetailField
|
||||||
| LinkDetailField
|
| ProgressBarField
|
||||||
| ProgressBarField
|
| StatusField
|
||||||
| StatusField
|
);
|
||||||
);
|
|
||||||
|
|
||||||
type BadgeType = 'owner' | 'user' | 'group';
|
type BadgeType = 'owner' | 'user' | 'group';
|
||||||
type ValueFormatterReturn = string | number | null | React.ReactNode;
|
type ValueFormatterReturn = string | number | null | React.ReactNode;
|
||||||
@ -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)
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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}
|
||||||
|
@ -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>
|
||||||
|
@ -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);
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -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 = {
|
||||||
|
@ -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' />
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -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 = {
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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)}
|
||||||
>
|
>
|
||||||
|
@ -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't have an account?</Trans>{' '}
|
<Trans>Don'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>
|
||||||
|
@ -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>
|
||||||
|
@ -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';
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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>
|
||||||
|
@ -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}
|
||||||
|
@ -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}
|
||||||
|
@ -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({
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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) => {
|
||||||
|
@ -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}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
@ -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} />
|
||||||
)}
|
)}
|
||||||
|
@ -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}
|
||||||
|
@ -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}
|
||||||
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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>
|
||||||
|
@ -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`
|
||||||
};
|
};
|
||||||
|
@ -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
|
||||||
|
@ -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>
|
||||||
|
@ -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}
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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;
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
</>
|
</>
|
||||||
|
@ -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}
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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'
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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}
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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';
|
||||||
|
|
||||||
|
@ -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' />;
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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>
|
||||||
|
@ -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);
|
||||||
|
@ -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);
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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', '/']}
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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}
|
||||||
|
@ -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} />
|
||||||
|
@ -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>
|
||||||
|
@ -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
Loading…
x
Reference in New Issue
Block a user