mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-28 11:36:44 +00:00
[PUI] Part detail page (#5521)
* Very basic part detail page - Simply displays the ID of the part (not any actual data) - Navigate from the part table * Implement generic PanelGroup component - Used for displaying sets of panelized data - Will be used a lot within the interface * Reload part page after edit form * Fix loading overlay for part page * Fix search panel * Add panels to part index page * Fix icons * Fix table row actions menu * PanelGroup: allow active panel to be changed externally * Fix SearchDrawer issue - AbortController does not work as expected - Might need to revisit this later * Improve form loading indicator
This commit is contained in:
parent
9a6c2d2953
commit
a210e905dc
@ -151,6 +151,7 @@ export function ApiForm({
|
|||||||
|
|
||||||
// Fetch initial data if the fetchInitialData property is set
|
// Fetch initial data if the fetchInitialData property is set
|
||||||
if (props.fetchInitialData) {
|
if (props.fetchInitialData) {
|
||||||
|
initialDataQuery.remove();
|
||||||
initialDataQuery.refetch();
|
initialDataQuery.refetch();
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
@ -162,7 +163,7 @@ export function ApiForm({
|
|||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
let method = props.method?.toLowerCase() ?? 'get';
|
let method = props.method?.toLowerCase() ?? 'get';
|
||||||
|
|
||||||
api({
|
return api({
|
||||||
method: method,
|
method: method,
|
||||||
url: url,
|
url: url,
|
||||||
data: form.values,
|
data: form.values,
|
||||||
@ -199,6 +200,8 @@ export function ApiForm({
|
|||||||
closeForm();
|
closeForm();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
if (error.response) {
|
if (error.response) {
|
||||||
|
87
src/frontend/src/components/nav/PanelGroup.tsx
Normal file
87
src/frontend/src/components/nav/PanelGroup.tsx
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import { Tabs } from '@mantine/core';
|
||||||
|
import { ReactNode } from 'react';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type used to specify a single panel in a panel group
|
||||||
|
*/
|
||||||
|
export type PanelType = {
|
||||||
|
name: string;
|
||||||
|
label: string;
|
||||||
|
icon?: ReactNode;
|
||||||
|
content: ReactNode;
|
||||||
|
hidden?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param panels : PanelDefinition[] - The list of panels to display
|
||||||
|
* @param activePanel : string - The name of the currently active panel (defaults to the first panel)
|
||||||
|
* @param setActivePanel : (panel: string) => void - Function to set the active panel
|
||||||
|
* @param onPanelChange : (panel: string) => void - Callback when the active panel changes
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function PanelGroup({
|
||||||
|
panels,
|
||||||
|
selectedPanel,
|
||||||
|
onPanelChange
|
||||||
|
}: {
|
||||||
|
panels: PanelType[];
|
||||||
|
selectedPanel?: string;
|
||||||
|
onPanelChange?: (panel: string) => void;
|
||||||
|
}): ReactNode {
|
||||||
|
// Default to the provided panel name, or the first panel
|
||||||
|
const [activePanelName, setActivePanelName] = useState<string>(
|
||||||
|
selectedPanel || panels.length > 0 ? panels[0].name : ''
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update the active panel when the selected panel changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedPanel) {
|
||||||
|
setActivePanelName(selectedPanel);
|
||||||
|
}
|
||||||
|
}, [selectedPanel]);
|
||||||
|
|
||||||
|
// Callback when the active panel changes
|
||||||
|
function handlePanelChange(panel: string) {
|
||||||
|
setActivePanelName(panel);
|
||||||
|
|
||||||
|
// Optionally call external callback hook
|
||||||
|
if (onPanelChange) {
|
||||||
|
onPanelChange(panel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tabs
|
||||||
|
value={activePanelName}
|
||||||
|
orientation="vertical"
|
||||||
|
onTabChange={handlePanelChange}
|
||||||
|
keepMounted={false}
|
||||||
|
>
|
||||||
|
<Tabs.List>
|
||||||
|
{panels.map(
|
||||||
|
(panel, idx) =>
|
||||||
|
!panel.hidden && (
|
||||||
|
<Tabs.Tab
|
||||||
|
value={panel.name}
|
||||||
|
icon={panel.icon}
|
||||||
|
hidden={panel.hidden}
|
||||||
|
>
|
||||||
|
{panel.label}
|
||||||
|
</Tabs.Tab>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</Tabs.List>
|
||||||
|
{panels.map(
|
||||||
|
(panel, idx) =>
|
||||||
|
!panel.hidden && (
|
||||||
|
<Tabs.Panel key={idx} value={panel.name}>
|
||||||
|
{panel.content}
|
||||||
|
</Tabs.Panel>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</Tabs>
|
||||||
|
);
|
||||||
|
}
|
@ -25,7 +25,8 @@ import {
|
|||||||
IconX
|
IconX
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
import { api } from '../../App';
|
import { api } from '../../App';
|
||||||
import { RenderInstance } from '../render/Instance';
|
import { RenderInstance } from '../render/Instance';
|
||||||
@ -172,10 +173,12 @@ function buildSearchQueries(): SearchQuery[] {
|
|||||||
*/
|
*/
|
||||||
function QueryResultGroup({
|
function QueryResultGroup({
|
||||||
query,
|
query,
|
||||||
onRemove
|
onRemove,
|
||||||
|
onResultClick
|
||||||
}: {
|
}: {
|
||||||
query: SearchQuery;
|
query: SearchQuery;
|
||||||
onRemove: (query: string) => void;
|
onRemove: (query: string) => void;
|
||||||
|
onResultClick: (query: string, pk: number) => void;
|
||||||
}) {
|
}) {
|
||||||
if (query.results.count == 0) {
|
if (query.results.count == 0) {
|
||||||
return null;
|
return null;
|
||||||
@ -206,7 +209,13 @@ function QueryResultGroup({
|
|||||||
<Divider />
|
<Divider />
|
||||||
<Stack>
|
<Stack>
|
||||||
{query.results.results.map((result: any) => (
|
{query.results.results.map((result: any) => (
|
||||||
<RenderInstance instance={result} model={query.name} />
|
<div onClick={() => onResultClick(query.name, result.pk)}>
|
||||||
|
<RenderInstance
|
||||||
|
key={`${query.name}-${result.pk}`}
|
||||||
|
instance={result}
|
||||||
|
model={query.name}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</Stack>
|
</Stack>
|
||||||
<Space />
|
<Space />
|
||||||
@ -263,14 +272,8 @@ export function SearchDrawer({
|
|||||||
params[query.name] = query.parameters;
|
params[query.name] = query.parameters;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Cancel any pending search queries
|
|
||||||
getAbortController().abort();
|
|
||||||
|
|
||||||
return api
|
return api
|
||||||
.post(`/search/`, {
|
.post(`/search/`, params)
|
||||||
params: params,
|
|
||||||
signal: getAbortController().signal
|
|
||||||
})
|
|
||||||
.then(function (response) {
|
.then(function (response) {
|
||||||
return response.data;
|
return response.data;
|
||||||
})
|
})
|
||||||
@ -315,26 +318,25 @@ export function SearchDrawer({
|
|||||||
}
|
}
|
||||||
}, [searchQuery.data]);
|
}, [searchQuery.data]);
|
||||||
|
|
||||||
// Controller to cancel previous search queries
|
|
||||||
const abortControllerRef = useRef<AbortController | null>(null);
|
|
||||||
const getAbortController = useCallback(() => {
|
|
||||||
if (!abortControllerRef.current) {
|
|
||||||
abortControllerRef.current = new AbortController();
|
|
||||||
}
|
|
||||||
|
|
||||||
return abortControllerRef.current;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Callback to remove a set of results from the list
|
// Callback to remove a set of results from the list
|
||||||
function removeResults(query: string) {
|
function removeResults(query: string) {
|
||||||
setQueryResults(queryResults.filter((q) => q.name != query));
|
setQueryResults(queryResults.filter((q) => q.name != query));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Callback when the drawer is closed
|
||||||
function closeDrawer() {
|
function closeDrawer() {
|
||||||
setValue('');
|
setValue('');
|
||||||
onClose();
|
onClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
// Callback when one of the search results is clicked
|
||||||
|
function onResultClick(query: string, pk: number) {
|
||||||
|
closeDrawer();
|
||||||
|
navigate(`/${query}/${pk}/`);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Drawer
|
<Drawer
|
||||||
opened={opened}
|
opened={opened}
|
||||||
@ -410,6 +412,7 @@ export function SearchDrawer({
|
|||||||
<QueryResultGroup
|
<QueryResultGroup
|
||||||
query={query}
|
query={query}
|
||||||
onRemove={(query) => removeResults(query)}
|
onRemove={(query) => removeResults(query)}
|
||||||
|
onResultClick={(query, pk) => onResultClick(query, pk)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
@ -25,7 +25,7 @@ export function RowActions({
|
|||||||
}): ReactNode {
|
}): ReactNode {
|
||||||
return (
|
return (
|
||||||
actions.length > 0 && (
|
actions.length > 0 && (
|
||||||
<Menu>
|
<Menu withinPortal={true}>
|
||||||
<Menu.Target>
|
<Menu.Target>
|
||||||
<ActionIcon variant="subtle" color="gray">
|
<ActionIcon variant="subtle" color="gray">
|
||||||
<IconDots />
|
<IconDots />
|
||||||
|
@ -2,6 +2,7 @@ import { t } from '@lingui/macro';
|
|||||||
import { Text } from '@mantine/core';
|
import { Text } from '@mantine/core';
|
||||||
import { IconEdit, IconTrash } from '@tabler/icons-react';
|
import { IconEdit, IconTrash } from '@tabler/icons-react';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
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';
|
||||||
@ -190,13 +191,11 @@ function partTableParams(params: any): any {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export function PartListTable({ params = {} }: { params?: any }) {
|
export function PartListTable({ params = {} }: { params?: any }) {
|
||||||
let tableParams = useMemo(() => partTableParams(params), []);
|
let tableParams = useMemo(() => partTableParams(params), [params]);
|
||||||
let tableColumns = useMemo(() => partTableColumns(), []);
|
let tableColumns = useMemo(() => partTableColumns(), []);
|
||||||
let tableFilters = useMemo(() => partTableFilters(), []);
|
let tableFilters = useMemo(() => partTableFilters(), []);
|
||||||
|
|
||||||
// Add required query parameters
|
// Callback function for generating set of row actions
|
||||||
tableParams.category_detail = true;
|
|
||||||
|
|
||||||
function partTableRowActions(record: any): RowAction[] {
|
function partTableRowActions(record: any): RowAction[] {
|
||||||
let actions: RowAction[] = [];
|
let actions: RowAction[] = [];
|
||||||
|
|
||||||
@ -213,16 +212,18 @@ export function PartListTable({ params = {} }: { params?: any }) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (record.IPN) {
|
|
||||||
actions.push({
|
actions.push({
|
||||||
title: t`View IPN`,
|
title: t`Detail`,
|
||||||
onClick: () => {}
|
onClick: () => {
|
||||||
});
|
navigate(`/part/${record.pk}/`);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return actions;
|
return actions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InvenTreeTable
|
<InvenTreeTable
|
||||||
url="part/"
|
url="part/"
|
||||||
|
@ -23,7 +23,7 @@ export const footerLinks = [
|
|||||||
export const navTabs = [
|
export const navTabs = [
|
||||||
{ text: <Trans>Home</Trans>, name: 'home' },
|
{ text: <Trans>Home</Trans>, name: 'home' },
|
||||||
{ text: <Trans>Dashboard</Trans>, name: 'dashboard' },
|
{ text: <Trans>Dashboard</Trans>, name: 'dashboard' },
|
||||||
{ text: <Trans>Parts</Trans>, name: 'parts' },
|
{ text: <Trans>Parts</Trans>, name: 'part' },
|
||||||
{ text: <Trans>Stock</Trans>, name: 'stock' },
|
{ text: <Trans>Stock</Trans>, name: 'stock' },
|
||||||
{ text: <Trans>Build</Trans>, name: 'build' }
|
{ text: <Trans>Build</Trans>, name: 'build' }
|
||||||
];
|
];
|
||||||
|
@ -97,7 +97,8 @@ export function editPart({
|
|||||||
url: '/part/',
|
url: '/part/',
|
||||||
pk: part_id,
|
pk: part_id,
|
||||||
successMessage: t`Part updated`,
|
successMessage: t`Part updated`,
|
||||||
fields: partFields({ editing: true })
|
fields: partFields({ editing: true }),
|
||||||
|
onFormSuccess: callback
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,20 +0,0 @@
|
|||||||
import { Trans } from '@lingui/macro';
|
|
||||||
import { Group } from '@mantine/core';
|
|
||||||
|
|
||||||
import { PlaceholderPill } from '../../components/items/Placeholder';
|
|
||||||
import { StylishText } from '../../components/items/StylishText';
|
|
||||||
import { PartListTable } from '../../components/tables/part/PartTable';
|
|
||||||
|
|
||||||
export default function Part() {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Group>
|
|
||||||
<StylishText>
|
|
||||||
<Trans>Parts</Trans>
|
|
||||||
</StylishText>
|
|
||||||
<PlaceholderPill />
|
|
||||||
</Group>
|
|
||||||
<PartListTable />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
200
src/frontend/src/pages/part/PartDetail.tsx
Normal file
200
src/frontend/src/pages/part/PartDetail.tsx
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Group,
|
||||||
|
LoadingOverlay,
|
||||||
|
Skeleton,
|
||||||
|
Space,
|
||||||
|
Stack,
|
||||||
|
Tabs,
|
||||||
|
Text
|
||||||
|
} from '@mantine/core';
|
||||||
|
import {
|
||||||
|
IconBox,
|
||||||
|
IconBuilding,
|
||||||
|
IconCurrencyDollar,
|
||||||
|
IconInfoCircle,
|
||||||
|
IconLayersLinked,
|
||||||
|
IconList,
|
||||||
|
IconListTree,
|
||||||
|
IconNotes,
|
||||||
|
IconPackages,
|
||||||
|
IconPaperclip,
|
||||||
|
IconShoppingCart,
|
||||||
|
IconTestPipe,
|
||||||
|
IconTools,
|
||||||
|
IconTruckDelivery,
|
||||||
|
IconVersions
|
||||||
|
} from '@tabler/icons-react';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { api } from '../../App';
|
||||||
|
import { PanelGroup, PanelType } from '../../components/nav/PanelGroup';
|
||||||
|
import { StockItemTable } from '../../components/tables/stock/StockItemTable';
|
||||||
|
import { editPart } from '../../functions/forms/PartForms';
|
||||||
|
|
||||||
|
export default function PartDetail() {
|
||||||
|
const { id } = useParams();
|
||||||
|
|
||||||
|
// Part data
|
||||||
|
const [part, setPart] = useState<any>({});
|
||||||
|
|
||||||
|
// Part data panels (recalculate when part data changes)
|
||||||
|
const partPanels: PanelType[] = useMemo(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: 'details',
|
||||||
|
label: t`Details`,
|
||||||
|
icon: <IconInfoCircle size="18" />,
|
||||||
|
content: <Text>part details go here</Text>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'stock',
|
||||||
|
label: t`Stock`,
|
||||||
|
icon: <IconPackages size="18" />,
|
||||||
|
content: partStockTab()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'variants',
|
||||||
|
label: t`Variants`,
|
||||||
|
icon: <IconVersions size="18" />,
|
||||||
|
hidden: !part.is_template,
|
||||||
|
content: <Text>part variants go here</Text>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'bom',
|
||||||
|
label: t`Bill of Materials`,
|
||||||
|
icon: <IconListTree size="18" />,
|
||||||
|
hidden: !part.assembly,
|
||||||
|
content: part.assembly && <Text>part BOM goes here</Text>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'builds',
|
||||||
|
label: t`Build Orders`,
|
||||||
|
icon: <IconTools size="18" />,
|
||||||
|
hidden: !part.assembly && !part.component,
|
||||||
|
content: <Text>part builds go here</Text>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'used_in',
|
||||||
|
label: t`Used In`,
|
||||||
|
icon: <IconList size="18" />,
|
||||||
|
hidden: !part.component,
|
||||||
|
content: <Text>part used in goes here</Text>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'pricing',
|
||||||
|
label: t`Pricing`,
|
||||||
|
icon: <IconCurrencyDollar size="18" />,
|
||||||
|
content: <Text>part pricing goes here</Text>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'suppliers',
|
||||||
|
label: t`Suppliers`,
|
||||||
|
icon: <IconBuilding size="18" />,
|
||||||
|
content: <Text>part suppliers go here</Text>,
|
||||||
|
hidden: !part.purchaseable
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'purchase_orders',
|
||||||
|
label: t`Purchase Orders`,
|
||||||
|
icon: <IconShoppingCart size="18" />,
|
||||||
|
content: <Text>part purchase orders go here</Text>,
|
||||||
|
hidden: !part.purchaseable
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'sales_orders',
|
||||||
|
label: t`Sales Orders`,
|
||||||
|
icon: <IconTruckDelivery size="18" />,
|
||||||
|
content: <Text>part sales orders go here</Text>,
|
||||||
|
hidden: !part.salable
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'test_templates',
|
||||||
|
label: t`Test Templates`,
|
||||||
|
icon: <IconTestPipe size="18" />,
|
||||||
|
content: <Text>part test templates go here</Text>,
|
||||||
|
hidden: !part.trackable
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'related_parts',
|
||||||
|
label: t`Related Parts`,
|
||||||
|
icon: <IconLayersLinked size="18" />,
|
||||||
|
content: <Text>part related parts go here</Text>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'attachments',
|
||||||
|
label: t`Attachments`,
|
||||||
|
icon: <IconPaperclip size="18" />,
|
||||||
|
content: <Text>part attachments go here</Text>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'notes',
|
||||||
|
label: t`Notes`,
|
||||||
|
icon: <IconNotes size="18" />,
|
||||||
|
content: <Text>part notes go here</Text>
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}, [part]);
|
||||||
|
|
||||||
|
// Query hook for fetching part data
|
||||||
|
const partQuery = useQuery(['part', id], async () => {
|
||||||
|
let url = `/part/${id}/`;
|
||||||
|
|
||||||
|
return api
|
||||||
|
.get(url)
|
||||||
|
.then((response) => {
|
||||||
|
setPart(response.data);
|
||||||
|
return response.data;
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
setPart({});
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function partStockTab(): React.ReactNode {
|
||||||
|
return (
|
||||||
|
<StockItemTable
|
||||||
|
params={{
|
||||||
|
part: part.pk ?? -1
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Stack spacing="xs">
|
||||||
|
<LoadingOverlay visible={partQuery.isFetching} />
|
||||||
|
<Group position="apart">
|
||||||
|
<Group position="left">
|
||||||
|
<Text size="lg">Part Detail</Text>
|
||||||
|
<Text>{part.name}</Text>
|
||||||
|
<Text size="sm">{part.description}</Text>
|
||||||
|
</Group>
|
||||||
|
<Space />
|
||||||
|
<Text>In Stock: {part.total_in_stock}</Text>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
color="blue"
|
||||||
|
onClick={() =>
|
||||||
|
part.pk &&
|
||||||
|
editPart({
|
||||||
|
part_id: part.pk,
|
||||||
|
callback: () => {
|
||||||
|
partQuery.refetch();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
<PanelGroup panels={partPanels} />
|
||||||
|
</Stack>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
52
src/frontend/src/pages/part/PartIndex.tsx
Normal file
52
src/frontend/src/pages/part/PartIndex.tsx
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
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 { 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 spacing="xs">
|
||||||
|
<StylishText>
|
||||||
|
<Trans>Parts</Trans>
|
||||||
|
</StylishText>
|
||||||
|
<PanelGroup panels={panels} />
|
||||||
|
</Stack>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -11,7 +11,7 @@ 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 Parts = Loadable(lazy(() => import('./pages/Index/Part')));
|
export const PartIndex = Loadable(lazy(() => import('./pages/part/PartIndex')));
|
||||||
export const Stock = Loadable(lazy(() => import('./pages/Index/Stock')));
|
export const Stock = Loadable(lazy(() => import('./pages/Index/Stock')));
|
||||||
export const Build = Loadable(lazy(() => import('./pages/Index/Build')));
|
export const Build = Loadable(lazy(() => import('./pages/Index/Build')));
|
||||||
|
|
||||||
@ -22,6 +22,11 @@ export const ErrorPage = Loadable(lazy(() => import('./pages/ErrorPage')));
|
|||||||
export const Profile = Loadable(
|
export const Profile = Loadable(
|
||||||
lazy(() => import('./pages/Index/Profile/Profile'))
|
lazy(() => import('./pages/Index/Profile/Profile'))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const PartDetail = Loadable(
|
||||||
|
lazy(() => import('./pages/part/PartDetail'))
|
||||||
|
);
|
||||||
|
|
||||||
export const NotFound = Loadable(lazy(() => import('./pages/NotFound')));
|
export const NotFound = Loadable(lazy(() => import('./pages/NotFound')));
|
||||||
export const Login = Loadable(lazy(() => import('./pages/Auth/Login')));
|
export const Login = Loadable(lazy(() => import('./pages/Auth/Login')));
|
||||||
export const Logged_In = Loadable(lazy(() => import('./pages/Auth/Logged-In')));
|
export const Logged_In = Loadable(lazy(() => import('./pages/Auth/Logged-In')));
|
||||||
@ -60,8 +65,12 @@ export const router = createBrowserRouter(
|
|||||||
element: <Playground />
|
element: <Playground />
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'parts/',
|
path: 'part/',
|
||||||
element: <Parts />
|
element: <PartIndex />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'part/:id',
|
||||||
|
element: <PartDetail />
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'stock/',
|
path: 'stock/',
|
||||||
|
Loading…
x
Reference in New Issue
Block a user