mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
[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:
parent
149e5c3696
commit
bf7c1b43bd
@ -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;
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -80,6 +80,7 @@ export function ChoiceField({
|
||||
data={choices}
|
||||
value={value}
|
||||
onChange={(value) => onChange(value)}
|
||||
withinPortal={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -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={{
|
||||
|
@ -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={{
|
||||
|
@ -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={{
|
||||
|
@ -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={{
|
||||
|
@ -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
|
||||
|
@ -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={{
|
||||
|
@ -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={{
|
||||
|
@ -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
|
||||
};
|
||||
|
@ -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;
|
||||
}) {
|
||||
|
@ -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 }),
|
||||
|
@ -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`
|
||||
|
@ -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) => {
|
||||
|
@ -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: {
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -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(
|
||||
() => [
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -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 [
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -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;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user