mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-10-29 04:17:41 +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:
		| @@ -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; | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user