[React] Use typed paths (#5686)

* Use typed paths for all tables

* Refactor usage of useInstance hook

* Refactor URLs for existing forms

* More URL fixes

* Further URL fixes
This commit is contained in:
Oliver 2023-10-12 00:13:37 +11:00 committed by GitHub
parent 149e5c3696
commit bf7c1b43bd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 205 additions and 114 deletions

View File

@ -17,6 +17,7 @@ import { useState } from 'react';
import { api } from '../../App';
import { constructFormUrl } from '../../functions/forms';
import { invalidResponse } from '../../functions/notifications';
import { ApiPaths } from '../../states/ApiState';
import {
ApiFormField,
ApiFormFieldSet,
@ -45,8 +46,8 @@ import {
*/
export interface ApiFormProps {
name: string;
url: string;
pk?: number;
url: ApiPaths;
pk?: number | string;
title: string;
fields?: ApiFormFieldSet;
cancelText?: string;

View File

@ -41,9 +41,8 @@ export type ApiFormChangeCallback = {
* @param value : The value of the field
* @param default : The default value of the field
* @param icon : An icon to display next to the field
* @param fieldType : The type of field to render
* @param field_type : The type of field to render
* @param api_url : The API endpoint to fetch data from (for related fields)
* @param read_only : Whether the field is read-only
* @param model : The model to use for related fields
* @param filters : Optional API filters to apply to related fields
* @param required : Whether the field is required
@ -61,9 +60,8 @@ export type ApiFormFieldType = {
value?: any;
default?: any;
icon?: ReactNode;
fieldType?: string;
field_type?: string;
api_url?: string;
read_only?: boolean;
model?: ModelType;
filters?: any;
required?: boolean;
@ -99,8 +97,6 @@ export function constructField({
...field
};
def.disabled = def.disabled || def.read_only;
// Retrieve the latest value from the form
let value = form.values[fieldName];
@ -109,7 +105,7 @@ export function constructField({
}
// Change value to a date object if required
switch (def.fieldType) {
switch (def.field_type) {
case 'date':
if (def.value) {
def.value = new Date(def.value);
@ -192,9 +188,23 @@ export function ApiFormField({
const value: any = useMemo(() => form.values[fieldName], [form.values]);
// Coerce the value to a numerical value
const numericalValue: number | undefined = useMemo(() => {
switch (definition.field_type) {
case 'integer':
return parseInt(value);
case 'decimal':
case 'float':
case 'number':
return parseFloat(value);
default:
return undefined;
}
}, [value]);
// Construct the individual field
function buildField() {
switch (definition.fieldType) {
switch (definition.field_type) {
case 'related field':
return (
<RelatedModelField
@ -213,8 +223,8 @@ export function ApiFormField({
<TextInput
{...definition}
id={fieldId}
type={definition.fieldType}
value={value}
type={definition.field_type}
value={value || ''}
error={error}
radius="sm"
onChange={(event) => onChange(event.currentTarget.value)}
@ -260,7 +270,7 @@ export function ApiFormField({
{...definition}
radius="sm"
id={fieldId}
value={value}
value={numericalValue}
error={error}
onChange={(value: number) => onChange(value)}
/>
@ -289,7 +299,8 @@ export function ApiFormField({
default:
return (
<Alert color="red" title={t`Error`}>
Invalid field type for field '{fieldName}': '{definition.fieldType}'
Invalid field type for field '{fieldName}': '{definition.field_type}
'
</Alert>
);
}

View File

@ -80,6 +80,7 @@ export function ChoiceField({
data={choices}
value={value}
onChange={(value) => onChange(value)}
withinPortal={true}
/>
);
}

View File

@ -21,7 +21,7 @@ export const BuildOrderRenderer = ({ pk }: { pk: string }) => {
};
return (
<GeneralRenderer
api_key={ApiPaths.build_order_detail}
api_key={ApiPaths.build_order_list}
api_ref="build_order"
link={`/build/${pk}`}
pk={pk}

View File

@ -12,7 +12,7 @@ export const PartRenderer = ({
}) => {
return (
<GeneralRenderer
api_key={ApiPaths.part_detail}
api_key={ApiPaths.part_list}
api_ref="part"
link={link ? `/part/${pk}` : ''}
pk={pk}

View File

@ -16,7 +16,7 @@ export const PurchaseOrderRenderer = ({ pk }: { pk: string }) => {
};
return (
<GeneralRenderer
api_key={ApiPaths.purchase_order_detail}
api_key={ApiPaths.purchase_order_list}
api_ref="pruchaseorder"
link={`/order/purchase-order/${pk}`}
pk={pk}

View File

@ -4,7 +4,7 @@ import { GeneralRenderer } from './GeneralRenderer';
export const SalesOrderRenderer = ({ pk }: { pk: string }) => {
return (
<GeneralRenderer
api_key={ApiPaths.sales_order_detail}
api_key={ApiPaths.sales_order_list}
api_ref="sales_order"
link={`/order/so/${pk}`}
pk={pk}

View File

@ -17,7 +17,7 @@ export const StockItemRenderer = ({ pk }: { pk: string }) => {
};
return (
<GeneralRenderer
api_key={ApiPaths.stock_item_detail}
api_key={ApiPaths.stock_item_list}
api_ref="stockitem"
link={`/stock/item/${pk}`}
pk={pk}

View File

@ -4,7 +4,7 @@ import { GeneralRenderer } from './GeneralRenderer';
export const StockLocationRenderer = ({ pk }: { pk: string }) => {
return (
<GeneralRenderer
api_key={ApiPaths.stock_location_detail}
api_key={ApiPaths.stock_location_list}
api_ref="stock_location"
link={`/stock/location/${pk}`}
pk={pk}

View File

@ -23,7 +23,7 @@ export const SupplierPartRenderer = ({ pk }: { pk: string }) => {
};
return (
<GeneralRenderer
api_key={ApiPaths.supplier_part_detail}
api_key={ApiPaths.supplier_part_list}
api_ref="supplier_part"
link={`/supplier-part/${pk}`}
pk={pk}

View File

@ -2,7 +2,6 @@ import { t } from '@lingui/macro';
import { Badge, Group, Stack, Text, Tooltip } from '@mantine/core';
import { ActionIcon } from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import { useId } from '@mantine/hooks';
import { notifications } from '@mantine/notifications';
import { IconExternalLink, IconFileUpload } from '@tabler/icons-react';
import { ReactNode, useEffect, useMemo, useState } from 'react';
@ -14,6 +13,7 @@ import {
editAttachment
} from '../../functions/forms/AttachmentForms';
import { useTableRefresh } from '../../hooks/TableRefresh';
import { ApiPaths } from '../../states/ApiState';
import { AttachmentLink } from '../items/AttachmentLink';
import { TableColumn } from './Column';
import { InvenTreeTable } from './InvenTreeTable';
@ -77,7 +77,7 @@ export function AttachmentTable({
model,
pk
}: {
url: string;
url: ApiPaths;
pk: number;
model: string;
}): ReactNode {

View File

@ -24,7 +24,6 @@ const defaultPageSize: number = 25;
/**
* Set of optional properties which can be passed to an InvenTreeTable component
*
* @param url : string - The API endpoint to query
* @param params : any - Base query parameters
* @param tableKey : string - Unique key for the table (used for local storage)
* @param refreshId : string - Unique ID for the table (used to trigger a refresh)

View File

@ -4,7 +4,7 @@ import { useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { useTableRefresh } from '../../../hooks/TableRefresh';
import { ThumbnailHoverCard } from '../../items/Thumbnail';
import { ApiPaths, url } from '../../../states/ApiState';
import { TableColumn } from '../Column';
import { TableFilter } from '../Filter';
import { InvenTreeTable } from '../InvenTreeTable';
@ -142,7 +142,7 @@ export function BuildOrderTable({ params = {} }: { params?: any }) {
return (
<InvenTreeTable
url="build/"
url={url(ApiPaths.build_order_list)}
tableKey={tableKey}
columns={tableColumns}
props={{

View File

@ -1,6 +1,7 @@
import { t } from '@lingui/macro';
import { useMemo } from 'react';
import { ApiPaths, url } from '../../../states/ApiState';
import { TableColumn } from '../Column';
import { InvenTreeTable } from '../InvenTreeTable';
import { RowAction } from '../RowActions';
@ -39,7 +40,7 @@ export function NotificationTable({
return (
<InvenTreeTable
url="/notifications/"
url={url(ApiPaths.notifications_list)}
tableKey={tableKey}
columns={columns}
props={{

View File

@ -3,6 +3,7 @@ import { useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { useTableRefresh } from '../../../hooks/TableRefresh';
import { ApiPaths, url } from '../../../states/ApiState';
import { TableColumn } from '../Column';
import { InvenTreeTable } from '../InvenTreeTable';
@ -45,7 +46,7 @@ export function PartCategoryTable({ params = {} }: { params?: any }) {
return (
<InvenTreeTable
url="part/category/"
url={url(ApiPaths.category_list)}
tableKey={tableKey}
columns={tableColumns}
props={{

View File

@ -7,7 +7,7 @@ import { editPart } from '../../../functions/forms/PartForms';
import { notYetImplemented } from '../../../functions/notifications';
import { shortenString } from '../../../functions/tables';
import { useTableRefresh } from '../../../hooks/TableRefresh';
import { ThumbnailHoverCard } from '../../items/Thumbnail';
import { ApiPaths, url } from '../../../states/ApiState';
import { TableColumn } from '../Column';
import { TableFilter } from '../Filter';
import { InvenTreeTable, InvenTreeTableProps } from '../InvenTreeTable';
@ -221,7 +221,7 @@ export function PartListTable({ props }: { props: InvenTreeTableProps }) {
return (
<InvenTreeTable
url="part/"
url={url(ApiPaths.part_list)}
tableKey={tableKey}
columns={tableColumns}
props={{

View File

@ -6,6 +6,7 @@ import { useNavigate } from 'react-router-dom';
import { openCreateApiForm, openDeleteApiForm } from '../../../functions/forms';
import { useTableRefresh } from '../../../hooks/TableRefresh';
import { ApiPaths, url } from '../../../states/ApiState';
import { Thumbnail } from '../../items/Thumbnail';
import { TableColumn } from '../Column';
import { InvenTreeTable } from '../InvenTreeTable';
@ -59,7 +60,7 @@ export function RelatedPartTable({ partId }: { partId: number }): ReactNode {
openCreateApiForm({
name: 'add-related-part',
title: t`Add Related Part`,
url: '/part/related/',
url: ApiPaths.related_part_list,
fields: {
part_1: {
hidden: true,
@ -99,7 +100,7 @@ export function RelatedPartTable({ partId }: { partId: number }): ReactNode {
onClick: () => {
openDeleteApiForm({
name: 'delete-related-part',
url: '/part/related/',
url: ApiPaths.related_part_list,
pk: record.pk,
title: t`Delete Related Part`,
successMessage: t`Related part deleted`,
@ -115,13 +116,13 @@ export function RelatedPartTable({ partId }: { partId: number }): ReactNode {
return (
<InvenTreeTable
url="/part/related/"
url={url(ApiPaths.related_part_list)}
tableKey={tableKey}
columns={tableColumns}
props={{
params: {
part: partId,
catefory_detail: true
category_detail: true
},
rowActions: rowActions,
customActionGroups: customActions

View File

@ -5,6 +5,7 @@ import { useNavigate } from 'react-router-dom';
import { notYetImplemented } from '../../../functions/notifications';
import { useTableRefresh } from '../../../hooks/TableRefresh';
import { ApiPaths, url } from '../../../states/ApiState';
import { ThumbnailHoverCard } from '../../items/Thumbnail';
import { TableColumn } from '../Column';
import { TableFilter } from '../Filter';
@ -125,7 +126,7 @@ export function StockItemTable({ params = {} }: { params?: any }) {
return (
<InvenTreeTable
url="stock/"
url={url(ApiPaths.stock_item_list)}
tableKey={tableKey}
columns={tableColumns}
props={{

View File

@ -3,6 +3,7 @@ import { useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { useTableRefresh } from '../../../hooks/TableRefresh';
import { ApiPaths, url } from '../../../states/ApiState';
import { TableColumn } from '../Column';
import { InvenTreeTable } from '../InvenTreeTable';
@ -59,7 +60,7 @@ export function StockLocationTable({ params = {} }: { params?: any }) {
return (
<InvenTreeTable
url="stock/location/"
url={url(ApiPaths.stock_location_list)}
tableKey={tableKey}
columns={tableColumns}
props={{

View File

@ -6,6 +6,7 @@ import { AxiosResponse } from 'axios';
import { api } from '../App';
import { ApiForm, ApiFormProps } from '../components/forms/ApiForm';
import { ApiFormFieldType } from '../components/forms/fields/ApiFormField';
import { url } from '../states/ApiState';
import { invalidResponse, permissionDenied } from './notifications';
import { generateUniqueId } from './uid';
@ -13,17 +14,7 @@ import { generateUniqueId } from './uid';
* Construct an API url from the provided ApiFormProps object
*/
export function constructFormUrl(props: ApiFormProps): string {
let url = props.url;
if (!url.endsWith('/')) {
url += '/';
}
if (props.pk && props.pk > 0) {
url += `${props.pk}/`;
}
return url;
return url(props.url, props.pk);
}
/**
@ -76,7 +67,7 @@ export function extractAvailableFields(
fields[fieldName] = {
...field,
name: fieldName,
fieldType: field.type,
field_type: field.type,
description: field.help_text,
value: field.value ?? field.default
};

View File

@ -2,6 +2,7 @@ import { t } from '@lingui/macro';
import { Text } from '@mantine/core';
import { ApiFormFieldSet } from '../../components/forms/fields/ApiFormField';
import { ApiPaths } from '../../states/ApiState';
import {
openCreateApiForm,
openDeleteApiForm,
@ -31,7 +32,7 @@ export function addAttachment({
attachmentType,
callback
}: {
url: string;
url: ApiPaths;
model: string;
pk: number;
attachmentType: 'file' | 'link';
@ -77,7 +78,7 @@ export function editAttachment({
attachmentType,
callback
}: {
url: string;
url: ApiPaths;
model: string;
pk: number;
attachmentType: 'file' | 'link';
@ -116,7 +117,7 @@ export function deleteAttachment({
pk,
callback
}: {
url: string;
url: ApiPaths;
pk: number;
callback: () => void;
}) {

View File

@ -4,6 +4,7 @@ import {
ApiFormFieldSet,
ApiFormFieldType
} from '../../components/forms/fields/ApiFormField';
import { ApiPaths } from '../../states/ApiState';
import { openCreateApiForm, openEditApiForm } from '../forms';
/**
@ -74,7 +75,7 @@ export function createPart() {
openCreateApiForm({
name: 'part-create',
title: t`Create Part`,
url: '/part/',
url: ApiPaths.part_list,
successMessage: t`Part created`,
fields: partFields({})
});
@ -94,7 +95,7 @@ export function editPart({
openEditApiForm({
name: 'part-edit',
title: t`Edit Part`,
url: '/part/',
url: ApiPaths.part_list,
pk: part_id,
successMessage: t`Part updated`,
fields: partFields({ editing: true }),

View File

@ -6,6 +6,7 @@ import {
ApiFormFieldSet,
ApiFormFieldType
} from '../../components/forms/fields/ApiFormField';
import { ApiPaths } from '../../states/ApiState';
import { openCreateApiForm, openEditApiForm } from '../forms';
/**
@ -54,7 +55,7 @@ export function stockFields({}: {}): ApiFormFieldSet {
},
serial_numbers: {
// TODO: icon
fieldType: 'string',
field_type: 'string',
label: t`Serial Numbers`,
description: t`Enter serial numbers for new stock (or leave blank)`,
required: false
@ -99,7 +100,7 @@ export function stockFields({}: {}): ApiFormFieldSet {
export function createStockItem() {
openCreateApiForm({
name: 'stockitem-create',
url: '/stock/',
url: ApiPaths.stock_item_list,
fields: stockFields({}),
title: t`Create Stock Item`
});
@ -112,7 +113,7 @@ export function createStockItem() {
export function editStockItem(item: number) {
openEditApiForm({
name: 'stockitem-edit',
url: '/stock/',
url: ApiPaths.stock_item_list,
pk: item,
fields: stockFields({}),
title: t`Edit Stock Item`

View File

@ -2,6 +2,7 @@ import { useQuery } from '@tanstack/react-query';
import { useCallback, useState } from 'react';
import { api } from '../App';
import { ApiPaths, url } from '../states/ApiState';
/**
* Custom hook for loading a single instance of an instance from the API
@ -12,15 +13,19 @@ import { api } from '../App';
* To use this hook:
* const { instance, refreshInstance } = useInstance(url: string, pk: number)
*/
export function useInstance(
url: string,
pk: string | undefined,
params: any = {}
) {
export function useInstance({
endpoint,
pk,
params = {}
}: {
endpoint: ApiPaths;
pk: string | undefined;
params?: any;
}) {
const [instance, setInstance] = useState<any>({});
const instanceQuery = useQuery({
queryKey: ['instance', url, pk, params],
queryKey: ['instance', endpoint, pk, params],
queryFn: async () => {
if (pk == null || pk == undefined || pk.length == 0) {
setInstance({});
@ -28,7 +33,7 @@ export function useInstance(
}
return api
.get(url + pk + '/', {
.get(url(endpoint, pk), {
params: params
})
.then((response) => {

View File

@ -15,6 +15,7 @@ import {
partCategoryFields
} from '../../functions/forms/PartForms';
import { createStockItem } from '../../functions/forms/StockForms';
import { ApiPaths } from '../../states/ApiState';
// Generate some example forms using the modal API forms interface
function ApiFormsPlayground() {
@ -22,7 +23,7 @@ function ApiFormsPlayground() {
const editCategoryForm: ApiFormProps = {
name: 'partcategory',
url: '/part/category/',
url: ApiPaths.category_list,
pk: 2,
title: 'Edit Category',
fields: fields
@ -30,7 +31,7 @@ function ApiFormsPlayground() {
const createAttachmentForm: ApiFormProps = {
name: 'createattachment',
url: '/part/attachment/',
url: ApiPaths.part_attachment_list,
title: 'Create Attachment',
successMessage: 'Attachment uploaded',
fields: {

View File

@ -6,16 +6,13 @@ import {
IconInfoCircle,
IconList,
IconListCheck,
IconListTree,
IconNotes,
IconPaperclip,
IconSitemap
} from '@tabler/icons-react';
import { useQuery } from '@tanstack/react-query';
import { useEffect, useMemo, useState } from 'react';
import { useMemo } from 'react';
import { useParams } from 'react-router-dom';
import { api } from '../../App';
import {
PlaceholderPanel,
PlaceholderPill
@ -27,6 +24,7 @@ import { BuildOrderTable } from '../../components/tables/build/BuildOrderTable';
import { StockItemTable } from '../../components/tables/stock/StockItemTable';
import { NotesEditor } from '../../components/widgets/MarkdownEditor';
import { useInstance } from '../../hooks/UseInstance';
import { ApiPaths, url } from '../../states/ApiState';
/**
* Detail page for a single Build Order
@ -38,8 +36,12 @@ export default function BuildDetail() {
instance: build,
refreshInstance,
instanceQuery
} = useInstance('/build/', id, {
part_detail: true
} = useInstance({
endpoint: ApiPaths.build_order_list,
pk: id,
params: {
part_detail: true
}
});
const buildPanels: PanelType[] = useMemo(() => {
@ -107,7 +109,7 @@ export default function BuildDetail() {
icon: <IconPaperclip size="18" />,
content: (
<AttachmentTable
url="/build/attachment/"
url={ApiPaths.build_order_attachment_list}
model="build"
pk={build.pk ?? -1}
/>
@ -119,7 +121,7 @@ export default function BuildDetail() {
icon: <IconNotes size="18" />,
content: (
<NotesEditor
url={`/build/${build.pk}/`}
url={url(ApiPaths.build_order_list, build.pk)}
data={build.notes ?? ''}
allowEdit={true}
/>

View File

@ -16,6 +16,7 @@ import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
import { PartCategoryTable } from '../../components/tables/part/PartCategoryTable';
import { PartListTable } from '../../components/tables/part/PartTable';
import { useInstance } from '../../hooks/UseInstance';
import { ApiPaths } from '../../states/ApiState';
/**
* Detail view for a single PartCategory instance.
@ -29,7 +30,13 @@ export default function CategoryDetail({}: {}) {
instance: category,
refreshInstance,
instanceQuery
} = useInstance('/part/category/', id, { path_detail: true });
} = useInstance({
endpoint: ApiPaths.category_list,
pk: id,
params: {
path_detail: true
}
});
const categoryPanels: PanelType[] = useMemo(
() => [

View File

@ -29,6 +29,7 @@ import { StockItemTable } from '../../components/tables/stock/StockItemTable';
import { NotesEditor } from '../../components/widgets/MarkdownEditor';
import { editPart } from '../../functions/forms/PartForms';
import { useInstance } from '../../hooks/UseInstance';
import { ApiPaths, url } from '../../states/ApiState';
/**
* Detail view for a single Part instance
@ -40,7 +41,13 @@ export default function PartDetail() {
instance: part,
refreshInstance,
instanceQuery
} = useInstance('/part/', id, { path_detail: true });
} = useInstance({
endpoint: ApiPaths.part_list,
pk: id,
params: {
path_detail: true
}
});
// Part data panels (recalculate when part data changes)
const partPanels: PanelType[] = useMemo(() => {
@ -123,7 +130,7 @@ export default function PartDetail() {
name: 'related_parts',
label: t`Related Parts`,
icon: <IconLayersLinked size="18" />,
content: partRelatedTab()
content: <RelatedPartTable partId={part.pk ?? -1} />
},
{
name: 'attachments',
@ -131,7 +138,7 @@ export default function PartDetail() {
icon: <IconPaperclip size="18" />,
content: (
<AttachmentTable
url="/part/attachment/"
url={ApiPaths.part_attachment_list}
model="part"
pk={part.pk ?? -1}
/>
@ -146,14 +153,11 @@ export default function PartDetail() {
];
}, [part]);
function partRelatedTab(): React.ReactNode {
return <RelatedPartTable partId={part.pk ?? -1} />;
}
function partNotesTab(): React.ReactNode {
// TODO: Set edit permission based on user permissions
return (
<NotesEditor
url={`/part/${part.pk}/`}
url={url(ApiPaths.part_list, part.pk)}
data={part.notes ?? ''}
allowEdit={true}
/>

View File

@ -9,6 +9,7 @@ import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
import { StockItemTable } from '../../components/tables/stock/StockItemTable';
import { StockLocationTable } from '../../components/tables/stock/StockLocationTable';
import { useInstance } from '../../hooks/UseInstance';
import { ApiPaths } from '../../states/ApiState';
export default function Stock() {
const { id } = useParams();
@ -17,7 +18,13 @@ export default function Stock() {
instance: location,
refreshInstance,
instanceQuery
} = useInstance('/stock/location/', id, { path_detail: true });
} = useInstance({
endpoint: ApiPaths.stock_location_list,
pk: id,
params: {
path_detail: true
}
});
const locationPanels: PanelType[] = useMemo(() => {
return [

View File

@ -7,10 +7,9 @@ import {
IconInfoCircle,
IconNotes,
IconPaperclip,
IconSitemap,
IconTransferIn
IconSitemap
} from '@tabler/icons-react';
import { useEffect, useMemo } from 'react';
import { useMemo } from 'react';
import { useParams } from 'react-router-dom';
import { PlaceholderPanel } from '../../components/items/Placeholder';
@ -19,6 +18,7 @@ import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
import { AttachmentTable } from '../../components/tables/AttachmentTable';
import { NotesEditor } from '../../components/widgets/MarkdownEditor';
import { useInstance } from '../../hooks/UseInstance';
import { ApiPaths, url } from '../../states/ApiState';
export default function StockDetail() {
const { id } = useParams();
@ -27,10 +27,14 @@ export default function StockDetail() {
instance: stockitem,
refreshInstance,
instanceQuery
} = useInstance('/stock/', id, {
part_detail: true,
location_detail: true,
path_detail: true
} = useInstance({
endpoint: ApiPaths.stock_item_list,
pk: id,
params: {
part_detail: true,
location_detail: true,
path_detail: true
}
});
const stockPanels: PanelType[] = useMemo(() => {
@ -71,7 +75,7 @@ export default function StockDetail() {
icon: <IconPaperclip size="18" />,
content: (
<AttachmentTable
url="/stock/attachment/"
url={ApiPaths.stock_attachment_list}
model="stock_item"
pk={stockitem.pk ?? -1}
/>
@ -83,7 +87,7 @@ export default function StockDetail() {
icon: <IconNotes size="18" />,
content: (
<NotesEditor
url={`/stock/${stockitem.pk}/`}
url={url(ApiPaths.stock_item_list, stockitem.pk)}
data={stockitem.notes ?? ''}
allowEdit={true}
/>

View File

@ -47,6 +47,7 @@ export const useServerApiState = create<ServerApiStateProps>((set, get) => ({
}));
export enum ApiPaths {
// User information
user_me = 'api-user-me',
user_roles = 'api-user-roles',
user_token = 'api-user-token',
@ -54,17 +55,40 @@ export enum ApiPaths {
user_reset = 'api-user-reset',
user_reset_set = 'api-user-reset-set',
notifications_list = 'api-notifications-list',
barcode = 'api-barcode',
part_detail = 'api-part-detail',
supplier_part_detail = 'api-supplier-part-detail',
stock_item_detail = 'api-stock-item-detail',
stock_location_detail = 'api-stock-location-detail',
purchase_order_detail = 'api-purchase-order-detail',
sales_order_detail = 'api-sales-order-detail',
build_order_detail = 'api-build-order-detail'
// Build order URLs
build_order_list = 'api-build-list',
build_order_attachment_list = 'api-build-attachment-list',
// Part URLs
part_list = 'api-part-list',
category_list = 'api-category-list',
related_part_list = 'api-related-part-list',
part_attachment_list = 'api-part-attachment-list',
// Company URLs
company_list = 'api-company-list',
supplier_part_list = 'api-supplier-part-list',
// Stock Item URLs
stock_item_list = 'api-stock-item-list',
stock_location_list = 'api-stock-location-list',
stock_attachment_list = 'api-stock-attachment-list',
// Purchase Order URLs
purchase_order_list = 'api-purchase-order-list',
// Sales Order URLs
sales_order_list = 'api-sales-order-list'
}
export function url(path: ApiPaths, pk?: any): string {
/**
* Return the endpoint associated with a given API path
*/
export function endpoint(path: ApiPaths): string {
switch (path) {
case ApiPaths.user_me:
return 'user/me/';
@ -78,25 +102,51 @@ export function url(path: ApiPaths, pk?: any): string {
return '/auth/password/reset/';
case ApiPaths.user_reset_set:
return '/auth/password/reset/confirm/';
case ApiPaths.notifications_list:
return 'notifications/';
case ApiPaths.barcode:
return 'barcode/';
case ApiPaths.part_detail:
return `part/${pk}/`;
case ApiPaths.supplier_part_detail:
return `company/part/${pk}/`;
case ApiPaths.stock_item_detail:
return `stock/${pk}/`;
case ApiPaths.stock_location_detail:
return `stock/location/${pk}/`;
case ApiPaths.purchase_order_detail:
return `order/po/${pk}/`;
case ApiPaths.sales_order_detail:
return `order/so/${pk}/`;
case ApiPaths.build_order_detail:
return `build/${pk}/`;
case ApiPaths.build_order_list:
return 'build/';
case ApiPaths.build_order_attachment_list:
return 'build/attachment/';
case ApiPaths.part_list:
return 'part/';
case ApiPaths.category_list:
return 'part/category/';
case ApiPaths.related_part_list:
return 'part/related/';
case ApiPaths.part_attachment_list:
return 'part/attachment/';
case ApiPaths.company_list:
return 'company/';
case ApiPaths.supplier_part_list:
return 'company/part/';
case ApiPaths.stock_item_list:
return 'stock/';
case ApiPaths.stock_location_list:
return 'stock/location/';
case ApiPaths.stock_attachment_list:
return 'stock/attachment/';
case ApiPaths.purchase_order_list:
return 'order/po/';
case ApiPaths.sales_order_list:
return 'order/so/';
default:
return '';
}
}
/**
* Construct an API URL with an endpoint and (optional) pk value
*/
export function url(path: ApiPaths, pk?: any): string {
let _url = endpoint(path);
if (_url && pk) {
_url += `${pk}/`;
}
return _url;
}