mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +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';
|
||||
|
||||
export function StylishText({ children }: { children: JSX.Element | string }) {
|
||||
export function StylishText({
|
||||
children,
|
||||
size = 'md'
|
||||
}: {
|
||||
children: JSX.Element | string;
|
||||
size?: string;
|
||||
}) {
|
||||
const { classes } = InvenTreeStyle();
|
||||
return (
|
||||
<Text className={classes.signText} variant="gradient">
|
||||
<Text size={size} className={classes.signText} variant="gradient">
|
||||
{children}
|
||||
</Text>
|
||||
);
|
||||
|
@ -62,7 +62,10 @@ export function Header() {
|
||||
<NavigationDrawer opened={navDrawerOpened} close={closeNavDrawer} />
|
||||
<NotificationDrawer
|
||||
opened={notificationDrawerOpened}
|
||||
onClose={closeNotificationDrawer}
|
||||
onClose={() => {
|
||||
notifications.refetch();
|
||||
closeNotificationDrawer();
|
||||
}}
|
||||
/>
|
||||
<Container className={classes.layoutHeaderSection} size={'xl'}>
|
||||
<Group position="apart">
|
||||
|
@ -1,16 +1,17 @@
|
||||
import { t } from '@lingui/macro';
|
||||
import {
|
||||
ActionIcon,
|
||||
Alert,
|
||||
Divider,
|
||||
Drawer,
|
||||
LoadingOverlay,
|
||||
Space,
|
||||
Tooltip
|
||||
} from '@mantine/core';
|
||||
import { Badge, Group, Stack, Text } from '@mantine/core';
|
||||
import { IconBellCheck, IconBellPlus, IconBookmark } from '@tabler/icons-react';
|
||||
import { IconMacro } from '@tabler/icons-react';
|
||||
import { Group, Stack, Text } from '@mantine/core';
|
||||
import { IconBellCheck, IconBellPlus } from '@tabler/icons-react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { api } from '../../App';
|
||||
@ -79,6 +80,11 @@ export function NotificationDrawer({
|
||||
<Stack spacing="xs">
|
||||
<Divider />
|
||||
<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) => (
|
||||
<Group position="apart">
|
||||
<Stack spacing="3">
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { Group, Paper, Space, Stack, Text } from '@mantine/core';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
import { StylishText } from '../items/StylishText';
|
||||
import { Breadcrumb, BreadcrumbList } from './BreadcrumbList';
|
||||
|
||||
/**
|
||||
@ -33,7 +34,7 @@ export function PageDetail({
|
||||
<Stack spacing="xs">
|
||||
<Group position="apart">
|
||||
<Group position="left">
|
||||
<Text size="xl">{title}</Text>
|
||||
<StylishText size="xl">{title}</StylishText>
|
||||
{subtitle && <Text size="lg">{subtitle}</Text>}
|
||||
</Group>
|
||||
<Space />
|
||||
|
@ -81,9 +81,7 @@ export function AttachmentTable({
|
||||
pk: number;
|
||||
model: string;
|
||||
}): ReactNode {
|
||||
const tableId = useId();
|
||||
|
||||
const { refreshId, refreshTable } = useTableRefresh();
|
||||
const { tableKey, refreshTable } = useTableRefresh(`${model}-attachments`);
|
||||
|
||||
const tableColumns = useMemo(() => attachmentTableColumns(), []);
|
||||
|
||||
@ -224,14 +222,16 @@ export function AttachmentTable({
|
||||
<Stack spacing="xs">
|
||||
<InvenTreeTable
|
||||
url={url}
|
||||
tableKey={tableId}
|
||||
refreshId={refreshId}
|
||||
params={{
|
||||
[model]: pk
|
||||
}}
|
||||
customActionGroups={customActionGroups}
|
||||
tableKey={tableKey}
|
||||
columns={tableColumns}
|
||||
rowActions={allowEdit && allowDelete ? rowActions : undefined}
|
||||
props={{
|
||||
enableSelection: true,
|
||||
customActionGroups: customActionGroups,
|
||||
rowActions: allowEdit && allowDelete ? rowActions : undefined,
|
||||
params: {
|
||||
[model]: pk
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{allowEdit && (
|
||||
<Dropzone onDrop={uploadFiles}>
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { t } from '@lingui/macro';
|
||||
import { ActionIcon, Indicator, Space, Stack, Tooltip } from '@mantine/core';
|
||||
import { Group } from '@mantine/core';
|
||||
import { useLocalStorage } from '@mantine/hooks';
|
||||
import { IconFilter, IconRefresh } from '@tabler/icons-react';
|
||||
import { IconBarcode, IconPrinter } from '@tabler/icons-react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
@ -18,96 +19,33 @@ import { FilterSelectModal } from './FilterSelectModal';
|
||||
import { RowAction, RowActions } from './RowActions';
|
||||
import { TableSearchInput } from './Search';
|
||||
|
||||
/*
|
||||
* 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}`) || '[]'
|
||||
);
|
||||
}
|
||||
const defaultPageSize: number = 25;
|
||||
|
||||
/**
|
||||
* Write list of hidden columns to local storage
|
||||
* @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
|
||||
* Set of optional properties which can be passed to an InvenTreeTable component
|
||||
*
|
||||
* 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({
|
||||
url,
|
||||
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;
|
||||
export type InvenTreeTableProps = {
|
||||
params?: any;
|
||||
defaultSortColumn?: string;
|
||||
noRecordsText?: string;
|
||||
enableDownload?: boolean;
|
||||
@ -117,23 +55,79 @@ export function InvenTreeTable({
|
||||
enablePagination?: boolean;
|
||||
enableRefresh?: boolean;
|
||||
pageSize?: number;
|
||||
printingActions?: any[];
|
||||
barcodeActions?: any[];
|
||||
customActionGroups?: any[];
|
||||
customFilters?: TableFilter[];
|
||||
customActionGroups?: any[];
|
||||
printingActions?: any[];
|
||||
rowActions?: (record: any) => RowAction[];
|
||||
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)
|
||||
const hasSwitchableColumns = columns.some(
|
||||
(col: TableColumn) => col.switchable
|
||||
);
|
||||
|
||||
// Manage state for switchable columns (initially load from local storage)
|
||||
let [hiddenColumns, setHiddenColumns] = useState(() =>
|
||||
loadHiddenColumns(tableKey)
|
||||
);
|
||||
// A list of hidden columns, saved to local storage
|
||||
const [hiddenColumns, setHiddenColumns] = useLocalStorage<string[]>({
|
||||
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
|
||||
const [selectedRecords, setSelectedRecords] = useState<any[]>([]);
|
||||
@ -158,7 +152,7 @@ export function InvenTreeTable({
|
||||
});
|
||||
|
||||
// If row actions are available, add a column for them
|
||||
if (rowActions) {
|
||||
if (tableProps.rowActions) {
|
||||
cols.push({
|
||||
accessor: 'actions',
|
||||
title: '',
|
||||
@ -168,7 +162,7 @@ export function InvenTreeTable({
|
||||
render: function (record: any) {
|
||||
return (
|
||||
<RowActions
|
||||
actions={rowActions(record)}
|
||||
actions={tableProps.rowActions?.(record) ?? []}
|
||||
disabled={selectedRecords.length > 0}
|
||||
/>
|
||||
);
|
||||
@ -177,7 +171,13 @@ export function InvenTreeTable({
|
||||
}
|
||||
|
||||
return cols;
|
||||
}, [columns, hiddenColumns, rowActions, enableSelection, selectedRecords]);
|
||||
}, [
|
||||
columns,
|
||||
hiddenColumns,
|
||||
tableProps.rowActions,
|
||||
tableProps.enableSelection,
|
||||
selectedRecords
|
||||
]);
|
||||
|
||||
// Callback when column visibility is toggled
|
||||
function toggleColumn(columnName: string) {
|
||||
@ -189,20 +189,11 @@ export function InvenTreeTable({
|
||||
newColumns[colIdx].hidden = !newColumns[colIdx].hidden;
|
||||
}
|
||||
|
||||
let hiddenColumnNames = newColumns
|
||||
.filter((col) => col.hidden)
|
||||
.map((col) => col.accessor);
|
||||
|
||||
// Save list of hidden columns to local storage
|
||||
saveHiddenColumns(tableKey, hiddenColumnNames);
|
||||
|
||||
// Refresh state
|
||||
setHiddenColumns(loadHiddenColumns(tableKey));
|
||||
setHiddenColumns(
|
||||
newColumns.filter((col) => col.hidden).map((col) => col.accessor)
|
||||
);
|
||||
}
|
||||
|
||||
// Check if custom filtering is enabled for this table
|
||||
const hasCustomFilters = enableFilters && customFilters.length > 0;
|
||||
|
||||
// Filter selection open state
|
||||
const [filterSelectOpen, setFilterSelectOpen] = useState<boolean>(false);
|
||||
|
||||
@ -212,11 +203,6 @@ export function InvenTreeTable({
|
||||
// Filter list visibility
|
||||
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.
|
||||
* Launches a modal dialog to add a new filter
|
||||
@ -224,7 +210,7 @@ export function InvenTreeTable({
|
||||
function onFilterAdd(name: string, value: string) {
|
||||
let filters = [...activeFilters];
|
||||
|
||||
let newFilter = customFilters.find((flt) => flt.name == name);
|
||||
let newFilter = tableProps.customFilters?.find((flt) => flt.name == name);
|
||||
|
||||
if (newFilter) {
|
||||
filters.push({
|
||||
@ -232,7 +218,6 @@ export function InvenTreeTable({
|
||||
value: value
|
||||
});
|
||||
|
||||
saveActiveFilters(tableKey, filters);
|
||||
setActiveFilters(filters);
|
||||
}
|
||||
}
|
||||
@ -242,7 +227,7 @@ export function InvenTreeTable({
|
||||
*/
|
||||
function onFilterRemove(filterName: string) {
|
||||
let filters = activeFilters.filter((flt) => flt.name != filterName);
|
||||
saveActiveFilters(tableKey, filters);
|
||||
|
||||
setActiveFilters(filters);
|
||||
}
|
||||
|
||||
@ -250,7 +235,6 @@ export function InvenTreeTable({
|
||||
* Callback function when all custom filters are removed from the table
|
||||
*/
|
||||
function onFilterClearAll() {
|
||||
saveActiveFilters(tableKey, []);
|
||||
setActiveFilters([]);
|
||||
}
|
||||
|
||||
@ -266,7 +250,9 @@ export function InvenTreeTable({
|
||||
* Construct query filters for the current table
|
||||
*/
|
||||
function getTableFilters(paginate: boolean = false) {
|
||||
let queryParams = { ...params };
|
||||
let queryParams = {
|
||||
...tableProps.params
|
||||
};
|
||||
|
||||
// Add custom filters
|
||||
activeFilters.forEach((flt) => (queryParams[flt.name] = flt.value));
|
||||
@ -277,7 +263,8 @@ export function InvenTreeTable({
|
||||
}
|
||||
|
||||
// Pagination
|
||||
if (enablePagination && paginate) {
|
||||
if (tableProps.enablePagination && paginate) {
|
||||
let pageSize = tableProps.pageSize ?? defaultPageSize;
|
||||
queryParams.limit = pageSize;
|
||||
queryParams.offset = (page - 1) * pageSize;
|
||||
}
|
||||
@ -315,7 +302,7 @@ export function InvenTreeTable({
|
||||
|
||||
// Data Sorting
|
||||
const [sortStatus, setSortStatus] = useState<DataTableSortStatus>({
|
||||
columnAccessor: defaultSortColumn,
|
||||
columnAccessor: tableProps.defaultSortColumn ?? '',
|
||||
direction: 'asc'
|
||||
});
|
||||
|
||||
@ -335,8 +322,9 @@ export function InvenTreeTable({
|
||||
}
|
||||
|
||||
// Missing records text (based on server response)
|
||||
const [missingRecordsText, setMissingRecordsText] =
|
||||
useState<string>(noRecordsText);
|
||||
const [missingRecordsText, setMissingRecordsText] = useState<string>(
|
||||
tableProps.noRecordsText ?? t`No records found`
|
||||
);
|
||||
|
||||
const handleSortStatusChange = (status: DataTableSortStatus) => {
|
||||
setPage(1);
|
||||
@ -355,7 +343,9 @@ export function InvenTreeTable({
|
||||
.then(function (response) {
|
||||
switch (response.status) {
|
||||
case 200:
|
||||
setMissingRecordsText(noRecordsText);
|
||||
setMissingRecordsText(
|
||||
tableProps.noRecordsText ?? t`No records found`
|
||||
);
|
||||
return response.data;
|
||||
case 400:
|
||||
setMissingRecordsText(t`Bad request`);
|
||||
@ -386,7 +376,7 @@ export function InvenTreeTable({
|
||||
|
||||
const { data, isError, isFetching, isLoading, refetch } = useQuery(
|
||||
[
|
||||
`table-${tableKey}`,
|
||||
`table-${tableName}`,
|
||||
sortStatus.columnAccessor,
|
||||
sortStatus.direction,
|
||||
page,
|
||||
@ -407,15 +397,13 @@ export function InvenTreeTable({
|
||||
* Implement this using the custom useTableRefresh hook
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (refreshId) {
|
||||
refetch();
|
||||
}
|
||||
}, [refreshId]);
|
||||
refetch();
|
||||
}, [tableKey, props.params]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<FilterSelectModal
|
||||
availableFilters={customFilters}
|
||||
availableFilters={tableProps.customFilters ?? []}
|
||||
activeFilters={activeFilters}
|
||||
opened={filterSelectOpen}
|
||||
onCreateFilter={onFilterAdd}
|
||||
@ -424,35 +412,37 @@ export function InvenTreeTable({
|
||||
<Stack>
|
||||
<Group position="apart">
|
||||
<Group position="left" spacing={5}>
|
||||
{customActionGroups.map((group: any, idx: number) => group)}
|
||||
{barcodeActions.length > 0 && (
|
||||
{tableProps.customActionGroups?.map(
|
||||
(group: any, idx: number) => group
|
||||
)}
|
||||
{(tableProps.barcodeActions?.length ?? 0 > 0) && (
|
||||
<ButtonMenu
|
||||
icon={<IconBarcode />}
|
||||
label={t`Barcode actions`}
|
||||
tooltip={t`Barcode actions`}
|
||||
actions={barcodeActions}
|
||||
actions={tableProps.barcodeActions ?? []}
|
||||
/>
|
||||
)}
|
||||
{printingActions.length > 0 && (
|
||||
{(tableProps.printingActions?.length ?? 0 > 0) && (
|
||||
<ButtonMenu
|
||||
icon={<IconPrinter />}
|
||||
label={t`Print actions`}
|
||||
tooltip={t`Print actions`}
|
||||
actions={printingActions}
|
||||
actions={tableProps.printingActions ?? []}
|
||||
/>
|
||||
)}
|
||||
{enableDownload && (
|
||||
{tableProps.enableDownload && (
|
||||
<DownloadAction downloadCallback={downloadData} />
|
||||
)}
|
||||
</Group>
|
||||
<Space />
|
||||
<Group position="right" spacing={5}>
|
||||
{enableSearch && (
|
||||
{tableProps.enableSearch && (
|
||||
<TableSearchInput
|
||||
searchCallback={(term: string) => setSearchTerm(term)}
|
||||
/>
|
||||
)}
|
||||
{enableRefresh && (
|
||||
{tableProps.enableRefresh && (
|
||||
<ActionIcon>
|
||||
<Tooltip label={t`Refresh data`}>
|
||||
<IconRefresh onClick={() => refetch()} />
|
||||
@ -465,21 +455,22 @@ export function InvenTreeTable({
|
||||
onToggleColumn={toggleColumn}
|
||||
/>
|
||||
)}
|
||||
{hasCustomFilters && (
|
||||
<Indicator
|
||||
size="xs"
|
||||
label={activeFilters.length}
|
||||
disabled={activeFilters.length == 0}
|
||||
>
|
||||
<ActionIcon>
|
||||
<Tooltip label={t`Table filters`}>
|
||||
<IconFilter
|
||||
onClick={() => setFiltersVisible(!filtersVisible)}
|
||||
/>
|
||||
</Tooltip>
|
||||
</ActionIcon>
|
||||
</Indicator>
|
||||
)}
|
||||
{tableProps.enableFilters &&
|
||||
(tableProps.customFilters?.length ?? 0 > 0) && (
|
||||
<Indicator
|
||||
size="xs"
|
||||
label={activeFilters.length}
|
||||
disabled={activeFilters.length == 0}
|
||||
>
|
||||
<ActionIcon>
|
||||
<Tooltip label={t`Table filters`}>
|
||||
<IconFilter
|
||||
onClick={() => setFiltersVisible(!filtersVisible)}
|
||||
/>
|
||||
</Tooltip>
|
||||
</ActionIcon>
|
||||
</Indicator>
|
||||
)}
|
||||
</Group>
|
||||
</Group>
|
||||
{filtersVisible && (
|
||||
@ -498,20 +489,22 @@ export function InvenTreeTable({
|
||||
idAccessor={'pk'}
|
||||
minHeight={200}
|
||||
totalRecords={data?.count ?? data?.length ?? 0}
|
||||
recordsPerPage={pageSize}
|
||||
recordsPerPage={tableProps.pageSize ?? defaultPageSize}
|
||||
page={page}
|
||||
onPageChange={setPage}
|
||||
sortStatus={sortStatus}
|
||||
onSortStatusChange={handleSortStatusChange}
|
||||
selectedRecords={enableSelection ? selectedRecords : undefined}
|
||||
selectedRecords={
|
||||
tableProps.enableSelection ? selectedRecords : undefined
|
||||
}
|
||||
onSelectedRecordsChange={
|
||||
enableSelection ? onSelectedRecordsChange : undefined
|
||||
tableProps.enableSelection ? onSelectedRecordsChange : undefined
|
||||
}
|
||||
fetching={isFetching}
|
||||
noRecordsText={missingRecordsText}
|
||||
records={data?.results ?? data ?? []}
|
||||
columns={dataColumns}
|
||||
onRowClick={onRowClick}
|
||||
onRowClick={tableProps.onRowClick}
|
||||
/>
|
||||
</Stack>
|
||||
</>
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { t } from '@lingui/macro';
|
||||
import { Progress } from '@mantine/core';
|
||||
import { Progress, Text } from '@mantine/core';
|
||||
import { useMemo } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { useTableRefresh } from '../../../hooks/TableRefresh';
|
||||
import { ThumbnailHoverCard } from '../../items/Thumbnail';
|
||||
import { TableColumn } from '../Column';
|
||||
import { TableFilter } from '../Filter';
|
||||
@ -27,11 +28,12 @@ function buildOrderTableColumns(): TableColumn[] {
|
||||
let part = record.part_detail;
|
||||
return (
|
||||
part && (
|
||||
<ThumbnailHoverCard
|
||||
src={part.thumbnail || part.image}
|
||||
text={part.full_name}
|
||||
link=""
|
||||
/>
|
||||
<Text>{part.full_name}</Text>
|
||||
// <ThumbnailHoverCard
|
||||
// src={part.thumbnail || part.image}
|
||||
// text={part.full_name}
|
||||
// link=""
|
||||
// />
|
||||
)
|
||||
);
|
||||
}
|
||||
@ -127,35 +129,31 @@ function buildOrderTableFilters(): TableFilter[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
function buildOrderTableParams(params: any): any {
|
||||
return {
|
||||
...params,
|
||||
part_detail: true
|
||||
};
|
||||
}
|
||||
|
||||
/*
|
||||
* Construct a table of build orders, according to the provided parameters
|
||||
*/
|
||||
export function BuildOrderTable({ params = {} }: { params?: any }) {
|
||||
// Add required query parameters
|
||||
const tableParams = useMemo(() => buildOrderTableParams(params), [params]);
|
||||
const tableColumns = useMemo(() => buildOrderTableColumns(), []);
|
||||
const tableFilters = useMemo(() => buildOrderTableFilters(), []);
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
tableParams.part_detail = true;
|
||||
const { tableKey, refreshTable } = useTableRefresh('buildorder');
|
||||
|
||||
return (
|
||||
<InvenTreeTable
|
||||
url="build/"
|
||||
enableDownload
|
||||
tableKey="build-order-table"
|
||||
params={tableParams}
|
||||
tableKey={tableKey}
|
||||
columns={tableColumns}
|
||||
customFilters={tableFilters}
|
||||
onRowClick={(row) => navigate(`/build/${row.pk}`)}
|
||||
props={{
|
||||
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({
|
||||
params,
|
||||
refreshId,
|
||||
tableKey,
|
||||
actions
|
||||
}: {
|
||||
params: any;
|
||||
refreshId: string;
|
||||
tableKey: string;
|
||||
actions: (record: any) => RowAction[];
|
||||
}) {
|
||||
@ -43,10 +41,12 @@ export function NotificationTable({
|
||||
<InvenTreeTable
|
||||
url="/notifications/"
|
||||
tableKey={tableKey}
|
||||
refreshId={refreshId}
|
||||
params={params}
|
||||
rowActions={actions}
|
||||
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 { Text } from '@mantine/core';
|
||||
import { IconEdit, IconTrash } from '@tabler/icons-react';
|
||||
import { useMemo } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
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 { TableColumn } from '../Column';
|
||||
import { TableFilter } from '../Filter';
|
||||
import { InvenTreeTable } from '../InvenTreeTable';
|
||||
import { InvenTreeTable, InvenTreeTableProps } from '../InvenTreeTable';
|
||||
import { RowAction } from '../RowActions';
|
||||
|
||||
/**
|
||||
@ -26,11 +26,12 @@ function partTableColumns(): TableColumn[] {
|
||||
render: function (record: any) {
|
||||
// TODO - Link to the part detail page
|
||||
return (
|
||||
<ThumbnailHoverCard
|
||||
src={record.thumbnail || record.image}
|
||||
text={record.name}
|
||||
link=""
|
||||
/>
|
||||
<Text>{record.full_name}</Text>
|
||||
// <ThumbnailHoverCard
|
||||
// src={record.thumbnail || record.image}
|
||||
// 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
|
||||
* @param {Object} params - The query parameters to pass to the API
|
||||
* @returns
|
||||
*/
|
||||
export function PartListTable({ params = {} }: { params?: any }) {
|
||||
let tableParams = useMemo(() => partTableParams(params), [params]);
|
||||
export function PartListTable({ props }: { props: InvenTreeTableProps }) {
|
||||
let tableColumns = useMemo(() => partTableColumns(), []);
|
||||
let tableFilters = useMemo(() => partTableFilters(), []);
|
||||
|
||||
const { tableKey, refreshTable } = useTableRefresh('part');
|
||||
|
||||
// Callback function for generating set of row actions
|
||||
function partTableRowActions(record: any): RowAction[] {
|
||||
let actions: RowAction[] = [];
|
||||
@ -227,16 +222,18 @@ export function PartListTable({ params = {} }: { params?: any }) {
|
||||
return (
|
||||
<InvenTreeTable
|
||||
url="part/"
|
||||
enableDownload
|
||||
tableKey="part-table"
|
||||
printingActions={[
|
||||
<Text onClick={notYetImplemented}>Hello</Text>,
|
||||
<Text onClick={notYetImplemented}>World</Text>
|
||||
]}
|
||||
params={tableParams}
|
||||
tableKey={tableKey}
|
||||
columns={tableColumns}
|
||||
customFilters={tableFilters}
|
||||
rowActions={partTableRowActions}
|
||||
props={{
|
||||
...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';
|
||||
|
||||
export function RelatedPartTable({ partId }: { partId: number }): ReactNode {
|
||||
const { refreshId, refreshTable } = useTableRefresh();
|
||||
const { tableKey, refreshTable } = useTableRefresh('relatedparts');
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
@ -116,14 +116,16 @@ export function RelatedPartTable({ partId }: { partId: number }): ReactNode {
|
||||
return (
|
||||
<InvenTreeTable
|
||||
url="/part/related/"
|
||||
tableKey="related-part-table"
|
||||
refreshId={refreshId}
|
||||
params={{
|
||||
part: partId
|
||||
}}
|
||||
rowActions={rowActions}
|
||||
tableKey={tableKey}
|
||||
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 { Group } from '@mantine/core';
|
||||
import { IconEdit, IconTrash } from '@tabler/icons-react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { Text } from '@mantine/core';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { notYetImplemented } from '../../../functions/notifications';
|
||||
import { ActionButton } from '../../items/ActionButton';
|
||||
import { useTableRefresh } from '../../../hooks/TableRefresh';
|
||||
import { ThumbnailHoverCard } from '../../items/Thumbnail';
|
||||
import { TableColumn } from '../Column';
|
||||
import { TableFilter } from '../Filter';
|
||||
@ -23,11 +22,12 @@ function stockItemTableColumns(): TableColumn[] {
|
||||
render: function (record: any) {
|
||||
let part = record.part_detail;
|
||||
return (
|
||||
<ThumbnailHoverCard
|
||||
src={part.thumbnail || part.image}
|
||||
text={part.name}
|
||||
link=""
|
||||
/>
|
||||
<Text>{part.full_name}</Text>
|
||||
// <ThumbnailHoverCard
|
||||
// src={part.thumbnail || part.image}
|
||||
// text={part.name}
|
||||
// link=""
|
||||
// />
|
||||
);
|
||||
}
|
||||
},
|
||||
@ -102,17 +102,11 @@ function stockItemTableFilters(): TableFilter[] {
|
||||
* Load a table of stock items
|
||||
*/
|
||||
export function StockItemTable({ params = {} }: { params?: any }) {
|
||||
let tableParams = useMemo(() => {
|
||||
return {
|
||||
part_detail: true,
|
||||
location_detail: true,
|
||||
...params
|
||||
};
|
||||
}, [params]);
|
||||
|
||||
let tableColumns = useMemo(() => stockItemTableColumns(), []);
|
||||
let tableFilters = useMemo(() => stockItemTableFilters(), []);
|
||||
|
||||
const { tableKey, refreshTable } = useTableRefresh('stockitem');
|
||||
|
||||
function stockItemRowActions(record: any): RowAction[] {
|
||||
let actions: RowAction[] = [];
|
||||
|
||||
@ -129,13 +123,19 @@ export function StockItemTable({ params = {} }: { params?: any }) {
|
||||
return (
|
||||
<InvenTreeTable
|
||||
url="stock/"
|
||||
tableKey="stock-table"
|
||||
enableDownload
|
||||
enableSelection
|
||||
params={tableParams}
|
||||
tableKey={tableKey}
|
||||
columns={tableColumns}
|
||||
customFilters={tableFilters}
|
||||
rowActions={stockItemRowActions}
|
||||
props={{
|
||||
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
|
||||
* 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:
|
||||
* const [refreshId, refreshTable] = useTableRefresh();
|
||||
* const { tableKey, refreshTable } = useTableRefresh();
|
||||
*
|
||||
* Then, pass the refreshId to the InvenTreeTable component:
|
||||
* <InvenTreeTable refreshId={refreshId} ... />
|
||||
* <InvenTreeTable tableKey={tableKey} ... />
|
||||
*/
|
||||
export function useTableRefresh() {
|
||||
const [refreshId, setRefreshId] = useState<string>(randomId());
|
||||
export function useTableRefresh(tableName: string) {
|
||||
const [tableKey, setTableKey] = useState<string>(generateTableName());
|
||||
|
||||
function generateTableName() {
|
||||
return `${tableName}-${randomId()}`;
|
||||
}
|
||||
|
||||
// Generate a new ID to refresh the table
|
||||
const refreshTable = useCallback(function () {
|
||||
setRefreshId(randomId());
|
||||
setTableKey(generateTableName());
|
||||
}, []);
|
||||
|
||||
return { refreshId, refreshTable };
|
||||
return { tableKey, refreshTable };
|
||||
}
|
||||
|
@ -1,20 +1,37 @@
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { Group } from '@mantine/core';
|
||||
import { t } from '@lingui/macro';
|
||||
import { Stack } from '@mantine/core';
|
||||
import { IconPackages, IconSitemap } from '@tabler/icons-react';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { PlaceholderPill } from '../../components/items/Placeholder';
|
||||
import { StylishText } from '../../components/items/StylishText';
|
||||
import { PlaceholderPanel } from '../../components/items/Placeholder';
|
||||
import { PageDetail } from '../../components/nav/PageDetail';
|
||||
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
|
||||
import { StockItemTable } from '../../components/tables/stock/StockItemTable';
|
||||
|
||||
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 (
|
||||
<>
|
||||
<Group>
|
||||
<StylishText>
|
||||
<Trans>Stock Items</Trans>
|
||||
</StylishText>
|
||||
<PlaceholderPill />
|
||||
</Group>
|
||||
<StockItemTable />
|
||||
<Stack>
|
||||
<PageDetail title={t`Stock Items`} />
|
||||
<PanelGroup panels={categoryPanels} />
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -5,13 +5,14 @@ import { useMemo } from 'react';
|
||||
|
||||
import { api } from '../App';
|
||||
import { StylishText } from '../components/items/StylishText';
|
||||
import { PageDetail } from '../components/nav/PageDetail';
|
||||
import { PanelGroup } from '../components/nav/PanelGroup';
|
||||
import { NotificationTable } from '../components/tables/notifications/NotificationsTable';
|
||||
import { useTableRefresh } from '../hooks/TableRefresh';
|
||||
|
||||
export default function NotificationsPage() {
|
||||
const unreadRefresh = useTableRefresh();
|
||||
const historyRefresh = useTableRefresh();
|
||||
const unreadRefresh = useTableRefresh('unreadnotifications');
|
||||
const historyRefresh = useTableRefresh('readnotifications');
|
||||
|
||||
const notificationPanels = useMemo(() => {
|
||||
return [
|
||||
@ -22,8 +23,7 @@ export default function NotificationsPage() {
|
||||
content: (
|
||||
<NotificationTable
|
||||
params={{ read: false }}
|
||||
refreshId={unreadRefresh.refreshId}
|
||||
tableKey="notifications-unread"
|
||||
tableKey={unreadRefresh.tableKey}
|
||||
actions={(record) => [
|
||||
{
|
||||
title: t`Mark as read`,
|
||||
@ -48,8 +48,7 @@ export default function NotificationsPage() {
|
||||
content: (
|
||||
<NotificationTable
|
||||
params={{ read: true }}
|
||||
refreshId={historyRefresh.refreshId}
|
||||
tableKey="notifications-history"
|
||||
tableKey={historyRefresh.tableKey}
|
||||
actions={(record) => [
|
||||
{
|
||||
title: t`Mark as unread`,
|
||||
@ -83,8 +82,8 @@ export default function NotificationsPage() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack spacing="xs">
|
||||
<StylishText>{t`Notifications`}</StylishText>
|
||||
<Stack>
|
||||
<PageDetail title={t`Notifications`} />
|
||||
<PanelGroup panels={notificationPanels} />
|
||||
</Stack>
|
||||
</>
|
||||
|
@ -12,7 +12,7 @@ import {
|
||||
IconSitemap
|
||||
} from '@tabler/icons-react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { api } from '../../App';
|
||||
@ -36,6 +36,10 @@ export default function BuildDetail() {
|
||||
// Build data
|
||||
const [build, setBuild] = useState<any>({});
|
||||
|
||||
useEffect(() => {
|
||||
setBuild({});
|
||||
}, [id]);
|
||||
|
||||
// Query hook for fetching build data
|
||||
const buildQuery = useQuery(['build', id ?? -1], async () => {
|
||||
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
|
||||
} from '@tabler/icons-react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import React, { useState } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
|
||||
import { api } from '../../App';
|
||||
import {
|
||||
PlaceholderPanel,
|
||||
PlaceholderPill
|
||||
} from '../../components/items/Placeholder';
|
||||
import { PlaceholderPanel } from '../../components/items/Placeholder';
|
||||
import { PageDetail } from '../../components/nav/PageDetail';
|
||||
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
|
||||
import { AttachmentTable } from '../../components/tables/AttachmentTable';
|
||||
@ -45,12 +42,19 @@ import {
|
||||
} from '../../components/widgets/MarkdownEditor';
|
||||
import { editPart } from '../../functions/forms/PartForms';
|
||||
|
||||
/**
|
||||
* Detail view for a single Part instance
|
||||
*/
|
||||
export default function PartDetail() {
|
||||
const { id } = useParams();
|
||||
|
||||
// Part data
|
||||
const [part, setPart] = useState<any>({});
|
||||
|
||||
useEffect(() => {
|
||||
setPart({});
|
||||
}, [id]);
|
||||
|
||||
// Part data panels (recalculate when part data changes)
|
||||
const partPanels: PanelType[] = useMemo(() => {
|
||||
return [
|
||||
@ -212,7 +216,7 @@ export default function PartDetail() {
|
||||
breadcrumbs={[
|
||||
{ name: t`Parts`, url: '/part' },
|
||||
{ name: '...', url: '' },
|
||||
{ name: part.full_name, url: `/part/${part.pk}` }
|
||||
{ name: part.name, url: `/part/${part.pk}` }
|
||||
]}
|
||||
actions={[
|
||||
<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(
|
||||
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(
|
||||
lazy(() => import('./pages/part/PartDetail'))
|
||||
);
|
||||
@ -87,7 +90,11 @@ export const router = createBrowserRouter(
|
||||
},
|
||||
{
|
||||
path: 'part/',
|
||||
element: <PartIndex />
|
||||
element: <CategoryDetail />
|
||||
},
|
||||
{
|
||||
path: 'part/category/:id',
|
||||
element: <CategoryDetail />
|
||||
},
|
||||
{
|
||||
path: 'part/:id',
|
||||
|
Loading…
Reference in New Issue
Block a user