[PUI] Small updates (#6320)

* Ensure .ts files are generated

- "yarn run compile" before "yarn run dev"
- ensures that .ts locale files are all generated

* Implement "Add Part Category" button

* Create new stock location

* Rename customActionGroups to tableActions

* Rename customFilters to tableFilters

* Edit category from table

* Edit stock location from table

* Add some placeholder buttons

* More placeholders
This commit is contained in:
Oliver 2024-01-23 00:55:44 +11:00 committed by GitHub
parent ab921ccb31
commit d502d93380
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
35 changed files with 287 additions and 71 deletions

View File

@ -20,8 +20,10 @@ export type ActionButtonProps = {
* Construct a simple action button with consistent styling
*/
export function ActionButton(props: ActionButtonProps) {
const hidden = props.hidden ?? false;
return (
!props.hidden && (
!hidden && (
<Tooltip
key={`tooltip-${props.key}`}
disabled={!props.tooltip && !props.text}

View File

@ -234,7 +234,6 @@ export function SearchDrawer({
// Re-fetch data whenever the search term is updated
useEffect(() => {
// TODO: Implement search functionality
searchQuery.refetch();
}, [searchText]);

View File

@ -1,3 +1,4 @@
import { t } from '@lingui/macro';
import { ReactNode } from 'react';
import { RenderInlineModel } from './Instance';
@ -6,10 +7,13 @@ import { RenderInlineModel } from './Instance';
* Inline rendering of a single Part instance
*/
export function RenderPart({ instance }: { instance: any }): ReactNode {
const stock = t`Stock` + `: ${instance.in_stock}`;
return (
<RenderInlineModel
primary={instance.name}
secondary={instance.description}
suffix={stock}
image={instance.thumnbnail || instance.image}
/>
);

View File

@ -46,8 +46,8 @@ const defaultPageSize: number = 25;
* @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 tableFilters : TableFilter[] - List of custom filters
* @param tableActions : any[] - List of custom action groups
* @param printingActions : any[] - List of printing actions
* @param dataFormatter : (data: any) => any - Callback function to reformat data returned by server (if not in default format)
* @param rowActions : (record: any) => RowAction[] - Callback function to generate row actions
@ -66,8 +66,8 @@ export type InvenTreeTableProps<T = any> = {
enableRefresh?: boolean;
pageSize?: number;
barcodeActions?: any[];
customFilters?: TableFilter[];
customActionGroups?: React.ReactNode[];
tableFilters?: TableFilter[];
tableActions?: React.ReactNode[];
printingActions?: any[];
idAccessor?: string;
dataFormatter?: (data: T) => any;
@ -91,8 +91,8 @@ const defaultInvenTreeTableProps: InvenTreeTableProps = {
defaultSortColumn: '',
printingActions: [],
barcodeActions: [],
customFilters: [],
customActionGroups: [],
tableFilters: [],
tableActions: [],
idAccessor: 'pk',
onRowClick: (record: any, index: number, event: any) => {}
};
@ -425,9 +425,9 @@ export function InvenTreeTable<T = any>({
return (
<>
{tableProps.enableFilters &&
(tableProps.customFilters?.length ?? 0) > 0 && (
(tableProps.tableFilters?.length ?? 0) > 0 && (
<FilterSelectDrawer
availableFilters={tableProps.customFilters ?? []}
availableFilters={tableProps.tableFilters ?? []}
tableState={tableState}
opened={filtersVisible}
onClose={() => setFiltersVisible(false)}
@ -436,7 +436,7 @@ export function InvenTreeTable<T = any>({
<Stack spacing="sm">
<Group position="apart">
<Group position="left" key="custom-actions" spacing={5}>
{tableProps.customActionGroups?.map((group, idx) => (
{tableProps.tableActions?.map((group, idx) => (
<Fragment key={idx}>{group}</Fragment>
))}
{(tableProps.barcodeActions?.length ?? 0 > 0) && (
@ -490,7 +490,7 @@ export function InvenTreeTable<T = any>({
/>
)}
{tableProps.enableFilters &&
(tableProps.customFilters?.length ?? 0 > 0) && (
(tableProps.tableFilters?.length ?? 0 > 0) && (
<Indicator
size="xs"
label={tableState.activeFilters.length}

View File

@ -363,7 +363,7 @@ export function BomTable({
part_detail: true,
sub_part_detail: true
},
customFilters: tableFilters,
tableFilters: tableFilters,
onRowClick: (row) => navigate(`/part/${row.sub_part}`),
rowActions: rowActions
}}

View File

@ -116,7 +116,7 @@ export function UsedInTable({
part_detail: true,
sub_part_detail: true
},
customFilters: tableFilters,
tableFilters: tableFilters,
onRowClick: (row) => navigate(`/part/${row.part}`)
}}
/>

View File

@ -155,7 +155,7 @@ export function BuildOrderTable({ params = {} }: { params?: any }) {
...params,
part_detail: true
},
customFilters: tableFilters,
tableFilters: tableFilters,
onRowClick: (row) => navigate(`/build/${row.pk}`)
}}
/>

View File

@ -185,7 +185,7 @@ export function AddressTable({
columns={columns}
props={{
rowActions: rowActions,
customActionGroups: tableActions,
tableActions: tableActions,
params: {
...params,
company: companyId

View File

@ -133,7 +133,7 @@ export function ContactTable({
columns={columns}
props={{
rowActions: rowActions,
customActionGroups: tableActions,
tableActions: tableActions,
params: {
...params,
company: companyId

View File

@ -178,7 +178,7 @@ export function AttachmentTable({
});
}
const customActionGroups: ReactNode[] = useMemo(() => {
const tableActions: ReactNode[] = useMemo(() => {
let actions = [];
if (allowEdit) {
@ -235,7 +235,7 @@ export function AttachmentTable({
props={{
noRecordsText: t`No attachments found`,
enableSelection: true,
customActionGroups: customActionGroups,
tableActions: tableActions,
rowActions: allowEdit && allowDelete ? rowActions : undefined,
params: {
[model]: pk

View File

@ -1,23 +1,30 @@
import { t } from '@lingui/macro';
import { useMemo } from 'react';
import { useCallback, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { ApiPaths } from '../../../enums/ApiEndpoints';
import { UserRoles } from '../../../enums/Roles';
import { partCategoryFields } from '../../../forms/PartForms';
import { openCreateApiForm, openEditApiForm } from '../../../functions/forms';
import { useTable } from '../../../hooks/UseTable';
import { apiUrl } from '../../../states/ApiState';
import { useUserState } from '../../../states/UserState';
import { AddItemButton } from '../../buttons/AddItemButton';
import { YesNoButton } from '../../items/YesNoButton';
import { TableColumn } from '../Column';
import { DescriptionColumn } from '../ColumnRenderers';
import { TableFilter } from '../Filter';
import { InvenTreeTable } from '../InvenTreeTable';
import { RowEditAction } from '../RowActions';
/**
* PartCategoryTable - Displays a table of part categories
*/
export function PartCategoryTable({ params = {} }: { params?: any }) {
export function PartCategoryTable({ parentId }: { parentId?: any }) {
const navigate = useNavigate();
const table = useTable('partcategory');
const user = useUserState();
const tableColumns: TableColumn[] = useMemo(() => {
return [
@ -64,6 +71,62 @@ export function PartCategoryTable({ params = {} }: { params?: any }) {
];
}, []);
const addCategory = useCallback(() => {
let fields = partCategoryFields({});
if (parentId) {
fields['parent'].value = parentId;
}
openCreateApiForm({
url: apiUrl(ApiPaths.category_list),
title: t`Add Part Category`,
fields: fields,
onFormSuccess(data: any) {
if (data.pk) {
navigate(`/part/category/${data.pk}`);
} else {
table.refreshTable();
}
}
});
}, [parentId]);
const tableActions = useMemo(() => {
let can_add = user.hasAddRole(UserRoles.part_category);
return [
<AddItemButton
tooltip={t`Add Part Category`}
onClick={addCategory}
disabled={!can_add}
/>
];
}, [user]);
const rowActions = useCallback(
(record: any) => {
let can_edit = user.hasChangeRole(UserRoles.part_category);
return [
RowEditAction({
hidden: !can_edit,
onClick: () => {
openEditApiForm({
url: ApiPaths.category_list,
pk: record.pk,
title: t`Edit Part Category`,
fields: partCategoryFields({}),
successMessage: t`Part category updated`,
onFormSuccess: table.refreshTable
});
}
})
];
},
[user]
);
return (
<InvenTreeTable
url={apiUrl(ApiPaths.category_list)}
@ -71,11 +134,12 @@ export function PartCategoryTable({ params = {} }: { params?: any }) {
columns={tableColumns}
props={{
enableDownload: true,
enableSelection: true,
params: {
...params
parent: parentId ?? 'null'
},
customFilters: tableFilters,
tableFilters: tableFilters,
tableActions: tableActions,
rowActions: rowActions,
onRowClick: (record, index, event) => {
navigate(`/part/category/${record.pk}`);
}

View File

@ -180,8 +180,8 @@ export function PartParameterTable({ partId }: { partId: any }) {
columns={tableColumns}
props={{
rowActions: rowActions,
customActionGroups: tableActions,
customFilters: [
tableActions: tableActions,
tableFilters: [
{
name: 'include_variants',
label: t`Include Variants`,

View File

@ -131,8 +131,8 @@ export default function PartParameterTemplateTable() {
columns={tableColumns}
props={{
rowActions: rowActions,
customFilters: tableFilters,
customActionGroups: tableActions
tableFilters: tableFilters,
tableActions: tableActions
}}
/>
);

View File

@ -275,7 +275,7 @@ export function PartListTable({ props }: { props: InvenTreeTableProps }) {
props={{
...props,
enableDownload: true,
customFilters: tableFilters,
tableFilters: tableFilters,
params: {
...props.params,
category_detail: true

View File

@ -140,8 +140,8 @@ export default function PartTestTemplateTable({ partId }: { partId: number }) {
params: {
part: partId
},
customFilters: tableFilters,
customActionGroups: tableActions,
tableFilters: tableFilters,
tableActions: tableActions,
rowActions: rowActions
}}
/>

View File

@ -37,7 +37,7 @@ export function PartVariantTable({ partId }: { partId: string }) {
<PartListTable
props={{
enableDownload: false,
customFilters: tableFilters,
tableFilters: tableFilters,
params: {
ancestor: partId
}

View File

@ -133,7 +133,7 @@ export function RelatedPartTable({ partId }: { partId: number }): ReactNode {
category_detail: true
},
rowActions: rowActions,
customActionGroups: customActions
tableActions: customActions
}}
/>
);

View File

@ -493,8 +493,8 @@ export function PluginListTable({ props }: { props: InvenTreeTableProps }) {
},
rowActions: rowActions,
onRowClick: (plugin) => navigate(`${plugin.pk}/`),
customActionGroups: tableActions,
customFilters: [
tableActions: tableActions,
tableFilters: [
{
name: 'active',
label: t`Active`,

View File

@ -5,9 +5,11 @@ import { ApiPaths } from '../../../enums/ApiEndpoints';
import { UserRoles } from '../../../enums/Roles';
import { useManufacturerPartFields } from '../../../forms/CompanyForms';
import { openDeleteApiForm, openEditApiForm } from '../../../functions/forms';
import { notYetImplemented } from '../../../functions/notifications';
import { useTable } from '../../../hooks/UseTable';
import { apiUrl } from '../../../states/ApiState';
import { useUserState } from '../../../states/UserState';
import { AddItemButton } from '../../buttons/AddItemButton';
import { Thumbnail } from '../../images/Thumbnail';
import { TableColumn } from '../Column';
import { DescriptionColumn, LinkColumn, PartColumn } from '../ColumnRenderers';
@ -57,9 +59,22 @@ export function ManufacturerPartTable({ params }: { params: any }): ReactNode {
];
}, [params]);
const addManufacturerPart = useCallback(() => {
notYetImplemented();
}, []);
const tableActions = useMemo(() => {
// TODO: Custom actions
return [];
let can_add =
user.hasAddRole(UserRoles.purchase_order) &&
user.hasAddRole(UserRoles.part);
return [
<AddItemButton
tooltip={t`Add Manufacturer Part`}
onClick={addManufacturerPart}
hidden={!can_add}
/>
];
}, [user]);
const editManufacturerPartFields = useManufacturerPartFields();
@ -112,7 +127,7 @@ export function ManufacturerPartTable({ params }: { params: any }): ReactNode {
manufacturer_detail: true
},
rowActions: rowActions,
customActionGroups: tableActions
tableActions: tableActions
}}
/>
);

View File

@ -260,7 +260,7 @@ export function PurchaseOrderLineItemTable({
part_detail: true
},
rowActions: rowActions,
customActionGroups: tableActions
tableActions: tableActions
}}
/>
);

View File

@ -1,11 +1,15 @@
import { t } from '@lingui/macro';
import { useMemo } from 'react';
import { useCallback, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { ApiPaths } from '../../../enums/ApiEndpoints';
import { ModelType } from '../../../enums/ModelType';
import { UserRoles } from '../../../enums/Roles';
import { notYetImplemented } from '../../../functions/notifications';
import { useTable } from '../../../hooks/UseTable';
import { apiUrl } from '../../../states/ApiState';
import { useUserState } from '../../../states/UserState';
import { AddItemButton } from '../../buttons/AddItemButton';
import { Thumbnail } from '../../images/Thumbnail';
import {
CreationDateColumn,
@ -33,6 +37,7 @@ export function PurchaseOrderTable({ params }: { params?: any }) {
const navigate = useNavigate();
const table = useTable('purchase-order');
const user = useUserState();
const tableFilters: TableFilter[] = useMemo(() => {
return [
@ -94,6 +99,20 @@ export function PurchaseOrderTable({ params }: { params?: any }) {
];
}, []);
const addPurchaseOrder = useCallback(() => {
notYetImplemented();
}, []);
const tableActions = useMemo(() => {
return [
<AddItemButton
tooltip={t`Add Purchase Order`}
onClick={addPurchaseOrder}
hidden={!user.hasAddRole(UserRoles.purchase_order)}
/>
];
}, [user]);
return (
<InvenTreeTable
url={apiUrl(ApiPaths.purchase_order_list)}
@ -104,7 +123,8 @@ export function PurchaseOrderTable({ params }: { params?: any }) {
...params,
supplier_detail: true
},
customFilters: tableFilters,
tableFilters: tableFilters,
tableActions: tableActions,
onRowClick: (row: any) => {
if (row.pk) {
navigate(`/purchasing/purchase-order/${row.pk}`);

View File

@ -229,7 +229,7 @@ export function SupplierPartTable({ params }: { params: any }): ReactNode {
manufacturer_detail: true
},
rowActions: rowActions,
customActionGroups: tableActions
tableActions: tableActions
}}
/>
</>

View File

@ -1,11 +1,15 @@
import { t } from '@lingui/macro';
import { useMemo } from 'react';
import { useCallback, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { ApiPaths } from '../../../enums/ApiEndpoints';
import { ModelType } from '../../../enums/ModelType';
import { UserRoles } from '../../../enums/Roles';
import { notYetImplemented } from '../../../functions/notifications';
import { useTable } from '../../../hooks/UseTable';
import { apiUrl } from '../../../states/ApiState';
import { useUserState } from '../../../states/UserState';
import { AddItemButton } from '../../buttons/AddItemButton';
import { Thumbnail } from '../../images/Thumbnail';
import {
CreationDateColumn,
@ -27,6 +31,7 @@ import { InvenTreeTable } from '../InvenTreeTable';
export function ReturnOrderTable({ params }: { params?: any }) {
const table = useTable('return-orders');
const user = useUserState();
const navigate = useNavigate();
@ -90,6 +95,20 @@ export function ReturnOrderTable({ params }: { params?: any }) {
];
}, []);
const addReturnOrder = useCallback(() => {
notYetImplemented();
}, []);
const tableActions = useMemo(() => {
return [
<AddItemButton
tooltip={t`Add Return Order`}
onClick={addReturnOrder}
hidden={!user.hasAddRole(UserRoles.sales_order)}
/>
];
}, [user]);
return (
<InvenTreeTable
url={apiUrl(ApiPaths.return_order_list)}
@ -100,7 +119,8 @@ export function ReturnOrderTable({ params }: { params?: any }) {
...params,
customer_detail: true
},
customFilters: tableFilters,
tableFilters: tableFilters,
tableActions: tableActions,
onRowClick: (row: any) => {
if (row.pk) {
navigate(`/sales/return-order/${row.pk}/`);

View File

@ -1,11 +1,15 @@
import { t } from '@lingui/macro';
import { useMemo } from 'react';
import { useCallback, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { ApiPaths } from '../../../enums/ApiEndpoints';
import { ModelType } from '../../../enums/ModelType';
import { UserRoles } from '../../../enums/Roles';
import { notYetImplemented } from '../../../functions/notifications';
import { useTable } from '../../../hooks/UseTable';
import { apiUrl } from '../../../states/ApiState';
import { useUserState } from '../../../states/UserState';
import { AddItemButton } from '../../buttons/AddItemButton';
import { Thumbnail } from '../../images/Thumbnail';
import {
CreationDateColumn,
@ -28,6 +32,7 @@ import { InvenTreeTable } from '../InvenTreeTable';
export function SalesOrderTable({ params }: { params?: any }) {
const table = useTable('sales-order');
const user = useUserState();
const navigate = useNavigate();
@ -91,6 +96,20 @@ export function SalesOrderTable({ params }: { params?: any }) {
];
}, []);
const addSalesOrder = useCallback(() => {
notYetImplemented();
}, []);
const tableActions = useMemo(() => {
return [
<AddItemButton
tooltip={t`Add Sales Order`}
onClick={addSalesOrder}
hidden={!user.hasAddRole(UserRoles.sales_order)}
/>
];
}, [user]);
return (
<InvenTreeTable
url={apiUrl(ApiPaths.sales_order_list)}
@ -101,7 +120,8 @@ export function SalesOrderTable({ params }: { params?: any }) {
...params,
customer_detail: true
},
customFilters: tableFilters,
tableFilters: tableFilters,
tableActions: tableActions,
onRowClick: (row: any) => {
if (row.pk) {
navigate(`/sales/sales-order/${row.pk}/`);

View File

@ -66,7 +66,7 @@ export function CurrencyTable() {
tableState={table}
columns={columns}
props={{
customActionGroups: tableActions,
tableActions: tableActions,
dataFormatter: (data) => {
let rates = data?.exchange_rates ?? {};

View File

@ -117,7 +117,7 @@ export default function CustomUnitsTable() {
columns={columns}
props={{
rowActions: rowActions,
customActionGroups: tableActions
tableActions: tableActions
}}
/>
);

View File

@ -166,7 +166,7 @@ export function GroupTable() {
columns={columns}
props={{
rowActions: rowActions,
customActionGroups: tableActions,
tableActions: tableActions,
onRowClick: (record) => openDetailDrawer(record.pk)
}}
/>

View File

@ -106,7 +106,7 @@ export default function ProjectCodeTable() {
columns={columns}
props={{
rowActions: rowActions,
customActionGroups: tableActions
tableActions: tableActions
}}
/>
);

View File

@ -261,7 +261,7 @@ export function UserTable() {
columns={columns}
props={{
rowActions: rowActions,
customActionGroups: tableActions,
tableActions: tableActions,
onRowClick: (record) => openDetailDrawer(record.pk)
}}
/>

View File

@ -344,7 +344,7 @@ export function StockItemTable({ params = {} }: { params?: any }) {
props={{
enableDownload: true,
enableSelection: true,
customFilters: tableFilters,
tableFilters: tableFilters,
onRowClick: (record) => navigate(`/stock/item/${record.pk}`),
params: {
...params,

View File

@ -1,21 +1,28 @@
import { t } from '@lingui/macro';
import { useMemo } from 'react';
import { useCallback, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { ApiPaths } from '../../../enums/ApiEndpoints';
import { UserRoles } from '../../../enums/Roles';
import { stockLocationFields } from '../../../forms/StockForms';
import { openCreateApiForm, openEditApiForm } from '../../../functions/forms';
import { useTable } from '../../../hooks/UseTable';
import { apiUrl } from '../../../states/ApiState';
import { useUserState } from '../../../states/UserState';
import { AddItemButton } from '../../buttons/AddItemButton';
import { YesNoButton } from '../../items/YesNoButton';
import { TableColumn } from '../Column';
import { DescriptionColumn } from '../ColumnRenderers';
import { TableFilter } from '../Filter';
import { InvenTreeTable } from '../InvenTreeTable';
import { RowEditAction } from '../RowActions';
/**
* Stock location table
*/
export function StockLocationTable({ params = {} }: { params?: any }) {
export function StockLocationTable({ parentId }: { parentId?: any }) {
const table = useTable('stocklocation');
const user = useUserState();
const navigate = useNavigate();
@ -85,7 +92,63 @@ export function StockLocationTable({ params = {} }: { params?: any }) {
render: (record: any) => record.location_type_detail?.name
}
];
}, [params]);
}, []);
const addLocation = useCallback(() => {
let fields = stockLocationFields({});
if (parentId) {
fields['parent'].value = parentId;
}
openCreateApiForm({
url: apiUrl(ApiPaths.stock_location_list),
title: t`Add Stock Location`,
fields: fields,
onFormSuccess(data: any) {
if (data.pk) {
navigate(`/stock/location/${data.pk}`);
} else {
table.refreshTable();
}
}
});
}, [parentId]);
const tableActions = useMemo(() => {
let can_add = user.hasAddRole(UserRoles.stock_location);
return [
<AddItemButton
tooltip={t`Add Stock Location`}
onClick={addLocation}
disabled={!can_add}
/>
];
}, [user]);
const rowActions = useCallback(
(record: any) => {
let can_edit = user.hasChangeRole(UserRoles.stock_location);
return [
RowEditAction({
hidden: !can_edit,
onClick: () => {
openEditApiForm({
url: ApiPaths.stock_location_list,
pk: record.pk,
title: t`Edit Stock Location`,
fields: stockLocationFields({}),
successMessage: t`Stock location updated`,
onFormSuccess: table.refreshTable
});
}
})
];
},
[user]
);
return (
<InvenTreeTable
@ -94,8 +157,12 @@ export function StockLocationTable({ params = {} }: { params?: any }) {
columns={tableColumns}
props={{
enableDownload: true,
params: params,
customFilters: tableFilters,
params: {
parent: parentId ?? 'null'
},
tableFilters: tableFilters,
tableActions: tableActions,
rowActions: rowActions,
onRowClick: (record) => {
navigate(`/stock/location/${record.pk}`);
}

View File

@ -133,3 +133,19 @@ export function useEditStockItem({
onFormSuccess: callback
});
}
export function stockLocationFields({}: {}): ApiFormFieldSet {
let fields: ApiFormFieldSet = {
parent: {
description: t`Parent stock location`,
required: false
},
name: {},
description: {},
structural: {},
external: {},
location_type: {}
};
return fields;
}

View File

@ -64,13 +64,7 @@ export default function CategoryDetail({}: {}) {
name: 'subcategories',
label: t`Part Categories`,
icon: <IconSitemap />,
content: (
<PartCategoryTable
params={{
parent: id
}}
/>
)
content: <PartCategoryTable parentId={id} />
},
{
name: 'parameters',

View File

@ -53,13 +53,7 @@ export default function Stock() {
name: 'sublocations',
label: t`Stock Locations`,
icon: <IconSitemap />,
content: (
<StockLocationTable
params={{
parent: id
}}
/>
)
content: <StockLocationTable parentId={id} />
}
];
}, [location, id]);

View File

@ -978,6 +978,7 @@ def frontend_dev(c):
c: Context variable
"""
print('Starting frontend development server')
yarn(c, 'yarn run compile')
yarn(c, 'yarn run dev')