Model render refactor (#5894)

* Embiggen search drawer

* Refactor search drawer queries:

- Use proper permission checks

* Actually check user settings

* Move StatusRenderer

* Update renderers

- Improve in-line render for different order types

* Update stockitem renderere

* Remove old renderer functions

* Better data handling in UserState

* Tweaks for settings pages

* "Fix" scanning page

- Rendering is a bit broken currently, as the barcode scan does not send back the model data

* "Fix" scanning page

- Rendering is a bit broken currently, as the barcode scan does not send back the model data
- Required refactoring enumerations out into separate files
- Some strange race condition / import loop was happening

* Fix incorrect imports

* Fixing hover card

- Use unique key

* fixes

* Fix urls.md

* More udpates

* Fix unused import
This commit is contained in:
Oliver 2023-11-10 15:44:02 +11:00 committed by GitHub
parent 9e2da947a9
commit e6db817c8f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
90 changed files with 724 additions and 744 deletions

View File

@ -18,6 +18,7 @@ class MyUrlsPlugin(UrlsMixin, InvenTreePlugin):
] ]
``` ```
The URLs get exposed under `/plugin/{plugin.slug}/*` and get exposed to the template engine with the prefix `plugin:{plugin.slug}:` (for usage with the [url tag](https://docs.djangoproject.com/en/stable/ref/templates/builtins/#url)). The URLs get exposed under `/plugin/{plugin.slug}/*` and get exposed to the template engine with the prefix `plugin:{plugin.slug}:` (for usage with the [url tag](https://docs.djangoproject.com/en/stable/ref/templates/builtins/#url)).
!!! info "Note" !!! info "Note"

View File

@ -3,7 +3,8 @@ import { useQuery } from '@tanstack/react-query';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { api } from '../App'; import { api } from '../App';
import { ApiPaths, apiUrl } from '../states/ApiState'; import { ApiPaths } from '../enums/ApiEndpoints';
import { apiUrl } from '../states/ApiState';
import { StatisticItem } from './items/DashboardItem'; import { StatisticItem } from './items/DashboardItem';
import { ErrorItem } from './items/ErrorItem'; import { ErrorItem } from './items/ErrorItem';

View File

@ -4,6 +4,7 @@ import { ReactNode } from 'react';
import { notYetImplemented } from '../../functions/notifications'; import { notYetImplemented } from '../../functions/notifications';
export type ActionButtonProps = { export type ActionButtonProps = {
key?: string;
icon?: ReactNode; icon?: ReactNode;
text?: string; text?: string;
color?: string; color?: string;
@ -22,12 +23,13 @@ export function ActionButton(props: ActionButtonProps) {
return ( return (
!props.hidden && ( !props.hidden && (
<Tooltip <Tooltip
key={props.text ?? props.tooltip} key={`tooltip-${props.key}`}
disabled={!props.tooltip && !props.text} disabled={!props.tooltip && !props.text}
label={props.tooltip ?? props.text} label={props.tooltip ?? props.text}
position="left" position="left"
> >
<ActionIcon <ActionIcon
key={`action-icon-${props.key}`}
disabled={props.disabled} disabled={props.disabled}
radius="xs" radius="xs"
color={props.color} color={props.color}

View File

@ -9,9 +9,9 @@ import { useEffect, useMemo } from 'react';
import { useState } from 'react'; import { useState } from 'react';
import { api, queryClient } from '../../App'; import { api, queryClient } from '../../App';
import { ApiPaths } from '../../enums/ApiEndpoints';
import { constructFormUrl } from '../../functions/forms'; import { constructFormUrl } from '../../functions/forms';
import { invalidResponse } from '../../functions/notifications'; import { invalidResponse } from '../../functions/notifications';
import { ApiPaths } from '../../states/ApiState';
import { ApiFormField, ApiFormFieldSet } from './fields/ApiFormField'; import { ApiFormField, ApiFormFieldSet } from './fields/ApiFormField';
/** /**

View File

@ -14,7 +14,7 @@ import { IconX } from '@tabler/icons-react';
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { ModelType } from '../../render/ModelType'; import { ModelType } from '../../../enums/ModelType';
import { ApiFormProps } from '../ApiForm'; import { ApiFormProps } from '../ApiForm';
import { ChoiceField } from './ChoiceField'; import { ChoiceField } from './ChoiceField';
import { RelatedModelField } from './RelatedModelField'; import { RelatedModelField } from './RelatedModelField';

View File

@ -81,7 +81,7 @@ export function BarcodeActionDropdown({
}) { }) {
return ( return (
<ActionDropdown <ActionDropdown
key="barcode" key="barcode-actions"
tooltip={t`Barcode Actions`} tooltip={t`Barcode Actions`}
icon={<IconQrcode />} icon={<IconQrcode />}
actions={actions} actions={actions}

View File

@ -4,7 +4,8 @@ import { ContextModalProps } from '@mantine/modals';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { api } from '../../App'; import { api } from '../../App';
import { ApiPaths, apiUrl, useServerApiState } from '../../states/ApiState'; import { ApiPaths } from '../../enums/ApiEndpoints';
import { apiUrl, useServerApiState } from '../../states/ApiState';
import { useLocalState } from '../../states/LocalState'; import { useLocalState } from '../../states/LocalState';
import { useUserState } from '../../states/UserState'; import { useUserState } from '../../states/UserState';
import { CopyButton } from '../items/CopyButton'; import { CopyButton } from '../items/CopyButton';

View File

@ -23,7 +23,8 @@ import { Html5QrcodeResult } from 'html5-qrcode/core';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { api } from '../../App'; import { api } from '../../App';
import { ApiPaths, apiUrl } from '../../states/ApiState'; import { ApiPaths } from '../../enums/ApiEndpoints';
import { apiUrl } from '../../states/ApiState';
export function QrCodeModal({ export function QrCodeModal({
context, context,

View File

@ -7,8 +7,9 @@ import { useNavigate, useParams } from 'react-router-dom';
import { api } from '../../App'; import { api } from '../../App';
import { navTabs as mainNavTabs } from '../../defaults/links'; import { navTabs as mainNavTabs } from '../../defaults/links';
import { ApiPaths } from '../../enums/ApiEndpoints';
import { InvenTreeStyle } from '../../globalStyle'; import { InvenTreeStyle } from '../../globalStyle';
import { ApiPaths, apiUrl } from '../../states/ApiState'; import { apiUrl } from '../../states/ApiState';
import { ScanButton } from '../items/ScanButton'; import { ScanButton } from '../items/ScanButton';
import { MainMenu } from './MainMenu'; import { MainMenu } from './MainMenu';
import { NavHoverMenu } from './NavHoverMenu'; import { NavHoverMenu } from './NavHoverMenu';

View File

@ -14,7 +14,8 @@ import { useQuery } from '@tanstack/react-query';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { api } from '../../App'; import { api } from '../../App';
import { ApiPaths, apiUrl } from '../../states/ApiState'; import { ApiPaths } from '../../enums/ApiEndpoints';
import { apiUrl } from '../../states/ApiState';
import { StylishText } from '../items/StylishText'; import { StylishText } from '../items/StylishText';
/** /**

View File

@ -58,7 +58,7 @@ export function PageDetail({
{detail} {detail}
<Space /> <Space />
{actions && ( {actions && (
<Group spacing={5} position="right"> <Group key="page-actions" spacing={5} position="right">
{actions} {actions}
</Group> </Group>
)} )}

View File

@ -6,7 +6,8 @@ import { useQuery } from '@tanstack/react-query';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { api } from '../../App'; import { api } from '../../App';
import { ApiPaths, apiUrl } from '../../states/ApiState'; import { ApiPaths } from '../../enums/ApiEndpoints';
import { apiUrl } from '../../states/ApiState';
import { StylishText } from '../items/StylishText'; import { StylishText } from '../items/StylishText';
export function PartCategoryTree({ export function PartCategoryTree({

View File

@ -26,139 +26,27 @@ import {
IconX IconX
} from '@tabler/icons-react'; } from '@tabler/icons-react';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { useEffect, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { api } from '../../App'; import { api } from '../../App';
import { ApiPaths, apiUrl } from '../../states/ApiState'; import { ApiPaths } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType';
import { UserRoles } from '../../enums/Roles';
import { apiUrl } from '../../states/ApiState';
import { useUserSettingsState } from '../../states/SettingsState';
import { useUserState } from '../../states/UserState';
import { RenderInstance } from '../render/Instance'; import { RenderInstance } from '../render/Instance';
import { ModelInformationDict, ModelType } from '../render/ModelType'; import { ModelInformationDict } from '../render/ModelType';
// Define type for handling individual search queries // Define type for handling individual search queries
type SearchQuery = { type SearchQuery = {
name: ModelType; model: ModelType;
enabled: boolean; enabled: boolean;
parameters: any; parameters: any;
results?: any; results?: any;
}; };
// Placeholder function for permissions checks (will be replaced with a proper implementation)
function permissionCheck(permission: string) {
return true;
}
// Placeholder function for settings checks (will be replaced with a proper implementation)
function settingsCheck(setting: string) {
return true;
}
/*
* Build a list of search queries based on user permissions
*/
function buildSearchQueries(): SearchQuery[] {
return [
{
name: ModelType.part,
parameters: {},
enabled:
permissionCheck('part.view') &&
settingsCheck('SEARCH_PREVIEW_SHOW_PARTS')
},
{
name: ModelType.supplierpart,
parameters: {
part_detail: true,
supplier_detail: true,
manufacturer_detail: true
},
enabled:
permissionCheck('part.view') &&
permissionCheck('purchase_order.view') &&
settingsCheck('SEARCH_PREVIEW_SHOW_SUPPLIER_PARTS')
},
{
name: ModelType.manufacturerpart,
parameters: {
part_detail: true,
supplier_detail: true,
manufacturer_detail: true
},
enabled:
permissionCheck('part.view') &&
permissionCheck('purchase_order.view') &&
settingsCheck('SEARCH_PREVIEW_SHOW_MANUFACTURER_PARTS')
},
{
name: ModelType.partcategory,
parameters: {},
enabled:
permissionCheck('part_category.view') &&
settingsCheck('SEARCH_PREVIEW_SHOW_CATEGORIES')
},
{
name: ModelType.stockitem,
parameters: {
part_detail: true,
location_detail: true
},
enabled:
permissionCheck('stock.view') &&
settingsCheck('SEARCH_PREVIEW_SHOW_STOCK')
},
{
name: ModelType.stocklocation,
parameters: {},
enabled:
permissionCheck('stock_location.view') &&
settingsCheck('SEARCH_PREVIEW_SHOW_LOCATIONS')
},
{
name: ModelType.build,
parameters: {
part_detail: true
},
enabled:
permissionCheck('build.view') &&
settingsCheck('SEARCH_PREVIEW_SHOW_BUILD_ORDERS')
},
{
name: ModelType.company,
parameters: {},
enabled:
(permissionCheck('sales_order.view') ||
permissionCheck('purchase_order.view')) &&
settingsCheck('SEARCH_PREVIEW_SHOW_COMPANIES')
},
{
name: ModelType.purchaseorder,
parameters: {
supplier_detail: true
},
enabled:
permissionCheck('purchase_order.view') &&
settingsCheck(`SEARCH_PREVIEW_SHOW_PURCHASE_ORDERS`)
},
{
name: ModelType.salesorder,
parameters: {
customer_detail: true
},
enabled:
permissionCheck('sales_order.view') &&
settingsCheck(`SEARCH_PREVIEW_SHOW_SALES_ORDERS`)
},
{
name: ModelType.returnorder,
parameters: {
customer_detail: true
},
enabled:
permissionCheck('return_order.view') &&
settingsCheck(`SEARCH_PREVIEW_SHOW_RETURN_ORDERS`)
}
];
}
/* /*
* Render the results for a single search query * Render the results for a single search query
*/ */
@ -175,10 +63,11 @@ function QueryResultGroup({
return null; return null;
} }
const model = ModelInformationDict[query.name]; const model = ModelInformationDict[query.model];
return ( return (
<Paper shadow="sm" radius="xs" p="md"> <Paper shadow="sm" radius="xs" p="md" key={`paper-${query.model}`}>
<Stack key={query.name}> <Stack key={`stack-${query.model}`}>
<Group position="apart" noWrap={true}> <Group position="apart" noWrap={true}>
<Group position="left" spacing={5} noWrap={true}> <Group position="left" spacing={5} noWrap={true}>
<Text size="lg">{model.label_multiple}</Text> <Text size="lg">{model.label_multiple}</Text>
@ -193,7 +82,7 @@ function QueryResultGroup({
color="red" color="red"
variant="transparent" variant="transparent"
radius="xs" radius="xs"
onClick={() => onRemove(query.name)} onClick={() => onRemove(query.model)}
> >
<IconX /> <IconX />
</ActionIcon> </ActionIcon>
@ -201,11 +90,11 @@ function QueryResultGroup({
<Divider /> <Divider />
<Stack> <Stack>
{query.results.results.map((result: any) => ( {query.results.results.map((result: any) => (
<Anchor onClick={() => onResultClick(query.name, result.pk)}> <Anchor onClick={() => onResultClick(query.model, result.pk)}>
<RenderInstance <RenderInstance
key={`${query.name}-${result.pk}`} key={`${query.model}-${result.pk}`}
instance={result} instance={result}
model={query.name} model={query.model}
/> />
</Anchor> </Anchor>
))} ))}
@ -233,10 +122,116 @@ export function SearchDrawer({
const [searchRegex, setSearchRegex] = useState<boolean>(false); const [searchRegex, setSearchRegex] = useState<boolean>(false);
const [searchWhole, setSearchWhole] = useState<boolean>(false); const [searchWhole, setSearchWhole] = useState<boolean>(false);
const user = useUserState();
const userSettings = useUserSettingsState();
// Build out search queries based on user permissions and preferences
const searchQueryList: SearchQuery[] = useMemo(() => {
return [
{
model: ModelType.part,
parameters: {},
enabled:
user.hasViewRole(UserRoles.part) &&
userSettings.isSet('SEARCH_PREVIEW_SHOW_PARTS')
},
{
model: ModelType.supplierpart,
parameters: {
part_detail: true,
supplier_detail: true,
manufacturer_detail: true
},
enabled:
user.hasViewRole(UserRoles.part) &&
user.hasViewRole(UserRoles.purchase_order) &&
userSettings.isSet('SEARCH_PREVIEW_SHOW_SUPPLIER_PARTS')
},
{
model: ModelType.manufacturerpart,
parameters: {
part_detail: true,
supplier_detail: true,
manufacturer_detail: true
},
enabled:
user.hasViewRole(UserRoles.part) &&
user.hasViewRole(UserRoles.purchase_order) &&
userSettings.isSet('SEARCH_PREVIEW_SHOW_MANUFACTURER_PARTS')
},
{
model: ModelType.partcategory,
parameters: {},
enabled:
user.hasViewRole(UserRoles.part_category) &&
userSettings.isSet('SEARCH_PREVIEW_SHOW_CATEGORIES')
},
{
model: ModelType.stockitem,
parameters: {
part_detail: true,
location_detail: true
},
enabled:
user.hasViewRole(UserRoles.stock) &&
userSettings.isSet('SEARCH_PREVIEW_SHOW_STOCK')
},
{
model: ModelType.stocklocation,
parameters: {},
enabled:
user.hasViewRole(UserRoles.stock_location) &&
userSettings.isSet('SEARCH_PREVIEW_SHOW_LOCATIONS')
},
{
model: ModelType.build,
parameters: {
part_detail: true
},
enabled:
user.hasViewRole(UserRoles.build) &&
userSettings.isSet('SEARCH_PREVIEW_SHOW_BUILD_ORDERS')
},
{
model: ModelType.company,
parameters: {},
enabled:
(user.hasViewRole(UserRoles.sales_order) ||
user.hasViewRole(UserRoles.purchase_order)) &&
userSettings.isSet('SEARCH_PREVIEW_SHOW_COMPANIES')
},
{
model: ModelType.purchaseorder,
parameters: {
supplier_detail: true
},
enabled:
user.hasViewRole(UserRoles.purchase_order) &&
userSettings.isSet(`SEARCH_PREVIEW_SHOW_PURCHASE_ORDERS`)
},
{
model: ModelType.salesorder,
parameters: {
customer_detail: true
},
enabled:
user.hasViewRole(UserRoles.sales_order) &&
userSettings.isSet(`SEARCH_PREVIEW_SHOW_SALES_ORDERS`)
},
{
model: ModelType.returnorder,
parameters: {
customer_detail: true
},
enabled:
user.hasViewRole(UserRoles.return_order) &&
userSettings.isSet(`SEARCH_PREVIEW_SHOW_RETURN_ORDERS`)
}
];
}, [user, userSettings]);
// Construct a list of search queries based on user permissions // Construct a list of search queries based on user permissions
const searchQueries: SearchQuery[] = buildSearchQueries().filter( const searchQueries: SearchQuery[] = searchQueryList.filter((q) => q.enabled);
(q) => q.enabled
);
// Re-fetch data whenever the search term is updated // Re-fetch data whenever the search term is updated
useEffect(() => { useEffect(() => {
@ -261,7 +256,7 @@ export function SearchDrawer({
// Add in custom query parameters // Add in custom query parameters
searchQueries.forEach((query) => { searchQueries.forEach((query) => {
params[query.name] = query.parameters; params[query.model] = query.parameters;
}); });
return api return api
@ -289,11 +284,11 @@ export function SearchDrawer({
useEffect(() => { useEffect(() => {
if (searchQuery.data) { if (searchQuery.data) {
let queries = searchQueries.filter( let queries = searchQueries.filter(
(query) => query.name in searchQuery.data (query) => query.model in searchQuery.data
); );
for (let key in searchQuery.data) { for (let key in searchQuery.data) {
let query = queries.find((q) => q.name == key); let query = queries.find((q) => q.model == key);
if (query) { if (query) {
query.results = searchQuery.data[key]; query.results = searchQuery.data[key];
} }
@ -310,7 +305,7 @@ export function SearchDrawer({
// Callback to remove a set of results from the list // Callback to remove a set of results from the list
function removeResults(query: ModelType) { function removeResults(query: ModelType) {
setQueryResults(queryResults.filter((q) => q.name != query)); setQueryResults(queryResults.filter((q) => q.model != query));
} }
// Callback when the drawer is closed // Callback when the drawer is closed
@ -332,7 +327,7 @@ export function SearchDrawer({
return ( return (
<Drawer <Drawer
opened={opened} opened={opened}
size="md" size="xl"
onClose={closeDrawer} onClose={closeDrawer}
position="right" position="right"
withCloseButton={false} withCloseButton={false}

View File

@ -1,8 +1,18 @@
import { Anchor, Group, Stack, Text, Title } from '@mantine/core'; import {
Anchor,
Button,
Group,
Paper,
Space,
Stack,
Text
} from '@mantine/core';
import { IconSwitch } from '@tabler/icons-react'; import { IconSwitch } from '@tabler/icons-react';
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { StylishText } from '../items/StylishText';
/** /**
* Construct a settings page header with interlinks to one other settings page * Construct a settings page header with interlinks to one other settings page
*/ */
@ -14,28 +24,35 @@ export function SettingsHeader({
switch_text, switch_text,
switch_link switch_link
}: { }: {
title: string | ReactNode; title: string;
shorthand?: string; shorthand?: string;
subtitle: string | ReactNode; subtitle?: string;
switch_condition?: boolean; switch_condition?: boolean;
switch_text?: string | ReactNode; switch_text?: string | ReactNode;
switch_link?: string; switch_link?: string;
}) { }) {
return ( return (
<Stack spacing="0" ml={'sm'}> <Paper shadow="xs" radius="xs" p="xs">
<Group> <Group position="apart">
<Title order={3}>{title}</Title> <Stack spacing="xs">
{shorthand && <Text c="dimmed">({shorthand})</Text>} <Group position="left" spacing="xs">
<StylishText size="xl">{title}</StylishText>
<Text size="sm">{shorthand}</Text>
</Group> </Group>
<Group> <Text italic>{subtitle}</Text>
<Text c="dimmed">{subtitle}</Text> </Stack>
<Space />
{switch_text && switch_link && switch_condition && ( {switch_text && switch_link && switch_condition && (
<Anchor component={Link} to={switch_link}> <Anchor component={Link} to={switch_link}>
<IconSwitch size={14} /> <Button variant="outline">
{switch_text} <Group spacing="sm">
<IconSwitch size={18} />
<Text>{switch_text}</Text>
</Group>
</Button>
</Anchor> </Anchor>
)} )}
</Group> </Group>
</Stack> </Paper>
); );
} }

View File

@ -6,7 +6,8 @@ import { useQuery } from '@tanstack/react-query';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { api } from '../../App'; import { api } from '../../App';
import { ApiPaths, apiUrl } from '../../states/ApiState'; import { ApiPaths } from '../../enums/ApiEndpoints';
import { apiUrl } from '../../states/ApiState';
import { StylishText } from '../items/StylishText'; import { StylishText } from '../items/StylishText';
export function StockLocationTree({ export function StockLocationTree({

View File

@ -1,6 +1,8 @@
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import { ModelType } from '../../enums/ModelType';
import { RenderInlineModel } from './Instance'; import { RenderInlineModel } from './Instance';
import { StatusRenderer } from './StatusRenderer';
/** /**
* Inline rendering of a single BuildOrder instance * Inline rendering of a single BuildOrder instance
@ -10,6 +12,10 @@ export function RenderBuildOrder({ instance }: { instance: any }): ReactNode {
<RenderInlineModel <RenderInlineModel
primary={instance.reference} primary={instance.reference}
secondary={instance.title} secondary={instance.title}
suffix={StatusRenderer({
status: instance.status,
type: ModelType.build
})}
image={instance.part_detail?.thumbnail || instance.part_detail?.image} image={instance.part_detail?.thumbnail || instance.part_detail?.image}
/> />
); );

View File

@ -3,6 +3,7 @@ import { Alert, Space } from '@mantine/core';
import { Group, Text } from '@mantine/core'; import { Group, Text } from '@mantine/core';
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import { ModelType } from '../../enums/ModelType';
import { Thumbnail } from '../images/Thumbnail'; import { Thumbnail } from '../images/Thumbnail';
import { RenderBuildOrder } from './Build'; import { RenderBuildOrder } from './Build';
import { import {
@ -13,7 +14,6 @@ import {
RenderSupplierPart RenderSupplierPart
} from './Company'; } from './Company';
import { RenderProjectCode } from './Generic'; import { RenderProjectCode } from './Generic';
import { ModelType } from './ModelType';
import { import {
RenderPurchaseOrder, RenderPurchaseOrder,
RenderReturnOrder, RenderReturnOrder,
@ -101,7 +101,7 @@ export function RenderInlineModel({
}: { }: {
primary: string; primary: string;
secondary?: string; secondary?: string;
suffix?: string; suffix?: ReactNode;
image?: string; image?: string;
labels?: string[]; labels?: string[];
url?: string; url?: string;

View File

@ -1,37 +1,18 @@
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
export enum ModelType { import { ApiPaths } from '../../enums/ApiEndpoints';
part = 'part', import { ModelType } from '../../enums/ModelType';
supplierpart = 'supplierpart',
manufacturerpart = 'manufacturerpart',
partcategory = 'partcategory',
partparametertemplate = 'partparametertemplate',
projectcode = 'projectcode',
stockitem = 'stockitem',
stocklocation = 'stocklocation',
stockhistory = 'stockhistory',
build = 'build',
company = 'company',
purchaseorder = 'purchaseorder',
purchaseorderline = 'purchaseorderline',
salesorder = 'salesorder',
salesordershipment = 'salesordershipment',
returnorder = 'returnorder',
address = 'address',
contact = 'contact',
owner = 'owner',
user = 'user'
}
interface ModelInformatonInterface { interface ModelInformationInterface {
label: string; label: string;
label_multiple: string; label_multiple: string;
url_overview?: string; url_overview?: string;
url_detail?: string; url_detail?: string;
api_endpoint?: ApiPaths;
} }
type ModelDictory = { type ModelDictory = {
[key in keyof typeof ModelType]: ModelInformatonInterface; [key in keyof typeof ModelType]: ModelInformationInterface;
}; };
export const ModelInformationDict: ModelDictory = { export const ModelInformationDict: ModelDictory = {
@ -39,116 +20,136 @@ export const ModelInformationDict: ModelDictory = {
label: t`Part`, label: t`Part`,
label_multiple: t`Parts`, label_multiple: t`Parts`,
url_overview: '/part', url_overview: '/part',
url_detail: '/part/:pk/' url_detail: '/part/:pk/',
api_endpoint: ApiPaths.part_list
}, },
partparametertemplate: { partparametertemplate: {
label: t`Part Parameter Template`, label: t`Part Parameter Template`,
label_multiple: t`Part Parameter Templates`, label_multiple: t`Part Parameter Templates`,
url_overview: '/partparametertemplate', url_overview: '/partparametertemplate',
url_detail: '/partparametertemplate/:pk/' url_detail: '/partparametertemplate/:pk/',
api_endpoint: ApiPaths.part_parameter_template_list
}, },
supplierpart: { supplierpart: {
label: t`Supplier Part`, label: t`Supplier Part`,
label_multiple: t`Supplier Parts`, label_multiple: t`Supplier Parts`,
url_overview: '/supplierpart', url_overview: '/supplierpart',
url_detail: '/supplierpart/:pk/' url_detail: '/supplierpart/:pk/',
api_endpoint: ApiPaths.supplier_part_list
}, },
manufacturerpart: { manufacturerpart: {
label: t`Manufacturer Part`, label: t`Manufacturer Part`,
label_multiple: t`Manufacturer Parts`, label_multiple: t`Manufacturer Parts`,
url_overview: '/manufacturerpart', url_overview: '/manufacturerpart',
url_detail: '/manufacturerpart/:pk/' url_detail: '/manufacturerpart/:pk/',
api_endpoint: ApiPaths.manufacturer_part_list
}, },
partcategory: { partcategory: {
label: t`Part Category`, label: t`Part Category`,
label_multiple: t`Part Categories`, label_multiple: t`Part Categories`,
url_overview: '/partcategory', url_overview: '/partcategory',
url_detail: '/partcategory/:pk/' url_detail: '/partcategory/:pk/',
api_endpoint: ApiPaths.category_list
}, },
stockitem: { stockitem: {
label: t`Stock Item`, label: t`Stock Item`,
label_multiple: t`Stock Items`, label_multiple: t`Stock Items`,
url_overview: '/stockitem', url_overview: '/stockitem',
url_detail: '/stockitem/:pk/' url_detail: '/stockitem/:pk/',
api_endpoint: ApiPaths.stock_item_list
}, },
stocklocation: { stocklocation: {
label: t`Stock Location`, label: t`Stock Location`,
label_multiple: t`Stock Locations`, label_multiple: t`Stock Locations`,
url_overview: '/stocklocation', url_overview: '/stocklocation',
url_detail: '/stocklocation/:pk/' url_detail: '/stocklocation/:pk/',
api_endpoint: ApiPaths.stock_location_list
}, },
stockhistory: { stockhistory: {
label: t`Stock History`, label: t`Stock History`,
label_multiple: t`Stock Histories` label_multiple: t`Stock Histories`,
api_endpoint: ApiPaths.stock_tracking_list
}, },
build: { build: {
label: t`Build`, label: t`Build`,
label_multiple: t`Builds`, label_multiple: t`Builds`,
url_overview: '/build', url_overview: '/build',
url_detail: '/build/:pk/' url_detail: '/build/:pk/',
api_endpoint: ApiPaths.build_order_list
}, },
company: { company: {
label: t`Company`, label: t`Company`,
label_multiple: t`Companies`, label_multiple: t`Companies`,
url_overview: '/company', url_overview: '/company',
url_detail: '/company/:pk/' url_detail: '/company/:pk/',
api_endpoint: ApiPaths.company_list
}, },
projectcode: { projectcode: {
label: t`Project Code`, label: t`Project Code`,
label_multiple: t`Project Codes`, label_multiple: t`Project Codes`,
url_overview: '/project-code', url_overview: '/project-code',
url_detail: '/project-code/:pk/' url_detail: '/project-code/:pk/',
api_endpoint: ApiPaths.project_code_list
}, },
purchaseorder: { purchaseorder: {
label: t`Purchase Order`, label: t`Purchase Order`,
label_multiple: t`Purchase Orders`, label_multiple: t`Purchase Orders`,
url_overview: '/purchaseorder', url_overview: '/purchaseorder',
url_detail: '/purchaseorder/:pk/' url_detail: '/purchaseorder/:pk/',
api_endpoint: ApiPaths.purchase_order_list
}, },
purchaseorderline: { purchaseorderline: {
label: t`Purchase Order Line`, label: t`Purchase Order Line`,
label_multiple: t`Purchase Order Lines` label_multiple: t`Purchase Order Lines`,
api_endpoint: ApiPaths.purchase_order_line_list
}, },
salesorder: { salesorder: {
label: t`Sales Order`, label: t`Sales Order`,
label_multiple: t`Sales Orders`, label_multiple: t`Sales Orders`,
url_overview: '/salesorder', url_overview: '/salesorder',
url_detail: '/salesorder/:pk/' url_detail: '/salesorder/:pk/',
api_endpoint: ApiPaths.sales_order_list
}, },
salesordershipment: { salesordershipment: {
label: t`Sales Order Shipment`, label: t`Sales Order Shipment`,
label_multiple: t`Sales Order Shipments`, label_multiple: t`Sales Order Shipments`,
url_overview: '/salesordershipment', url_overview: '/salesordershipment',
url_detail: '/salesordershipment/:pk/' url_detail: '/salesordershipment/:pk/',
api_endpoint: ApiPaths.sales_order_shipment_list
}, },
returnorder: { returnorder: {
label: t`Return Order`, label: t`Return Order`,
label_multiple: t`Return Orders`, label_multiple: t`Return Orders`,
url_overview: '/returnorder', url_overview: '/returnorder',
url_detail: '/returnorder/:pk/' url_detail: '/returnorder/:pk/',
api_endpoint: ApiPaths.return_order_list
}, },
address: { address: {
label: t`Address`, label: t`Address`,
label_multiple: t`Addresses`, label_multiple: t`Addresses`,
url_overview: '/address', url_overview: '/address',
url_detail: '/address/:pk/' url_detail: '/address/:pk/',
api_endpoint: ApiPaths.address_list
}, },
contact: { contact: {
label: t`Contact`, label: t`Contact`,
label_multiple: t`Contacts`, label_multiple: t`Contacts`,
url_overview: '/contact', url_overview: '/contact',
url_detail: '/contact/:pk/' url_detail: '/contact/:pk/',
api_endpoint: ApiPaths.contact_list
}, },
owner: { owner: {
label: t`Owner`, label: t`Owner`,
label_multiple: t`Owners`, label_multiple: t`Owners`,
url_overview: '/owner', url_overview: '/owner',
url_detail: '/owner/:pk/' url_detail: '/owner/:pk/',
api_endpoint: ApiPaths.owner_list
}, },
user: { user: {
label: t`User`, label: t`User`,
label_multiple: t`Users`, label_multiple: t`Users`,
url_overview: '/user', url_overview: '/user',
url_detail: '/user/:pk/' url_detail: '/user/:pk/',
api_endpoint: ApiPaths.user_list
} }
}; };

View File

@ -1,7 +1,9 @@
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import { ModelType } from '../../enums/ModelType';
import { RenderInlineModel } from './Instance'; import { RenderInlineModel } from './Instance';
import { StatusRenderer } from './StatusRenderer';
/** /**
* Inline rendering of a single PurchaseOrder instance * Inline rendering of a single PurchaseOrder instance
@ -18,6 +20,10 @@ export function RenderPurchaseOrder({
<RenderInlineModel <RenderInlineModel
primary={instance.reference} primary={instance.reference}
secondary={instance.description} secondary={instance.description}
suffix={StatusRenderer({
status: instance.status,
type: ModelType.purchaseorder
})}
image={supplier.thumnbnail || supplier.image} image={supplier.thumnbnail || supplier.image}
/> />
); );
@ -33,6 +39,10 @@ export function RenderReturnOrder({ instance }: { instance: any }): ReactNode {
<RenderInlineModel <RenderInlineModel
primary={instance.reference} primary={instance.reference}
secondary={instance.description} secondary={instance.description}
suffix={StatusRenderer({
status: instance.status,
type: ModelType.returnorder
})}
image={customer.thumnbnail || customer.image} image={customer.thumnbnail || customer.image}
/> />
); );
@ -50,6 +60,10 @@ export function RenderSalesOrder({ instance }: { instance: any }): ReactNode {
<RenderInlineModel <RenderInlineModel
primary={instance.reference} primary={instance.reference}
secondary={instance.description} secondary={instance.description}
suffix={StatusRenderer({
status: instance.status,
type: ModelType.salesorder
})}
image={customer.thumnbnail || customer.image} image={customer.thumnbnail || customer.image}
/> />
); );

View File

@ -1,8 +1,8 @@
import { Badge, Center, MantineSize } from '@mantine/core'; import { Badge, Center, MantineSize } from '@mantine/core';
import { colorMap } from '../../defaults/backendMappings'; import { colorMap } from '../../defaults/backendMappings';
import { ModelType } from '../../enums/ModelType';
import { useServerApiState } from '../../states/ApiState'; import { useServerApiState } from '../../states/ApiState';
import { ModelType } from '../render/ModelType';
interface StatusCodeInterface { interface StatusCodeInterface {
key: string; key: string;

View File

@ -1,3 +1,4 @@
import { t } from '@lingui/macro';
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import { RenderInlineModel } from './Instance'; import { RenderInlineModel } from './Instance';
@ -19,10 +20,18 @@ export function RenderStockLocation({
} }
export function RenderStockItem({ instance }: { instance: any }): ReactNode { export function RenderStockItem({ instance }: { instance: any }): ReactNode {
let quantity_string = '';
if (instance?.serial !== null && instance?.serial !== undefined) {
quantity_string += t`Serial Number` + `: ${instance.serial}`;
} else if (instance?.quantity) {
quantity_string = t`Quantity` + `: ${instance.quantity}`;
}
return ( return (
<RenderInlineModel <RenderInlineModel
primary={instance.part_detail?.full_name} primary={instance.part_detail?.full_name}
secondary={instance.quantity} suffix={quantity_string}
image={instance.part_detail?.thumbnail || instance.part_detail?.image} image={instance.part_detail?.thumbnail || instance.part_detail?.image}
/> />
); );

View File

@ -1,31 +0,0 @@
import { Group } from '@mantine/core';
import { ApiPaths } from '../../states/ApiState';
import { GeneralRenderer } from './GeneralRenderer';
import { PartRenderer } from './PartRenderer';
export const BuildOrderRenderer = ({ pk }: { pk: string }) => {
const DetailRenderer = (data: any) => {
return (
<Group position="apart">
{data?.reference}
<small>
<PartRenderer
pk={data?.part_detail?.pk}
data={data?.part_detail}
link={true}
/>
</small>
</Group>
);
};
return (
<GeneralRenderer
api_key={ApiPaths.build_order_list}
api_ref="build_order"
link={`/build/${pk}`}
pk={pk}
renderer={DetailRenderer}
/>
);
};

View File

@ -1,83 +0,0 @@
import { Anchor, Loader } from '@mantine/core';
import { useQuery } from '@tanstack/react-query';
import { api } from '../../App';
import { ApiPaths, apiUrl } from '../../states/ApiState';
import { ThumbnailHoverCard } from '../images/Thumbnail';
export function GeneralRenderer({
api_key,
api_ref: ref,
link,
pk,
image = true,
data = undefined,
renderer
}: {
api_key: ApiPaths;
api_ref: string;
link: string;
pk: string;
image?: boolean;
data?: any;
renderer?: (data: any) => JSX.Element;
}) {
// check if data was passed - or fetch it
if (!data) {
const {
data: fetched_data,
isError,
isFetching,
isLoading
} = useQuery({
queryKey: [ref, pk],
queryFn: () => {
return api
.get(apiUrl(api_key, pk))
.then((res) => res.data)
.catch(() => {
{
}
});
}
});
// Loading section
if (isError) {
return <div>Something went wrong...</div>;
}
if (isFetching || isLoading) {
return <Loader />;
}
data = fetched_data;
}
// Renderers
let content = undefined;
// Specific renderer was passed
if (renderer) content = renderer(data);
// No image and no content no default renderer
if (image === false && !content) content = data.name;
// Wrap in link if link was passed
if (content && link) {
content = (
<Anchor href={link} style={{ textDecoration: 'none' }}>
{content}
</Anchor>
);
}
// Return content if it exists, else default
if (content !== undefined) {
return content;
}
return (
<ThumbnailHoverCard
src={data.thumbnail || data.image}
text={data.name}
link={link}
/>
);
}

View File

@ -1,22 +0,0 @@
import { ApiPaths } from '../../states/ApiState';
import { GeneralRenderer } from './GeneralRenderer';
export const PartRenderer = ({
pk,
data = undefined,
link = true
}: {
pk: string;
data?: any;
link?: boolean;
}) => {
return (
<GeneralRenderer
api_key={ApiPaths.part_list}
api_ref="part"
link={link ? `/part/${pk}` : ''}
pk={pk}
data={data}
/>
);
};

View File

@ -1,26 +0,0 @@
import { Group } from '@mantine/core';
import { ApiPaths } from '../../states/ApiState';
import { GeneralRenderer } from './GeneralRenderer';
export const PurchaseOrderRenderer = ({ pk }: { pk: string }) => {
const DetailRenderer = (data: any) => {
const code = data?.project_code_detail?.code;
return (
<Group position="apart">
<div>{data?.reference}</div>
{code && <div>({code})</div>}
{data?.supplier_reference && <div>{data?.supplier_reference}</div>}
</Group>
);
};
return (
<GeneralRenderer
api_key={ApiPaths.purchase_order_list}
api_ref="purchaseorder"
link={`/order/purchase-order/${pk}`}
pk={pk}
renderer={DetailRenderer}
/>
);
};

View File

@ -1,16 +0,0 @@
import { ApiPaths } from '../../states/ApiState';
import { GeneralRenderer } from './GeneralRenderer';
export const SalesOrderRenderer = ({ pk }: { pk: string }) => {
return (
<GeneralRenderer
api_key={ApiPaths.sales_order_list}
api_ref="sales_order"
link={`/order/so/${pk}`}
pk={pk}
renderer={(data: any) => {
return data.reference;
}}
/>
);
};

View File

@ -1,27 +0,0 @@
import { Group } from '@mantine/core';
import { ApiPaths } from '../../states/ApiState';
import { GeneralRenderer } from './GeneralRenderer';
import { PartRenderer } from './PartRenderer';
export const StockItemRenderer = ({ pk }: { pk: string }) => {
const DetailRenderer = (data: any) => {
return (
<Group position="apart">
{data?.quantity}
<small>
<PartRenderer pk={data?.part_detail.pk} data={data?.part_detail} />
</small>
</Group>
);
};
return (
<GeneralRenderer
api_key={ApiPaths.stock_item_list}
api_ref="stockitem"
link={`/stock/item/${pk}`}
pk={pk}
renderer={DetailRenderer}
/>
);
};

View File

@ -1,14 +0,0 @@
import { ApiPaths } from '../../states/ApiState';
import { GeneralRenderer } from './GeneralRenderer';
export const StockLocationRenderer = ({ pk }: { pk: string }) => {
return (
<GeneralRenderer
api_key={ApiPaths.stock_location_list}
api_ref="stock_location"
link={`/stock/location/${pk}`}
pk={pk}
image={false}
/>
);
};

View File

@ -1,33 +0,0 @@
import { Group } from '@mantine/core';
import { ApiPaths } from '../../states/ApiState';
import { GeneralRenderer } from './GeneralRenderer';
import { PartRenderer } from './PartRenderer';
export const SupplierPartRenderer = ({ pk }: { pk: string }) => {
const DetailRenderer = (data: any) => {
return (
<Group position="apart">
{data?.SKU}
<small>
<span style={{ color: 'white' }}>
<PartRenderer
pk={data?.part_detail?.pk}
data={data?.part_detail}
link={false}
/>
</span>
</small>
</Group>
);
};
return (
<GeneralRenderer
api_key={ApiPaths.supplier_part_list}
api_ref="supplier_part"
link={`/supplier-part/${pk}`}
pk={pk}
renderer={DetailRenderer}
/>
);
};

View File

@ -1,39 +0,0 @@
import { BuildOrderRenderer } from './BuildOrderRenderer';
import { PartRenderer } from './PartRenderer';
import { PurchaseOrderRenderer } from './PurchaseOrderRenderer';
import { SalesOrderRenderer } from './SalesOrderRenderer';
import { StockItemRenderer } from './StockItemRenderer';
import { StockLocationRenderer } from './StockLocationRenderer';
import { SupplierPartRenderer } from './SupplierPartRenderer';
export enum RenderTypes {
part = 'part',
stock_item = 'stockitem',
stock_location = 'stocklocation',
supplier_part = 'supplierpart',
purchase_order = 'purchase_order',
sales_order = 'sales_order',
build_order = 'build_order'
}
// dict of renderers
const renderers = {
[RenderTypes.part]: PartRenderer,
[RenderTypes.stock_item]: StockItemRenderer,
[RenderTypes.stock_location]: StockLocationRenderer,
[RenderTypes.supplier_part]: SupplierPartRenderer,
[RenderTypes.purchase_order]: PurchaseOrderRenderer,
[RenderTypes.sales_order]: SalesOrderRenderer,
[RenderTypes.build_order]: BuildOrderRenderer
};
export interface RenderProps {
type: RenderTypes;
pk: string;
}
export function Render(props: RenderProps) {
const { type, ...rest } = props;
const RendererComponent = renderers[type];
return <RendererComponent {...rest} />;
}

View File

@ -4,11 +4,11 @@
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { formatCurrency, renderDate } from '../../defaults/formatters'; import { formatCurrency, renderDate } from '../../defaults/formatters';
import { ModelType } from '../../enums/ModelType';
import { ProgressBar } from '../items/ProgressBar'; import { ProgressBar } from '../items/ProgressBar';
import { YesNoButton } from '../items/YesNoButton'; import { YesNoButton } from '../items/YesNoButton';
import { ModelType } from '../render/ModelType'; import { TableStatusRenderer } from '../render/StatusRenderer';
import { RenderOwner } from '../render/User'; import { RenderOwner } from '../render/User';
import { TableStatusRenderer } from '../renderers/StatusRenderer';
import { TableColumn } from './Column'; import { TableColumn } from './Column';
import { ProjectCodeHoverCard } from './TableHoverCard'; import { ProjectCodeHoverCard } from './TableHoverCard';

View File

@ -56,7 +56,11 @@ export function ProjectCodeHoverCard({ projectCode }: { projectCode: any }) {
<TableHoverCard <TableHoverCard
value={projectCode?.code} value={projectCode?.code}
title={t`Project Code`} title={t`Project Code`}
extra={projectCode?.description} extra={
projectCode && (
<Text key="project-code">{projectCode?.description}</Text>
)
}
/> />
) : ( ) : (
'-' '-'

View File

@ -8,11 +8,13 @@ import {
import { ReactNode, useCallback, useMemo } from 'react'; import { ReactNode, useCallback, useMemo } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { ApiPaths } from '../../../enums/ApiEndpoints';
import { UserRoles } from '../../../enums/Roles';
import { bomItemFields } from '../../../forms/BomForms'; import { bomItemFields } from '../../../forms/BomForms';
import { openDeleteApiForm, openEditApiForm } from '../../../functions/forms'; import { openDeleteApiForm, openEditApiForm } from '../../../functions/forms';
import { useTableRefresh } from '../../../hooks/TableRefresh'; import { useTableRefresh } from '../../../hooks/TableRefresh';
import { ApiPaths, apiUrl } from '../../../states/ApiState'; import { apiUrl } from '../../../states/ApiState';
import { UserRoles, useUserState } from '../../../states/UserState'; import { useUserState } from '../../../states/UserState';
import { Thumbnail } from '../../images/Thumbnail'; import { Thumbnail } from '../../images/Thumbnail';
import { YesNoButton } from '../../items/YesNoButton'; import { YesNoButton } from '../../items/YesNoButton';
import { TableColumn } from '../Column'; import { TableColumn } from '../Column';
@ -64,7 +66,9 @@ export function BomTable({
let extra = []; let extra = [];
if (record.part != partId) { if (record.part != partId) {
extra.push(t`This BOM item is defined for a different parent`); extra.push(
<Text key="different-parent">{t`This BOM item is defined for a different parent`}</Text>
);
} }
return ( return (

View File

@ -2,8 +2,9 @@ import { t } from '@lingui/macro';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { ApiPaths } from '../../../enums/ApiEndpoints';
import { useTableRefresh } from '../../../hooks/TableRefresh'; import { useTableRefresh } from '../../../hooks/TableRefresh';
import { ApiPaths, apiUrl } from '../../../states/ApiState'; import { apiUrl } from '../../../states/ApiState';
import { ThumbnailHoverCard } from '../../images/Thumbnail'; import { ThumbnailHoverCard } from '../../images/Thumbnail';
import { TableColumn } from '../Column'; import { TableColumn } from '../Column';
import { TableFilter } from '../Filter'; import { TableFilter } from '../Filter';

View File

@ -3,11 +3,12 @@ import { useMemo } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { renderDate } from '../../../defaults/formatters'; import { renderDate } from '../../../defaults/formatters';
import { ApiPaths } from '../../../enums/ApiEndpoints';
import { ModelType } from '../../../enums/ModelType';
import { useTableRefresh } from '../../../hooks/TableRefresh'; import { useTableRefresh } from '../../../hooks/TableRefresh';
import { ApiPaths, apiUrl } from '../../../states/ApiState'; import { apiUrl } from '../../../states/ApiState';
import { ThumbnailHoverCard } from '../../images/Thumbnail'; import { ThumbnailHoverCard } from '../../images/Thumbnail';
import { ProgressBar } from '../../items/ProgressBar'; import { ProgressBar } from '../../items/ProgressBar';
import { ModelType } from '../../render/ModelType';
import { RenderUser } from '../../render/User'; import { RenderUser } from '../../render/User';
import { TableColumn } from '../Column'; import { TableColumn } from '../Column';
import { import {

View File

@ -7,13 +7,14 @@ import { IconExternalLink, IconFileUpload } from '@tabler/icons-react';
import { ReactNode, useEffect, useMemo, useState } from 'react'; import { ReactNode, useEffect, useMemo, useState } from 'react';
import { api } from '../../../App'; import { api } from '../../../App';
import { ApiPaths } from '../../../enums/ApiEndpoints';
import { import {
addAttachment, addAttachment,
deleteAttachment, deleteAttachment,
editAttachment editAttachment
} from '../../../forms/AttachmentForms'; } from '../../../forms/AttachmentForms';
import { useTableRefresh } from '../../../hooks/TableRefresh'; import { useTableRefresh } from '../../../hooks/TableRefresh';
import { ApiPaths, apiUrl } from '../../../states/ApiState'; import { apiUrl } from '../../../states/ApiState';
import { AttachmentLink } from '../../items/AttachmentLink'; import { AttachmentLink } from '../../items/AttachmentLink';
import { TableColumn } from '../Column'; import { TableColumn } from '../Column';
import { InvenTreeTable } from '../InvenTreeTable'; import { InvenTreeTable } from '../InvenTreeTable';

View File

@ -3,8 +3,9 @@ import { Group, Text } from '@mantine/core';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { ApiPaths } from '../../../enums/ApiEndpoints';
import { useTableRefresh } from '../../../hooks/TableRefresh'; import { useTableRefresh } from '../../../hooks/TableRefresh';
import { ApiPaths, apiUrl } from '../../../states/ApiState'; import { apiUrl } from '../../../states/ApiState';
import { Thumbnail } from '../../images/Thumbnail'; import { Thumbnail } from '../../images/Thumbnail';
import { DescriptionColumn } from '../ColumnRenderers'; import { DescriptionColumn } from '../ColumnRenderers';
import { InvenTreeTable } from '../InvenTreeTable'; import { InvenTreeTable } from '../InvenTreeTable';

View File

@ -1,7 +1,8 @@
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { ApiPaths, apiUrl } from '../../../states/ApiState'; import { ApiPaths } from '../../../enums/ApiEndpoints';
import { apiUrl } from '../../../states/ApiState';
import { TableColumn } from '../Column'; import { TableColumn } from '../Column';
import { InvenTreeTable } from '../InvenTreeTable'; import { InvenTreeTable } from '../InvenTreeTable';
import { RowAction } from '../RowActions'; import { RowAction } from '../RowActions';

View File

@ -2,8 +2,9 @@ import { t } from '@lingui/macro';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { ApiPaths } from '../../../enums/ApiEndpoints';
import { useTableRefresh } from '../../../hooks/TableRefresh'; import { useTableRefresh } from '../../../hooks/TableRefresh';
import { ApiPaths, apiUrl } from '../../../states/ApiState'; import { apiUrl } from '../../../states/ApiState';
import { TableColumn } from '../Column'; import { TableColumn } from '../Column';
import { DescriptionColumn } from '../ColumnRenderers'; import { DescriptionColumn } from '../ColumnRenderers';
import { InvenTreeTable } from '../InvenTreeTable'; import { InvenTreeTable } from '../InvenTreeTable';

View File

@ -2,14 +2,16 @@ import { t } from '@lingui/macro';
import { Group, Text } from '@mantine/core'; import { Group, Text } from '@mantine/core';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import { ApiPaths } from '../../../enums/ApiEndpoints';
import { UserRoles } from '../../../enums/Roles';
import { import {
openCreateApiForm, openCreateApiForm,
openDeleteApiForm, openDeleteApiForm,
openEditApiForm openEditApiForm
} from '../../../functions/forms'; } from '../../../functions/forms';
import { useTableRefresh } from '../../../hooks/TableRefresh'; import { useTableRefresh } from '../../../hooks/TableRefresh';
import { ApiPaths, apiUrl } from '../../../states/ApiState'; import { apiUrl } from '../../../states/ApiState';
import { UserRoles, useUserState } from '../../../states/UserState'; import { useUserState } from '../../../states/UserState';
import { AddItemButton } from '../../buttons/AddItemButton'; import { AddItemButton } from '../../buttons/AddItemButton';
import { Thumbnail } from '../../images/Thumbnail'; import { Thumbnail } from '../../images/Thumbnail';
import { YesNoButton } from '../../items/YesNoButton'; import { YesNoButton } from '../../items/YesNoButton';
@ -178,7 +180,7 @@ export function PartParameterTable({ partId }: { partId: any }) {
// TODO: Hide if user does not have permission to edit parts // TODO: Hide if user does not have permission to edit parts
actions.push( actions.push(
<AddItemButton tooltip="Add parameter" onClick={addParameter} /> <AddItemButton tooltip={t`Add parameter`} onClick={addParameter} />
); );
return actions; return actions;

View File

@ -2,6 +2,8 @@ import { t } from '@lingui/macro';
import { Text } from '@mantine/core'; import { Text } from '@mantine/core';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import { ApiPaths } from '../../../enums/ApiEndpoints';
import { UserRoles } from '../../../enums/Roles';
import { partParameterTemplateFields } from '../../../forms/PartForms'; import { partParameterTemplateFields } from '../../../forms/PartForms';
import { import {
openCreateApiForm, openCreateApiForm,
@ -9,8 +11,8 @@ import {
openEditApiForm openEditApiForm
} from '../../../functions/forms'; } from '../../../functions/forms';
import { useTableRefresh } from '../../../hooks/TableRefresh'; import { useTableRefresh } from '../../../hooks/TableRefresh';
import { ApiPaths, apiUrl } from '../../../states/ApiState'; import { apiUrl } from '../../../states/ApiState';
import { UserRoles, useUserState } from '../../../states/UserState'; import { useUserState } from '../../../states/UserState';
import { AddItemButton } from '../../buttons/AddItemButton'; import { AddItemButton } from '../../buttons/AddItemButton';
import { TableColumn } from '../Column'; import { TableColumn } from '../Column';
import { InvenTreeTable } from '../InvenTreeTable'; import { InvenTreeTable } from '../InvenTreeTable';

View File

@ -3,9 +3,10 @@ import { Group, Text } from '@mantine/core';
import { ReactNode, useMemo } from 'react'; import { ReactNode, useMemo } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { ApiPaths } from '../../../enums/ApiEndpoints';
import { shortenString } from '../../../functions/tables'; import { shortenString } from '../../../functions/tables';
import { useTableRefresh } from '../../../hooks/TableRefresh'; import { useTableRefresh } from '../../../hooks/TableRefresh';
import { ApiPaths, apiUrl } from '../../../states/ApiState'; import { apiUrl } from '../../../states/ApiState';
import { Thumbnail } from '../../images/Thumbnail'; import { Thumbnail } from '../../images/Thumbnail';
import { TableColumn } from '../Column'; import { TableColumn } from '../Column';
import { DescriptionColumn, LinkColumn } from '../ColumnRenderers'; import { DescriptionColumn, LinkColumn } from '../ColumnRenderers';
@ -77,23 +78,29 @@ function partTableColumns(): TableColumn[] {
if (min_stock > stock) { if (min_stock > stock) {
extra.push( extra.push(
<Text color="orange">{t`Minimum stock` + `: ${min_stock}`}</Text> <Text key="min-stock" color="orange">
{t`Minimum stock` + `: ${min_stock}`}
</Text>
); );
color = 'orange'; color = 'orange';
} }
if (record.ordering > 0) { if (record.ordering > 0) {
extra.push(<Text>{t`On Order` + `: ${record.ordering}`}</Text>); extra.push(
<Text key="on-order">{t`On Order` + `: ${record.ordering}`}</Text>
);
} }
if (record.building) { if (record.building) {
extra.push(<Text>{t`Building` + `: ${record.building}`}</Text>); extra.push(
<Text key="building">{t`Building` + `: ${record.building}`}</Text>
);
} }
if (record.allocated_to_build_orders > 0) { if (record.allocated_to_build_orders > 0) {
extra.push( extra.push(
<Text> <Text key="bo-allocations">
{t`Build Order Allocations` + {t`Build Order Allocations` +
`: ${record.allocated_to_build_orders}`} `: ${record.allocated_to_build_orders}`}
</Text> </Text>
@ -102,7 +109,7 @@ function partTableColumns(): TableColumn[] {
if (record.allocated_to_sales_orders > 0) { if (record.allocated_to_sales_orders > 0) {
extra.push( extra.push(
<Text> <Text key="so-allocations">
{t`Sales Order Allocations` + {t`Sales Order Allocations` +
`: ${record.allocated_to_sales_orders}`} `: ${record.allocated_to_sales_orders}`}
</Text> </Text>
@ -110,7 +117,9 @@ function partTableColumns(): TableColumn[] {
} }
if (available != stock) { if (available != stock) {
extra.push(<Text>{t`Available` + `: ${available}`}</Text>); extra.push(
<Text key="available">{t`Available` + `: ${available}`}</Text>
);
} }
// TODO: Add extra information on stock "demand" // TODO: Add extra information on stock "demand"

View File

@ -4,10 +4,12 @@ import { IconLayersLinked } from '@tabler/icons-react';
import { ReactNode, useCallback, useMemo } from 'react'; import { ReactNode, useCallback, useMemo } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { ApiPaths } from '../../../enums/ApiEndpoints';
import { UserRoles } from '../../../enums/Roles';
import { openCreateApiForm, openDeleteApiForm } from '../../../functions/forms'; import { openCreateApiForm, openDeleteApiForm } from '../../../functions/forms';
import { useTableRefresh } from '../../../hooks/TableRefresh'; import { useTableRefresh } from '../../../hooks/TableRefresh';
import { ApiPaths, apiUrl } from '../../../states/ApiState'; import { apiUrl } from '../../../states/ApiState';
import { UserRoles, useUserState } from '../../../states/UserState'; import { useUserState } from '../../../states/UserState';
import { Thumbnail } from '../../images/Thumbnail'; import { Thumbnail } from '../../images/Thumbnail';
import { TableColumn } from '../Column'; import { TableColumn } from '../Column';
import { InvenTreeTable } from '../InvenTreeTable'; import { InvenTreeTable } from '../InvenTreeTable';

View File

@ -10,8 +10,9 @@ import {
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import { api } from '../../../App'; import { api } from '../../../App';
import { ApiPaths } from '../../../enums/ApiEndpoints';
import { useTableRefresh } from '../../../hooks/TableRefresh'; import { useTableRefresh } from '../../../hooks/TableRefresh';
import { ApiPaths, apiUrl } from '../../../states/ApiState'; import { apiUrl } from '../../../states/ApiState';
import { StylishText } from '../../items/StylishText'; import { StylishText } from '../../items/StylishText';
import { TableColumn } from '../Column'; import { TableColumn } from '../Column';
import { InvenTreeTable, InvenTreeTableProps } from '../InvenTreeTable'; import { InvenTreeTable, InvenTreeTableProps } from '../InvenTreeTable';

View File

@ -4,11 +4,13 @@ import { IconSquareArrowRight } from '@tabler/icons-react';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import { ProgressBar } from '../../../components/items/ProgressBar'; import { ProgressBar } from '../../../components/items/ProgressBar';
import { ApiPaths } from '../../../enums/ApiEndpoints';
import { UserRoles } from '../../../enums/Roles';
import { purchaseOrderLineItemFields } from '../../../forms/PurchaseOrderForms'; import { purchaseOrderLineItemFields } from '../../../forms/PurchaseOrderForms';
import { openCreateApiForm, openEditApiForm } from '../../../functions/forms'; import { openCreateApiForm, openEditApiForm } from '../../../functions/forms';
import { useTableRefresh } from '../../../hooks/TableRefresh'; import { useTableRefresh } from '../../../hooks/TableRefresh';
import { ApiPaths, apiUrl } from '../../../states/ApiState'; import { apiUrl } from '../../../states/ApiState';
import { UserRoles, useUserState } from '../../../states/UserState'; import { useUserState } from '../../../states/UserState';
import { ActionButton } from '../../buttons/ActionButton'; import { ActionButton } from '../../buttons/ActionButton';
import { AddItemButton } from '../../buttons/AddItemButton'; import { AddItemButton } from '../../buttons/AddItemButton';
import { Thumbnail } from '../../images/Thumbnail'; import { Thumbnail } from '../../images/Thumbnail';
@ -64,7 +66,8 @@ export function PurchaseOrderLineItemTable({
} }
let fields = purchaseOrderLineItemFields({ let fields = purchaseOrderLineItemFields({
supplierId: supplier supplierId: supplier,
create: false
}); });
openEditApiForm({ openEditApiForm({
@ -219,21 +222,29 @@ export function PurchaseOrderLineItemTable({
openCreateApiForm({ openCreateApiForm({
url: ApiPaths.purchase_order_line_list, url: ApiPaths.purchase_order_line_list,
title: t`Add Line Item`, title: t`Add Line Item`,
fields: purchaseOrderLineItemFields({}), fields: purchaseOrderLineItemFields({
create: true,
orderId: orderId
}),
onFormSuccess: refreshTable, onFormSuccess: refreshTable,
successMessage: t`Line item added` successMessage: t`Line item added`
}); });
}, []); }, [orderId]);
// Custom table actions // Custom table actions
const tableActions = useMemo(() => { const tableActions = useMemo(() => {
return [ return [
<AddItemButton <AddItemButton
key="add-line-item"
tooltip={t`Add line item`} tooltip={t`Add line item`}
onClick={addLine} onClick={addLine}
hidden={!user?.hasAddRole(UserRoles.purchase_order)} hidden={!user?.hasAddRole(UserRoles.purchase_order)}
/>, />,
<ActionButton text={t`Receive items`} icon={<IconSquareArrowRight />} /> <ActionButton
key="receive-items"
text={t`Receive items`}
icon={<IconSquareArrowRight />}
/>
]; ];
}, [orderId, user]); }, [orderId, user]);

View File

@ -2,10 +2,11 @@ import { t } from '@lingui/macro';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { ApiPaths } from '../../../enums/ApiEndpoints';
import { ModelType } from '../../../enums/ModelType';
import { useTableRefresh } from '../../../hooks/TableRefresh'; import { useTableRefresh } from '../../../hooks/TableRefresh';
import { ApiPaths, apiUrl } from '../../../states/ApiState'; import { apiUrl } from '../../../states/ApiState';
import { Thumbnail } from '../../images/Thumbnail'; import { Thumbnail } from '../../images/Thumbnail';
import { ModelType } from '../../render/ModelType';
import { import {
CreationDateColumn, CreationDateColumn,
DescriptionColumn, DescriptionColumn,

View File

@ -2,6 +2,8 @@ import { t } from '@lingui/macro';
import { Text } from '@mantine/core'; import { Text } from '@mantine/core';
import { ReactNode, useCallback, useMemo } from 'react'; import { ReactNode, useCallback, useMemo } from 'react';
import { ApiPaths } from '../../../enums/ApiEndpoints';
import { UserRoles } from '../../../enums/Roles';
import { supplierPartFields } from '../../../forms/CompanyForms'; import { supplierPartFields } from '../../../forms/CompanyForms';
import { import {
openCreateApiForm, openCreateApiForm,
@ -9,8 +11,8 @@ import {
openEditApiForm openEditApiForm
} from '../../../functions/forms'; } from '../../../functions/forms';
import { useTableRefresh } from '../../../hooks/TableRefresh'; import { useTableRefresh } from '../../../hooks/TableRefresh';
import { ApiPaths, apiUrl } from '../../../states/ApiState'; import { apiUrl } from '../../../states/ApiState';
import { UserRoles, useUserState } from '../../../states/UserState'; import { useUserState } from '../../../states/UserState';
import { AddItemButton } from '../../buttons/AddItemButton'; import { AddItemButton } from '../../buttons/AddItemButton';
import { Thumbnail } from '../../images/Thumbnail'; import { Thumbnail } from '../../images/Thumbnail';
import { TableColumn } from '../Column'; import { TableColumn } from '../Column';
@ -110,7 +112,7 @@ export function SupplierPartTable({ params }: { params: any }): ReactNode {
if (part.units) { if (part.units) {
extra.push( extra.push(
<Text> <Text key="base">
{t`Base units`} : {part.units} {t`Base units`} : {part.units}
</Text> </Text>
); );

View File

@ -2,10 +2,11 @@ import { t } from '@lingui/macro';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { ApiPaths } from '../../../enums/ApiEndpoints';
import { ModelType } from '../../../enums/ModelType';
import { useTableRefresh } from '../../../hooks/TableRefresh'; import { useTableRefresh } from '../../../hooks/TableRefresh';
import { ApiPaths, apiUrl } from '../../../states/ApiState'; import { apiUrl } from '../../../states/ApiState';
import { Thumbnail } from '../../images/Thumbnail'; import { Thumbnail } from '../../images/Thumbnail';
import { ModelType } from '../../render/ModelType';
import { import {
CreationDateColumn, CreationDateColumn,
DescriptionColumn, DescriptionColumn,

View File

@ -2,10 +2,11 @@ import { t } from '@lingui/macro';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { ApiPaths } from '../../../enums/ApiEndpoints';
import { ModelType } from '../../../enums/ModelType';
import { useTableRefresh } from '../../../hooks/TableRefresh'; import { useTableRefresh } from '../../../hooks/TableRefresh';
import { ApiPaths, apiUrl } from '../../../states/ApiState'; import { apiUrl } from '../../../states/ApiState';
import { Thumbnail } from '../../images/Thumbnail'; import { Thumbnail } from '../../images/Thumbnail';
import { ModelType } from '../../render/ModelType';
import { import {
CreationDateColumn, CreationDateColumn,
DescriptionColumn, DescriptionColumn,

View File

@ -4,8 +4,9 @@ import { IconReload } from '@tabler/icons-react';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import { api } from '../../../App'; import { api } from '../../../App';
import { ApiPaths } from '../../../enums/ApiEndpoints';
import { useTableRefresh } from '../../../hooks/TableRefresh'; import { useTableRefresh } from '../../../hooks/TableRefresh';
import { ApiPaths, apiUrl } from '../../../states/ApiState'; import { apiUrl } from '../../../states/ApiState';
import { ActionButton } from '../../buttons/ActionButton'; import { ActionButton } from '../../buttons/ActionButton';
import { InvenTreeTable } from '../InvenTreeTable'; import { InvenTreeTable } from '../InvenTreeTable';

View File

@ -2,14 +2,16 @@ import { t } from '@lingui/macro';
import { Text } from '@mantine/core'; import { Text } from '@mantine/core';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import { ApiPaths } from '../../../enums/ApiEndpoints';
import { UserRoles } from '../../../enums/Roles';
import { import {
openCreateApiForm, openCreateApiForm,
openDeleteApiForm, openDeleteApiForm,
openEditApiForm openEditApiForm
} from '../../../functions/forms'; } from '../../../functions/forms';
import { useTableRefresh } from '../../../hooks/TableRefresh'; import { useTableRefresh } from '../../../hooks/TableRefresh';
import { ApiPaths, apiUrl } from '../../../states/ApiState'; import { apiUrl } from '../../../states/ApiState';
import { UserRoles, useUserState } from '../../../states/UserState'; import { useUserState } from '../../../states/UserState';
import { AddItemButton } from '../../buttons/AddItemButton'; import { AddItemButton } from '../../buttons/AddItemButton';
import { TableColumn } from '../Column'; import { TableColumn } from '../Column';
import { InvenTreeTable } from '../InvenTreeTable'; import { InvenTreeTable } from '../InvenTreeTable';

View File

@ -2,14 +2,16 @@ import { t } from '@lingui/macro';
import { Text } from '@mantine/core'; import { Text } from '@mantine/core';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import { ApiPaths } from '../../../enums/ApiEndpoints';
import { UserRoles } from '../../../enums/Roles';
import { import {
openCreateApiForm, openCreateApiForm,
openDeleteApiForm, openDeleteApiForm,
openEditApiForm openEditApiForm
} from '../../../functions/forms'; } from '../../../functions/forms';
import { useTableRefresh } from '../../../hooks/TableRefresh'; import { useTableRefresh } from '../../../hooks/TableRefresh';
import { ApiPaths, apiUrl } from '../../../states/ApiState'; import { apiUrl } from '../../../states/ApiState';
import { UserRoles, useUserState } from '../../../states/UserState'; import { useUserState } from '../../../states/UserState';
import { AddItemButton } from '../../buttons/AddItemButton'; import { AddItemButton } from '../../buttons/AddItemButton';
import { TableColumn } from '../Column'; import { TableColumn } from '../Column';
import { DescriptionColumn } from '../ColumnRenderers'; import { DescriptionColumn } from '../ColumnRenderers';

View File

@ -4,10 +4,11 @@ import { ReactNode, useMemo } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { formatCurrency, renderDate } from '../../../defaults/formatters'; import { formatCurrency, renderDate } from '../../../defaults/formatters';
import { ApiPaths } from '../../../enums/ApiEndpoints';
import { ModelType } from '../../../enums/ModelType';
import { useTableRefresh } from '../../../hooks/TableRefresh'; import { useTableRefresh } from '../../../hooks/TableRefresh';
import { ApiPaths, apiUrl } from '../../../states/ApiState'; import { apiUrl } from '../../../states/ApiState';
import { Thumbnail } from '../../images/Thumbnail'; import { Thumbnail } from '../../images/Thumbnail';
import { ModelType } from '../../render/ModelType';
import { TableColumn } from '../Column'; import { TableColumn } from '../Column';
import { StatusColumn } from '../ColumnRenderers'; import { StatusColumn } from '../ColumnRenderers';
import { TableFilter } from '../Filter'; import { TableFilter } from '../Filter';
@ -65,49 +66,77 @@ function stockItemTableColumns(): TableColumn[] {
if (record.is_building) { if (record.is_building) {
color = 'blue'; color = 'blue';
extra.push( extra.push(
<Text size="sm">{t`This stock item is in production`}</Text> <Text
key="production"
size="sm"
>{t`This stock item is in production`}</Text>
); );
} }
if (record.sales_order) { if (record.sales_order) {
extra.push( extra.push(
<Text size="sm">{t`This stock item has been assigned to a sales order`}</Text> <Text
key="sales-order"
size="sm"
>{t`This stock item has been assigned to a sales order`}</Text>
); );
} }
if (record.customer) { if (record.customer) {
extra.push( extra.push(
<Text size="sm">{t`This stock item has been assigned to a customer`}</Text> <Text
key="customer"
size="sm"
>{t`This stock item has been assigned to a customer`}</Text>
); );
} }
if (record.belongs_to) { if (record.belongs_to) {
extra.push( extra.push(
<Text size="sm">{t`This stock item is installed in another stock item`}</Text> <Text
key="belongs-to"
size="sm"
>{t`This stock item is installed in another stock item`}</Text>
); );
} }
if (record.consumed_by) { if (record.consumed_by) {
extra.push( extra.push(
<Text size="sm">{t`This stock item has been consumed by a build order`}</Text> <Text
key="consumed-by"
size="sm"
>{t`This stock item has been consumed by a build order`}</Text>
); );
} }
if (record.expired) { if (record.expired) {
extra.push(<Text size="sm">{t`This stock item has expired`}</Text>); extra.push(
<Text
key="expired"
size="sm"
>{t`This stock item has expired`}</Text>
);
} else if (record.stale) { } else if (record.stale) {
extra.push(<Text size="sm">{t`This stock item is stale`}</Text>); extra.push(
<Text key="stale" size="sm">{t`This stock item is stale`}</Text>
);
} }
if (allocated > 0) { if (allocated > 0) {
if (allocated >= quantity) { if (allocated >= quantity) {
color = 'orange'; color = 'orange';
extra.push( extra.push(
<Text size="sm">{t`This stock item is fully allocated`}</Text> <Text
key="fully-allocated"
size="sm"
>{t`This stock item is fully allocated`}</Text>
); );
} else { } else {
extra.push( extra.push(
<Text size="sm">{t`This stock item is partially allocated`}</Text> <Text
key="partially-allocated"
size="sm"
>{t`This stock item is partially allocated`}</Text>
); );
} }
} }
@ -115,13 +144,17 @@ function stockItemTableColumns(): TableColumn[] {
if (available != quantity) { if (available != quantity) {
if (available > 0) { if (available > 0) {
extra.push( extra.push(
<Text size="sm" color="orange"> <Text key="available" size="sm" color="orange">
{t`Available` + `: ${available}`} {t`Available` + `: ${available}`}
</Text> </Text>
); );
} else { } else {
extra.push( extra.push(
<Text size="sm" color="red">{t`No stock available`}</Text> <Text
key="no-stock"
size="sm"
color="red"
>{t`No stock available`}</Text>
); );
} }
} }
@ -129,7 +162,10 @@ function stockItemTableColumns(): TableColumn[] {
if (quantity <= 0) { if (quantity <= 0) {
color = 'red'; color = 'red';
extra.push( extra.push(
<Text size="sm">{t`This stock item has been depleted`}</Text> <Text
key="depleted"
size="sm"
>{t`This stock item has been depleted`}</Text>
); );
} }

View File

@ -2,8 +2,9 @@ import { t } from '@lingui/macro';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { ApiPaths } from '../../../enums/ApiEndpoints';
import { useTableRefresh } from '../../../hooks/TableRefresh'; import { useTableRefresh } from '../../../hooks/TableRefresh';
import { ApiPaths, apiUrl } from '../../../states/ApiState'; import { apiUrl } from '../../../states/ApiState';
import { YesNoButton } from '../../items/YesNoButton'; import { YesNoButton } from '../../items/YesNoButton';
import { TableColumn } from '../Column'; import { TableColumn } from '../Column';
import { DescriptionColumn } from '../ColumnRenderers'; import { DescriptionColumn } from '../ColumnRenderers';

View File

@ -1,4 +1,4 @@
import { ModelType } from '../components/render/ModelType'; import { ModelType } from '../enums/ModelType';
/* Lookup tables for mapping backend responses to internal types */ /* Lookup tables for mapping backend responses to internal types */

View File

@ -1,6 +1,6 @@
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { ApiPaths } from '../states/ApiState'; import { ApiPaths } from '../enums/ApiEndpoints';
interface DashboardItems { interface DashboardItems {
id: string; id: string;

View File

@ -0,0 +1,89 @@
/*
* Enumeration of available API endpoints.
*/
export enum ApiPaths {
api_server_info = 'api-server-info',
api_search = 'api-search',
// User information
user_me = 'api-user-me',
user_roles = 'api-user-roles',
user_token = 'api-user-token',
user_simple_login = 'api-user-simple-login',
user_reset = 'api-user-reset',
user_reset_set = 'api-user-reset-set',
user_sso = 'api-user-sso',
user_sso_remove = 'api-user-sso-remove',
user_emails = 'api-user-emails',
user_email_verify = 'api-user-email-verify',
user_email_primary = 'api-user-email-primary',
user_email_remove = 'api-user-email-remove',
user_list = 'api-user-list',
owner_list = 'api-owner-list',
settings_global_list = 'api-settings-global-list',
settings_user_list = 'api-settings-user-list',
notifications_list = 'api-notifications-list',
currency_list = 'api-currency-list',
currency_refresh = 'api-currency-refresh',
barcode = 'api-barcode',
news = 'news',
global_status = 'api-global-status',
version = 'api-version',
sso_providers = 'api-sso-providers',
// Build order URLs
build_order_list = 'api-build-list',
build_order_attachment_list = 'api-build-attachment-list',
// BOM URLs
bom_list = 'api-bom-list',
// Part URLs
part_list = 'api-part-list',
category_list = 'api-category-list',
category_tree = 'api-category-tree',
related_part_list = 'api-related-part-list',
part_attachment_list = 'api-part-attachment-list',
part_parameter_list = 'api-part-parameter-list',
part_parameter_template_list = 'api-part-parameter-template-list',
// Company URLs
company_list = 'api-company-list',
company_attachment_list = 'api-company-attachment-list',
supplier_part_list = 'api-supplier-part-list',
manufacturer_part_list = 'api-manufacturer-part-list',
address_list = 'api-address-list',
contact_list = 'api-contact-list',
// Stock Item URLs
stock_item_list = 'api-stock-item-list',
stock_tracking_list = 'api-stock-tracking-list',
stock_location_list = 'api-stock-location-list',
stock_location_tree = 'api-stock-location-tree',
stock_attachment_list = 'api-stock-attachment-list',
// Purchase Order URLs
purchase_order_list = 'api-purchase-order-list',
purchase_order_line_list = 'api-purchase-order-line-list',
purchase_order_attachment_list = 'api-purchase-order-attachment-list',
// Sales Order URLs
sales_order_list = 'api-sales-order-list',
sales_order_attachment_list = 'api-sales-order-attachment-list',
sales_order_shipment_list = 'api_sales_order_shipment_list',
// Return Order URLs
return_order_list = 'api-return-order-list',
return_order_attachment_list = 'api-return-order-attachment-list',
// Plugin URLs
plugin_list = 'api-plugin-list',
project_code_list = 'api-project-code-list',
custom_unit_list = 'api-custom-unit-list'
}

View File

@ -0,0 +1,25 @@
/*
* Enumeration of available API model types
*/
export enum ModelType {
part = 'part',
supplierpart = 'supplierpart',
manufacturerpart = 'manufacturerpart',
partcategory = 'partcategory',
partparametertemplate = 'partparametertemplate',
projectcode = 'projectcode',
stockitem = 'stockitem',
stocklocation = 'stocklocation',
stockhistory = 'stockhistory',
build = 'build',
company = 'company',
purchaseorder = 'purchaseorder',
purchaseorderline = 'purchaseorderline',
salesorder = 'salesorder',
salesordershipment = 'salesordershipment',
returnorder = 'returnorder',
address = 'address',
contact = 'contact',
owner = 'owner',
user = 'user'
}

View File

@ -0,0 +1,25 @@
/*
* Enumeration of available user role groups
*/
export enum UserRoles {
admin = 'admin',
build = 'build',
part = 'part',
part_category = 'part_category',
purchase_order = 'purchase_order',
return_order = 'return_order',
sales_order = 'sales_order',
stock = 'stock',
stock_location = 'stocklocation',
stocktake = 'stocktake'
}
/*
* Enumeration of available user permissions within each role group
*/
export enum UserPermissions {
view = 'view',
add = 'add',
change = 'change',
delete = 'delete'
}

View File

@ -2,12 +2,12 @@ import { t } from '@lingui/macro';
import { Text } from '@mantine/core'; import { Text } from '@mantine/core';
import { ApiFormFieldSet } from '../components/forms/fields/ApiFormField'; import { ApiFormFieldSet } from '../components/forms/fields/ApiFormField';
import { ApiPaths } from '../enums/ApiEndpoints';
import { import {
openCreateApiForm, openCreateApiForm,
openDeleteApiForm, openDeleteApiForm,
openEditApiForm openEditApiForm
} from '../functions/forms'; } from '../functions/forms';
import { ApiPaths } from '../states/ApiState';
export function attachmentFields(editing: boolean): ApiFormFieldSet { export function attachmentFields(editing: boolean): ApiFormFieldSet {
let fields: ApiFormFieldSet = { let fields: ApiFormFieldSet = {

View File

@ -14,8 +14,8 @@ import {
ApiFormData, ApiFormData,
ApiFormFieldSet ApiFormFieldSet
} from '../components/forms/fields/ApiFormField'; } from '../components/forms/fields/ApiFormField';
import { ApiPaths } from '../enums/ApiEndpoints';
import { openEditApiForm } from '../functions/forms'; import { openEditApiForm } from '../functions/forms';
import { ApiPaths } from '../states/ApiState';
/** /**
* Field set for SupplierPart instance * Field set for SupplierPart instance

View File

@ -1,8 +1,8 @@
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { ApiFormFieldSet } from '../components/forms/fields/ApiFormField'; import { ApiFormFieldSet } from '../components/forms/fields/ApiFormField';
import { ApiPaths } from '../enums/ApiEndpoints';
import { openCreateApiForm, openEditApiForm } from '../functions/forms'; import { openCreateApiForm, openEditApiForm } from '../functions/forms';
import { ApiPaths } from '../states/ApiState';
/** /**
* Construct a set of fields for creating / editing a Part instance * Construct a set of fields for creating / editing a Part instance

View File

@ -16,15 +16,21 @@ import {
* Construct a set of fields for creating / editing a PurchaseOrderLineItem instance * Construct a set of fields for creating / editing a PurchaseOrderLineItem instance
*/ */
export function purchaseOrderLineItemFields({ export function purchaseOrderLineItemFields({
supplierId supplierId,
orderId,
create = false
}: { }: {
supplierId?: number; supplierId?: number;
orderId?: number;
create?: boolean;
}) { }) {
let fields: ApiFormFieldSet = { let fields: ApiFormFieldSet = {
order: { order: {
filters: { filters: {
supplier_detail: true supplier_detail: true
} },
value: orderId,
hidden: create != true || orderId != undefined
}, },
part: { part: {
filters: { filters: {

View File

@ -5,8 +5,8 @@ import {
ApiFormData, ApiFormData,
ApiFormFieldSet ApiFormFieldSet
} from '../components/forms/fields/ApiFormField'; } from '../components/forms/fields/ApiFormField';
import { ApiPaths } from '../enums/ApiEndpoints';
import { openCreateApiForm, openEditApiForm } from '../functions/forms'; import { openCreateApiForm, openEditApiForm } from '../functions/forms';
import { ApiPaths } from '../states/ApiState';
/** /**
* Construct a set of fields for creating / editing a StockItem instance * Construct a set of fields for creating / editing a StockItem instance

View File

@ -4,7 +4,8 @@ import { IconCheck } from '@tabler/icons-react';
import axios from 'axios'; import axios from 'axios';
import { api } from '../App'; import { api } from '../App';
import { ApiPaths, apiUrl, useServerApiState } from '../states/ApiState'; import { ApiPaths } from '../enums/ApiEndpoints';
import { apiUrl, useServerApiState } from '../states/ApiState';
import { useLocalState } from '../states/LocalState'; import { useLocalState } from '../states/LocalState';
import { useSessionState } from '../states/SessionState'; import { useSessionState } from '../states/SessionState';
import { import {

View File

@ -0,0 +1,21 @@
/*
* Determine if the provided value is "true":
*
* Many settings stored on the server are true/false,
* but stored as string values, "true" / "false".
*
* This function provides a wrapper to ensure that the return type is boolean
*/
export function isTrue(value: any): boolean {
if (value === true) {
return true;
}
if (value === false) {
return false;
}
let s = String(value).trim().toLowerCase();
return ['true', 'yes', '1', 'on', 't', 'y'].includes(s);
}

View File

@ -2,7 +2,8 @@ import { useQuery } from '@tanstack/react-query';
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import { api } from '../App'; import { api } from '../App';
import { ApiPaths, apiUrl } from '../states/ApiState'; import { ApiPaths } from '../enums/ApiEndpoints';
import { apiUrl } from '../states/ApiState';
/** /**
* Custom hook for loading a single instance of an instance from the API * Custom hook for loading a single instance of an instance from the API

View File

@ -14,7 +14,8 @@ import { useNavigate, useSearchParams } from 'react-router-dom';
import { api } from '../../App'; import { api } from '../../App';
import { LanguageContext } from '../../contexts/LanguageContext'; import { LanguageContext } from '../../contexts/LanguageContext';
import { ApiPaths, apiUrl } from '../../states/ApiState'; import { ApiPaths } from '../../enums/ApiEndpoints';
import { apiUrl } from '../../states/ApiState';
export default function Set_Password() { export default function Set_Password() {
const simpleForm = useForm({ initialValues: { password: '' } }); const simpleForm = useForm({ initialValues: { password: '' } });

View File

@ -7,8 +7,9 @@ import { ReactNode, useState } from 'react';
import { ApiFormProps } from '../../components/forms/ApiForm'; import { ApiFormProps } from '../../components/forms/ApiForm';
import { PlaceholderPill } from '../../components/items/Placeholder'; import { PlaceholderPill } from '../../components/items/Placeholder';
import { StylishText } from '../../components/items/StylishText'; import { StylishText } from '../../components/items/StylishText';
import { ModelType } from '../../components/render/ModelType'; import { StatusRenderer } from '../../components/render/StatusRenderer';
import { StatusRenderer } from '../../components/renderers/StatusRenderer'; import { ApiPaths } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType';
import { import {
createPart, createPart,
editPart, editPart,
@ -16,7 +17,6 @@ import {
} from '../../forms/PartForms'; } from '../../forms/PartForms';
import { createStockItem } from '../../forms/StockForms'; import { createStockItem } from '../../forms/StockForms';
import { openCreateApiForm, openEditApiForm } from '../../functions/forms'; import { openCreateApiForm, openEditApiForm } from '../../functions/forms';
import { ApiPaths } from '../../states/ApiState';
// Generate some example forms using the modal API forms interface // Generate some example forms using the modal API forms interface
function ApiFormsPlayground() { function ApiFormsPlayground() {

View File

@ -47,37 +47,44 @@ import { api } from '../../App';
import { DocInfo } from '../../components/items/DocInfo'; import { DocInfo } from '../../components/items/DocInfo';
import { StylishText } from '../../components/items/StylishText'; import { StylishText } from '../../components/items/StylishText';
import { TitleWithDoc } from '../../components/items/TitleWithDoc'; import { TitleWithDoc } from '../../components/items/TitleWithDoc';
import { Render, RenderTypes } from '../../components/renderers'; import { RenderInstance } from '../../components/render/Instance';
import { ModelInformationDict } from '../../components/render/ModelType';
import { ApiPaths } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType';
import { notYetImplemented } from '../../functions/notifications'; import { notYetImplemented } from '../../functions/notifications';
import { IS_DEV_OR_DEMO } from '../../main'; import { IS_DEV_OR_DEMO } from '../../main';
import { ApiPaths, apiUrl } from '../../states/ApiState'; import { apiUrl } from '../../states/ApiState';
interface ScanItem { interface ScanItem {
id: string; id: string;
ref: string; ref: string;
data: any; data: any;
instance?: any;
timestamp: Date; timestamp: Date;
source: string; source: string;
link?: string; link?: string;
objectType?: RenderTypes; model?: ModelType;
objectPk?: string; pk?: string;
} }
function matchObject(rd: any): [RenderTypes | undefined, string | undefined] { /*
* Match the scanned object to a known internal model type
*/
function matchObject(rd: any): [ModelType | undefined, string | undefined] {
if (rd?.part) { if (rd?.part) {
return [RenderTypes.part, rd?.part.pk]; return [ModelType.part, rd?.part.pk];
} else if (rd?.stockitem) { } else if (rd?.stockitem) {
return [RenderTypes.stock_item, rd?.stockitem.pk]; return [ModelType.stockitem, rd?.stockitem.pk];
} else if (rd?.stocklocation) { } else if (rd?.stocklocation) {
return [RenderTypes.stock_location, rd?.stocklocation.pk]; return [ModelType.stocklocation, rd?.stocklocation.pk];
} else if (rd?.supplierpart) { } else if (rd?.supplierpart) {
return [RenderTypes.supplier_part, rd?.supplierpart.pk]; return [ModelType.supplierpart, rd?.supplierpart.pk];
} else if (rd?.purchaseorder) { } else if (rd?.purchaseorder) {
return [RenderTypes.purchase_order, rd?.purchaseorder.pk]; return [ModelType.purchaseorder, rd?.purchaseorder.pk];
} else if (rd?.salesorder) { } else if (rd?.salesorder) {
return [RenderTypes.sales_order, rd?.salesorder.pk]; return [ModelType.salesorder, rd?.salesorder.pk];
} else if (rd?.build) { } else if (rd?.build) {
return [RenderTypes.build_order, rd?.build.pk]; return [ModelType.build, rd?.build.pk];
} else { } else {
return [undefined, undefined]; return [undefined, undefined];
} }
@ -147,8 +154,27 @@ export default function Scan() {
item.link = response.data?.url; item.link = response.data?.url;
const rsp = matchObject(response.data); const rsp = matchObject(response.data);
item.objectType = rsp[0]; item.model = rsp[0];
item.objectPk = rsp[1]; item.pk = rsp[1];
// Fetch instance data
if (item.model && item.pk) {
let model_info = ModelInformationDict[item.model];
if (model_info && model_info.api_endpoint) {
let url = apiUrl(model_info.api_endpoint, item.pk);
api
.get(url)
.then((response) => {
item.instance = response.data;
})
.catch((err) => {
console.error('error while fetching instance data at', url);
console.info(err);
});
}
}
historyHandlers.setState(history); historyHandlers.setState(history);
}) })
@ -206,7 +232,7 @@ export default function Scan() {
...new Set( ...new Set(
selection selection
.map((id) => { .map((id) => {
return history.find((item) => item.id === id)?.objectType; return history.find((item) => item.id === id)?.model;
}) })
.filter((item) => item != undefined) .filter((item) => item != undefined)
) )
@ -384,13 +410,13 @@ function HistoryTable({
/> />
</td> </td>
<td> <td>
{item.objectPk && item.objectType ? ( {item.pk && item.model && item.instance ? (
<Render type={item.objectType} pk={item.objectPk} /> <RenderInstance model={item.model} instance={item.instance} />
) : ( ) : (
item.ref item.ref
)} )}
</td> </td>
<td>{item.objectType}</td> <td>{item.model}</td>
<td>{item.source}</td> <td>{item.source}</td>
<td>{item.timestamp?.toString()}</td> <td>{item.timestamp?.toString()}</td>
</tr> </tr>

View File

@ -5,7 +5,8 @@ import { useToggle } from '@mantine/hooks';
import { api } from '../../../../App'; import { api } from '../../../../App';
import { EditButton } from '../../../../components/items/EditButton'; import { EditButton } from '../../../../components/items/EditButton';
import { ApiPaths, apiUrl } from '../../../../states/ApiState'; import { ApiPaths } from '../../../../enums/ApiEndpoints';
import { apiUrl } from '../../../../states/ApiState';
import { useUserState } from '../../../../states/UserState'; import { useUserState } from '../../../../states/UserState';
export function AccountDetailPanel() { export function AccountDetailPanel() {

View File

@ -19,7 +19,8 @@ import { useEffect, useState } from 'react';
import { api, queryClient } from '../../../../App'; import { api, queryClient } from '../../../../App';
import { PlaceholderPill } from '../../../../components/items/Placeholder'; import { PlaceholderPill } from '../../../../components/items/Placeholder';
import { ApiPaths, apiUrl } from '../../../../states/ApiState'; import { ApiPaths } from '../../../../enums/ApiEndpoints';
import { apiUrl } from '../../../../states/ApiState';
export function SecurityContent() { export function SecurityContent() {
const [isSsoEnabled, setIsSsoEnabled] = useState<boolean>(false); const [isSsoEnabled, setIsSsoEnabled] = useState<boolean>(false);

View File

@ -86,10 +86,8 @@ export default function AdminCenter() {
<> <>
<Stack spacing="xs"> <Stack spacing="xs">
<SettingsHeader <SettingsHeader
title={<Trans>Admin Center</Trans>} title={t`Admin Center`}
subtitle={ subtitle={t`Advanced Amininistrative Options for InvenTree`}
<Trans>Advanced Amininistrative Options for InvenTree</Trans>
}
switch_link="/settings/system" switch_link="/settings/system"
switch_text="System Settings" switch_text="System Settings"
/> />

View File

@ -3,11 +3,11 @@ import { LoadingOverlay, Stack } from '@mantine/core';
import { IconPlugConnected } from '@tabler/icons-react'; import { IconPlugConnected } from '@tabler/icons-react';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { PageDetail } from '../../../components/nav/PageDetail';
import { PanelGroup, PanelType } from '../../../components/nav/PanelGroup'; import { PanelGroup, PanelType } from '../../../components/nav/PanelGroup';
import { SettingsHeader } from '../../../components/nav/SettingsHeader';
import { PluginListTable } from '../../../components/tables/plugin/PluginListTable'; import { PluginListTable } from '../../../components/tables/plugin/PluginListTable';
import { ApiPaths } from '../../../enums/ApiEndpoints';
import { useInstance } from '../../../hooks/UseInstance'; import { useInstance } from '../../../hooks/UseInstance';
import { ApiPaths } from '../../../states/ApiState';
/** /**
* Plugins settings page * Plugins settings page
@ -44,7 +44,7 @@ export default function PluginSettings() {
<> <>
<Stack spacing="xs"> <Stack spacing="xs">
<LoadingOverlay visible={settingsQuery.isFetching} /> <LoadingOverlay visible={settingsQuery.isFetching} />
<PageDetail title={t`Plugin Settings`} /> <SettingsHeader title={t`Plugin Settings`} switch_condition={false} />
<PanelGroup pageKey="plugin-settings" panels={pluginPanels} /> <PanelGroup pageKey="plugin-settings" panels={pluginPanels} />
</Stack> </Stack>
</> </>

View File

@ -292,8 +292,8 @@ export default function SystemSettings() {
<> <>
<Stack spacing="xs"> <Stack spacing="xs">
<SettingsHeader <SettingsHeader
title={server.instance || ''} title={t`System Settings`}
subtitle={<Trans>System Settings</Trans>} subtitle={server.instance || ''}
switch_link="/settings/user" switch_link="/settings/user"
switch_text={<Trans>Switch to User Setting</Trans>} switch_text={<Trans>Switch to User Setting</Trans>}
/> />

View File

@ -112,9 +112,9 @@ export default function UserSettings() {
<> <>
<Stack spacing="xs"> <Stack spacing="xs">
<SettingsHeader <SettingsHeader
title={`${user?.first_name} ${user?.last_name}`} title={t`Account Settings`}
subtitle={`${user?.first_name} ${user?.last_name}`}
shorthand={user?.username || ''} shorthand={user?.username || ''}
subtitle={<Trans>Account Settings</Trans>}
switch_link="/settings/system" switch_link="/settings/system"
switch_text={<Trans>Switch to System Setting</Trans>} switch_text={<Trans>Switch to System Setting</Trans>}
switch_condition={user?.is_staff || false} switch_condition={user?.is_staff || false}

View File

@ -13,8 +13,9 @@ import { api } from '../App';
import { PageDetail } from '../components/nav/PageDetail'; import { PageDetail } from '../components/nav/PageDetail';
import { PanelGroup } from '../components/nav/PanelGroup'; import { PanelGroup } from '../components/nav/PanelGroup';
import { NotificationTable } from '../components/tables/notifications/NotificationsTable'; import { NotificationTable } from '../components/tables/notifications/NotificationsTable';
import { ApiPaths } from '../enums/ApiEndpoints';
import { useTableRefresh } from '../hooks/TableRefresh'; import { useTableRefresh } from '../hooks/TableRefresh';
import { ApiPaths, apiUrl } from '../states/ApiState'; import { apiUrl } from '../states/ApiState';
export default function NotificationsPage() { export default function NotificationsPage() {
const unreadRefresh = useTableRefresh('unreadnotifications'); const unreadRefresh = useTableRefresh('unreadnotifications');

View File

@ -14,8 +14,7 @@ import {
IconPaperclip, IconPaperclip,
IconPrinter, IconPrinter,
IconQrcode, IconQrcode,
IconSitemap, IconSitemap
IconTrash
} from '@tabler/icons-react'; } from '@tabler/icons-react';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
@ -31,16 +30,17 @@ import {
} from '../../components/items/ActionDropdown'; } from '../../components/items/ActionDropdown';
import { PageDetail } from '../../components/nav/PageDetail'; import { PageDetail } from '../../components/nav/PageDetail';
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup'; import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
import { ModelType } from '../../components/render/ModelType'; import { StatusRenderer } from '../../components/render/StatusRenderer';
import { StatusRenderer } from '../../components/renderers/StatusRenderer';
import { BuildOrderTable } from '../../components/tables/build/BuildOrderTable'; import { BuildOrderTable } from '../../components/tables/build/BuildOrderTable';
import { AttachmentTable } from '../../components/tables/general/AttachmentTable'; import { AttachmentTable } from '../../components/tables/general/AttachmentTable';
import { StockItemTable } from '../../components/tables/stock/StockItemTable'; import { StockItemTable } from '../../components/tables/stock/StockItemTable';
import { NotesEditor } from '../../components/widgets/MarkdownEditor'; import { NotesEditor } from '../../components/widgets/MarkdownEditor';
import { ApiPaths } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType';
import { buildOrderFields } from '../../forms/BuildForms'; import { buildOrderFields } from '../../forms/BuildForms';
import { openEditApiForm } from '../../functions/forms'; import { openEditApiForm } from '../../functions/forms';
import { useInstance } from '../../hooks/UseInstance'; import { useInstance } from '../../hooks/UseInstance';
import { ApiPaths, apiUrl } from '../../states/ApiState'; import { apiUrl } from '../../states/ApiState';
import { useUserState } from '../../states/UserState'; import { useUserState } from '../../states/UserState';
/** /**

View File

@ -5,9 +5,9 @@ import { useNavigate } from 'react-router-dom';
import { PageDetail } from '../../components/nav/PageDetail'; import { PageDetail } from '../../components/nav/PageDetail';
import { BuildOrderTable } from '../../components/tables/build/BuildOrderTable'; import { BuildOrderTable } from '../../components/tables/build/BuildOrderTable';
import { ApiPaths } from '../../enums/ApiEndpoints';
import { buildOrderFields } from '../../forms/BuildForms'; import { buildOrderFields } from '../../forms/BuildForms';
import { openCreateApiForm } from '../../functions/forms'; import { openCreateApiForm } from '../../functions/forms';
import { ApiPaths } from '../../states/ApiState';
/** /**
* Build Order index page * Build Order index page

View File

@ -33,10 +33,12 @@ import { ReturnOrderTable } from '../../components/tables/sales/ReturnOrderTable
import { SalesOrderTable } from '../../components/tables/sales/SalesOrderTable'; import { SalesOrderTable } from '../../components/tables/sales/SalesOrderTable';
import { StockItemTable } from '../../components/tables/stock/StockItemTable'; import { StockItemTable } from '../../components/tables/stock/StockItemTable';
import { NotesEditor } from '../../components/widgets/MarkdownEditor'; import { NotesEditor } from '../../components/widgets/MarkdownEditor';
import { ApiPaths } from '../../enums/ApiEndpoints';
import { UserRoles } from '../../enums/Roles';
import { editCompany } from '../../forms/CompanyForms'; import { editCompany } from '../../forms/CompanyForms';
import { useInstance } from '../../hooks/UseInstance'; import { useInstance } from '../../hooks/UseInstance';
import { ApiPaths, apiUrl } from '../../states/ApiState'; import { apiUrl } from '../../states/ApiState';
import { UserRoles, useUserState } from '../../states/UserState'; import { useUserState } from '../../states/UserState';
export type CompanyDetailProps = { export type CompanyDetailProps = {
title: string; title: string;

View File

@ -14,8 +14,8 @@ import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
import { PartCategoryTree } from '../../components/nav/PartCategoryTree'; import { PartCategoryTree } from '../../components/nav/PartCategoryTree';
import { PartCategoryTable } from '../../components/tables/part/PartCategoryTable'; import { PartCategoryTable } from '../../components/tables/part/PartCategoryTable';
import { PartListTable } from '../../components/tables/part/PartTable'; import { PartListTable } from '../../components/tables/part/PartTable';
import { ApiPaths } from '../../enums/ApiEndpoints';
import { useInstance } from '../../hooks/UseInstance'; import { useInstance } from '../../hooks/UseInstance';
import { ApiPaths } from '../../states/ApiState';
/** /**
* Detail view for a single PartCategory instance. * Detail view for a single PartCategory instance.

View File

@ -50,9 +50,10 @@ import { SupplierPartTable } from '../../components/tables/purchasing/SupplierPa
import { SalesOrderTable } from '../../components/tables/sales/SalesOrderTable'; import { SalesOrderTable } from '../../components/tables/sales/SalesOrderTable';
import { StockItemTable } from '../../components/tables/stock/StockItemTable'; import { StockItemTable } from '../../components/tables/stock/StockItemTable';
import { NotesEditor } from '../../components/widgets/MarkdownEditor'; import { NotesEditor } from '../../components/widgets/MarkdownEditor';
import { ApiPaths } from '../../enums/ApiEndpoints';
import { editPart } from '../../forms/PartForms'; import { editPart } from '../../forms/PartForms';
import { useInstance } from '../../hooks/UseInstance'; import { useInstance } from '../../hooks/UseInstance';
import { ApiPaths, apiUrl } from '../../states/ApiState'; import { apiUrl } from '../../states/ApiState';
import { useUserState } from '../../states/UserState'; import { useUserState } from '../../states/UserState';
/** /**

View File

@ -26,8 +26,9 @@ import { AttachmentTable } from '../../components/tables/general/AttachmentTable
import { PurchaseOrderLineItemTable } from '../../components/tables/purchasing/PurchaseOrderLineItemTable'; import { PurchaseOrderLineItemTable } from '../../components/tables/purchasing/PurchaseOrderLineItemTable';
import { StockItemTable } from '../../components/tables/stock/StockItemTable'; import { StockItemTable } from '../../components/tables/stock/StockItemTable';
import { NotesEditor } from '../../components/widgets/MarkdownEditor'; import { NotesEditor } from '../../components/widgets/MarkdownEditor';
import { ApiPaths } from '../../enums/ApiEndpoints';
import { useInstance } from '../../hooks/UseInstance'; import { useInstance } from '../../hooks/UseInstance';
import { ApiPaths, apiUrl } from '../../states/ApiState'; import { apiUrl } from '../../states/ApiState';
import { useUserState } from '../../states/UserState'; import { useUserState } from '../../states/UserState';
/** /**
@ -114,7 +115,7 @@ export default function PurchaseOrderDetail() {
]} ]}
/>, />,
<ActionDropdown <ActionDropdown
key="order" key="order-actions"
tooltip={t`Order Actions`} tooltip={t`Order Actions`}
icon={<IconDots />} icon={<IconDots />}
actions={[EditItemAction({}), DeleteItemAction({})]} actions={[EditItemAction({}), DeleteItemAction({})]}

View File

@ -8,8 +8,9 @@ import { PageDetail } from '../../components/nav/PageDetail';
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup'; import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
import { AttachmentTable } from '../../components/tables/general/AttachmentTable'; import { AttachmentTable } from '../../components/tables/general/AttachmentTable';
import { NotesEditor } from '../../components/widgets/MarkdownEditor'; import { NotesEditor } from '../../components/widgets/MarkdownEditor';
import { ApiPaths } from '../../enums/ApiEndpoints';
import { useInstance } from '../../hooks/UseInstance'; import { useInstance } from '../../hooks/UseInstance';
import { ApiPaths, apiUrl } from '../../states/ApiState'; import { apiUrl } from '../../states/ApiState';
/** /**
* Detail page for a single ReturnOrder * Detail page for a single ReturnOrder

View File

@ -16,8 +16,9 @@ import { PageDetail } from '../../components/nav/PageDetail';
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup'; import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
import { AttachmentTable } from '../../components/tables/general/AttachmentTable'; import { AttachmentTable } from '../../components/tables/general/AttachmentTable';
import { NotesEditor } from '../../components/widgets/MarkdownEditor'; import { NotesEditor } from '../../components/widgets/MarkdownEditor';
import { ApiPaths } from '../../enums/ApiEndpoints';
import { useInstance } from '../../hooks/UseInstance'; import { useInstance } from '../../hooks/UseInstance';
import { ApiPaths, apiUrl } from '../../states/ApiState'; import { apiUrl } from '../../states/ApiState';
/** /**
* Detail page for a single SalesOrder * Detail page for a single SalesOrder

View File

@ -9,8 +9,8 @@ import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
import { StockLocationTree } from '../../components/nav/StockLocationTree'; import { StockLocationTree } from '../../components/nav/StockLocationTree';
import { StockItemTable } from '../../components/tables/stock/StockItemTable'; import { StockItemTable } from '../../components/tables/stock/StockItemTable';
import { StockLocationTable } from '../../components/tables/stock/StockLocationTable'; import { StockLocationTable } from '../../components/tables/stock/StockLocationTable';
import { ApiPaths } from '../../enums/ApiEndpoints';
import { useInstance } from '../../hooks/UseInstance'; import { useInstance } from '../../hooks/UseInstance';
import { ApiPaths } from '../../states/ApiState';
export default function Stock() { export default function Stock() {
const { id } = useParams(); const { id } = useParams();

View File

@ -35,9 +35,10 @@ import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
import { StockLocationTree } from '../../components/nav/StockLocationTree'; import { StockLocationTree } from '../../components/nav/StockLocationTree';
import { AttachmentTable } from '../../components/tables/general/AttachmentTable'; import { AttachmentTable } from '../../components/tables/general/AttachmentTable';
import { NotesEditor } from '../../components/widgets/MarkdownEditor'; import { NotesEditor } from '../../components/widgets/MarkdownEditor';
import { ApiPaths } from '../../enums/ApiEndpoints';
import { editStockItem } from '../../forms/StockForms'; import { editStockItem } from '../../forms/StockForms';
import { useInstance } from '../../hooks/UseInstance'; import { useInstance } from '../../hooks/UseInstance';
import { ApiPaths, apiUrl } from '../../states/ApiState'; import { apiUrl } from '../../states/ApiState';
import { useUserState } from '../../states/UserState'; import { useUserState } from '../../states/UserState';
export default function StockDetail() { export default function StockDetail() {

View File

@ -2,10 +2,11 @@ import { create } from 'zustand';
import { persist } from 'zustand/middleware'; import { persist } from 'zustand/middleware';
import { api } from '../App'; import { api } from '../App';
import { ModelType } from '../components/render/ModelType'; import { StatusCodeListInterface } from '../components/render/StatusRenderer';
import { StatusCodeListInterface } from '../components/renderers/StatusRenderer';
import { statusCodeList } from '../defaults/backendMappings'; import { statusCodeList } from '../defaults/backendMappings';
import { emptyServerAPI } from '../defaults/defaults'; import { emptyServerAPI } from '../defaults/defaults';
import { ApiPaths } from '../enums/ApiEndpoints';
import { ModelType } from '../enums/ModelType';
import { ServerAPIProps } from './states'; import { ServerAPIProps } from './states';
type StatusLookup = Record<ModelType, StatusCodeListInterface>; type StatusLookup = Record<ModelType, StatusCodeListInterface>;
@ -48,87 +49,6 @@ export const useServerApiState = create<ServerApiStateProps>()(
) )
); );
export enum ApiPaths {
api_server_info = 'api-server-info',
api_search = 'api-search',
// User information
user_me = 'api-user-me',
user_roles = 'api-user-roles',
user_token = 'api-user-token',
user_simple_login = 'api-user-simple-login',
user_reset = 'api-user-reset',
user_reset_set = 'api-user-reset-set',
user_sso = 'api-user-sso',
user_sso_remove = 'api-user-sso-remove',
user_emails = 'api-user-emails',
user_email_verify = 'api-user-email-verify',
user_email_primary = 'api-user-email-primary',
user_email_remove = 'api-user-email-remove',
settings_global_list = 'api-settings-global-list',
settings_user_list = 'api-settings-user-list',
notifications_list = 'api-notifications-list',
currency_list = 'api-currency-list',
currency_refresh = 'api-currency-refresh',
barcode = 'api-barcode',
news = 'news',
global_status = 'api-global-status',
version = 'api-version',
sso_providers = 'api-sso-providers',
// Build order URLs
build_order_list = 'api-build-list',
build_order_attachment_list = 'api-build-attachment-list',
// BOM URLs
bom_list = 'api-bom-list',
// Part URLs
part_list = 'api-part-list',
category_list = 'api-category-list',
category_tree = 'api-category-tree',
related_part_list = 'api-related-part-list',
part_attachment_list = 'api-part-attachment-list',
part_parameter_list = 'api-part-parameter-list',
part_parameter_template_list = 'api-part-parameter-template-list',
// Company URLs
company_list = 'api-company-list',
company_attachment_list = 'api-company-attachment-list',
supplier_part_list = 'api-supplier-part-list',
manufacturer_part_list = 'api-manufacturer-part-list',
// Stock Item URLs
stock_item_list = 'api-stock-item-list',
stock_tracking_list = 'api-stock-tracking-list',
stock_location_list = 'api-stock-location-list',
stock_location_tree = 'api-stock-location-tree',
stock_attachment_list = 'api-stock-attachment-list',
// Purchase Order URLs
purchase_order_list = 'api-purchase-order-list',
purchase_order_line_list = 'api-purchase-order-line-list',
purchase_order_attachment_list = 'api-purchase-order-attachment-list',
// Sales Order URLs
sales_order_list = 'api-sales-order-list',
sales_order_attachment_list = 'api-sales-order-attachment-list',
// Return Order URLs
return_order_list = 'api-return-order-list',
return_order_attachment_list = 'api-return-order-attachment-list',
// Plugin URLs
plugin_list = 'api-plugin-list',
project_code_list = 'api-project-code-list',
custom_unit_list = 'api-custom-unit-list'
}
/** /**
* Function to return the API prefix. * Function to return the API prefix.
* For now it is fixed, but may be configurable in the future. * For now it is fixed, but may be configurable in the future.
@ -144,6 +64,10 @@ export function apiEndpoint(path: ApiPaths): string {
switch (path) { switch (path) {
case ApiPaths.api_server_info: case ApiPaths.api_server_info:
return ''; return '';
case ApiPaths.user_list:
return 'user/';
case ApiPaths.owner_list:
return 'user/owner/';
case ApiPaths.user_me: case ApiPaths.user_me:
return 'user/me/'; return 'user/me/';
case ApiPaths.user_roles: case ApiPaths.user_roles:
@ -214,6 +138,10 @@ export function apiEndpoint(path: ApiPaths): string {
return 'part/attachment/'; return 'part/attachment/';
case ApiPaths.company_list: case ApiPaths.company_list:
return 'company/'; return 'company/';
case ApiPaths.contact_list:
return 'company/contact/';
case ApiPaths.address_list:
return 'company/address/';
case ApiPaths.company_attachment_list: case ApiPaths.company_attachment_list:
return 'company/attachment/'; return 'company/attachment/';
case ApiPaths.supplier_part_list: case ApiPaths.supplier_part_list:
@ -240,6 +168,8 @@ export function apiEndpoint(path: ApiPaths): string {
return 'order/so/'; return 'order/so/';
case ApiPaths.sales_order_attachment_list: case ApiPaths.sales_order_attachment_list:
return 'order/so/attachment/'; return 'order/so/attachment/';
case ApiPaths.sales_order_shipment_list:
return 'order/so/shipment/';
case ApiPaths.return_order_list: case ApiPaths.return_order_list:
return 'order/ro/'; return 'order/ro/';
case ApiPaths.return_order_attachment_list: case ApiPaths.return_order_attachment_list:

View File

@ -4,7 +4,9 @@
import { create } from 'zustand'; import { create } from 'zustand';
import { api } from '../App'; import { api } from '../App';
import { ApiPaths, apiUrl } from './ApiState'; import { ApiPaths } from '../enums/ApiEndpoints';
import { isTrue } from '../functions/conversion';
import { apiUrl } from './ApiState';
import { Setting, SettingsLookup } from './states'; import { Setting, SettingsLookup } from './states';
export interface SettingsStateProps { export interface SettingsStateProps {
@ -12,6 +14,8 @@ export interface SettingsStateProps {
lookup: SettingsLookup; lookup: SettingsLookup;
fetchSettings: () => void; fetchSettings: () => void;
endpoint: ApiPaths; endpoint: ApiPaths;
getSetting: (key: string, default_value?: string) => string; // Return a raw setting value
isSet: (key: string, default_value?: boolean) => boolean; // Check a "boolean" setting
} }
/** /**
@ -34,6 +38,13 @@ export const useGlobalSettingsState = create<SettingsStateProps>(
.catch((error) => { .catch((error) => {
console.error('Error fetching global settings:', error); console.error('Error fetching global settings:', error);
}); });
},
getSetting: (key: string, default_value?: string) => {
return get().lookup[key] ?? default_value ?? '';
},
isSet: (key: string, default_value?: boolean) => {
let value = get().lookup[key] ?? default_value ?? 'false';
return isTrue(value);
} }
}) })
); );
@ -57,6 +68,13 @@ export const useUserSettingsState = create<SettingsStateProps>((set, get) => ({
.catch((error) => { .catch((error) => {
console.error('Error fetching user settings:', error); console.error('Error fetching user settings:', error);
}); });
},
getSetting: (key: string, default_value?: string) => {
return get().lookup[key] ?? default_value ?? '';
},
isSet: (key: string, default_value?: boolean) => {
let value = get().lookup[key] ?? default_value ?? 'false';
return isTrue(value);
} }
})); }));

View File

@ -1,36 +1,12 @@
import { create } from 'zustand'; import { create } from 'zustand';
import { api } from '../App'; import { api } from '../App';
import { ApiPaths } from '../enums/ApiEndpoints';
import { UserPermissions, UserRoles } from '../enums/Roles';
import { doClassicLogout } from '../functions/auth'; import { doClassicLogout } from '../functions/auth';
import { ApiPaths, apiUrl } from './ApiState'; import { apiUrl } from './ApiState';
import { UserProps } from './states'; import { UserProps } from './states';
/*
* Enumeration of available user role groups
*/
export enum UserRoles {
admin = 'admin',
build = 'build',
part = 'part',
part_category = 'part_category',
purchase_order = 'purchase_order',
return_order = 'return_order',
sales_order = 'sales_order',
stock = 'stock',
stock_location = 'stocklocation',
stocktake = 'stocktake'
}
/*
* Enumeration of available user permissions within each role group
*/
export enum UserPermissions {
view = 'view',
add = 'add',
change = 'change',
delete = 'delete'
}
interface UserStateProps { interface UserStateProps {
user: UserProps | undefined; user: UserProps | undefined;
username: () => string; username: () => string;
@ -86,9 +62,9 @@ export const useUserState = create<UserStateProps>((set, get) => ({
const user: UserProps = get().user as UserProps; const user: UserProps = get().user as UserProps;
// Update user with role data // Update user with role data
user.roles = response.data.roles; user.roles = response.data?.roles ?? {};
user.is_staff = response.data.is_staff ?? false; user.is_staff = response.data?.is_staff ?? false;
user.is_superuser = response.data.is_superuser ?? false; user.is_superuser = response.data?.is_superuser ?? false;
set({ user: user }); set({ user: user });
}) })
.catch((error) => { .catch((error) => {
@ -99,11 +75,15 @@ export const useUserState = create<UserStateProps>((set, get) => ({
// Check if the user has the specified permission for the specified role // Check if the user has the specified permission for the specified role
const user: UserProps = get().user as UserProps; const user: UserProps = get().user as UserProps;
if (user.is_superuser) return true; if (!user) {
if (user.roles === undefined) return false; return false;
if (user.roles[role] === undefined) return false; }
return user.roles[role].includes(permission); if (user?.is_superuser) return true;
if (user?.roles === undefined) return false;
if (user?.roles[role] === undefined) return false;
return user?.roles[role].includes(permission);
}, },
hasDeleteRole: (role: UserRoles) => { hasDeleteRole: (role: UserRoles) => {
return get().checkUserRole(role, UserPermissions.delete); return get().checkUserRole(role, UserPermissions.delete);