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)).
!!! info "Note"
@ -117,4 +118,4 @@ Example:
onPanelLoad('loans', function() {
...
});;
```
```

View File

@ -3,7 +3,8 @@ import { useQuery } from '@tanstack/react-query';
import { useEffect, useState } from 'react';
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 { ErrorItem } from './items/ErrorItem';

View File

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

View File

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

View File

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

View File

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

View File

@ -4,7 +4,8 @@ import { ContextModalProps } from '@mantine/modals';
import { useQuery } from '@tanstack/react-query';
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 { useUserState } from '../../states/UserState';
import { CopyButton } from '../items/CopyButton';

View File

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

View File

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

View File

@ -14,7 +14,8 @@ import { useQuery } from '@tanstack/react-query';
import { useNavigate } from 'react-router-dom';
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';
/**

View File

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

View File

@ -6,7 +6,8 @@ import { useQuery } from '@tanstack/react-query';
import { useNavigate } from 'react-router-dom';
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';
export function PartCategoryTree({

View File

@ -26,139 +26,27 @@ import {
IconX
} from '@tabler/icons-react';
import { useQuery } from '@tanstack/react-query';
import { useEffect, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom';
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 { ModelInformationDict, ModelType } from '../render/ModelType';
import { ModelInformationDict } from '../render/ModelType';
// Define type for handling individual search queries
type SearchQuery = {
name: ModelType;
model: ModelType;
enabled: boolean;
parameters: 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
*/
@ -175,10 +63,11 @@ function QueryResultGroup({
return null;
}
const model = ModelInformationDict[query.name];
const model = ModelInformationDict[query.model];
return (
<Paper shadow="sm" radius="xs" p="md">
<Stack key={query.name}>
<Paper shadow="sm" radius="xs" p="md" key={`paper-${query.model}`}>
<Stack key={`stack-${query.model}`}>
<Group position="apart" noWrap={true}>
<Group position="left" spacing={5} noWrap={true}>
<Text size="lg">{model.label_multiple}</Text>
@ -193,7 +82,7 @@ function QueryResultGroup({
color="red"
variant="transparent"
radius="xs"
onClick={() => onRemove(query.name)}
onClick={() => onRemove(query.model)}
>
<IconX />
</ActionIcon>
@ -201,11 +90,11 @@ function QueryResultGroup({
<Divider />
<Stack>
{query.results.results.map((result: any) => (
<Anchor onClick={() => onResultClick(query.name, result.pk)}>
<Anchor onClick={() => onResultClick(query.model, result.pk)}>
<RenderInstance
key={`${query.name}-${result.pk}`}
key={`${query.model}-${result.pk}`}
instance={result}
model={query.name}
model={query.model}
/>
</Anchor>
))}
@ -233,10 +122,116 @@ export function SearchDrawer({
const [searchRegex, setSearchRegex] = 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
const searchQueries: SearchQuery[] = buildSearchQueries().filter(
(q) => q.enabled
);
const searchQueries: SearchQuery[] = searchQueryList.filter((q) => q.enabled);
// Re-fetch data whenever the search term is updated
useEffect(() => {
@ -261,7 +256,7 @@ export function SearchDrawer({
// Add in custom query parameters
searchQueries.forEach((query) => {
params[query.name] = query.parameters;
params[query.model] = query.parameters;
});
return api
@ -289,11 +284,11 @@ export function SearchDrawer({
useEffect(() => {
if (searchQuery.data) {
let queries = searchQueries.filter(
(query) => query.name in searchQuery.data
(query) => query.model 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) {
query.results = searchQuery.data[key];
}
@ -310,7 +305,7 @@ export function SearchDrawer({
// Callback to remove a set of results from the list
function removeResults(query: ModelType) {
setQueryResults(queryResults.filter((q) => q.name != query));
setQueryResults(queryResults.filter((q) => q.model != query));
}
// Callback when the drawer is closed
@ -332,7 +327,7 @@ export function SearchDrawer({
return (
<Drawer
opened={opened}
size="md"
size="xl"
onClose={closeDrawer}
position="right"
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 { ReactNode } from 'react';
import { Link } from 'react-router-dom';
import { StylishText } from '../items/StylishText';
/**
* Construct a settings page header with interlinks to one other settings page
*/
@ -14,28 +24,35 @@ export function SettingsHeader({
switch_text,
switch_link
}: {
title: string | ReactNode;
title: string;
shorthand?: string;
subtitle: string | ReactNode;
subtitle?: string;
switch_condition?: boolean;
switch_text?: string | ReactNode;
switch_link?: string;
}) {
return (
<Stack spacing="0" ml={'sm'}>
<Group>
<Title order={3}>{title}</Title>
{shorthand && <Text c="dimmed">({shorthand})</Text>}
</Group>
<Group>
<Text c="dimmed">{subtitle}</Text>
<Paper shadow="xs" radius="xs" p="xs">
<Group position="apart">
<Stack spacing="xs">
<Group position="left" spacing="xs">
<StylishText size="xl">{title}</StylishText>
<Text size="sm">{shorthand}</Text>
</Group>
<Text italic>{subtitle}</Text>
</Stack>
<Space />
{switch_text && switch_link && switch_condition && (
<Anchor component={Link} to={switch_link}>
<IconSwitch size={14} />
{switch_text}
<Button variant="outline">
<Group spacing="sm">
<IconSwitch size={18} />
<Text>{switch_text}</Text>
</Group>
</Button>
</Anchor>
)}
</Group>
</Stack>
</Paper>
);
}

View File

@ -6,7 +6,8 @@ import { useQuery } from '@tanstack/react-query';
import { useNavigate } from 'react-router-dom';
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';
export function StockLocationTree({

View File

@ -1,6 +1,8 @@
import { ReactNode } from 'react';
import { ModelType } from '../../enums/ModelType';
import { RenderInlineModel } from './Instance';
import { StatusRenderer } from './StatusRenderer';
/**
* Inline rendering of a single BuildOrder instance
@ -10,6 +12,10 @@ export function RenderBuildOrder({ instance }: { instance: any }): ReactNode {
<RenderInlineModel
primary={instance.reference}
secondary={instance.title}
suffix={StatusRenderer({
status: instance.status,
type: ModelType.build
})}
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 { ReactNode } from 'react';
import { ModelType } from '../../enums/ModelType';
import { Thumbnail } from '../images/Thumbnail';
import { RenderBuildOrder } from './Build';
import {
@ -13,7 +14,6 @@ import {
RenderSupplierPart
} from './Company';
import { RenderProjectCode } from './Generic';
import { ModelType } from './ModelType';
import {
RenderPurchaseOrder,
RenderReturnOrder,
@ -101,7 +101,7 @@ export function RenderInlineModel({
}: {
primary: string;
secondary?: string;
suffix?: string;
suffix?: ReactNode;
image?: string;
labels?: string[];
url?: string;

View File

@ -1,37 +1,18 @@
import { t } from '@lingui/macro';
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'
}
import { ApiPaths } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType';
interface ModelInformatonInterface {
interface ModelInformationInterface {
label: string;
label_multiple: string;
url_overview?: string;
url_detail?: string;
api_endpoint?: ApiPaths;
}
type ModelDictory = {
[key in keyof typeof ModelType]: ModelInformatonInterface;
[key in keyof typeof ModelType]: ModelInformationInterface;
};
export const ModelInformationDict: ModelDictory = {
@ -39,116 +20,136 @@ export const ModelInformationDict: ModelDictory = {
label: t`Part`,
label_multiple: t`Parts`,
url_overview: '/part',
url_detail: '/part/:pk/'
url_detail: '/part/:pk/',
api_endpoint: ApiPaths.part_list
},
partparametertemplate: {
label: t`Part Parameter Template`,
label_multiple: t`Part Parameter Templates`,
url_overview: '/partparametertemplate',
url_detail: '/partparametertemplate/:pk/'
url_detail: '/partparametertemplate/:pk/',
api_endpoint: ApiPaths.part_parameter_template_list
},
supplierpart: {
label: t`Supplier Part`,
label_multiple: t`Supplier Parts`,
url_overview: '/supplierpart',
url_detail: '/supplierpart/:pk/'
url_detail: '/supplierpart/:pk/',
api_endpoint: ApiPaths.supplier_part_list
},
manufacturerpart: {
label: t`Manufacturer Part`,
label_multiple: t`Manufacturer Parts`,
url_overview: '/manufacturerpart',
url_detail: '/manufacturerpart/:pk/'
url_detail: '/manufacturerpart/:pk/',
api_endpoint: ApiPaths.manufacturer_part_list
},
partcategory: {
label: t`Part Category`,
label_multiple: t`Part Categories`,
url_overview: '/partcategory',
url_detail: '/partcategory/:pk/'
url_detail: '/partcategory/:pk/',
api_endpoint: ApiPaths.category_list
},
stockitem: {
label: t`Stock Item`,
label_multiple: t`Stock Items`,
url_overview: '/stockitem',
url_detail: '/stockitem/:pk/'
url_detail: '/stockitem/:pk/',
api_endpoint: ApiPaths.stock_item_list
},
stocklocation: {
label: t`Stock Location`,
label_multiple: t`Stock Locations`,
url_overview: '/stocklocation',
url_detail: '/stocklocation/:pk/'
url_detail: '/stocklocation/:pk/',
api_endpoint: ApiPaths.stock_location_list
},
stockhistory: {
label: t`Stock History`,
label_multiple: t`Stock Histories`
label_multiple: t`Stock Histories`,
api_endpoint: ApiPaths.stock_tracking_list
},
build: {
label: t`Build`,
label_multiple: t`Builds`,
url_overview: '/build',
url_detail: '/build/:pk/'
url_detail: '/build/:pk/',
api_endpoint: ApiPaths.build_order_list
},
company: {
label: t`Company`,
label_multiple: t`Companies`,
url_overview: '/company',
url_detail: '/company/:pk/'
url_detail: '/company/:pk/',
api_endpoint: ApiPaths.company_list
},
projectcode: {
label: t`Project Code`,
label_multiple: t`Project Codes`,
url_overview: '/project-code',
url_detail: '/project-code/:pk/'
url_detail: '/project-code/:pk/',
api_endpoint: ApiPaths.project_code_list
},
purchaseorder: {
label: t`Purchase Order`,
label_multiple: t`Purchase Orders`,
url_overview: '/purchaseorder',
url_detail: '/purchaseorder/:pk/'
url_detail: '/purchaseorder/:pk/',
api_endpoint: ApiPaths.purchase_order_list
},
purchaseorderline: {
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: {
label: t`Sales Order`,
label_multiple: t`Sales Orders`,
url_overview: '/salesorder',
url_detail: '/salesorder/:pk/'
url_detail: '/salesorder/:pk/',
api_endpoint: ApiPaths.sales_order_list
},
salesordershipment: {
label: t`Sales Order Shipment`,
label_multiple: t`Sales Order Shipments`,
url_overview: '/salesordershipment',
url_detail: '/salesordershipment/:pk/'
url_detail: '/salesordershipment/:pk/',
api_endpoint: ApiPaths.sales_order_shipment_list
},
returnorder: {
label: t`Return Order`,
label_multiple: t`Return Orders`,
url_overview: '/returnorder',
url_detail: '/returnorder/:pk/'
url_detail: '/returnorder/:pk/',
api_endpoint: ApiPaths.return_order_list
},
address: {
label: t`Address`,
label_multiple: t`Addresses`,
url_overview: '/address',
url_detail: '/address/:pk/'
url_detail: '/address/:pk/',
api_endpoint: ApiPaths.address_list
},
contact: {
label: t`Contact`,
label_multiple: t`Contacts`,
url_overview: '/contact',
url_detail: '/contact/:pk/'
url_detail: '/contact/:pk/',
api_endpoint: ApiPaths.contact_list
},
owner: {
label: t`Owner`,
label_multiple: t`Owners`,
url_overview: '/owner',
url_detail: '/owner/:pk/'
url_detail: '/owner/:pk/',
api_endpoint: ApiPaths.owner_list
},
user: {
label: t`User`,
label_multiple: t`Users`,
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 { ReactNode } from 'react';
import { ModelType } from '../../enums/ModelType';
import { RenderInlineModel } from './Instance';
import { StatusRenderer } from './StatusRenderer';
/**
* Inline rendering of a single PurchaseOrder instance
@ -18,6 +20,10 @@ export function RenderPurchaseOrder({
<RenderInlineModel
primary={instance.reference}
secondary={instance.description}
suffix={StatusRenderer({
status: instance.status,
type: ModelType.purchaseorder
})}
image={supplier.thumnbnail || supplier.image}
/>
);
@ -33,6 +39,10 @@ export function RenderReturnOrder({ instance }: { instance: any }): ReactNode {
<RenderInlineModel
primary={instance.reference}
secondary={instance.description}
suffix={StatusRenderer({
status: instance.status,
type: ModelType.returnorder
})}
image={customer.thumnbnail || customer.image}
/>
);
@ -50,6 +60,10 @@ export function RenderSalesOrder({ instance }: { instance: any }): ReactNode {
<RenderInlineModel
primary={instance.reference}
secondary={instance.description}
suffix={StatusRenderer({
status: instance.status,
type: ModelType.salesorder
})}
image={customer.thumnbnail || customer.image}
/>
);

View File

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

View File

@ -1,3 +1,4 @@
import { t } from '@lingui/macro';
import { ReactNode } from 'react';
import { RenderInlineModel } from './Instance';
@ -19,10 +20,18 @@ export function RenderStockLocation({
}
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 (
<RenderInlineModel
primary={instance.part_detail?.full_name}
secondary={instance.quantity}
suffix={quantity_string}
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 { formatCurrency, renderDate } from '../../defaults/formatters';
import { ModelType } from '../../enums/ModelType';
import { ProgressBar } from '../items/ProgressBar';
import { YesNoButton } from '../items/YesNoButton';
import { ModelType } from '../render/ModelType';
import { TableStatusRenderer } from '../render/StatusRenderer';
import { RenderOwner } from '../render/User';
import { TableStatusRenderer } from '../renderers/StatusRenderer';
import { TableColumn } from './Column';
import { ProjectCodeHoverCard } from './TableHoverCard';

View File

@ -56,7 +56,11 @@ export function ProjectCodeHoverCard({ projectCode }: { projectCode: any }) {
<TableHoverCard
value={projectCode?.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 { useNavigate } from 'react-router-dom';
import { ApiPaths } from '../../../enums/ApiEndpoints';
import { UserRoles } from '../../../enums/Roles';
import { bomItemFields } from '../../../forms/BomForms';
import { openDeleteApiForm, openEditApiForm } from '../../../functions/forms';
import { useTableRefresh } from '../../../hooks/TableRefresh';
import { ApiPaths, apiUrl } from '../../../states/ApiState';
import { UserRoles, useUserState } from '../../../states/UserState';
import { apiUrl } from '../../../states/ApiState';
import { useUserState } from '../../../states/UserState';
import { Thumbnail } from '../../images/Thumbnail';
import { YesNoButton } from '../../items/YesNoButton';
import { TableColumn } from '../Column';
@ -64,7 +66,9 @@ export function BomTable({
let extra = [];
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 (

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,8 @@
import { t } from '@lingui/macro';
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 { InvenTreeTable } from '../InvenTreeTable';
import { RowAction } from '../RowActions';

View File

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

View File

@ -2,14 +2,16 @@ import { t } from '@lingui/macro';
import { Group, Text } from '@mantine/core';
import { useCallback, useMemo } from 'react';
import { ApiPaths } from '../../../enums/ApiEndpoints';
import { UserRoles } from '../../../enums/Roles';
import {
openCreateApiForm,
openDeleteApiForm,
openEditApiForm
} from '../../../functions/forms';
import { useTableRefresh } from '../../../hooks/TableRefresh';
import { ApiPaths, apiUrl } from '../../../states/ApiState';
import { UserRoles, useUserState } from '../../../states/UserState';
import { apiUrl } from '../../../states/ApiState';
import { useUserState } from '../../../states/UserState';
import { AddItemButton } from '../../buttons/AddItemButton';
import { Thumbnail } from '../../images/Thumbnail';
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
actions.push(
<AddItemButton tooltip="Add parameter" onClick={addParameter} />
<AddItemButton tooltip={t`Add parameter`} onClick={addParameter} />
);
return actions;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,10 +4,11 @@ import { ReactNode, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { formatCurrency, renderDate } from '../../../defaults/formatters';
import { ApiPaths } from '../../../enums/ApiEndpoints';
import { ModelType } from '../../../enums/ModelType';
import { useTableRefresh } from '../../../hooks/TableRefresh';
import { ApiPaths, apiUrl } from '../../../states/ApiState';
import { apiUrl } from '../../../states/ApiState';
import { Thumbnail } from '../../images/Thumbnail';
import { ModelType } from '../../render/ModelType';
import { TableColumn } from '../Column';
import { StatusColumn } from '../ColumnRenderers';
import { TableFilter } from '../Filter';
@ -65,49 +66,77 @@ function stockItemTableColumns(): TableColumn[] {
if (record.is_building) {
color = 'blue';
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) {
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) {
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) {
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) {
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) {
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) {
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 >= quantity) {
color = 'orange';
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 {
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 > 0) {
extra.push(
<Text size="sm" color="orange">
<Text key="available" size="sm" color="orange">
{t`Available` + `: ${available}`}
</Text>
);
} else {
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) {
color = 'red';
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 { useNavigate } from 'react-router-dom';
import { ApiPaths } from '../../../enums/ApiEndpoints';
import { useTableRefresh } from '../../../hooks/TableRefresh';
import { ApiPaths, apiUrl } from '../../../states/ApiState';
import { apiUrl } from '../../../states/ApiState';
import { YesNoButton } from '../../items/YesNoButton';
import { TableColumn } from '../Column';
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 */

View File

@ -1,6 +1,6 @@
import { t } from '@lingui/macro';
import { ApiPaths } from '../states/ApiState';
import { ApiPaths } from '../enums/ApiEndpoints';
interface DashboardItems {
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 { ApiFormFieldSet } from '../components/forms/fields/ApiFormField';
import { ApiPaths } from '../enums/ApiEndpoints';
import {
openCreateApiForm,
openDeleteApiForm,
openEditApiForm
} from '../functions/forms';
import { ApiPaths } from '../states/ApiState';
export function attachmentFields(editing: boolean): ApiFormFieldSet {
let fields: ApiFormFieldSet = {

View File

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

View File

@ -1,8 +1,8 @@
import { t } from '@lingui/macro';
import { ApiFormFieldSet } from '../components/forms/fields/ApiFormField';
import { ApiPaths } from '../enums/ApiEndpoints';
import { openCreateApiForm, openEditApiForm } from '../functions/forms';
import { ApiPaths } from '../states/ApiState';
/**
* 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
*/
export function purchaseOrderLineItemFields({
supplierId
supplierId,
orderId,
create = false
}: {
supplierId?: number;
orderId?: number;
create?: boolean;
}) {
let fields: ApiFormFieldSet = {
order: {
filters: {
supplier_detail: true
}
},
value: orderId,
hidden: create != true || orderId != undefined
},
part: {
filters: {

View File

@ -5,8 +5,8 @@ import {
ApiFormData,
ApiFormFieldSet
} from '../components/forms/fields/ApiFormField';
import { ApiPaths } from '../enums/ApiEndpoints';
import { openCreateApiForm, openEditApiForm } from '../functions/forms';
import { ApiPaths } from '../states/ApiState';
/**
* 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 { 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 { useSessionState } from '../states/SessionState';
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 { 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

View File

@ -14,7 +14,8 @@ import { useNavigate, useSearchParams } from 'react-router-dom';
import { api } from '../../App';
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() {
const simpleForm = useForm({ initialValues: { password: '' } });

View File

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

View File

@ -47,37 +47,44 @@ import { api } from '../../App';
import { DocInfo } from '../../components/items/DocInfo';
import { StylishText } from '../../components/items/StylishText';
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 { IS_DEV_OR_DEMO } from '../../main';
import { ApiPaths, apiUrl } from '../../states/ApiState';
import { apiUrl } from '../../states/ApiState';
interface ScanItem {
id: string;
ref: string;
data: any;
instance?: any;
timestamp: Date;
source: string;
link?: string;
objectType?: RenderTypes;
objectPk?: string;
model?: ModelType;
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) {
return [RenderTypes.part, rd?.part.pk];
return [ModelType.part, rd?.part.pk];
} else if (rd?.stockitem) {
return [RenderTypes.stock_item, rd?.stockitem.pk];
return [ModelType.stockitem, rd?.stockitem.pk];
} else if (rd?.stocklocation) {
return [RenderTypes.stock_location, rd?.stocklocation.pk];
return [ModelType.stocklocation, rd?.stocklocation.pk];
} else if (rd?.supplierpart) {
return [RenderTypes.supplier_part, rd?.supplierpart.pk];
return [ModelType.supplierpart, rd?.supplierpart.pk];
} else if (rd?.purchaseorder) {
return [RenderTypes.purchase_order, rd?.purchaseorder.pk];
return [ModelType.purchaseorder, rd?.purchaseorder.pk];
} else if (rd?.salesorder) {
return [RenderTypes.sales_order, rd?.salesorder.pk];
return [ModelType.salesorder, rd?.salesorder.pk];
} else if (rd?.build) {
return [RenderTypes.build_order, rd?.build.pk];
return [ModelType.build, rd?.build.pk];
} else {
return [undefined, undefined];
}
@ -147,8 +154,27 @@ export default function Scan() {
item.link = response.data?.url;
const rsp = matchObject(response.data);
item.objectType = rsp[0];
item.objectPk = rsp[1];
item.model = rsp[0];
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);
})
@ -206,7 +232,7 @@ export default function Scan() {
...new Set(
selection
.map((id) => {
return history.find((item) => item.id === id)?.objectType;
return history.find((item) => item.id === id)?.model;
})
.filter((item) => item != undefined)
)
@ -384,13 +410,13 @@ function HistoryTable({
/>
</td>
<td>
{item.objectPk && item.objectType ? (
<Render type={item.objectType} pk={item.objectPk} />
{item.pk && item.model && item.instance ? (
<RenderInstance model={item.model} instance={item.instance} />
) : (
item.ref
)}
</td>
<td>{item.objectType}</td>
<td>{item.model}</td>
<td>{item.source}</td>
<td>{item.timestamp?.toString()}</td>
</tr>

View File

@ -5,7 +5,8 @@ import { useToggle } from '@mantine/hooks';
import { api } from '../../../../App';
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';
export function AccountDetailPanel() {

View File

@ -19,7 +19,8 @@ import { useEffect, useState } from 'react';
import { api, queryClient } from '../../../../App';
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() {
const [isSsoEnabled, setIsSsoEnabled] = useState<boolean>(false);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,9 +5,9 @@ import { useNavigate } from 'react-router-dom';
import { PageDetail } from '../../components/nav/PageDetail';
import { BuildOrderTable } from '../../components/tables/build/BuildOrderTable';
import { ApiPaths } from '../../enums/ApiEndpoints';
import { buildOrderFields } from '../../forms/BuildForms';
import { openCreateApiForm } from '../../functions/forms';
import { ApiPaths } from '../../states/ApiState';
/**
* 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 { StockItemTable } from '../../components/tables/stock/StockItemTable';
import { NotesEditor } from '../../components/widgets/MarkdownEditor';
import { ApiPaths } from '../../enums/ApiEndpoints';
import { UserRoles } from '../../enums/Roles';
import { editCompany } from '../../forms/CompanyForms';
import { useInstance } from '../../hooks/UseInstance';
import { ApiPaths, apiUrl } from '../../states/ApiState';
import { UserRoles, useUserState } from '../../states/UserState';
import { apiUrl } from '../../states/ApiState';
import { useUserState } from '../../states/UserState';
export type CompanyDetailProps = {
title: string;

View File

@ -14,8 +14,8 @@ import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
import { PartCategoryTree } from '../../components/nav/PartCategoryTree';
import { PartCategoryTable } from '../../components/tables/part/PartCategoryTable';
import { PartListTable } from '../../components/tables/part/PartTable';
import { ApiPaths } from '../../enums/ApiEndpoints';
import { useInstance } from '../../hooks/UseInstance';
import { ApiPaths } from '../../states/ApiState';
/**
* 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 { StockItemTable } from '../../components/tables/stock/StockItemTable';
import { NotesEditor } from '../../components/widgets/MarkdownEditor';
import { ApiPaths } from '../../enums/ApiEndpoints';
import { editPart } from '../../forms/PartForms';
import { useInstance } from '../../hooks/UseInstance';
import { ApiPaths, apiUrl } from '../../states/ApiState';
import { apiUrl } from '../../states/ApiState';
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 { StockItemTable } from '../../components/tables/stock/StockItemTable';
import { NotesEditor } from '../../components/widgets/MarkdownEditor';
import { ApiPaths } from '../../enums/ApiEndpoints';
import { useInstance } from '../../hooks/UseInstance';
import { ApiPaths, apiUrl } from '../../states/ApiState';
import { apiUrl } from '../../states/ApiState';
import { useUserState } from '../../states/UserState';
/**
@ -114,7 +115,7 @@ export default function PurchaseOrderDetail() {
]}
/>,
<ActionDropdown
key="order"
key="order-actions"
tooltip={t`Order Actions`}
icon={<IconDots />}
actions={[EditItemAction({}), DeleteItemAction({})]}

View File

@ -8,8 +8,9 @@ import { PageDetail } from '../../components/nav/PageDetail';
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
import { AttachmentTable } from '../../components/tables/general/AttachmentTable';
import { NotesEditor } from '../../components/widgets/MarkdownEditor';
import { ApiPaths } from '../../enums/ApiEndpoints';
import { useInstance } from '../../hooks/UseInstance';
import { ApiPaths, apiUrl } from '../../states/ApiState';
import { apiUrl } from '../../states/ApiState';
/**
* 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 { AttachmentTable } from '../../components/tables/general/AttachmentTable';
import { NotesEditor } from '../../components/widgets/MarkdownEditor';
import { ApiPaths } from '../../enums/ApiEndpoints';
import { useInstance } from '../../hooks/UseInstance';
import { ApiPaths, apiUrl } from '../../states/ApiState';
import { apiUrl } from '../../states/ApiState';
/**
* 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 { StockItemTable } from '../../components/tables/stock/StockItemTable';
import { StockLocationTable } from '../../components/tables/stock/StockLocationTable';
import { ApiPaths } from '../../enums/ApiEndpoints';
import { useInstance } from '../../hooks/UseInstance';
import { ApiPaths } from '../../states/ApiState';
export default function Stock() {
const { id } = useParams();

View File

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

View File

@ -2,10 +2,11 @@ import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { api } from '../App';
import { ModelType } from '../components/render/ModelType';
import { StatusCodeListInterface } from '../components/renderers/StatusRenderer';
import { StatusCodeListInterface } from '../components/render/StatusRenderer';
import { statusCodeList } from '../defaults/backendMappings';
import { emptyServerAPI } from '../defaults/defaults';
import { ApiPaths } from '../enums/ApiEndpoints';
import { ModelType } from '../enums/ModelType';
import { ServerAPIProps } from './states';
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.
* For now it is fixed, but may be configurable in the future.
@ -144,6 +64,10 @@ export function apiEndpoint(path: ApiPaths): string {
switch (path) {
case ApiPaths.api_server_info:
return '';
case ApiPaths.user_list:
return 'user/';
case ApiPaths.owner_list:
return 'user/owner/';
case ApiPaths.user_me:
return 'user/me/';
case ApiPaths.user_roles:
@ -214,6 +138,10 @@ export function apiEndpoint(path: ApiPaths): string {
return 'part/attachment/';
case ApiPaths.company_list:
return 'company/';
case ApiPaths.contact_list:
return 'company/contact/';
case ApiPaths.address_list:
return 'company/address/';
case ApiPaths.company_attachment_list:
return 'company/attachment/';
case ApiPaths.supplier_part_list:
@ -240,6 +168,8 @@ export function apiEndpoint(path: ApiPaths): string {
return 'order/so/';
case ApiPaths.sales_order_attachment_list:
return 'order/so/attachment/';
case ApiPaths.sales_order_shipment_list:
return 'order/so/shipment/';
case ApiPaths.return_order_list:
return 'order/ro/';
case ApiPaths.return_order_attachment_list:

View File

@ -4,7 +4,9 @@
import { create } from 'zustand';
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';
export interface SettingsStateProps {
@ -12,6 +14,8 @@ export interface SettingsStateProps {
lookup: SettingsLookup;
fetchSettings: () => void;
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) => {
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) => {
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 { api } from '../App';
import { ApiPaths } from '../enums/ApiEndpoints';
import { UserPermissions, UserRoles } from '../enums/Roles';
import { doClassicLogout } from '../functions/auth';
import { ApiPaths, apiUrl } from './ApiState';
import { apiUrl } from './ApiState';
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 {
user: UserProps | undefined;
username: () => string;
@ -86,9 +62,9 @@ export const useUserState = create<UserStateProps>((set, get) => ({
const user: UserProps = get().user as UserProps;
// Update user with role data
user.roles = response.data.roles;
user.is_staff = response.data.is_staff ?? false;
user.is_superuser = response.data.is_superuser ?? false;
user.roles = response.data?.roles ?? {};
user.is_staff = response.data?.is_staff ?? false;
user.is_superuser = response.data?.is_superuser ?? false;
set({ user: user });
})
.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
const user: UserProps = get().user as UserProps;
if (user.is_superuser) return true;
if (user.roles === undefined) return false;
if (user.roles[role] === undefined) return false;
if (!user) {
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) => {
return get().checkUserRole(role, UserPermissions.delete);