mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-29 12:06:44 +00:00
[PUI] Part category page (#5555)
* Replace PartIndex with CategoryDetail - Can pass a category ID to show a single category - Otherwise, show the top-level parts category * Refactor <InvenTreeTable> component - Simplify property passing - Easier tableRefresh mechanism * Refetch table data when base parameters change * Correctly update pages when ID changes * Notification panel cleanup * Remove column from InvenTreeTableProps type * more fancy * Fix notification alert * Implement useLocalStorage hook * useLocalStorage hook for table filters too
This commit is contained in:
parent
41cbe30db1
commit
a68c1d28c6
@ -2,10 +2,16 @@ import { Text } from '@mantine/core';
|
|||||||
|
|
||||||
import { InvenTreeStyle } from '../../globalStyle';
|
import { InvenTreeStyle } from '../../globalStyle';
|
||||||
|
|
||||||
export function StylishText({ children }: { children: JSX.Element | string }) {
|
export function StylishText({
|
||||||
|
children,
|
||||||
|
size = 'md'
|
||||||
|
}: {
|
||||||
|
children: JSX.Element | string;
|
||||||
|
size?: string;
|
||||||
|
}) {
|
||||||
const { classes } = InvenTreeStyle();
|
const { classes } = InvenTreeStyle();
|
||||||
return (
|
return (
|
||||||
<Text className={classes.signText} variant="gradient">
|
<Text size={size} className={classes.signText} variant="gradient">
|
||||||
{children}
|
{children}
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
|
@ -62,7 +62,10 @@ export function Header() {
|
|||||||
<NavigationDrawer opened={navDrawerOpened} close={closeNavDrawer} />
|
<NavigationDrawer opened={navDrawerOpened} close={closeNavDrawer} />
|
||||||
<NotificationDrawer
|
<NotificationDrawer
|
||||||
opened={notificationDrawerOpened}
|
opened={notificationDrawerOpened}
|
||||||
onClose={closeNotificationDrawer}
|
onClose={() => {
|
||||||
|
notifications.refetch();
|
||||||
|
closeNotificationDrawer();
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<Container className={classes.layoutHeaderSection} size={'xl'}>
|
<Container className={classes.layoutHeaderSection} size={'xl'}>
|
||||||
<Group position="apart">
|
<Group position="apart">
|
||||||
|
@ -1,16 +1,17 @@
|
|||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import {
|
import {
|
||||||
ActionIcon,
|
ActionIcon,
|
||||||
|
Alert,
|
||||||
Divider,
|
Divider,
|
||||||
Drawer,
|
Drawer,
|
||||||
LoadingOverlay,
|
LoadingOverlay,
|
||||||
Space,
|
Space,
|
||||||
Tooltip
|
Tooltip
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { Badge, Group, Stack, Text } from '@mantine/core';
|
import { Group, Stack, Text } from '@mantine/core';
|
||||||
import { IconBellCheck, IconBellPlus, IconBookmark } from '@tabler/icons-react';
|
import { IconBellCheck, IconBellPlus } from '@tabler/icons-react';
|
||||||
import { IconMacro } from '@tabler/icons-react';
|
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
import { api } from '../../App';
|
import { api } from '../../App';
|
||||||
@ -79,6 +80,11 @@ export function NotificationDrawer({
|
|||||||
<Stack spacing="xs">
|
<Stack spacing="xs">
|
||||||
<Divider />
|
<Divider />
|
||||||
<LoadingOverlay visible={notificationQuery.isFetching} />
|
<LoadingOverlay visible={notificationQuery.isFetching} />
|
||||||
|
{notificationQuery.data?.results?.length == 0 && (
|
||||||
|
<Alert color="green">
|
||||||
|
<Text size="sm">{t`You have no unread notifications.`}</Text>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
{notificationQuery.data?.results.map((notification: any) => (
|
{notificationQuery.data?.results.map((notification: any) => (
|
||||||
<Group position="apart">
|
<Group position="apart">
|
||||||
<Stack spacing="3">
|
<Stack spacing="3">
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { Group, Paper, Space, Stack, Text } from '@mantine/core';
|
import { Group, Paper, Space, Stack, Text } from '@mantine/core';
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
|
|
||||||
|
import { StylishText } from '../items/StylishText';
|
||||||
import { Breadcrumb, BreadcrumbList } from './BreadcrumbList';
|
import { Breadcrumb, BreadcrumbList } from './BreadcrumbList';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -33,7 +34,7 @@ export function PageDetail({
|
|||||||
<Stack spacing="xs">
|
<Stack spacing="xs">
|
||||||
<Group position="apart">
|
<Group position="apart">
|
||||||
<Group position="left">
|
<Group position="left">
|
||||||
<Text size="xl">{title}</Text>
|
<StylishText size="xl">{title}</StylishText>
|
||||||
{subtitle && <Text size="lg">{subtitle}</Text>}
|
{subtitle && <Text size="lg">{subtitle}</Text>}
|
||||||
</Group>
|
</Group>
|
||||||
<Space />
|
<Space />
|
||||||
|
@ -81,9 +81,7 @@ export function AttachmentTable({
|
|||||||
pk: number;
|
pk: number;
|
||||||
model: string;
|
model: string;
|
||||||
}): ReactNode {
|
}): ReactNode {
|
||||||
const tableId = useId();
|
const { tableKey, refreshTable } = useTableRefresh(`${model}-attachments`);
|
||||||
|
|
||||||
const { refreshId, refreshTable } = useTableRefresh();
|
|
||||||
|
|
||||||
const tableColumns = useMemo(() => attachmentTableColumns(), []);
|
const tableColumns = useMemo(() => attachmentTableColumns(), []);
|
||||||
|
|
||||||
@ -224,14 +222,16 @@ export function AttachmentTable({
|
|||||||
<Stack spacing="xs">
|
<Stack spacing="xs">
|
||||||
<InvenTreeTable
|
<InvenTreeTable
|
||||||
url={url}
|
url={url}
|
||||||
tableKey={tableId}
|
tableKey={tableKey}
|
||||||
refreshId={refreshId}
|
|
||||||
params={{
|
|
||||||
[model]: pk
|
|
||||||
}}
|
|
||||||
customActionGroups={customActionGroups}
|
|
||||||
columns={tableColumns}
|
columns={tableColumns}
|
||||||
rowActions={allowEdit && allowDelete ? rowActions : undefined}
|
props={{
|
||||||
|
enableSelection: true,
|
||||||
|
customActionGroups: customActionGroups,
|
||||||
|
rowActions: allowEdit && allowDelete ? rowActions : undefined,
|
||||||
|
params: {
|
||||||
|
[model]: pk
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
{allowEdit && (
|
{allowEdit && (
|
||||||
<Dropzone onDrop={uploadFiles}>
|
<Dropzone onDrop={uploadFiles}>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { ActionIcon, Indicator, Space, Stack, Tooltip } from '@mantine/core';
|
import { ActionIcon, Indicator, Space, Stack, Tooltip } from '@mantine/core';
|
||||||
import { Group } from '@mantine/core';
|
import { Group } from '@mantine/core';
|
||||||
|
import { useLocalStorage } from '@mantine/hooks';
|
||||||
import { IconFilter, IconRefresh } from '@tabler/icons-react';
|
import { IconFilter, IconRefresh } from '@tabler/icons-react';
|
||||||
import { IconBarcode, IconPrinter } from '@tabler/icons-react';
|
import { IconBarcode, IconPrinter } from '@tabler/icons-react';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
@ -18,96 +19,33 @@ import { FilterSelectModal } from './FilterSelectModal';
|
|||||||
import { RowAction, RowActions } from './RowActions';
|
import { RowAction, RowActions } from './RowActions';
|
||||||
import { TableSearchInput } from './Search';
|
import { TableSearchInput } from './Search';
|
||||||
|
|
||||||
/*
|
const defaultPageSize: number = 25;
|
||||||
* Load list of hidden columns from local storage.
|
|
||||||
* Returns a list of column names which are "hidden" for the current table
|
|
||||||
*/
|
|
||||||
function loadHiddenColumns(tableKey: string) {
|
|
||||||
return JSON.parse(
|
|
||||||
localStorage.getItem(`inventree-hidden-table-columns-${tableKey}`) || '[]'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Write list of hidden columns to local storage
|
* Set of optional properties which can be passed to an InvenTreeTable component
|
||||||
* @param tableKey : string - unique key for the table
|
|
||||||
* @param columns : string[] - list of column names
|
|
||||||
*/
|
|
||||||
function saveHiddenColumns(tableKey: string, columns: any[]) {
|
|
||||||
localStorage.setItem(
|
|
||||||
`inventree-hidden-table-columns-${tableKey}`,
|
|
||||||
JSON.stringify(columns)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Loads the list of active filters from local storage
|
|
||||||
* @param tableKey : string - unique key for the table
|
|
||||||
* @param filterList : TableFilter[] - list of available filters
|
|
||||||
* @returns a map of active filters for the current table, {name: value}
|
|
||||||
*/
|
|
||||||
function loadActiveFilters(tableKey: string, filterList: TableFilter[]) {
|
|
||||||
let active = JSON.parse(
|
|
||||||
localStorage.getItem(`inventree-active-table-filters-${tableKey}`) || '{}'
|
|
||||||
);
|
|
||||||
|
|
||||||
// We expect that the active filter list is a map of {name: value}
|
|
||||||
// Return *only* those filters which are in the filter list
|
|
||||||
let x = filterList
|
|
||||||
.filter((f) => f.name in active)
|
|
||||||
.map((f) => ({
|
|
||||||
...f,
|
|
||||||
value: active[f.name]
|
|
||||||
}));
|
|
||||||
|
|
||||||
return x;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Write the list of active filters to local storage
|
|
||||||
* @param tableKey : string - unique key for the table
|
|
||||||
* @param filters : any - map of active filters, {name: value}
|
|
||||||
*/
|
|
||||||
function saveActiveFilters(tableKey: string, filters: TableFilter[]) {
|
|
||||||
let active = Object.fromEntries(filters.map((flt) => [flt.name, flt.value]));
|
|
||||||
|
|
||||||
localStorage.setItem(
|
|
||||||
`inventree-active-table-filters-${tableKey}`,
|
|
||||||
JSON.stringify(active)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Table Component which extends DataTable with custom InvenTree functionality
|
|
||||||
*
|
*
|
||||||
* TODO: Refactor table props into a single type
|
* @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)
|
||||||
|
* @param defaultSortColumn : string - Default column to sort by
|
||||||
|
* @param noRecordsText : string - Text to display when no records are found
|
||||||
|
* @param enableDownload : boolean - Enable download actions
|
||||||
|
* @param enableFilters : boolean - Enable filter actions
|
||||||
|
* @param enableSelection : boolean - Enable row selection
|
||||||
|
* @param enableSearch : boolean - Enable search actions
|
||||||
|
* @param enablePagination : boolean - Enable pagination
|
||||||
|
* @param enableRefresh : boolean - Enable refresh actions
|
||||||
|
* @param pageSize : number - Number of records per page
|
||||||
|
* @param barcodeActions : any[] - List of barcode actions
|
||||||
|
* @param customFilters : TableFilter[] - List of custom filters
|
||||||
|
* @param customActionGroups : any[] - List of custom action groups
|
||||||
|
* @param printingActions : any[] - List of printing actions
|
||||||
|
* @param rowActions : (record: any) => RowAction[] - Callback function to generate row actions
|
||||||
|
* @param onRowClick : (record: any, index: number, event: any) => void - Callback function when a row is clicked
|
||||||
*/
|
*/
|
||||||
export function InvenTreeTable({
|
export type InvenTreeTableProps = {
|
||||||
url,
|
params?: any;
|
||||||
params,
|
|
||||||
columns,
|
|
||||||
enableDownload = false,
|
|
||||||
enableFilters = true,
|
|
||||||
enablePagination = true,
|
|
||||||
enableRefresh = true,
|
|
||||||
enableSearch = true,
|
|
||||||
enableSelection = false,
|
|
||||||
pageSize = 25,
|
|
||||||
tableKey = '',
|
|
||||||
defaultSortColumn = '',
|
|
||||||
noRecordsText = t`No records found`,
|
|
||||||
printingActions = [],
|
|
||||||
barcodeActions = [],
|
|
||||||
customActionGroups = [],
|
|
||||||
customFilters = [],
|
|
||||||
rowActions,
|
|
||||||
onRowClick,
|
|
||||||
refreshId
|
|
||||||
}: {
|
|
||||||
url: string;
|
|
||||||
params: any;
|
|
||||||
columns: TableColumn[];
|
|
||||||
tableKey: string;
|
|
||||||
defaultSortColumn?: string;
|
defaultSortColumn?: string;
|
||||||
noRecordsText?: string;
|
noRecordsText?: string;
|
||||||
enableDownload?: boolean;
|
enableDownload?: boolean;
|
||||||
@ -117,23 +55,79 @@ export function InvenTreeTable({
|
|||||||
enablePagination?: boolean;
|
enablePagination?: boolean;
|
||||||
enableRefresh?: boolean;
|
enableRefresh?: boolean;
|
||||||
pageSize?: number;
|
pageSize?: number;
|
||||||
printingActions?: any[];
|
|
||||||
barcodeActions?: any[];
|
barcodeActions?: any[];
|
||||||
customActionGroups?: any[];
|
|
||||||
customFilters?: TableFilter[];
|
customFilters?: TableFilter[];
|
||||||
|
customActionGroups?: any[];
|
||||||
|
printingActions?: any[];
|
||||||
rowActions?: (record: any) => RowAction[];
|
rowActions?: (record: any) => RowAction[];
|
||||||
onRowClick?: (record: any, index: number, event: any) => void;
|
onRowClick?: (record: any, index: number, event: any) => void;
|
||||||
refreshId?: string;
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default table properties (used if not specified)
|
||||||
|
*/
|
||||||
|
const defaultInvenTreeTableProps: InvenTreeTableProps = {
|
||||||
|
params: {},
|
||||||
|
noRecordsText: t`No records found`,
|
||||||
|
enableDownload: false,
|
||||||
|
enableFilters: true,
|
||||||
|
enablePagination: true,
|
||||||
|
enableRefresh: true,
|
||||||
|
enableSearch: true,
|
||||||
|
enableSelection: false,
|
||||||
|
pageSize: defaultPageSize,
|
||||||
|
defaultSortColumn: '',
|
||||||
|
printingActions: [],
|
||||||
|
barcodeActions: [],
|
||||||
|
customFilters: [],
|
||||||
|
customActionGroups: [],
|
||||||
|
rowActions: (record: any) => [],
|
||||||
|
onRowClick: (record: any, index: number, event: any) => {}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Table Component which extends DataTable with custom InvenTree functionality
|
||||||
|
*/
|
||||||
|
export function InvenTreeTable({
|
||||||
|
url,
|
||||||
|
tableKey,
|
||||||
|
columns,
|
||||||
|
props
|
||||||
|
}: {
|
||||||
|
url: string;
|
||||||
|
tableKey: string;
|
||||||
|
columns: TableColumn[];
|
||||||
|
props: InvenTreeTableProps;
|
||||||
}) {
|
}) {
|
||||||
|
// Use the first part of the table key as the table name
|
||||||
|
const tableName: string = useMemo(() => {
|
||||||
|
return tableKey.split('-')[0];
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Build table properties based on provided props (and default props)
|
||||||
|
const tableProps: InvenTreeTableProps = useMemo(() => {
|
||||||
|
return {
|
||||||
|
...defaultInvenTreeTableProps,
|
||||||
|
...props
|
||||||
|
};
|
||||||
|
}, [props]);
|
||||||
|
|
||||||
// Check if any columns are switchable (can be hidden)
|
// Check if any columns are switchable (can be hidden)
|
||||||
const hasSwitchableColumns = columns.some(
|
const hasSwitchableColumns = columns.some(
|
||||||
(col: TableColumn) => col.switchable
|
(col: TableColumn) => col.switchable
|
||||||
);
|
);
|
||||||
|
|
||||||
// Manage state for switchable columns (initially load from local storage)
|
// A list of hidden columns, saved to local storage
|
||||||
let [hiddenColumns, setHiddenColumns] = useState(() =>
|
const [hiddenColumns, setHiddenColumns] = useLocalStorage<string[]>({
|
||||||
loadHiddenColumns(tableKey)
|
key: `inventree-hidden-table-columns-${tableName}`,
|
||||||
);
|
defaultValue: []
|
||||||
|
});
|
||||||
|
|
||||||
|
// Active filters (saved to local storage)
|
||||||
|
const [activeFilters, setActiveFilters] = useLocalStorage<any[]>({
|
||||||
|
key: `inventree-active-table-filters-${tableName}`,
|
||||||
|
defaultValue: []
|
||||||
|
});
|
||||||
|
|
||||||
// Data selection
|
// Data selection
|
||||||
const [selectedRecords, setSelectedRecords] = useState<any[]>([]);
|
const [selectedRecords, setSelectedRecords] = useState<any[]>([]);
|
||||||
@ -158,7 +152,7 @@ export function InvenTreeTable({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// If row actions are available, add a column for them
|
// If row actions are available, add a column for them
|
||||||
if (rowActions) {
|
if (tableProps.rowActions) {
|
||||||
cols.push({
|
cols.push({
|
||||||
accessor: 'actions',
|
accessor: 'actions',
|
||||||
title: '',
|
title: '',
|
||||||
@ -168,7 +162,7 @@ export function InvenTreeTable({
|
|||||||
render: function (record: any) {
|
render: function (record: any) {
|
||||||
return (
|
return (
|
||||||
<RowActions
|
<RowActions
|
||||||
actions={rowActions(record)}
|
actions={tableProps.rowActions?.(record) ?? []}
|
||||||
disabled={selectedRecords.length > 0}
|
disabled={selectedRecords.length > 0}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@ -177,7 +171,13 @@ export function InvenTreeTable({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return cols;
|
return cols;
|
||||||
}, [columns, hiddenColumns, rowActions, enableSelection, selectedRecords]);
|
}, [
|
||||||
|
columns,
|
||||||
|
hiddenColumns,
|
||||||
|
tableProps.rowActions,
|
||||||
|
tableProps.enableSelection,
|
||||||
|
selectedRecords
|
||||||
|
]);
|
||||||
|
|
||||||
// Callback when column visibility is toggled
|
// Callback when column visibility is toggled
|
||||||
function toggleColumn(columnName: string) {
|
function toggleColumn(columnName: string) {
|
||||||
@ -189,20 +189,11 @@ export function InvenTreeTable({
|
|||||||
newColumns[colIdx].hidden = !newColumns[colIdx].hidden;
|
newColumns[colIdx].hidden = !newColumns[colIdx].hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
let hiddenColumnNames = newColumns
|
setHiddenColumns(
|
||||||
.filter((col) => col.hidden)
|
newColumns.filter((col) => col.hidden).map((col) => col.accessor)
|
||||||
.map((col) => col.accessor);
|
);
|
||||||
|
|
||||||
// Save list of hidden columns to local storage
|
|
||||||
saveHiddenColumns(tableKey, hiddenColumnNames);
|
|
||||||
|
|
||||||
// Refresh state
|
|
||||||
setHiddenColumns(loadHiddenColumns(tableKey));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if custom filtering is enabled for this table
|
|
||||||
const hasCustomFilters = enableFilters && customFilters.length > 0;
|
|
||||||
|
|
||||||
// Filter selection open state
|
// Filter selection open state
|
||||||
const [filterSelectOpen, setFilterSelectOpen] = useState<boolean>(false);
|
const [filterSelectOpen, setFilterSelectOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
@ -212,11 +203,6 @@ export function InvenTreeTable({
|
|||||||
// Filter list visibility
|
// Filter list visibility
|
||||||
const [filtersVisible, setFiltersVisible] = useState<boolean>(false);
|
const [filtersVisible, setFiltersVisible] = useState<boolean>(false);
|
||||||
|
|
||||||
// Map of currently active filters, {name: value}
|
|
||||||
const [activeFilters, setActiveFilters] = useState(() =>
|
|
||||||
loadActiveFilters(tableKey, customFilters)
|
|
||||||
);
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Callback for the "add filter" button.
|
* Callback for the "add filter" button.
|
||||||
* Launches a modal dialog to add a new filter
|
* Launches a modal dialog to add a new filter
|
||||||
@ -224,7 +210,7 @@ export function InvenTreeTable({
|
|||||||
function onFilterAdd(name: string, value: string) {
|
function onFilterAdd(name: string, value: string) {
|
||||||
let filters = [...activeFilters];
|
let filters = [...activeFilters];
|
||||||
|
|
||||||
let newFilter = customFilters.find((flt) => flt.name == name);
|
let newFilter = tableProps.customFilters?.find((flt) => flt.name == name);
|
||||||
|
|
||||||
if (newFilter) {
|
if (newFilter) {
|
||||||
filters.push({
|
filters.push({
|
||||||
@ -232,7 +218,6 @@ export function InvenTreeTable({
|
|||||||
value: value
|
value: value
|
||||||
});
|
});
|
||||||
|
|
||||||
saveActiveFilters(tableKey, filters);
|
|
||||||
setActiveFilters(filters);
|
setActiveFilters(filters);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -242,7 +227,7 @@ export function InvenTreeTable({
|
|||||||
*/
|
*/
|
||||||
function onFilterRemove(filterName: string) {
|
function onFilterRemove(filterName: string) {
|
||||||
let filters = activeFilters.filter((flt) => flt.name != filterName);
|
let filters = activeFilters.filter((flt) => flt.name != filterName);
|
||||||
saveActiveFilters(tableKey, filters);
|
|
||||||
setActiveFilters(filters);
|
setActiveFilters(filters);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -250,7 +235,6 @@ export function InvenTreeTable({
|
|||||||
* Callback function when all custom filters are removed from the table
|
* Callback function when all custom filters are removed from the table
|
||||||
*/
|
*/
|
||||||
function onFilterClearAll() {
|
function onFilterClearAll() {
|
||||||
saveActiveFilters(tableKey, []);
|
|
||||||
setActiveFilters([]);
|
setActiveFilters([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -266,7 +250,9 @@ export function InvenTreeTable({
|
|||||||
* Construct query filters for the current table
|
* Construct query filters for the current table
|
||||||
*/
|
*/
|
||||||
function getTableFilters(paginate: boolean = false) {
|
function getTableFilters(paginate: boolean = false) {
|
||||||
let queryParams = { ...params };
|
let queryParams = {
|
||||||
|
...tableProps.params
|
||||||
|
};
|
||||||
|
|
||||||
// Add custom filters
|
// Add custom filters
|
||||||
activeFilters.forEach((flt) => (queryParams[flt.name] = flt.value));
|
activeFilters.forEach((flt) => (queryParams[flt.name] = flt.value));
|
||||||
@ -277,7 +263,8 @@ export function InvenTreeTable({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Pagination
|
// Pagination
|
||||||
if (enablePagination && paginate) {
|
if (tableProps.enablePagination && paginate) {
|
||||||
|
let pageSize = tableProps.pageSize ?? defaultPageSize;
|
||||||
queryParams.limit = pageSize;
|
queryParams.limit = pageSize;
|
||||||
queryParams.offset = (page - 1) * pageSize;
|
queryParams.offset = (page - 1) * pageSize;
|
||||||
}
|
}
|
||||||
@ -315,7 +302,7 @@ export function InvenTreeTable({
|
|||||||
|
|
||||||
// Data Sorting
|
// Data Sorting
|
||||||
const [sortStatus, setSortStatus] = useState<DataTableSortStatus>({
|
const [sortStatus, setSortStatus] = useState<DataTableSortStatus>({
|
||||||
columnAccessor: defaultSortColumn,
|
columnAccessor: tableProps.defaultSortColumn ?? '',
|
||||||
direction: 'asc'
|
direction: 'asc'
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -335,8 +322,9 @@ export function InvenTreeTable({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Missing records text (based on server response)
|
// Missing records text (based on server response)
|
||||||
const [missingRecordsText, setMissingRecordsText] =
|
const [missingRecordsText, setMissingRecordsText] = useState<string>(
|
||||||
useState<string>(noRecordsText);
|
tableProps.noRecordsText ?? t`No records found`
|
||||||
|
);
|
||||||
|
|
||||||
const handleSortStatusChange = (status: DataTableSortStatus) => {
|
const handleSortStatusChange = (status: DataTableSortStatus) => {
|
||||||
setPage(1);
|
setPage(1);
|
||||||
@ -355,7 +343,9 @@ export function InvenTreeTable({
|
|||||||
.then(function (response) {
|
.then(function (response) {
|
||||||
switch (response.status) {
|
switch (response.status) {
|
||||||
case 200:
|
case 200:
|
||||||
setMissingRecordsText(noRecordsText);
|
setMissingRecordsText(
|
||||||
|
tableProps.noRecordsText ?? t`No records found`
|
||||||
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
case 400:
|
case 400:
|
||||||
setMissingRecordsText(t`Bad request`);
|
setMissingRecordsText(t`Bad request`);
|
||||||
@ -386,7 +376,7 @@ export function InvenTreeTable({
|
|||||||
|
|
||||||
const { data, isError, isFetching, isLoading, refetch } = useQuery(
|
const { data, isError, isFetching, isLoading, refetch } = useQuery(
|
||||||
[
|
[
|
||||||
`table-${tableKey}`,
|
`table-${tableName}`,
|
||||||
sortStatus.columnAccessor,
|
sortStatus.columnAccessor,
|
||||||
sortStatus.direction,
|
sortStatus.direction,
|
||||||
page,
|
page,
|
||||||
@ -407,15 +397,13 @@ export function InvenTreeTable({
|
|||||||
* Implement this using the custom useTableRefresh hook
|
* Implement this using the custom useTableRefresh hook
|
||||||
*/
|
*/
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (refreshId) {
|
refetch();
|
||||||
refetch();
|
}, [tableKey, props.params]);
|
||||||
}
|
|
||||||
}, [refreshId]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<FilterSelectModal
|
<FilterSelectModal
|
||||||
availableFilters={customFilters}
|
availableFilters={tableProps.customFilters ?? []}
|
||||||
activeFilters={activeFilters}
|
activeFilters={activeFilters}
|
||||||
opened={filterSelectOpen}
|
opened={filterSelectOpen}
|
||||||
onCreateFilter={onFilterAdd}
|
onCreateFilter={onFilterAdd}
|
||||||
@ -424,35 +412,37 @@ export function InvenTreeTable({
|
|||||||
<Stack>
|
<Stack>
|
||||||
<Group position="apart">
|
<Group position="apart">
|
||||||
<Group position="left" spacing={5}>
|
<Group position="left" spacing={5}>
|
||||||
{customActionGroups.map((group: any, idx: number) => group)}
|
{tableProps.customActionGroups?.map(
|
||||||
{barcodeActions.length > 0 && (
|
(group: any, idx: number) => group
|
||||||
|
)}
|
||||||
|
{(tableProps.barcodeActions?.length ?? 0 > 0) && (
|
||||||
<ButtonMenu
|
<ButtonMenu
|
||||||
icon={<IconBarcode />}
|
icon={<IconBarcode />}
|
||||||
label={t`Barcode actions`}
|
label={t`Barcode actions`}
|
||||||
tooltip={t`Barcode actions`}
|
tooltip={t`Barcode actions`}
|
||||||
actions={barcodeActions}
|
actions={tableProps.barcodeActions ?? []}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{printingActions.length > 0 && (
|
{(tableProps.printingActions?.length ?? 0 > 0) && (
|
||||||
<ButtonMenu
|
<ButtonMenu
|
||||||
icon={<IconPrinter />}
|
icon={<IconPrinter />}
|
||||||
label={t`Print actions`}
|
label={t`Print actions`}
|
||||||
tooltip={t`Print actions`}
|
tooltip={t`Print actions`}
|
||||||
actions={printingActions}
|
actions={tableProps.printingActions ?? []}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{enableDownload && (
|
{tableProps.enableDownload && (
|
||||||
<DownloadAction downloadCallback={downloadData} />
|
<DownloadAction downloadCallback={downloadData} />
|
||||||
)}
|
)}
|
||||||
</Group>
|
</Group>
|
||||||
<Space />
|
<Space />
|
||||||
<Group position="right" spacing={5}>
|
<Group position="right" spacing={5}>
|
||||||
{enableSearch && (
|
{tableProps.enableSearch && (
|
||||||
<TableSearchInput
|
<TableSearchInput
|
||||||
searchCallback={(term: string) => setSearchTerm(term)}
|
searchCallback={(term: string) => setSearchTerm(term)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{enableRefresh && (
|
{tableProps.enableRefresh && (
|
||||||
<ActionIcon>
|
<ActionIcon>
|
||||||
<Tooltip label={t`Refresh data`}>
|
<Tooltip label={t`Refresh data`}>
|
||||||
<IconRefresh onClick={() => refetch()} />
|
<IconRefresh onClick={() => refetch()} />
|
||||||
@ -465,21 +455,22 @@ export function InvenTreeTable({
|
|||||||
onToggleColumn={toggleColumn}
|
onToggleColumn={toggleColumn}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{hasCustomFilters && (
|
{tableProps.enableFilters &&
|
||||||
<Indicator
|
(tableProps.customFilters?.length ?? 0 > 0) && (
|
||||||
size="xs"
|
<Indicator
|
||||||
label={activeFilters.length}
|
size="xs"
|
||||||
disabled={activeFilters.length == 0}
|
label={activeFilters.length}
|
||||||
>
|
disabled={activeFilters.length == 0}
|
||||||
<ActionIcon>
|
>
|
||||||
<Tooltip label={t`Table filters`}>
|
<ActionIcon>
|
||||||
<IconFilter
|
<Tooltip label={t`Table filters`}>
|
||||||
onClick={() => setFiltersVisible(!filtersVisible)}
|
<IconFilter
|
||||||
/>
|
onClick={() => setFiltersVisible(!filtersVisible)}
|
||||||
</Tooltip>
|
/>
|
||||||
</ActionIcon>
|
</Tooltip>
|
||||||
</Indicator>
|
</ActionIcon>
|
||||||
)}
|
</Indicator>
|
||||||
|
)}
|
||||||
</Group>
|
</Group>
|
||||||
</Group>
|
</Group>
|
||||||
{filtersVisible && (
|
{filtersVisible && (
|
||||||
@ -498,20 +489,22 @@ export function InvenTreeTable({
|
|||||||
idAccessor={'pk'}
|
idAccessor={'pk'}
|
||||||
minHeight={200}
|
minHeight={200}
|
||||||
totalRecords={data?.count ?? data?.length ?? 0}
|
totalRecords={data?.count ?? data?.length ?? 0}
|
||||||
recordsPerPage={pageSize}
|
recordsPerPage={tableProps.pageSize ?? defaultPageSize}
|
||||||
page={page}
|
page={page}
|
||||||
onPageChange={setPage}
|
onPageChange={setPage}
|
||||||
sortStatus={sortStatus}
|
sortStatus={sortStatus}
|
||||||
onSortStatusChange={handleSortStatusChange}
|
onSortStatusChange={handleSortStatusChange}
|
||||||
selectedRecords={enableSelection ? selectedRecords : undefined}
|
selectedRecords={
|
||||||
|
tableProps.enableSelection ? selectedRecords : undefined
|
||||||
|
}
|
||||||
onSelectedRecordsChange={
|
onSelectedRecordsChange={
|
||||||
enableSelection ? onSelectedRecordsChange : undefined
|
tableProps.enableSelection ? onSelectedRecordsChange : undefined
|
||||||
}
|
}
|
||||||
fetching={isFetching}
|
fetching={isFetching}
|
||||||
noRecordsText={missingRecordsText}
|
noRecordsText={missingRecordsText}
|
||||||
records={data?.results ?? data ?? []}
|
records={data?.results ?? data ?? []}
|
||||||
columns={dataColumns}
|
columns={dataColumns}
|
||||||
onRowClick={onRowClick}
|
onRowClick={tableProps.onRowClick}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
</>
|
</>
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { Progress } from '@mantine/core';
|
import { Progress, Text } from '@mantine/core';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { useTableRefresh } from '../../../hooks/TableRefresh';
|
||||||
import { ThumbnailHoverCard } from '../../items/Thumbnail';
|
import { ThumbnailHoverCard } from '../../items/Thumbnail';
|
||||||
import { TableColumn } from '../Column';
|
import { TableColumn } from '../Column';
|
||||||
import { TableFilter } from '../Filter';
|
import { TableFilter } from '../Filter';
|
||||||
@ -27,11 +28,12 @@ function buildOrderTableColumns(): TableColumn[] {
|
|||||||
let part = record.part_detail;
|
let part = record.part_detail;
|
||||||
return (
|
return (
|
||||||
part && (
|
part && (
|
||||||
<ThumbnailHoverCard
|
<Text>{part.full_name}</Text>
|
||||||
src={part.thumbnail || part.image}
|
// <ThumbnailHoverCard
|
||||||
text={part.full_name}
|
// src={part.thumbnail || part.image}
|
||||||
link=""
|
// text={part.full_name}
|
||||||
/>
|
// link=""
|
||||||
|
// />
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -127,35 +129,31 @@ function buildOrderTableFilters(): TableFilter[] {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildOrderTableParams(params: any): any {
|
|
||||||
return {
|
|
||||||
...params,
|
|
||||||
part_detail: true
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Construct a table of build orders, according to the provided parameters
|
* Construct a table of build orders, according to the provided parameters
|
||||||
*/
|
*/
|
||||||
export function BuildOrderTable({ params = {} }: { params?: any }) {
|
export function BuildOrderTable({ params = {} }: { params?: any }) {
|
||||||
// Add required query parameters
|
|
||||||
const tableParams = useMemo(() => buildOrderTableParams(params), [params]);
|
|
||||||
const tableColumns = useMemo(() => buildOrderTableColumns(), []);
|
const tableColumns = useMemo(() => buildOrderTableColumns(), []);
|
||||||
const tableFilters = useMemo(() => buildOrderTableFilters(), []);
|
const tableFilters = useMemo(() => buildOrderTableFilters(), []);
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
tableParams.part_detail = true;
|
const { tableKey, refreshTable } = useTableRefresh('buildorder');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InvenTreeTable
|
<InvenTreeTable
|
||||||
url="build/"
|
url="build/"
|
||||||
enableDownload
|
tableKey={tableKey}
|
||||||
tableKey="build-order-table"
|
|
||||||
params={tableParams}
|
|
||||||
columns={tableColumns}
|
columns={tableColumns}
|
||||||
customFilters={tableFilters}
|
props={{
|
||||||
onRowClick={(row) => navigate(`/build/${row.pk}`)}
|
enableDownload: true,
|
||||||
|
params: {
|
||||||
|
...params,
|
||||||
|
part_detail: true
|
||||||
|
},
|
||||||
|
customFilters: tableFilters,
|
||||||
|
onRowClick: (row) => navigate(`/build/${row.pk}`)
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -7,12 +7,10 @@ import { RowAction } from '../RowActions';
|
|||||||
|
|
||||||
export function NotificationTable({
|
export function NotificationTable({
|
||||||
params,
|
params,
|
||||||
refreshId,
|
|
||||||
tableKey,
|
tableKey,
|
||||||
actions
|
actions
|
||||||
}: {
|
}: {
|
||||||
params: any;
|
params: any;
|
||||||
refreshId: string;
|
|
||||||
tableKey: string;
|
tableKey: string;
|
||||||
actions: (record: any) => RowAction[];
|
actions: (record: any) => RowAction[];
|
||||||
}) {
|
}) {
|
||||||
@ -43,10 +41,12 @@ export function NotificationTable({
|
|||||||
<InvenTreeTable
|
<InvenTreeTable
|
||||||
url="/notifications/"
|
url="/notifications/"
|
||||||
tableKey={tableKey}
|
tableKey={tableKey}
|
||||||
refreshId={refreshId}
|
|
||||||
params={params}
|
|
||||||
rowActions={actions}
|
|
||||||
columns={columns}
|
columns={columns}
|
||||||
|
props={{
|
||||||
|
rowActions: actions,
|
||||||
|
enableSelection: true,
|
||||||
|
params: params
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,63 @@
|
|||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { useTableRefresh } from '../../../hooks/TableRefresh';
|
||||||
|
import { TableColumn } from '../Column';
|
||||||
|
import { InvenTreeTable } from '../InvenTreeTable';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PartCategoryTable - Displays a table of part categories
|
||||||
|
*/
|
||||||
|
export function PartCategoryTable({ params = {} }: { params?: any }) {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const { tableKey, refreshTable } = useTableRefresh('partcategory');
|
||||||
|
|
||||||
|
const tableColumns: TableColumn[] = useMemo(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
accessor: 'name',
|
||||||
|
title: t`Name`,
|
||||||
|
sortable: true,
|
||||||
|
switchable: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessor: 'description',
|
||||||
|
title: t`Description`,
|
||||||
|
sortable: false,
|
||||||
|
switchable: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessor: 'pathstring',
|
||||||
|
title: t`Path`,
|
||||||
|
sortable: false,
|
||||||
|
switchable: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessor: 'part_count',
|
||||||
|
title: t`Parts`,
|
||||||
|
sortable: true,
|
||||||
|
switchable: true
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<InvenTreeTable
|
||||||
|
url="part/category/"
|
||||||
|
tableKey={tableKey}
|
||||||
|
columns={tableColumns}
|
||||||
|
props={{
|
||||||
|
enableDownload: true,
|
||||||
|
enableSelection: true,
|
||||||
|
params: {
|
||||||
|
...params
|
||||||
|
},
|
||||||
|
onRowClick: (record, index, event) => {
|
||||||
|
navigate(`/part/category/${record.pk}`);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
@ -1,16 +1,16 @@
|
|||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { Text } from '@mantine/core';
|
import { Text } from '@mantine/core';
|
||||||
import { IconEdit, IconTrash } from '@tabler/icons-react';
|
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
import { editPart } from '../../../functions/forms/PartForms';
|
import { editPart } from '../../../functions/forms/PartForms';
|
||||||
import { notYetImplemented } from '../../../functions/notifications';
|
import { notYetImplemented } from '../../../functions/notifications';
|
||||||
import { shortenString } from '../../../functions/tables';
|
import { shortenString } from '../../../functions/tables';
|
||||||
|
import { useTableRefresh } from '../../../hooks/TableRefresh';
|
||||||
import { ThumbnailHoverCard } from '../../items/Thumbnail';
|
import { ThumbnailHoverCard } from '../../items/Thumbnail';
|
||||||
import { TableColumn } from '../Column';
|
import { TableColumn } from '../Column';
|
||||||
import { TableFilter } from '../Filter';
|
import { TableFilter } from '../Filter';
|
||||||
import { InvenTreeTable } from '../InvenTreeTable';
|
import { InvenTreeTable, InvenTreeTableProps } from '../InvenTreeTable';
|
||||||
import { RowAction } from '../RowActions';
|
import { RowAction } from '../RowActions';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -26,11 +26,12 @@ function partTableColumns(): TableColumn[] {
|
|||||||
render: function (record: any) {
|
render: function (record: any) {
|
||||||
// TODO - Link to the part detail page
|
// TODO - Link to the part detail page
|
||||||
return (
|
return (
|
||||||
<ThumbnailHoverCard
|
<Text>{record.full_name}</Text>
|
||||||
src={record.thumbnail || record.image}
|
// <ThumbnailHoverCard
|
||||||
text={record.name}
|
// src={record.thumbnail || record.image}
|
||||||
link=""
|
// text={record.name}
|
||||||
/>
|
// link=""
|
||||||
|
// />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -178,23 +179,17 @@ function partTableFilters(): TableFilter[] {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
function partTableParams(params: any): any {
|
|
||||||
return {
|
|
||||||
...params,
|
|
||||||
category_detail: true
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PartListTable - Displays a list of parts, based on the provided parameters
|
* PartListTable - Displays a list of parts, based on the provided parameters
|
||||||
* @param {Object} params - The query parameters to pass to the API
|
* @param {Object} params - The query parameters to pass to the API
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export function PartListTable({ params = {} }: { params?: any }) {
|
export function PartListTable({ props }: { props: InvenTreeTableProps }) {
|
||||||
let tableParams = useMemo(() => partTableParams(params), [params]);
|
|
||||||
let tableColumns = useMemo(() => partTableColumns(), []);
|
let tableColumns = useMemo(() => partTableColumns(), []);
|
||||||
let tableFilters = useMemo(() => partTableFilters(), []);
|
let tableFilters = useMemo(() => partTableFilters(), []);
|
||||||
|
|
||||||
|
const { tableKey, refreshTable } = useTableRefresh('part');
|
||||||
|
|
||||||
// Callback function for generating set of row actions
|
// Callback function for generating set of row actions
|
||||||
function partTableRowActions(record: any): RowAction[] {
|
function partTableRowActions(record: any): RowAction[] {
|
||||||
let actions: RowAction[] = [];
|
let actions: RowAction[] = [];
|
||||||
@ -227,16 +222,18 @@ export function PartListTable({ params = {} }: { params?: any }) {
|
|||||||
return (
|
return (
|
||||||
<InvenTreeTable
|
<InvenTreeTable
|
||||||
url="part/"
|
url="part/"
|
||||||
enableDownload
|
tableKey={tableKey}
|
||||||
tableKey="part-table"
|
|
||||||
printingActions={[
|
|
||||||
<Text onClick={notYetImplemented}>Hello</Text>,
|
|
||||||
<Text onClick={notYetImplemented}>World</Text>
|
|
||||||
]}
|
|
||||||
params={tableParams}
|
|
||||||
columns={tableColumns}
|
columns={tableColumns}
|
||||||
customFilters={tableFilters}
|
props={{
|
||||||
rowActions={partTableRowActions}
|
...props,
|
||||||
|
enableDownload: true,
|
||||||
|
customFilters: tableFilters,
|
||||||
|
rowActions: partTableRowActions,
|
||||||
|
params: {
|
||||||
|
...props.params,
|
||||||
|
category_detail: true
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,7 @@ import { TableColumn } from '../Column';
|
|||||||
import { InvenTreeTable } from '../InvenTreeTable';
|
import { InvenTreeTable } from '../InvenTreeTable';
|
||||||
|
|
||||||
export function RelatedPartTable({ partId }: { partId: number }): ReactNode {
|
export function RelatedPartTable({ partId }: { partId: number }): ReactNode {
|
||||||
const { refreshId, refreshTable } = useTableRefresh();
|
const { tableKey, refreshTable } = useTableRefresh('relatedparts');
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
@ -116,14 +116,16 @@ export function RelatedPartTable({ partId }: { partId: number }): ReactNode {
|
|||||||
return (
|
return (
|
||||||
<InvenTreeTable
|
<InvenTreeTable
|
||||||
url="/part/related/"
|
url="/part/related/"
|
||||||
tableKey="related-part-table"
|
tableKey={tableKey}
|
||||||
refreshId={refreshId}
|
|
||||||
params={{
|
|
||||||
part: partId
|
|
||||||
}}
|
|
||||||
rowActions={rowActions}
|
|
||||||
columns={tableColumns}
|
columns={tableColumns}
|
||||||
customActionGroups={customActions}
|
props={{
|
||||||
|
params: {
|
||||||
|
part: partId,
|
||||||
|
catefory_detail: true
|
||||||
|
},
|
||||||
|
rowActions: rowActions,
|
||||||
|
customActionGroups: customActions
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { Group } from '@mantine/core';
|
import { Text } from '@mantine/core';
|
||||||
import { IconEdit, IconTrash } from '@tabler/icons-react';
|
import { useMemo } from 'react';
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
|
||||||
|
|
||||||
import { notYetImplemented } from '../../../functions/notifications';
|
import { notYetImplemented } from '../../../functions/notifications';
|
||||||
import { ActionButton } from '../../items/ActionButton';
|
import { useTableRefresh } from '../../../hooks/TableRefresh';
|
||||||
import { ThumbnailHoverCard } from '../../items/Thumbnail';
|
import { ThumbnailHoverCard } from '../../items/Thumbnail';
|
||||||
import { TableColumn } from '../Column';
|
import { TableColumn } from '../Column';
|
||||||
import { TableFilter } from '../Filter';
|
import { TableFilter } from '../Filter';
|
||||||
@ -23,11 +22,12 @@ function stockItemTableColumns(): TableColumn[] {
|
|||||||
render: function (record: any) {
|
render: function (record: any) {
|
||||||
let part = record.part_detail;
|
let part = record.part_detail;
|
||||||
return (
|
return (
|
||||||
<ThumbnailHoverCard
|
<Text>{part.full_name}</Text>
|
||||||
src={part.thumbnail || part.image}
|
// <ThumbnailHoverCard
|
||||||
text={part.name}
|
// src={part.thumbnail || part.image}
|
||||||
link=""
|
// text={part.name}
|
||||||
/>
|
// link=""
|
||||||
|
// />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -102,17 +102,11 @@ function stockItemTableFilters(): TableFilter[] {
|
|||||||
* Load a table of stock items
|
* Load a table of stock items
|
||||||
*/
|
*/
|
||||||
export function StockItemTable({ params = {} }: { params?: any }) {
|
export function StockItemTable({ params = {} }: { params?: any }) {
|
||||||
let tableParams = useMemo(() => {
|
|
||||||
return {
|
|
||||||
part_detail: true,
|
|
||||||
location_detail: true,
|
|
||||||
...params
|
|
||||||
};
|
|
||||||
}, [params]);
|
|
||||||
|
|
||||||
let tableColumns = useMemo(() => stockItemTableColumns(), []);
|
let tableColumns = useMemo(() => stockItemTableColumns(), []);
|
||||||
let tableFilters = useMemo(() => stockItemTableFilters(), []);
|
let tableFilters = useMemo(() => stockItemTableFilters(), []);
|
||||||
|
|
||||||
|
const { tableKey, refreshTable } = useTableRefresh('stockitem');
|
||||||
|
|
||||||
function stockItemRowActions(record: any): RowAction[] {
|
function stockItemRowActions(record: any): RowAction[] {
|
||||||
let actions: RowAction[] = [];
|
let actions: RowAction[] = [];
|
||||||
|
|
||||||
@ -129,13 +123,19 @@ export function StockItemTable({ params = {} }: { params?: any }) {
|
|||||||
return (
|
return (
|
||||||
<InvenTreeTable
|
<InvenTreeTable
|
||||||
url="stock/"
|
url="stock/"
|
||||||
tableKey="stock-table"
|
tableKey={tableKey}
|
||||||
enableDownload
|
|
||||||
enableSelection
|
|
||||||
params={tableParams}
|
|
||||||
columns={tableColumns}
|
columns={tableColumns}
|
||||||
customFilters={tableFilters}
|
props={{
|
||||||
rowActions={stockItemRowActions}
|
enableDownload: true,
|
||||||
|
enableSelection: true,
|
||||||
|
customFilters: tableFilters,
|
||||||
|
rowActions: stockItemRowActions,
|
||||||
|
params: {
|
||||||
|
...params,
|
||||||
|
part_detail: true,
|
||||||
|
location_detail: true
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -5,21 +5,25 @@ import { useCallback, useState } from 'react';
|
|||||||
* Custom hook for refreshing an InvenTreeTable externally
|
* Custom hook for refreshing an InvenTreeTable externally
|
||||||
* Returns a unique ID for the table, which can be updated to trigger a refresh of the <table className=""></table>
|
* Returns a unique ID for the table, which can be updated to trigger a refresh of the <table className=""></table>
|
||||||
*
|
*
|
||||||
* @returns [refreshId, refreshTable]
|
* @returns { tableKey, refreshTable }
|
||||||
*
|
*
|
||||||
* To use this hook:
|
* To use this hook:
|
||||||
* const [refreshId, refreshTable] = useTableRefresh();
|
* const { tableKey, refreshTable } = useTableRefresh();
|
||||||
*
|
*
|
||||||
* Then, pass the refreshId to the InvenTreeTable component:
|
* Then, pass the refreshId to the InvenTreeTable component:
|
||||||
* <InvenTreeTable refreshId={refreshId} ... />
|
* <InvenTreeTable tableKey={tableKey} ... />
|
||||||
*/
|
*/
|
||||||
export function useTableRefresh() {
|
export function useTableRefresh(tableName: string) {
|
||||||
const [refreshId, setRefreshId] = useState<string>(randomId());
|
const [tableKey, setTableKey] = useState<string>(generateTableName());
|
||||||
|
|
||||||
|
function generateTableName() {
|
||||||
|
return `${tableName}-${randomId()}`;
|
||||||
|
}
|
||||||
|
|
||||||
// Generate a new ID to refresh the table
|
// Generate a new ID to refresh the table
|
||||||
const refreshTable = useCallback(function () {
|
const refreshTable = useCallback(function () {
|
||||||
setRefreshId(randomId());
|
setTableKey(generateTableName());
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return { refreshId, refreshTable };
|
return { tableKey, refreshTable };
|
||||||
}
|
}
|
||||||
|
@ -1,20 +1,37 @@
|
|||||||
import { Trans } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { Group } from '@mantine/core';
|
import { Stack } from '@mantine/core';
|
||||||
|
import { IconPackages, IconSitemap } from '@tabler/icons-react';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
import { PlaceholderPill } from '../../components/items/Placeholder';
|
import { PlaceholderPanel } from '../../components/items/Placeholder';
|
||||||
import { StylishText } from '../../components/items/StylishText';
|
import { PageDetail } from '../../components/nav/PageDetail';
|
||||||
|
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
|
||||||
import { StockItemTable } from '../../components/tables/stock/StockItemTable';
|
import { StockItemTable } from '../../components/tables/stock/StockItemTable';
|
||||||
|
|
||||||
export default function Stock() {
|
export default function Stock() {
|
||||||
|
const categoryPanels: PanelType[] = useMemo(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'stock-items',
|
||||||
|
label: t`Stock Items`,
|
||||||
|
icon: <IconPackages size="18" />,
|
||||||
|
content: <StockItemTable />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'sublocations',
|
||||||
|
label: t`Sublocations`,
|
||||||
|
icon: <IconSitemap size="18" />,
|
||||||
|
content: <PlaceholderPanel />
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Group>
|
<Stack>
|
||||||
<StylishText>
|
<PageDetail title={t`Stock Items`} />
|
||||||
<Trans>Stock Items</Trans>
|
<PanelGroup panels={categoryPanels} />
|
||||||
</StylishText>
|
</Stack>
|
||||||
<PlaceholderPill />
|
|
||||||
</Group>
|
|
||||||
<StockItemTable />
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -5,13 +5,14 @@ import { useMemo } from 'react';
|
|||||||
|
|
||||||
import { api } from '../App';
|
import { api } from '../App';
|
||||||
import { StylishText } from '../components/items/StylishText';
|
import { StylishText } from '../components/items/StylishText';
|
||||||
|
import { PageDetail } from '../components/nav/PageDetail';
|
||||||
import { PanelGroup } from '../components/nav/PanelGroup';
|
import { PanelGroup } from '../components/nav/PanelGroup';
|
||||||
import { NotificationTable } from '../components/tables/notifications/NotificationsTable';
|
import { NotificationTable } from '../components/tables/notifications/NotificationsTable';
|
||||||
import { useTableRefresh } from '../hooks/TableRefresh';
|
import { useTableRefresh } from '../hooks/TableRefresh';
|
||||||
|
|
||||||
export default function NotificationsPage() {
|
export default function NotificationsPage() {
|
||||||
const unreadRefresh = useTableRefresh();
|
const unreadRefresh = useTableRefresh('unreadnotifications');
|
||||||
const historyRefresh = useTableRefresh();
|
const historyRefresh = useTableRefresh('readnotifications');
|
||||||
|
|
||||||
const notificationPanels = useMemo(() => {
|
const notificationPanels = useMemo(() => {
|
||||||
return [
|
return [
|
||||||
@ -22,8 +23,7 @@ export default function NotificationsPage() {
|
|||||||
content: (
|
content: (
|
||||||
<NotificationTable
|
<NotificationTable
|
||||||
params={{ read: false }}
|
params={{ read: false }}
|
||||||
refreshId={unreadRefresh.refreshId}
|
tableKey={unreadRefresh.tableKey}
|
||||||
tableKey="notifications-unread"
|
|
||||||
actions={(record) => [
|
actions={(record) => [
|
||||||
{
|
{
|
||||||
title: t`Mark as read`,
|
title: t`Mark as read`,
|
||||||
@ -48,8 +48,7 @@ export default function NotificationsPage() {
|
|||||||
content: (
|
content: (
|
||||||
<NotificationTable
|
<NotificationTable
|
||||||
params={{ read: true }}
|
params={{ read: true }}
|
||||||
refreshId={historyRefresh.refreshId}
|
tableKey={historyRefresh.tableKey}
|
||||||
tableKey="notifications-history"
|
|
||||||
actions={(record) => [
|
actions={(record) => [
|
||||||
{
|
{
|
||||||
title: t`Mark as unread`,
|
title: t`Mark as unread`,
|
||||||
@ -83,8 +82,8 @@ export default function NotificationsPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Stack spacing="xs">
|
<Stack>
|
||||||
<StylishText>{t`Notifications`}</StylishText>
|
<PageDetail title={t`Notifications`} />
|
||||||
<PanelGroup panels={notificationPanels} />
|
<PanelGroup panels={notificationPanels} />
|
||||||
</Stack>
|
</Stack>
|
||||||
</>
|
</>
|
||||||
|
@ -12,7 +12,7 @@ import {
|
|||||||
IconSitemap
|
IconSitemap
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
|
|
||||||
import { api } from '../../App';
|
import { api } from '../../App';
|
||||||
@ -36,6 +36,10 @@ export default function BuildDetail() {
|
|||||||
// Build data
|
// Build data
|
||||||
const [build, setBuild] = useState<any>({});
|
const [build, setBuild] = useState<any>({});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setBuild({});
|
||||||
|
}, [id]);
|
||||||
|
|
||||||
// Query hook for fetching build data
|
// Query hook for fetching build data
|
||||||
const buildQuery = useQuery(['build', id ?? -1], async () => {
|
const buildQuery = useQuery(['build', id ?? -1], async () => {
|
||||||
let url = `/build/${id}/`;
|
let url = `/build/${id}/`;
|
||||||
|
108
src/frontend/src/pages/part/CategoryDetail.tsx
Normal file
108
src/frontend/src/pages/part/CategoryDetail.tsx
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import { Stack, Text } from '@mantine/core';
|
||||||
|
import {
|
||||||
|
IconCategory,
|
||||||
|
IconListDetails,
|
||||||
|
IconSitemap
|
||||||
|
} from '@tabler/icons-react';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { api } from '../../App';
|
||||||
|
import { PlaceholderPanel } from '../../components/items/Placeholder';
|
||||||
|
import { PageDetail } from '../../components/nav/PageDetail';
|
||||||
|
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
|
||||||
|
import { PartCategoryTable } from '../../components/tables/part/PartCategoryTable';
|
||||||
|
import { PartListTable } from '../../components/tables/part/PartTable';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detail view for a single PartCategory instance.
|
||||||
|
*
|
||||||
|
* Note: If no category ID is supplied, this acts as the top-level part category page
|
||||||
|
*/
|
||||||
|
export default function CategoryDetail({}: {}) {
|
||||||
|
const { id } = useParams();
|
||||||
|
|
||||||
|
const [category, setCategory] = useState<any>({});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setCategory({});
|
||||||
|
}, [id]);
|
||||||
|
|
||||||
|
const categoryQuery = useQuery({
|
||||||
|
enabled: id != null && id != undefined,
|
||||||
|
queryKey: ['category', id],
|
||||||
|
queryFn: async () => {
|
||||||
|
return api
|
||||||
|
.get(`/part/category/${id}/`)
|
||||||
|
.then((response) => {
|
||||||
|
setCategory(response.data);
|
||||||
|
return response.data;
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Error fetching category data:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const categoryPanels: PanelType[] = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
name: 'parts',
|
||||||
|
label: t`Parts`,
|
||||||
|
icon: <IconCategory size="18" />,
|
||||||
|
content: (
|
||||||
|
<PartListTable
|
||||||
|
props={{
|
||||||
|
params: {
|
||||||
|
category: category.pk ?? null
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'subcategories',
|
||||||
|
label: t`Subcategories`,
|
||||||
|
icon: <IconSitemap size="18" />,
|
||||||
|
content: (
|
||||||
|
<PartCategoryTable
|
||||||
|
params={{
|
||||||
|
parent: category.pk ?? null
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'parameters',
|
||||||
|
label: t`Parameters`,
|
||||||
|
icon: <IconListDetails size="18" />,
|
||||||
|
content: <PlaceholderPanel />
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[category, id]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack spacing="xs">
|
||||||
|
<PageDetail
|
||||||
|
title={t`Part Category`}
|
||||||
|
detail={<Text>{category.name ?? 'Top level'}</Text>}
|
||||||
|
breadcrumbs={
|
||||||
|
id
|
||||||
|
? [
|
||||||
|
{ name: t`Parts`, url: '/part' },
|
||||||
|
{ name: '...', url: '' },
|
||||||
|
{
|
||||||
|
name: category.name ?? t`Top level`,
|
||||||
|
url: `/part/category/${category.pk}`
|
||||||
|
}
|
||||||
|
]
|
||||||
|
: []
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<PanelGroup panels={categoryPanels} />
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
@ -25,15 +25,12 @@ import {
|
|||||||
IconVersions
|
IconVersions
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import React, { useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
|
|
||||||
import { api } from '../../App';
|
import { api } from '../../App';
|
||||||
import {
|
import { PlaceholderPanel } from '../../components/items/Placeholder';
|
||||||
PlaceholderPanel,
|
|
||||||
PlaceholderPill
|
|
||||||
} from '../../components/items/Placeholder';
|
|
||||||
import { PageDetail } from '../../components/nav/PageDetail';
|
import { PageDetail } from '../../components/nav/PageDetail';
|
||||||
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
|
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
|
||||||
import { AttachmentTable } from '../../components/tables/AttachmentTable';
|
import { AttachmentTable } from '../../components/tables/AttachmentTable';
|
||||||
@ -45,12 +42,19 @@ import {
|
|||||||
} from '../../components/widgets/MarkdownEditor';
|
} from '../../components/widgets/MarkdownEditor';
|
||||||
import { editPart } from '../../functions/forms/PartForms';
|
import { editPart } from '../../functions/forms/PartForms';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detail view for a single Part instance
|
||||||
|
*/
|
||||||
export default function PartDetail() {
|
export default function PartDetail() {
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
|
|
||||||
// Part data
|
// Part data
|
||||||
const [part, setPart] = useState<any>({});
|
const [part, setPart] = useState<any>({});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setPart({});
|
||||||
|
}, [id]);
|
||||||
|
|
||||||
// Part data panels (recalculate when part data changes)
|
// Part data panels (recalculate when part data changes)
|
||||||
const partPanels: PanelType[] = useMemo(() => {
|
const partPanels: PanelType[] = useMemo(() => {
|
||||||
return [
|
return [
|
||||||
@ -212,7 +216,7 @@ export default function PartDetail() {
|
|||||||
breadcrumbs={[
|
breadcrumbs={[
|
||||||
{ name: t`Parts`, url: '/part' },
|
{ name: t`Parts`, url: '/part' },
|
||||||
{ name: '...', url: '' },
|
{ name: '...', url: '' },
|
||||||
{ name: part.full_name, url: `/part/${part.pk}` }
|
{ name: part.name, url: `/part/${part.pk}` }
|
||||||
]}
|
]}
|
||||||
actions={[
|
actions={[
|
||||||
<Button
|
<Button
|
||||||
|
@ -1,61 +0,0 @@
|
|||||||
import { Trans, t } from '@lingui/macro';
|
|
||||||
import { Stack } from '@mantine/core';
|
|
||||||
import {
|
|
||||||
IconCategory,
|
|
||||||
IconListDetails,
|
|
||||||
IconSitemap
|
|
||||||
} from '@tabler/icons-react';
|
|
||||||
import { useMemo } from 'react';
|
|
||||||
|
|
||||||
import { PlaceholderPill } from '../../components/items/Placeholder';
|
|
||||||
import { StylishText } from '../../components/items/StylishText';
|
|
||||||
import { PageDetail } from '../../components/nav/PageDetail';
|
|
||||||
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
|
|
||||||
import { PartListTable } from '../../components/tables/part/PartTable';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Part index page
|
|
||||||
*/
|
|
||||||
export default function PartIndex() {
|
|
||||||
const panels: PanelType[] = useMemo(() => {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
name: 'parts',
|
|
||||||
label: t`Parts`,
|
|
||||||
icon: <IconCategory size="18" />,
|
|
||||||
content: <PartListTable />
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'categories',
|
|
||||||
label: t`Categories`,
|
|
||||||
icon: <IconSitemap size="18" />,
|
|
||||||
content: <PlaceholderPill />
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'parameters',
|
|
||||||
label: t`Parameters`,
|
|
||||||
icon: <IconListDetails size="18" />,
|
|
||||||
content: <PlaceholderPill />
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Stack>
|
|
||||||
<PageDetail
|
|
||||||
title={t`Parts`}
|
|
||||||
breadcrumbs={
|
|
||||||
[
|
|
||||||
// {
|
|
||||||
// name: t`Parts`,
|
|
||||||
// url: '/part',
|
|
||||||
// }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<PanelGroup panels={panels} />
|
|
||||||
</Stack>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
@ -11,7 +11,10 @@ export const Home = Loadable(lazy(() => import('./pages/Index/Home')));
|
|||||||
export const Playground = Loadable(
|
export const Playground = Loadable(
|
||||||
lazy(() => import('./pages/Index/Playground'))
|
lazy(() => import('./pages/Index/Playground'))
|
||||||
);
|
);
|
||||||
export const PartIndex = Loadable(lazy(() => import('./pages/part/PartIndex')));
|
|
||||||
|
export const CategoryDetail = Loadable(
|
||||||
|
lazy(() => import('./pages/part/CategoryDetail'))
|
||||||
|
);
|
||||||
export const PartDetail = Loadable(
|
export const PartDetail = Loadable(
|
||||||
lazy(() => import('./pages/part/PartDetail'))
|
lazy(() => import('./pages/part/PartDetail'))
|
||||||
);
|
);
|
||||||
@ -87,7 +90,11 @@ export const router = createBrowserRouter(
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'part/',
|
path: 'part/',
|
||||||
element: <PartIndex />
|
element: <CategoryDetail />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'part/category/:id',
|
||||||
|
element: <CategoryDetail />
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'part/:id',
|
path: 'part/:id',
|
||||||
|
Loading…
x
Reference in New Issue
Block a user