mirror of
https://github.com/inventree/InvenTree.git
synced 2025-12-16 17:28:11 +00:00
[WIP] Generic parameters (#10699)
* Add ParameterTemplate model - Data structure duplicated from PartParameterTemplate * Apply data migration for templates * Admin integration * API endpoints for ParameterTemplate * Scaffolding * Add validator for ParameterTemplate model type - Update migrations - Make Parameter class abstract (for now) - Validators * API updates - Fix options for model_type - Add API filters * Add definition for Parameter model * Add django admin site integration * Update InvenTreeParameterMixin class - Fetch queryset of all linked Parameter instances - Ensure deletion of linked instances * API endpoints for Parameter instances * Refactor UI table for parameter templates * Add comment for later * Add "enabled" field to ParameterTemplate model * Add new field to serializer * Rough-in new table * Implement generic "parameter" table * Enable parameters for Company model * Change migration for part parameter - Make it "universal" * Remove code for ManufacturerPartParameter * Fix for filters * Add data import for parameter table * Add verbose name to ParameterTemplate model * Removed dead API code * Update global setting * Fix typos * Check global setting for unit validation * Use GenericForeignKey * Add generic relationship to allow reverse lookups * Fixes for table structure * Add custom serializer field for ContentType with choices * Adds ContentTypeField - Handles representation of content type - Provides human-readable options * Refactor API filtering for endpoints - Specify ContentType by ID, model or app label * Revert change to parameters property * Define GenericRelationship for linking model * Refactoring some code * Add a generic way to back-annotate and prefetch parameters for any model type * Change panel position * Directly annotate parameters against different model serializers * remove defunct admin classes * Run plugin validation against parameter * Fix prefetching for PartSerializer * Implement generic "filtering" against queryset * Implement generic "ordering" by parameter * Make parametric table generic * Refactor segmented panels * Consolidate part table views * Fix for parametric part table - Only display parameters for which we know there is a value * Add parametric tables for company views * Fix typo in file name * Prefetch to reduce hits * Add generic API mixin for filtering and ordering by parameter * Fix hook for rebuilding template parameters * Remove serializer * Remove old models * Fix code for copying parameters from category * Implement more parametric tables: - ManufacturerPart - SupplierPart - Fixes and enhancements * Add parameter support for orders * Add UI support for parameters against orders * Update API version * Update CHANGELOG.md * Add parameter support for build orders * Tweak frontend * Add renderer * Remove defunct endpoints * Add migration requirement * Require contenttypes to be updated * Update migration * Try using ID val * Adjust migration dependencies * fix params fixture * fix schema export * fix modelset * Fixes for data migration * tweak table * Fix for Category Parameters * Use branch of demo dataset for testing * Add parameteric build order table * disable broken imports * remove old model from ruleset * correct test * Table tweaks * fix test * Remove old model type * fix test * fix test * Refactor mixin to avoid specifying model type manually * fix test * fix resolve name * remove unneeded import * Tweak unit testing * Fix unit test * Enable bulk-create * More fixes * More unit test tweaks * Enhancements * Unit test fixes * Add some migration tests * Fix admin tests * Fix part tests * adapt expectation * fix remaining typecheck * Docs updates * Rearrange models * fix paramater caching * fix doc links * adjust assumption * Adjust data migration unit tests * docs fixes * Fix docs link * Fixes * Tweak formatting * Add doc for setting * Add metadata view for parameters * Add metadata view for ParamterTemplate * Update CHANGELOG file * Deconflict model_type fields * Invert key:value * Revert "Invert key:value" This reverts commitd555658db2. * fix assert * Update API rev notes * Initial unit tests for API * Test parameter create / edit / delete via the API * Add some more unit tests for the API * Validate queryset annotation - Add unit test with large dataset - Ensure number of queries is fixed - Fix for prefetching check * Add breaking change info to CHANGELOG.md * Ensure that parameters are removed when deleting the linked object * Enhance type hinting * Refactor part parameter exporter plugin - Any model which supports parameters can use this now - Update documentation * Improve serializer field * Adjust unit test * Reimplement checks for locked parts * Fix unit test for data migration * Fix for unit test * Allow disable edit for ParameterTable * Fix supplier part import wizard * Add unit tests for template API filtering * Add playwright tests for purchasing index * Add tests for manufacturing index page * ui tests for sales index * Add data migration tests for ManufacturerPartParameter * Pull specific branch for python binding tests * Specify target migration * Remove debug statement * Tweak migration unit tests * Add options for spectacular * Add explicit choice options * Ensure empty string values are converted to None * Don't use custom branch for python checks * Fix for migration test * Fix migration test * Fix reference target * Remove duplicate enum in spectactular.py * Add null choice to custom serializer class * [UI] Edit shipment details - Pass "pending" status through to the form * New migration strategy: part.0144: - Add new "enabled" field to PartParameterTemplate model - Add new ContentType fields to the "PartParameterTemplate" and "PartParameter" models - Data migration for existing "PartParameter" records part.0145: - Set NOT NULL constraints on new fields - Remove the obsolete "part" field from the "PartParameter" model * More migration updates: - Create new "models" (without moving the existing tables) - Data migration for PartCataegoryParameterTemplate model - Remove PartParameterTemplate and PartParameter models * Overhaul of migration strategy - New models simply point to the old database tables - Perform schema and data migrations on the old models first (in the part app) - Swap model references in correct order * Improve checks for data migrations * Bug fix for data migration * Add migration unit test to ensure that primary keys are maintained * Add playwright test for company parameters * Rename underlying database tables * Fixes for migration unit tests * Revert "Rename underlying database tables" This reverts commit477c692076. * Fix for migration sequencing * Simplify new playwright test * Remove spectacular collision * Monkey patch the drf-spectacular warn function * Do not use custom branch for playwright testing --------- Co-authored-by: Matthias Mair <code@mjmair.com>
This commit is contained in:
@@ -111,8 +111,6 @@ export enum ApiEndpoints {
|
||||
|
||||
// Part API endpoints
|
||||
part_list = 'part/',
|
||||
part_parameter_list = 'part/parameter/',
|
||||
part_parameter_template_list = 'part/parameter/template/',
|
||||
part_thumbs_list = 'part/thumbs/',
|
||||
part_pricing = 'part/:id/pricing/',
|
||||
part_requirements = 'part/:id/requirements/',
|
||||
@@ -134,7 +132,6 @@ export enum ApiEndpoints {
|
||||
supplier_part_list = 'company/part/',
|
||||
supplier_part_pricing_list = 'company/price-break/',
|
||||
manufacturer_part_list = 'company/part/manufacturer/',
|
||||
manufacturer_part_parameter_list = 'company/part/manufacturer/parameter/',
|
||||
|
||||
// Stock location endpoints
|
||||
stock_location_list = 'stock/location/',
|
||||
@@ -243,5 +240,7 @@ export enum ApiEndpoints {
|
||||
notes_image_upload = 'notes-image-upload/',
|
||||
email_list = 'admin/email/',
|
||||
email_test = 'admin/email/test/',
|
||||
config_list = 'admin/config/'
|
||||
config_list = 'admin/config/',
|
||||
parameter_list = 'parameter/',
|
||||
parameter_template_list = 'parameter/template/'
|
||||
}
|
||||
|
||||
@@ -33,13 +33,18 @@ export const ModelInformationDict: ModelDict = {
|
||||
admin_url: '/part/part/',
|
||||
icon: 'part'
|
||||
},
|
||||
partparametertemplate: {
|
||||
label: () => t`Part Parameter Template`,
|
||||
label_multiple: () => t`Part Parameter Templates`,
|
||||
url_overview: '/settings/admin/part-parameters',
|
||||
url_detail: '/partparametertemplate/:pk/',
|
||||
api_endpoint: ApiEndpoints.part_parameter_template_list,
|
||||
icon: 'test_templates'
|
||||
parameter: {
|
||||
label: () => t`Parameter`,
|
||||
label_multiple: () => t`Parameters`,
|
||||
api_endpoint: ApiEndpoints.parameter_list,
|
||||
icon: 'list_details'
|
||||
},
|
||||
parametertemplate: {
|
||||
label: () => t`Parameter Template`,
|
||||
label_multiple: () => t`Parameter Templates`,
|
||||
api_endpoint: ApiEndpoints.parameter_template_list,
|
||||
admin_url: '/common/parametertemplate/',
|
||||
icon: 'list'
|
||||
},
|
||||
parttesttemplate: {
|
||||
label: () => t`Part Test Template`,
|
||||
|
||||
@@ -6,7 +6,6 @@ export enum ModelType {
|
||||
supplierpart = 'supplierpart',
|
||||
manufacturerpart = 'manufacturerpart',
|
||||
partcategory = 'partcategory',
|
||||
partparametertemplate = 'partparametertemplate',
|
||||
parttesttemplate = 'parttesttemplate',
|
||||
projectcode = 'projectcode',
|
||||
stockitem = 'stockitem',
|
||||
@@ -17,6 +16,8 @@ export enum ModelType {
|
||||
buildline = 'buildline',
|
||||
builditem = 'builditem',
|
||||
company = 'company',
|
||||
parameter = 'parameter',
|
||||
parametertemplate = 'parametertemplate',
|
||||
purchaseorder = 'purchaseorder',
|
||||
purchaseorderlineitem = 'purchaseorderlineitem',
|
||||
salesorder = 'salesorder',
|
||||
|
||||
@@ -33,7 +33,7 @@ export default function SegmentedIconControl({
|
||||
data={data.map((item) => ({
|
||||
value: item.value,
|
||||
label: (
|
||||
<Tooltip label={item.label}>
|
||||
<Tooltip label={item.label} position='top-end'>
|
||||
<ActionIcon
|
||||
variant='transparent'
|
||||
color={color}
|
||||
|
||||
@@ -8,7 +8,7 @@ export type PanelType = {
|
||||
label: string;
|
||||
controls?: ReactNode;
|
||||
icon?: ReactNode;
|
||||
content: ReactNode;
|
||||
content?: ReactNode;
|
||||
hidden?: boolean;
|
||||
disabled?: boolean;
|
||||
showHeadline?: boolean;
|
||||
|
||||
32
src/frontend/src/components/panels/ParametersPanel.tsx
Normal file
32
src/frontend/src/components/panels/ParametersPanel.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import type { ModelType } from '@lib/enums/ModelType';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { Skeleton } from '@mantine/core';
|
||||
import { IconListDetails } from '@tabler/icons-react';
|
||||
import { ParameterTable } from '../../tables/general/ParameterTable';
|
||||
import type { PanelType } from './Panel';
|
||||
|
||||
export default function ParametersPanel({
|
||||
model_type,
|
||||
model_id,
|
||||
allowEdit = true
|
||||
}: {
|
||||
model_type: ModelType;
|
||||
model_id: number | undefined;
|
||||
allowEdit?: boolean;
|
||||
}): PanelType {
|
||||
return {
|
||||
name: 'parameters',
|
||||
label: t`Parameters`,
|
||||
icon: <IconListDetails />,
|
||||
content:
|
||||
model_type && model_id ? (
|
||||
<ParameterTable
|
||||
allowEdit={allowEdit}
|
||||
modelType={model_type}
|
||||
modelId={model_id}
|
||||
/>
|
||||
) : (
|
||||
<Skeleton />
|
||||
)
|
||||
};
|
||||
}
|
||||
53
src/frontend/src/components/panels/SegmentedControlPanel.tsx
Normal file
53
src/frontend/src/components/panels/SegmentedControlPanel.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import SegmentedIconControl from '../buttons/SegmentedIconControl';
|
||||
import type { PanelType } from './Panel';
|
||||
|
||||
export type SegmentedControlPanelSelection = {
|
||||
value: string;
|
||||
label: string;
|
||||
icon: React.ReactNode;
|
||||
content: React.ReactNode;
|
||||
};
|
||||
|
||||
interface SegmentedPanelType extends PanelType {
|
||||
options: SegmentedControlPanelSelection[];
|
||||
selection: string;
|
||||
onChange: (value: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a panel which can be used to display multiple options,
|
||||
* based on a built-in segmented control.
|
||||
*/
|
||||
export default function SegmentedControlPanel(
|
||||
props: SegmentedPanelType
|
||||
): PanelType {
|
||||
// Extract the content based on the selection
|
||||
let content = null;
|
||||
|
||||
for (const option of props.options) {
|
||||
if (option.value === props.selection) {
|
||||
content = option.content;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (content === null && props.options.length > 0) {
|
||||
content = props.options[0].content;
|
||||
}
|
||||
|
||||
return {
|
||||
...props,
|
||||
content: content,
|
||||
controls: (
|
||||
<SegmentedIconControl
|
||||
value={props.selection}
|
||||
onChange={props.onChange}
|
||||
data={props.options.map((option: any) => ({
|
||||
value: option.value,
|
||||
label: option.label,
|
||||
icon: option.icon
|
||||
}))}
|
||||
/>
|
||||
)
|
||||
};
|
||||
}
|
||||
@@ -2,6 +2,30 @@ import type { ReactNode } from 'react';
|
||||
|
||||
import { type InstanceRenderInterface, RenderInlineModel } from './Instance';
|
||||
|
||||
export function RenderParameterTemplate({
|
||||
instance
|
||||
}: Readonly<InstanceRenderInterface>): ReactNode {
|
||||
return (
|
||||
<RenderInlineModel
|
||||
primary={instance.name}
|
||||
secondary={instance.description}
|
||||
suffix={instance.units}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function RenderParameter({
|
||||
instance
|
||||
}: Readonly<InstanceRenderInterface>): ReactNode {
|
||||
return (
|
||||
<RenderInlineModel
|
||||
primary={instance.template?.name || ''}
|
||||
secondary={instance.description}
|
||||
suffix={instance.data || instance.data_numeric || ''}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function RenderProjectCode({
|
||||
instance
|
||||
}: Readonly<InstanceRenderInterface>): ReactNode {
|
||||
|
||||
@@ -36,6 +36,8 @@ import {
|
||||
RenderContentType,
|
||||
RenderError,
|
||||
RenderImportSession,
|
||||
RenderParameter,
|
||||
RenderParameterTemplate,
|
||||
RenderProjectCode,
|
||||
RenderSelectionList
|
||||
} from './Generic';
|
||||
@@ -46,12 +48,7 @@ import {
|
||||
RenderSalesOrder,
|
||||
RenderSalesOrderShipment
|
||||
} from './Order';
|
||||
import {
|
||||
RenderPart,
|
||||
RenderPartCategory,
|
||||
RenderPartParameterTemplate,
|
||||
RenderPartTestTemplate
|
||||
} from './Part';
|
||||
import { RenderPart, RenderPartCategory, RenderPartTestTemplate } from './Part';
|
||||
import { RenderPlugin } from './Plugin';
|
||||
import { RenderLabelTemplate, RenderReportTemplate } from './Report';
|
||||
import {
|
||||
@@ -71,11 +68,12 @@ export const RendererLookup: ModelRendererDict = {
|
||||
[ModelType.builditem]: RenderBuildItem,
|
||||
[ModelType.company]: RenderCompany,
|
||||
[ModelType.contact]: RenderContact,
|
||||
[ModelType.parameter]: RenderParameter,
|
||||
[ModelType.parametertemplate]: RenderParameterTemplate,
|
||||
[ModelType.manufacturerpart]: RenderManufacturerPart,
|
||||
[ModelType.owner]: RenderOwner,
|
||||
[ModelType.part]: RenderPart,
|
||||
[ModelType.partcategory]: RenderPartCategory,
|
||||
[ModelType.partparametertemplate]: RenderPartParameterTemplate,
|
||||
[ModelType.parttesttemplate]: RenderPartTestTemplate,
|
||||
[ModelType.projectcode]: RenderProjectCode,
|
||||
[ModelType.purchaseorder]: RenderPurchaseOrder,
|
||||
|
||||
@@ -141,23 +141,6 @@ export function RenderPartCategory(
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inline rendering of a PartParameterTemplate instance
|
||||
*/
|
||||
export function RenderPartParameterTemplate({
|
||||
instance
|
||||
}: Readonly<{
|
||||
instance: any;
|
||||
}>): ReactNode {
|
||||
return (
|
||||
<RenderInlineModel
|
||||
primary={instance.name}
|
||||
secondary={instance.description}
|
||||
suffix={instance.units}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function RenderPartTestTemplate({
|
||||
instance
|
||||
}: Readonly<{
|
||||
|
||||
@@ -419,8 +419,8 @@ const ParametersStep = ({
|
||||
hideLabels
|
||||
fieldDefinition={{
|
||||
field_type: 'related field',
|
||||
model: ModelType.partparametertemplate,
|
||||
api_url: apiUrl(ApiEndpoints.part_parameter_template_list),
|
||||
model: ModelType.parametertemplate,
|
||||
api_url: apiUrl(ApiEndpoints.parameter_template_list),
|
||||
disabled: p.on_category,
|
||||
value: p.parameter_template,
|
||||
onValueChange: (v) => {
|
||||
@@ -677,13 +677,14 @@ export default function ImportPartWizard({
|
||||
{} as Record<number, number>
|
||||
);
|
||||
const createParameters = useParameters.map((p) => ({
|
||||
part: importResult!.part_id,
|
||||
model_type: 'part',
|
||||
model_id: importResult!.part_id,
|
||||
template: p.parameter_template,
|
||||
data: p.value
|
||||
}));
|
||||
try {
|
||||
await api.post(
|
||||
apiUrl(ApiEndpoints.part_parameter_list),
|
||||
apiUrl(ApiEndpoints.parameter_list),
|
||||
createParameters
|
||||
);
|
||||
showNotification({
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import { IconUsers } from '@tabler/icons-react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
|
||||
import type { ModelType } from '@lib/enums/ModelType';
|
||||
import { apiUrl } from '@lib/functions/Api';
|
||||
import type { ApiFormFieldSet } from '@lib/types/Forms';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import type {
|
||||
StatusCodeInterface,
|
||||
StatusCodeListInterface
|
||||
} from '../components/render/StatusRenderer';
|
||||
import { useApi } from '../contexts/ApiContext';
|
||||
import { useGlobalStatusState } from '../states/GlobalStatusState';
|
||||
|
||||
export function projectCodeFields(): ApiFormFieldSet {
|
||||
@@ -91,3 +95,117 @@ export function extraLineItemFields(): ApiFormFieldSet {
|
||||
link: {}
|
||||
};
|
||||
}
|
||||
|
||||
export function useParameterFields({
|
||||
modelType,
|
||||
modelId
|
||||
}: {
|
||||
modelType: ModelType;
|
||||
modelId: number;
|
||||
}): ApiFormFieldSet {
|
||||
const api = useApi();
|
||||
|
||||
// Valid field choices
|
||||
const [choices, setChoices] = useState<any[]>([]);
|
||||
|
||||
// Field type for "data" input
|
||||
const [fieldType, setFieldType] = useState<'string' | 'boolean' | 'choice'>(
|
||||
'string'
|
||||
);
|
||||
|
||||
const [data, setData] = useState<string>('');
|
||||
|
||||
// Reset the field type and choices when the model changes
|
||||
useEffect(() => {
|
||||
setFieldType('string');
|
||||
setChoices([]);
|
||||
setData('');
|
||||
}, [modelType, modelId]);
|
||||
|
||||
return useMemo(() => {
|
||||
return {
|
||||
model_type: {
|
||||
hidden: true,
|
||||
value: modelType
|
||||
},
|
||||
model_id: {
|
||||
hidden: true,
|
||||
value: modelId
|
||||
},
|
||||
template: {
|
||||
filters: {
|
||||
for_model: modelType,
|
||||
enabled: true
|
||||
},
|
||||
onValueChange: (value: any, record: any) => {
|
||||
// Adjust the type of the "data" field based on the selected template
|
||||
if (record?.checkbox) {
|
||||
// This is a "checkbox" field
|
||||
setChoices([]);
|
||||
setFieldType('boolean');
|
||||
} else if (record?.choices) {
|
||||
const _choices: string[] = record.choices.split(',');
|
||||
|
||||
if (_choices.length > 0) {
|
||||
setChoices(
|
||||
_choices.map((choice) => {
|
||||
return {
|
||||
display_name: choice.trim(),
|
||||
value: choice.trim()
|
||||
};
|
||||
})
|
||||
);
|
||||
setFieldType('choice');
|
||||
} else {
|
||||
setChoices([]);
|
||||
setFieldType('string');
|
||||
}
|
||||
} else if (record?.selectionlist) {
|
||||
api
|
||||
.get(
|
||||
apiUrl(ApiEndpoints.selectionlist_detail, record.selectionlist)
|
||||
)
|
||||
.then((res) => {
|
||||
setChoices(
|
||||
res.data.choices.map((item: any) => {
|
||||
return {
|
||||
value: item.value,
|
||||
display_name: item.label
|
||||
};
|
||||
})
|
||||
);
|
||||
setFieldType('choice');
|
||||
});
|
||||
} else {
|
||||
setChoices([]);
|
||||
setFieldType('string');
|
||||
}
|
||||
}
|
||||
},
|
||||
data: {
|
||||
value: data,
|
||||
onValueChange: (value: any) => {
|
||||
setData(value);
|
||||
},
|
||||
type: fieldType,
|
||||
field_type: fieldType,
|
||||
choices: fieldType === 'choice' ? choices : undefined,
|
||||
default: fieldType === 'boolean' ? false : undefined,
|
||||
adjustValue: (value: any) => {
|
||||
// Coerce boolean value into a string (required by backend)
|
||||
|
||||
let v: string = value.toString().trim();
|
||||
|
||||
if (fieldType === 'boolean') {
|
||||
if (v.toLowerCase() !== 'true') {
|
||||
v = 'false';
|
||||
}
|
||||
}
|
||||
|
||||
return v;
|
||||
}
|
||||
},
|
||||
note: {}
|
||||
};
|
||||
}, [data, modelType, fieldType, choices, modelId]);
|
||||
}
|
||||
|
||||
@@ -97,21 +97,6 @@ export function useManufacturerPartFields() {
|
||||
}, []);
|
||||
}
|
||||
|
||||
export function useManufacturerPartParameterFields() {
|
||||
return useMemo(() => {
|
||||
const fields: ApiFormFieldSet = {
|
||||
manufacturer_part: {
|
||||
disabled: true
|
||||
},
|
||||
name: {},
|
||||
value: {},
|
||||
units: {}
|
||||
};
|
||||
|
||||
return fields;
|
||||
}, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Field set for editing a company instance
|
||||
*/
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import type { ApiFormFieldSet } from '@lib/types/Forms';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { IconBuildingStore, IconCopy, IconPackages } from '@tabler/icons-react';
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
|
||||
import { apiUrl } from '@lib/functions/Api';
|
||||
import type { ApiFormFieldSet } from '@lib/types/Forms';
|
||||
import { useApi } from '../contexts/ApiContext';
|
||||
import { useGlobalSettingsState } from '../states/SettingsStates';
|
||||
|
||||
/**
|
||||
@@ -224,97 +220,6 @@ export function partCategoryFields({
|
||||
return fields;
|
||||
}
|
||||
|
||||
export function usePartParameterFields({
|
||||
editTemplate
|
||||
}: {
|
||||
editTemplate?: boolean;
|
||||
}): ApiFormFieldSet {
|
||||
const api = useApi();
|
||||
|
||||
// Valid field choices
|
||||
const [choices, setChoices] = useState<any[]>([]);
|
||||
|
||||
// Field type for "data" input
|
||||
const [fieldType, setFieldType] = useState<'string' | 'boolean' | 'choice'>(
|
||||
'string'
|
||||
);
|
||||
|
||||
return useMemo(() => {
|
||||
return {
|
||||
part: {
|
||||
disabled: true
|
||||
},
|
||||
template: {
|
||||
disabled: editTemplate == false,
|
||||
onValueChange: (value: any, record: any) => {
|
||||
// Adjust the type of the "data" field based on the selected template
|
||||
if (record?.checkbox) {
|
||||
// This is a "checkbox" field
|
||||
setChoices([]);
|
||||
setFieldType('boolean');
|
||||
} else if (record?.choices) {
|
||||
const _choices: string[] = record.choices.split(',');
|
||||
|
||||
if (_choices.length > 0) {
|
||||
setChoices(
|
||||
_choices.map((choice) => {
|
||||
return {
|
||||
display_name: choice.trim(),
|
||||
value: choice.trim()
|
||||
};
|
||||
})
|
||||
);
|
||||
setFieldType('choice');
|
||||
} else {
|
||||
setChoices([]);
|
||||
setFieldType('string');
|
||||
}
|
||||
} else if (record?.selectionlist) {
|
||||
api
|
||||
.get(
|
||||
apiUrl(ApiEndpoints.selectionlist_detail, record.selectionlist)
|
||||
)
|
||||
.then((res) => {
|
||||
setChoices(
|
||||
res.data.choices.map((item: any) => {
|
||||
return {
|
||||
value: item.value,
|
||||
display_name: item.label
|
||||
};
|
||||
})
|
||||
);
|
||||
setFieldType('choice');
|
||||
});
|
||||
} else {
|
||||
setChoices([]);
|
||||
setFieldType('string');
|
||||
}
|
||||
}
|
||||
},
|
||||
data: {
|
||||
type: fieldType,
|
||||
field_type: fieldType,
|
||||
choices: fieldType === 'choice' ? choices : undefined,
|
||||
default: fieldType === 'boolean' ? false : undefined,
|
||||
adjustValue: (value: any) => {
|
||||
// Coerce boolean value into a string (required by backend)
|
||||
|
||||
let v: string = value.toString().trim();
|
||||
|
||||
if (fieldType === 'boolean') {
|
||||
if (v.toLowerCase() !== 'true') {
|
||||
v = 'false';
|
||||
}
|
||||
}
|
||||
|
||||
return v;
|
||||
}
|
||||
},
|
||||
note: {}
|
||||
};
|
||||
}, [editTemplate, fieldType, choices]);
|
||||
}
|
||||
|
||||
export function partStocktakeFields(): ApiFormFieldSet {
|
||||
return {
|
||||
part: {
|
||||
|
||||
@@ -71,7 +71,7 @@ const MachineManagementPanel = Loadable(
|
||||
lazy(() => import('./MachineManagementPanel'))
|
||||
);
|
||||
|
||||
const PartParameterPanel = Loadable(lazy(() => import('./PartParameterPanel')));
|
||||
const ParameterPanel = Loadable(lazy(() => import('./ParameterPanel')));
|
||||
|
||||
const ErrorReportTable = Loadable(
|
||||
lazy(() => import('../../../../tables/settings/ErrorTable'))
|
||||
@@ -191,10 +191,10 @@ export default function AdminCenter() {
|
||||
content: <UnitManagementPanel />
|
||||
},
|
||||
{
|
||||
name: 'part-parameters',
|
||||
label: t`Part Parameters`,
|
||||
name: 'parameters',
|
||||
label: t`Parameters`,
|
||||
icon: <IconList />,
|
||||
content: <PartParameterPanel />,
|
||||
content: <ParameterPanel />,
|
||||
hidden: !user.hasViewRole(UserRoles.part)
|
||||
},
|
||||
{
|
||||
@@ -274,7 +274,7 @@ export default function AdminCenter() {
|
||||
id: 'plm',
|
||||
label: t`PLM`,
|
||||
panelIDs: [
|
||||
'part-parameters',
|
||||
'parameters',
|
||||
'category-parameters',
|
||||
'location-types',
|
||||
'stocktake'
|
||||
|
||||
@@ -2,21 +2,21 @@ import { t } from '@lingui/core/macro';
|
||||
import { Accordion } from '@mantine/core';
|
||||
|
||||
import { StylishText } from '../../../../components/items/StylishText';
|
||||
import PartParameterTemplateTable from '../../../../tables/part/PartParameterTemplateTable';
|
||||
import ParameterTemplateTable from '../../../../tables/general/ParameterTemplateTable';
|
||||
import SelectionListTable from '../../../../tables/part/SelectionListTable';
|
||||
|
||||
export default function PartParameterPanel() {
|
||||
return (
|
||||
<Accordion defaultValue='parametertemplate'>
|
||||
<Accordion.Item value='parametertemplate' key='parametertemplate'>
|
||||
<Accordion multiple defaultValue={['parameter-templates']}>
|
||||
<Accordion.Item value='parameter-templates' key='parameter-templates'>
|
||||
<Accordion.Control>
|
||||
<StylishText size='lg'>{t`Part Parameter Template`}</StylishText>
|
||||
<StylishText size='lg'>{t`Parameter Templates`}</StylishText>
|
||||
</Accordion.Control>
|
||||
<Accordion.Panel>
|
||||
<PartParameterTemplateTable />
|
||||
<ParameterTemplateTable />
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
<Accordion.Item value='selectionlist' key='selectionlist'>
|
||||
<Accordion.Item value='selection-lists' key='selection-lists'>
|
||||
<Accordion.Control>
|
||||
<StylishText size='lg'>{t`Selection Lists`}</StylishText>
|
||||
</Accordion.Control>
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
IconCurrencyDollar,
|
||||
IconFileAnalytics,
|
||||
IconFingerprint,
|
||||
IconList,
|
||||
IconPackages,
|
||||
IconPlugConnected,
|
||||
IconQrcode,
|
||||
@@ -185,6 +186,12 @@ export default function SystemSettings() {
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'parameters',
|
||||
label: t`Parameters`,
|
||||
icon: <IconList />,
|
||||
content: <GlobalSettingList keys={['PARAMETER_ENFORCE_UNITS']} />
|
||||
},
|
||||
{
|
||||
name: 'parts',
|
||||
label: t`Parts`,
|
||||
@@ -213,8 +220,7 @@ export default function SystemSettings() {
|
||||
'PART_COPY_PARAMETERS',
|
||||
'PART_COPY_TESTS',
|
||||
'PART_CATEGORY_PARAMETERS',
|
||||
'PART_CATEGORY_DEFAULT_ICON',
|
||||
'PART_PARAMETER_ENFORCE_UNITS'
|
||||
'PART_CATEGORY_DEFAULT_ICON'
|
||||
]}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -45,6 +45,7 @@ import AttachmentPanel from '../../components/panels/AttachmentPanel';
|
||||
import NotesPanel from '../../components/panels/NotesPanel';
|
||||
import type { PanelType } from '../../components/panels/Panel';
|
||||
import { PanelGroup } from '../../components/panels/PanelGroup';
|
||||
import ParametersPanel from '../../components/panels/ParametersPanel';
|
||||
import { StatusRenderer } from '../../components/render/StatusRenderer';
|
||||
import { RenderStockLocation } from '../../components/render/Stock';
|
||||
import { useBuildOrderFields } from '../../forms/BuildForms';
|
||||
@@ -519,6 +520,10 @@ export default function BuildDetail() {
|
||||
<Skeleton />
|
||||
)
|
||||
},
|
||||
ParametersPanel({
|
||||
model_type: ModelType.build,
|
||||
model_id: build.pk
|
||||
}),
|
||||
AttachmentPanel({
|
||||
model_type: ModelType.build,
|
||||
model_id: build.pk
|
||||
|
||||
@@ -1,21 +1,27 @@
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { Stack } from '@mantine/core';
|
||||
import { IconCalendar, IconTable, IconTools } from '@tabler/icons-react';
|
||||
import {
|
||||
IconCalendar,
|
||||
IconListDetails,
|
||||
IconTable,
|
||||
IconTools
|
||||
} from '@tabler/icons-react';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { ModelType } from '@lib/enums/ModelType';
|
||||
import { UserRoles } from '@lib/enums/Roles';
|
||||
import type { TableFilter } from '@lib/types/Filters';
|
||||
import { useLocalStorage } from '@mantine/hooks';
|
||||
import SegmentedIconControl from '../../components/buttons/SegmentedIconControl';
|
||||
import OrderCalendar from '../../components/calendar/OrderCalendar';
|
||||
import PermissionDenied from '../../components/errors/PermissionDenied';
|
||||
import { PageDetail } from '../../components/nav/PageDetail';
|
||||
import type { PanelType } from '../../components/panels/Panel';
|
||||
import { PanelGroup } from '../../components/panels/PanelGroup';
|
||||
import SegmentedControlPanel from '../../components/panels/SegmentedControlPanel';
|
||||
import { useGlobalSettingsState } from '../../states/SettingsStates';
|
||||
import { useUserState } from '../../states/UserState';
|
||||
import { PartCategoryFilter } from '../../tables/Filter';
|
||||
import BuildOrderParametricTable from '../../tables/build/BuildOrderParametricTable';
|
||||
import { BuildOrderTable } from '../../tables/build/BuildOrderTable';
|
||||
|
||||
function BuildOrderCalendar() {
|
||||
@@ -43,20 +49,6 @@ function BuildOrderCalendar() {
|
||||
);
|
||||
}
|
||||
|
||||
function BuildOverview({
|
||||
view
|
||||
}: {
|
||||
view: string;
|
||||
}) {
|
||||
switch (view) {
|
||||
case 'calendar':
|
||||
return <BuildOrderCalendar />;
|
||||
case 'table':
|
||||
default:
|
||||
return <BuildOrderTable />;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build Order index page
|
||||
*/
|
||||
@@ -64,34 +56,41 @@ export default function BuildIndex() {
|
||||
const user = useUserState();
|
||||
|
||||
const [buildOrderView, setBuildOrderView] = useLocalStorage<string>({
|
||||
key: 'buildOrderView',
|
||||
key: 'build-order-view',
|
||||
defaultValue: 'table'
|
||||
});
|
||||
|
||||
const panels: PanelType[] = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
name: 'buildorders',
|
||||
SegmentedControlPanel({
|
||||
name: 'buildorder',
|
||||
label: t`Build Orders`,
|
||||
content: <BuildOverview view={buildOrderView} />,
|
||||
icon: <IconTools />,
|
||||
controls: (
|
||||
<SegmentedIconControl
|
||||
value={buildOrderView}
|
||||
onChange={setBuildOrderView}
|
||||
data={[
|
||||
{ value: 'table', label: t`Table View`, icon: <IconTable /> },
|
||||
{
|
||||
value: 'calendar',
|
||||
label: t`Calendar View`,
|
||||
icon: <IconCalendar />
|
||||
}
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
selection: buildOrderView,
|
||||
onChange: setBuildOrderView,
|
||||
options: [
|
||||
{
|
||||
value: 'table',
|
||||
label: t`Table View`,
|
||||
icon: <IconTable />,
|
||||
content: <BuildOrderTable />
|
||||
},
|
||||
{
|
||||
value: 'calendar',
|
||||
label: t`Calendar View`,
|
||||
icon: <IconCalendar />,
|
||||
content: <BuildOrderCalendar />
|
||||
},
|
||||
{
|
||||
value: 'parametric',
|
||||
label: t`Parametric View`,
|
||||
icon: <IconListDetails />,
|
||||
content: <BuildOrderParametricTable />
|
||||
}
|
||||
]
|
||||
})
|
||||
];
|
||||
}, [buildOrderView, setBuildOrderView]);
|
||||
}, [user, buildOrderView]);
|
||||
|
||||
if (!user.isLoggedIn() || !user.hasViewRole(UserRoles.build)) {
|
||||
return <PermissionDenied />;
|
||||
|
||||
@@ -39,6 +39,7 @@ import AttachmentPanel from '../../components/panels/AttachmentPanel';
|
||||
import NotesPanel from '../../components/panels/NotesPanel';
|
||||
import type { PanelType } from '../../components/panels/Panel';
|
||||
import { PanelGroup } from '../../components/panels/PanelGroup';
|
||||
import ParametersPanel from '../../components/panels/ParametersPanel';
|
||||
import { companyFields } from '../../forms/CompanyForms';
|
||||
import {
|
||||
useDeleteApiFormModal,
|
||||
@@ -265,6 +266,10 @@ export default function CompanyDetail(props: Readonly<CompanyDetailProps>) {
|
||||
icon: <IconMap2 />,
|
||||
content: company?.pk && <AddressTable companyId={company.pk} />
|
||||
},
|
||||
ParametersPanel({
|
||||
model_type: ModelType.company,
|
||||
model_id: company?.pk
|
||||
}),
|
||||
AttachmentPanel({
|
||||
model_type: ModelType.company,
|
||||
model_id: company.pk
|
||||
|
||||
@@ -3,7 +3,6 @@ import { Grid, Skeleton, Stack } from '@mantine/core';
|
||||
import {
|
||||
IconBuildingWarehouse,
|
||||
IconInfoCircle,
|
||||
IconList,
|
||||
IconPackages
|
||||
} from '@tabler/icons-react';
|
||||
import { useMemo } from 'react';
|
||||
@@ -33,6 +32,7 @@ import AttachmentPanel from '../../components/panels/AttachmentPanel';
|
||||
import NotesPanel from '../../components/panels/NotesPanel';
|
||||
import type { PanelType } from '../../components/panels/Panel';
|
||||
import { PanelGroup } from '../../components/panels/PanelGroup';
|
||||
import ParametersPanel from '../../components/panels/ParametersPanel';
|
||||
import { useManufacturerPartFields } from '../../forms/CompanyForms';
|
||||
import {
|
||||
useCreateApiFormModal,
|
||||
@@ -41,7 +41,6 @@ import {
|
||||
} from '../../hooks/UseForm';
|
||||
import { useInstance } from '../../hooks/UseInstance';
|
||||
import { useUserState } from '../../states/UserState';
|
||||
import ManufacturerPartParameterTable from '../../tables/purchasing/ManufacturerPartParameterTable';
|
||||
import { SupplierPartTable } from '../../tables/purchasing/SupplierPartTable';
|
||||
import { StockItemTable } from '../../tables/stock/StockItemTable';
|
||||
|
||||
@@ -161,18 +160,6 @@ export default function ManufacturerPartDetail() {
|
||||
icon: <IconInfoCircle />,
|
||||
content: detailsPanel
|
||||
},
|
||||
{
|
||||
name: 'parameters',
|
||||
label: t`Parameters`,
|
||||
icon: <IconList />,
|
||||
content: manufacturerPart?.pk ? (
|
||||
<ManufacturerPartParameterTable
|
||||
params={{ manufacturer_part: manufacturerPart.pk }}
|
||||
/>
|
||||
) : (
|
||||
<Skeleton />
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'stock',
|
||||
label: t`Received Stock`,
|
||||
@@ -201,6 +188,10 @@ export default function ManufacturerPartDetail() {
|
||||
<Skeleton />
|
||||
)
|
||||
},
|
||||
ParametersPanel({
|
||||
model_type: ModelType.manufacturerpart,
|
||||
model_id: manufacturerPart?.pk
|
||||
}),
|
||||
AttachmentPanel({
|
||||
model_type: ModelType.manufacturerpart,
|
||||
model_id: manufacturerPart?.pk
|
||||
|
||||
@@ -36,6 +36,7 @@ import AttachmentPanel from '../../components/panels/AttachmentPanel';
|
||||
import NotesPanel from '../../components/panels/NotesPanel';
|
||||
import type { PanelType } from '../../components/panels/Panel';
|
||||
import { PanelGroup } from '../../components/panels/PanelGroup';
|
||||
import ParametersPanel from '../../components/panels/ParametersPanel';
|
||||
import { useSupplierPartFields } from '../../forms/CompanyForms';
|
||||
import {
|
||||
useCreateApiFormModal,
|
||||
@@ -284,6 +285,10 @@ export default function SupplierPartDetail() {
|
||||
<Skeleton />
|
||||
)
|
||||
},
|
||||
ParametersPanel({
|
||||
model_type: ModelType.supplierpart,
|
||||
model_id: supplierPart?.pk
|
||||
}),
|
||||
AttachmentPanel({
|
||||
model_type: ModelType.supplierpart,
|
||||
model_id: supplierPart?.pk
|
||||
|
||||
@@ -6,7 +6,8 @@ import {
|
||||
IconListCheck,
|
||||
IconListDetails,
|
||||
IconPackages,
|
||||
IconSitemap
|
||||
IconSitemap,
|
||||
IconTable
|
||||
} from '@tabler/icons-react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
@@ -15,6 +16,7 @@ import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
|
||||
import { ModelType } from '@lib/enums/ModelType';
|
||||
import { UserRoles } from '@lib/enums/Roles';
|
||||
import { getDetailUrl } from '@lib/functions/Navigation';
|
||||
import { useLocalStorage } from '@mantine/hooks';
|
||||
import AdminButton from '../../components/buttons/AdminButton';
|
||||
import StarredToggleButton from '../../components/buttons/StarredToggleButton';
|
||||
import {
|
||||
@@ -33,6 +35,7 @@ import NavigationTree from '../../components/nav/NavigationTree';
|
||||
import { PageDetail } from '../../components/nav/PageDetail';
|
||||
import type { PanelType } from '../../components/panels/Panel';
|
||||
import { PanelGroup } from '../../components/panels/PanelGroup';
|
||||
import SegmentedControlPanel from '../../components/panels/SegmentedControlPanel';
|
||||
import { partCategoryFields } from '../../forms/PartForms';
|
||||
import {
|
||||
useDeleteApiFormModal,
|
||||
@@ -258,6 +261,11 @@ export default function CategoryDetail() {
|
||||
];
|
||||
}, [id, user, category.pk, category.starred]);
|
||||
|
||||
const [partsView, setPartsView] = useLocalStorage<string>({
|
||||
key: 'category-parts-view',
|
||||
defaultValue: 'table'
|
||||
});
|
||||
|
||||
const panels: PanelType[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
@@ -272,20 +280,35 @@ export default function CategoryDetail() {
|
||||
icon: <IconSitemap />,
|
||||
content: <PartCategoryTable parentId={id} />
|
||||
},
|
||||
{
|
||||
SegmentedControlPanel({
|
||||
name: 'parts',
|
||||
label: t`Parts`,
|
||||
icon: <IconCategory />,
|
||||
content: (
|
||||
<PartListTable
|
||||
props={{
|
||||
params: {
|
||||
category: id
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
},
|
||||
selection: partsView,
|
||||
onChange: setPartsView,
|
||||
options: [
|
||||
{
|
||||
value: 'table',
|
||||
label: t`Table View`,
|
||||
icon: <IconTable />,
|
||||
content: (
|
||||
<PartListTable
|
||||
props={{
|
||||
params: {
|
||||
category: id
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
value: 'parametric',
|
||||
label: t`Parametric View`,
|
||||
icon: <IconListDetails />,
|
||||
content: <ParametricPartTable categoryId={id} />
|
||||
}
|
||||
]
|
||||
}),
|
||||
{
|
||||
name: 'stockitem',
|
||||
label: t`Stock Items`,
|
||||
@@ -307,15 +330,9 @@ export default function CategoryDetail() {
|
||||
icon: <IconListCheck />,
|
||||
hidden: !id || !category.pk,
|
||||
content: <PartCategoryTemplateTable categoryId={category?.pk} />
|
||||
},
|
||||
{
|
||||
name: 'parameters',
|
||||
label: t`Part Parameters`,
|
||||
icon: <IconListDetails />,
|
||||
content: <ParametricPartTable categoryId={id} />
|
||||
}
|
||||
],
|
||||
[category, id]
|
||||
[category, id, partsView]
|
||||
);
|
||||
|
||||
const breadcrumbs = useMemo(
|
||||
|
||||
@@ -22,8 +22,8 @@ import {
|
||||
IconExclamationCircle,
|
||||
IconInfoCircle,
|
||||
IconLayersLinked,
|
||||
IconList,
|
||||
IconListCheck,
|
||||
IconListDetails,
|
||||
IconListTree,
|
||||
IconLock,
|
||||
IconPackages,
|
||||
@@ -97,7 +97,7 @@ import { useUserState } from '../../states/UserState';
|
||||
import { BomTable } from '../../tables/bom/BomTable';
|
||||
import { UsedInTable } from '../../tables/bom/UsedInTable';
|
||||
import { BuildOrderTable } from '../../tables/build/BuildOrderTable';
|
||||
import { PartParameterTable } from '../../tables/part/PartParameterTable';
|
||||
import { ParameterTable } from '../../tables/general/ParameterTable';
|
||||
import PartPurchaseOrdersTable from '../../tables/part/PartPurchaseOrdersTable';
|
||||
import PartTestResultTable from '../../tables/part/PartTestResultTable';
|
||||
import PartTestTemplateTable from '../../tables/part/PartTestTemplateTable';
|
||||
@@ -788,17 +788,6 @@ export default function PartDetail() {
|
||||
icon: <IconInfoCircle />,
|
||||
content: detailsPanel
|
||||
},
|
||||
{
|
||||
name: 'parameters',
|
||||
label: t`Parameters`,
|
||||
icon: <IconList />,
|
||||
content: (
|
||||
<PartParameterTable
|
||||
partId={id ?? -1}
|
||||
partLocked={part?.locked == true}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
name: 'stock',
|
||||
label: t`Stock`,
|
||||
@@ -949,6 +938,30 @@ export default function PartDetail() {
|
||||
icon: <IconLayersLinked />,
|
||||
content: <RelatedPartTable partId={part.pk} />
|
||||
},
|
||||
{
|
||||
name: 'parameters',
|
||||
label: t`Parameters`,
|
||||
icon: <IconListDetails />,
|
||||
content: (
|
||||
<>
|
||||
{part.locked && (
|
||||
<Alert
|
||||
title={t`Part is Locked`}
|
||||
color='orange'
|
||||
icon={<IconLock />}
|
||||
p='xs'
|
||||
>
|
||||
<Text>{t`Part parameters cannot be edited, as the part is locked`}</Text>
|
||||
</Alert>
|
||||
)}
|
||||
<ParameterTable
|
||||
modelType={ModelType.part}
|
||||
modelId={part?.pk}
|
||||
allowEdit={part?.locked != true}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
},
|
||||
AttachmentPanel({
|
||||
model_type: ModelType.part,
|
||||
model_id: part?.pk
|
||||
|
||||
@@ -32,6 +32,7 @@ import AttachmentPanel from '../../components/panels/AttachmentPanel';
|
||||
import NotesPanel from '../../components/panels/NotesPanel';
|
||||
import type { PanelType } from '../../components/panels/Panel';
|
||||
import { PanelGroup } from '../../components/panels/PanelGroup';
|
||||
import ParametersPanel from '../../components/panels/ParametersPanel';
|
||||
import { StatusRenderer } from '../../components/render/StatusRenderer';
|
||||
import { formatCurrency } from '../../defaults/formatters';
|
||||
import { usePurchaseOrderFields } from '../../forms/PurchaseOrderForms';
|
||||
@@ -389,6 +390,10 @@ export default function PurchaseOrderDetail() {
|
||||
/>
|
||||
)
|
||||
},
|
||||
ParametersPanel({
|
||||
model_type: ModelType.purchaseorder,
|
||||
model_id: order.pk
|
||||
}),
|
||||
AttachmentPanel({
|
||||
model_type: ModelType.purchaseorder,
|
||||
model_id: order.pk
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
IconBuildingStore,
|
||||
IconBuildingWarehouse,
|
||||
IconCalendar,
|
||||
IconListDetails,
|
||||
IconPackageExport,
|
||||
IconShoppingCart,
|
||||
IconTable
|
||||
@@ -14,104 +15,193 @@ import { useMemo } from 'react';
|
||||
import { ModelType } from '@lib/enums/ModelType';
|
||||
import { UserRoles } from '@lib/enums/Roles';
|
||||
import { useLocalStorage } from '@mantine/hooks';
|
||||
import SegmentedIconControl from '../../components/buttons/SegmentedIconControl';
|
||||
import OrderCalendar from '../../components/calendar/OrderCalendar';
|
||||
import PermissionDenied from '../../components/errors/PermissionDenied';
|
||||
import { PageDetail } from '../../components/nav/PageDetail';
|
||||
import { PanelGroup } from '../../components/panels/PanelGroup';
|
||||
import SegmentedControlPanel from '../../components/panels/SegmentedControlPanel';
|
||||
import { useUserState } from '../../states/UserState';
|
||||
import { CompanyTable } from '../../tables/company/CompanyTable';
|
||||
import ParametricCompanyTable from '../../tables/company/ParametricCompanyTable';
|
||||
import ManufacturerPartParametricTable from '../../tables/purchasing/ManufacturerPartParametricTable';
|
||||
import { ManufacturerPartTable } from '../../tables/purchasing/ManufacturerPartTable';
|
||||
import PurchaseOrderParametricTable from '../../tables/purchasing/PurchaseOrderParametricTable';
|
||||
import { PurchaseOrderTable } from '../../tables/purchasing/PurchaseOrderTable';
|
||||
import SupplierPartParametricTable from '../../tables/purchasing/SupplierPartParametricTable';
|
||||
import { SupplierPartTable } from '../../tables/purchasing/SupplierPartTable';
|
||||
|
||||
function PurchaseOrderOverview({
|
||||
view
|
||||
}: {
|
||||
view: string;
|
||||
}) {
|
||||
switch (view) {
|
||||
case 'calendar':
|
||||
return (
|
||||
<OrderCalendar
|
||||
model={ModelType.purchaseorder}
|
||||
role={UserRoles.purchase_order}
|
||||
params={{ outstanding: true }}
|
||||
/>
|
||||
);
|
||||
case 'table':
|
||||
default:
|
||||
return <PurchaseOrderTable />;
|
||||
}
|
||||
}
|
||||
|
||||
export default function PurchasingIndex() {
|
||||
const user = useUserState();
|
||||
|
||||
const [purchaseOrderView, setpurchaseOrderView] = useLocalStorage<string>({
|
||||
key: 'purchaseOrderView',
|
||||
const [purchaseOrderView, setPurchaseOrderView] = useLocalStorage<string>({
|
||||
key: 'purchase-order-view',
|
||||
defaultValue: 'table'
|
||||
});
|
||||
|
||||
const [supplierView, setSupplierView] = useLocalStorage<string>({
|
||||
key: 'supplier-view',
|
||||
defaultValue: 'table'
|
||||
});
|
||||
|
||||
const [manufacturerView, setManufacturerView] = useLocalStorage<string>({
|
||||
key: 'manufacturer-view',
|
||||
defaultValue: 'table'
|
||||
});
|
||||
|
||||
const [manufacturerPartsView, setManufacturerPartsView] =
|
||||
useLocalStorage<string>({
|
||||
key: 'manufacturer-parts-view',
|
||||
defaultValue: 'table'
|
||||
});
|
||||
|
||||
const [supplierPartsView, setSupplierPartsView] = useLocalStorage<string>({
|
||||
key: 'supplier-parts-view',
|
||||
defaultValue: 'table'
|
||||
});
|
||||
|
||||
const panels = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
SegmentedControlPanel({
|
||||
name: 'purchaseorders',
|
||||
label: t`Purchase Orders`,
|
||||
icon: <IconShoppingCart />,
|
||||
hidden: !user.hasViewRole(UserRoles.purchase_order),
|
||||
content: <PurchaseOrderOverview view={purchaseOrderView} />,
|
||||
controls: (
|
||||
<SegmentedIconControl
|
||||
value={purchaseOrderView}
|
||||
onChange={setpurchaseOrderView}
|
||||
data={[
|
||||
{ value: 'table', label: t`Table View`, icon: <IconTable /> },
|
||||
{
|
||||
value: 'calendar',
|
||||
label: t`Calendar View`,
|
||||
icon: <IconCalendar />
|
||||
}
|
||||
]}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
selection: purchaseOrderView,
|
||||
onChange: setPurchaseOrderView,
|
||||
options: [
|
||||
{
|
||||
value: 'table',
|
||||
label: t`Table View`,
|
||||
icon: <IconTable />,
|
||||
content: <PurchaseOrderTable />
|
||||
},
|
||||
{
|
||||
value: 'calendar',
|
||||
label: t`Calendar View`,
|
||||
icon: <IconCalendar />,
|
||||
content: (
|
||||
<OrderCalendar
|
||||
model={ModelType.purchaseorder}
|
||||
role={UserRoles.purchase_order}
|
||||
params={{ outstanding: true }}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
value: 'parametric',
|
||||
label: t`Parametric View`,
|
||||
icon: <IconListDetails />,
|
||||
content: <PurchaseOrderParametricTable />
|
||||
}
|
||||
]
|
||||
}),
|
||||
SegmentedControlPanel({
|
||||
name: 'suppliers',
|
||||
label: t`Suppliers`,
|
||||
icon: <IconBuildingStore />,
|
||||
content: (
|
||||
<CompanyTable
|
||||
path='purchasing/supplier'
|
||||
params={{ is_supplier: true }}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
selection: supplierView,
|
||||
onChange: setSupplierView,
|
||||
options: [
|
||||
{
|
||||
value: 'table',
|
||||
label: t`Table View`,
|
||||
icon: <IconTable />,
|
||||
content: (
|
||||
<CompanyTable
|
||||
path='purchasing/supplier'
|
||||
params={{ is_supplier: true }}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
value: 'parametric',
|
||||
label: t`Parametric View`,
|
||||
icon: <IconListDetails />,
|
||||
content: (
|
||||
<ParametricCompanyTable queryParams={{ is_supplier: true }} />
|
||||
)
|
||||
}
|
||||
]
|
||||
}),
|
||||
SegmentedControlPanel({
|
||||
name: 'supplier-parts',
|
||||
label: t`Supplier Parts`,
|
||||
icon: <IconPackageExport />,
|
||||
content: <SupplierPartTable />
|
||||
},
|
||||
{
|
||||
selection: supplierPartsView,
|
||||
onChange: setSupplierPartsView,
|
||||
options: [
|
||||
{
|
||||
value: 'table',
|
||||
label: t`Table View`,
|
||||
icon: <IconTable />,
|
||||
content: <SupplierPartTable />
|
||||
},
|
||||
{
|
||||
value: 'parametric',
|
||||
label: t`Parametric View`,
|
||||
icon: <IconListDetails />,
|
||||
content: <SupplierPartParametricTable />
|
||||
}
|
||||
]
|
||||
}),
|
||||
SegmentedControlPanel({
|
||||
name: 'manufacturer',
|
||||
label: t`Manufacturers`,
|
||||
icon: <IconBuildingFactory2 />,
|
||||
content: (
|
||||
<CompanyTable
|
||||
path='purchasing/manufacturer'
|
||||
params={{ is_manufacturer: true }}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
selection: manufacturerView,
|
||||
onChange: setManufacturerView,
|
||||
options: [
|
||||
{
|
||||
value: 'table',
|
||||
label: t`Table View`,
|
||||
icon: <IconTable />,
|
||||
content: (
|
||||
<CompanyTable
|
||||
path='purchasing/manufacturer'
|
||||
params={{ is_manufacturer: true }}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
value: 'parametric',
|
||||
label: t`Parametric View`,
|
||||
icon: <IconListDetails />,
|
||||
content: (
|
||||
<ParametricCompanyTable queryParams={{ is_manufacturer: true }} />
|
||||
)
|
||||
}
|
||||
]
|
||||
}),
|
||||
SegmentedControlPanel({
|
||||
name: 'manufacturer-parts',
|
||||
label: t`Manufacturer Parts`,
|
||||
icon: <IconBuildingWarehouse />,
|
||||
content: <ManufacturerPartTable />
|
||||
}
|
||||
selection: manufacturerPartsView,
|
||||
onChange: setManufacturerPartsView,
|
||||
options: [
|
||||
{
|
||||
value: 'table',
|
||||
label: t`Table View`,
|
||||
icon: <IconTable />,
|
||||
content: <ManufacturerPartTable />
|
||||
},
|
||||
{
|
||||
value: 'parametric',
|
||||
label: t`Parametric View`,
|
||||
icon: <IconListDetails />,
|
||||
content: <ManufacturerPartParametricTable />
|
||||
}
|
||||
]
|
||||
})
|
||||
];
|
||||
}, [user, purchaseOrderView]);
|
||||
}, [
|
||||
user,
|
||||
manufacturerPartsView,
|
||||
manufacturerView,
|
||||
purchaseOrderView,
|
||||
supplierPartsView,
|
||||
supplierView
|
||||
]);
|
||||
|
||||
if (!user.isLoggedIn() || !user.hasViewRole(UserRoles.purchase_order)) {
|
||||
return <PermissionDenied />;
|
||||
|
||||
@@ -32,6 +32,7 @@ import AttachmentPanel from '../../components/panels/AttachmentPanel';
|
||||
import NotesPanel from '../../components/panels/NotesPanel';
|
||||
import type { PanelType } from '../../components/panels/Panel';
|
||||
import { PanelGroup } from '../../components/panels/PanelGroup';
|
||||
import ParametersPanel from '../../components/panels/ParametersPanel';
|
||||
import { RenderAddress } from '../../components/render/Company';
|
||||
import { StatusRenderer } from '../../components/render/StatusRenderer';
|
||||
import { formatCurrency } from '../../defaults/formatters';
|
||||
@@ -354,6 +355,10 @@ export default function ReturnOrderDetail() {
|
||||
</Accordion>
|
||||
)
|
||||
},
|
||||
ParametersPanel({
|
||||
model_type: ModelType.returnorder,
|
||||
model_id: order.pk
|
||||
}),
|
||||
AttachmentPanel({
|
||||
model_type: ModelType.returnorder,
|
||||
model_id: order.pk
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
IconBuildingStore,
|
||||
IconCalendar,
|
||||
IconCubeSend,
|
||||
IconListDetails,
|
||||
IconTable,
|
||||
IconTruckDelivery,
|
||||
IconTruckReturn
|
||||
@@ -13,93 +14,74 @@ import { useMemo } from 'react';
|
||||
import { ModelType } from '@lib/enums/ModelType';
|
||||
import { UserRoles } from '@lib/enums/Roles';
|
||||
import { useLocalStorage } from '@mantine/hooks';
|
||||
import SegmentedIconControl from '../../components/buttons/SegmentedIconControl';
|
||||
import OrderCalendar from '../../components/calendar/OrderCalendar';
|
||||
import PermissionDenied from '../../components/errors/PermissionDenied';
|
||||
import { PageDetail } from '../../components/nav/PageDetail';
|
||||
import { PanelGroup } from '../../components/panels/PanelGroup';
|
||||
import SegmentedControlPanel from '../../components/panels/SegmentedControlPanel';
|
||||
import { useUserState } from '../../states/UserState';
|
||||
import { CompanyTable } from '../../tables/company/CompanyTable';
|
||||
import ParametricCompanyTable from '../../tables/company/ParametricCompanyTable';
|
||||
import ReturnOrderParametricTable from '../../tables/sales/ReturnOrderParametricTable';
|
||||
import { ReturnOrderTable } from '../../tables/sales/ReturnOrderTable';
|
||||
import SalesOrderParametricTable from '../../tables/sales/SalesOrderParametricTable';
|
||||
import SalesOrderShipmentTable from '../../tables/sales/SalesOrderShipmentTable';
|
||||
import { SalesOrderTable } from '../../tables/sales/SalesOrderTable';
|
||||
|
||||
function SalesOrderOverview({
|
||||
view
|
||||
}: {
|
||||
view: string;
|
||||
}) {
|
||||
switch (view) {
|
||||
case 'calendar':
|
||||
return (
|
||||
<OrderCalendar
|
||||
model={ModelType.salesorder}
|
||||
role={UserRoles.sales_order}
|
||||
params={{ outstanding: true }}
|
||||
/>
|
||||
);
|
||||
case 'table':
|
||||
default:
|
||||
return <SalesOrderTable />;
|
||||
}
|
||||
}
|
||||
|
||||
function ReturnOrderOverview({
|
||||
view
|
||||
}: {
|
||||
view: string;
|
||||
}) {
|
||||
switch (view) {
|
||||
case 'calendar':
|
||||
return (
|
||||
<OrderCalendar
|
||||
model={ModelType.returnorder}
|
||||
role={UserRoles.return_order}
|
||||
params={{ outstanding: true }}
|
||||
/>
|
||||
);
|
||||
case 'table':
|
||||
default:
|
||||
return <ReturnOrderTable />;
|
||||
}
|
||||
}
|
||||
|
||||
export default function SalesIndex() {
|
||||
const user = useUserState();
|
||||
|
||||
const [customersView, setCustomersView] = useLocalStorage<string>({
|
||||
key: 'customer-view',
|
||||
defaultValue: 'table'
|
||||
});
|
||||
|
||||
const [salesOrderView, setSalesOrderView] = useLocalStorage<string>({
|
||||
key: 'salesOrderView',
|
||||
key: 'sales-order-view',
|
||||
defaultValue: 'table'
|
||||
});
|
||||
|
||||
const [returnOrderView, setReturnOrderView] = useLocalStorage<string>({
|
||||
key: 'returnOrderView',
|
||||
key: 'return-order-view',
|
||||
defaultValue: 'table'
|
||||
});
|
||||
|
||||
const panels = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
SegmentedControlPanel({
|
||||
name: 'salesorders',
|
||||
label: t`Sales Orders`,
|
||||
icon: <IconTruckDelivery />,
|
||||
content: <SalesOrderOverview view={salesOrderView} />,
|
||||
controls: (
|
||||
<SegmentedIconControl
|
||||
value={salesOrderView}
|
||||
onChange={setSalesOrderView}
|
||||
data={[
|
||||
{ value: 'table', label: t`Table View`, icon: <IconTable /> },
|
||||
{
|
||||
value: 'calendar',
|
||||
label: t`Calendar View`,
|
||||
icon: <IconCalendar />
|
||||
}
|
||||
]}
|
||||
/>
|
||||
),
|
||||
hidden: !user.hasViewRole(UserRoles.sales_order)
|
||||
},
|
||||
hidden: !user.hasViewRole(UserRoles.sales_order),
|
||||
selection: salesOrderView,
|
||||
onChange: setSalesOrderView,
|
||||
options: [
|
||||
{
|
||||
value: 'table',
|
||||
label: t`Table View`,
|
||||
icon: <IconTable />,
|
||||
content: <SalesOrderTable />
|
||||
},
|
||||
{
|
||||
value: 'calendar',
|
||||
label: t`Calendar View`,
|
||||
icon: <IconCalendar />,
|
||||
content: (
|
||||
<OrderCalendar
|
||||
model={ModelType.returnorder}
|
||||
role={UserRoles.return_order}
|
||||
params={{ outstanding: true }}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
value: 'parametric',
|
||||
label: t`Parametric View`,
|
||||
icon: <IconListDetails />,
|
||||
content: <SalesOrderParametricTable />
|
||||
}
|
||||
]
|
||||
}),
|
||||
{
|
||||
name: 'shipments',
|
||||
label: t`Pending Shipments`,
|
||||
@@ -112,37 +94,70 @@ export default function SalesIndex() {
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
SegmentedControlPanel({
|
||||
name: 'returnorders',
|
||||
label: t`Return Orders`,
|
||||
icon: <IconTruckReturn />,
|
||||
content: <ReturnOrderOverview view={returnOrderView} />,
|
||||
controls: (
|
||||
<SegmentedIconControl
|
||||
value={returnOrderView}
|
||||
onChange={setReturnOrderView}
|
||||
data={[
|
||||
{ value: 'table', label: t`Table View`, icon: <IconTable /> },
|
||||
{
|
||||
value: 'calendar',
|
||||
label: t`Calendar View`,
|
||||
icon: <IconCalendar />
|
||||
}
|
||||
]}
|
||||
/>
|
||||
),
|
||||
hidden: !user.hasViewRole(UserRoles.return_order)
|
||||
},
|
||||
{
|
||||
hidden: !user.hasViewRole(UserRoles.return_order),
|
||||
selection: returnOrderView,
|
||||
onChange: setReturnOrderView,
|
||||
options: [
|
||||
{
|
||||
value: 'table',
|
||||
label: t`Table View`,
|
||||
icon: <IconTable />,
|
||||
content: <ReturnOrderTable />
|
||||
},
|
||||
{
|
||||
value: 'calendar',
|
||||
label: t`Calendar View`,
|
||||
icon: <IconCalendar />,
|
||||
content: (
|
||||
<OrderCalendar
|
||||
model={ModelType.returnorder}
|
||||
role={UserRoles.return_order}
|
||||
params={{ outstanding: true }}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
value: 'parametric',
|
||||
label: t`Parametric View`,
|
||||
icon: <IconListDetails />,
|
||||
content: <ReturnOrderParametricTable />
|
||||
}
|
||||
]
|
||||
}),
|
||||
SegmentedControlPanel({
|
||||
name: 'customers',
|
||||
label: t`Customers`,
|
||||
icon: <IconBuildingStore />,
|
||||
content: (
|
||||
<CompanyTable path='sales/customer' params={{ is_customer: true }} />
|
||||
)
|
||||
}
|
||||
selection: customersView,
|
||||
onChange: setCustomersView,
|
||||
options: [
|
||||
{
|
||||
value: 'table',
|
||||
label: t`Table View`,
|
||||
icon: <IconTable />,
|
||||
content: (
|
||||
<CompanyTable
|
||||
path='sales/customer'
|
||||
params={{ is_customer: true }}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
value: 'parametric',
|
||||
label: t`Parametric View`,
|
||||
icon: <IconListDetails />,
|
||||
content: (
|
||||
<ParametricCompanyTable queryParams={{ is_customer: true }} />
|
||||
)
|
||||
}
|
||||
]
|
||||
})
|
||||
];
|
||||
}, [user, salesOrderView, returnOrderView]);
|
||||
}, [user, customersView, salesOrderView, returnOrderView]);
|
||||
|
||||
if (!user.isLoggedIn() || !user.hasViewRole(UserRoles.sales_order)) {
|
||||
return <PermissionDenied />;
|
||||
|
||||
@@ -38,6 +38,7 @@ import AttachmentPanel from '../../components/panels/AttachmentPanel';
|
||||
import NotesPanel from '../../components/panels/NotesPanel';
|
||||
import type { PanelType } from '../../components/panels/Panel';
|
||||
import { PanelGroup } from '../../components/panels/PanelGroup';
|
||||
import ParametersPanel from '../../components/panels/ParametersPanel';
|
||||
import { RenderAddress } from '../../components/render/Company';
|
||||
import { StatusRenderer } from '../../components/render/StatusRenderer';
|
||||
import { formatCurrency } from '../../defaults/formatters';
|
||||
@@ -427,6 +428,10 @@ export default function SalesOrderDetail() {
|
||||
<Skeleton />
|
||||
)
|
||||
},
|
||||
ParametersPanel({
|
||||
model_type: ModelType.salesorder,
|
||||
model_id: order.pk
|
||||
}),
|
||||
AttachmentPanel({
|
||||
model_type: ModelType.salesorder,
|
||||
model_id: order.pk
|
||||
|
||||
40
src/frontend/src/tables/build/BuildOrderParametricTable.tsx
Normal file
40
src/frontend/src/tables/build/BuildOrderParametricTable.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { ApiEndpoints, ModelType } from '@lib/index';
|
||||
import type { TableFilter } from '@lib/types/Filters';
|
||||
import type { TableColumn } from '@lib/types/Tables';
|
||||
import { type ReactNode, useMemo } from 'react';
|
||||
import { DescriptionColumn, ReferenceColumn } from '../ColumnRenderers';
|
||||
import { OrderStatusFilter, OutstandingFilter } from '../Filter';
|
||||
import ParametricDataTable from '../general/ParametricDataTable';
|
||||
|
||||
export default function BuildOrderParametricTable({
|
||||
queryParams
|
||||
}: {
|
||||
queryParams?: Record<string, any>;
|
||||
}): ReactNode {
|
||||
const customColumns: TableColumn[] = useMemo(() => {
|
||||
return [
|
||||
ReferenceColumn({
|
||||
switchable: false
|
||||
}),
|
||||
DescriptionColumn({
|
||||
accessor: 'title'
|
||||
})
|
||||
];
|
||||
}, []);
|
||||
|
||||
const customFilters: TableFilter[] = useMemo(() => {
|
||||
return [OutstandingFilter(), OrderStatusFilter({ model: ModelType.build })];
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ParametricDataTable
|
||||
modelType={ModelType.build}
|
||||
endpoint={ApiEndpoints.build_order_list}
|
||||
customColumns={customColumns}
|
||||
customFilters={customFilters}
|
||||
queryParams={{
|
||||
...queryParams
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
39
src/frontend/src/tables/company/ParametricCompanyTable.tsx
Normal file
39
src/frontend/src/tables/company/ParametricCompanyTable.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
|
||||
import { ModelType } from '@lib/enums/ModelType';
|
||||
import type { TableColumn } from '@lib/types/Tables';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useMemo } from 'react';
|
||||
import { CompanyColumn, DescriptionColumn } from '../ColumnRenderers';
|
||||
import ParametricDataTable from '../general/ParametricDataTable';
|
||||
|
||||
export default function ParametricCompanyTable({
|
||||
queryParams
|
||||
}: {
|
||||
queryParams?: any;
|
||||
}) {
|
||||
const customColumns: TableColumn[] = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
accessor: 'name',
|
||||
title: t`Company`,
|
||||
sortable: true,
|
||||
switchable: false,
|
||||
render: (record: any) => {
|
||||
return <CompanyColumn company={record} />;
|
||||
}
|
||||
},
|
||||
DescriptionColumn({})
|
||||
];
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ParametricDataTable
|
||||
modelType={ModelType.company}
|
||||
endpoint={ApiEndpoints.company_list}
|
||||
customColumns={customColumns}
|
||||
queryParams={{
|
||||
...queryParams
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
276
src/frontend/src/tables/general/ParameterTable.tsx
Normal file
276
src/frontend/src/tables/general/ParameterTable.tsx
Normal file
@@ -0,0 +1,276 @@
|
||||
import {
|
||||
ApiEndpoints,
|
||||
ModelType,
|
||||
RowDeleteAction,
|
||||
RowEditAction,
|
||||
YesNoButton,
|
||||
apiUrl,
|
||||
formatDecimal
|
||||
} from '@lib/index';
|
||||
import type { TableFilter } from '@lib/types/Filters';
|
||||
import type { TableColumn } from '@lib/types/Tables';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { IconFileUpload, IconPlus } from '@tabler/icons-react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import ImporterDrawer from '../../components/importer/ImporterDrawer';
|
||||
import { ActionDropdown } from '../../components/items/ActionDropdown';
|
||||
import { useParameterFields } from '../../forms/CommonForms';
|
||||
import { dataImporterSessionFields } from '../../forms/ImporterForms';
|
||||
import {
|
||||
useCreateApiFormModal,
|
||||
useDeleteApiFormModal,
|
||||
useEditApiFormModal
|
||||
} from '../../hooks/UseForm';
|
||||
import { useTable } from '../../hooks/UseTable';
|
||||
import { useUserState } from '../../states/UserState';
|
||||
import {
|
||||
DateColumn,
|
||||
DescriptionColumn,
|
||||
NoteColumn,
|
||||
UserColumn
|
||||
} from '../ColumnRenderers';
|
||||
import { UserFilter } from '../Filter';
|
||||
import { InvenTreeTable } from '../InvenTreeTable';
|
||||
import { TableHoverCard } from '../TableHoverCard';
|
||||
|
||||
/**
|
||||
* Construct a table listing parameters
|
||||
*/
|
||||
export function ParameterTable({
|
||||
modelType,
|
||||
modelId,
|
||||
allowEdit = true
|
||||
}: {
|
||||
modelType: ModelType;
|
||||
modelId: number;
|
||||
allowEdit?: boolean;
|
||||
}) {
|
||||
const table = useTable('parameters');
|
||||
const user = useUserState();
|
||||
|
||||
const tableColumns: TableColumn[] = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
accessor: 'template_detail.name',
|
||||
switchable: false,
|
||||
sortable: true,
|
||||
ordering: 'name'
|
||||
},
|
||||
DescriptionColumn({
|
||||
accessor: 'template_detail.description'
|
||||
}),
|
||||
{
|
||||
accessor: 'data',
|
||||
switchable: false,
|
||||
sortable: true,
|
||||
render: (record) => {
|
||||
const template = record.template_detail;
|
||||
|
||||
if (template?.checkbox) {
|
||||
return <YesNoButton value={record.data} />;
|
||||
}
|
||||
|
||||
const extra: any[] = [];
|
||||
|
||||
if (
|
||||
template.units &&
|
||||
record.data_numeric &&
|
||||
record.data_numeric != record.data
|
||||
) {
|
||||
const numeric = formatDecimal(record.data_numeric, { digits: 15 });
|
||||
extra.push(`${numeric} [${template.units}]`);
|
||||
}
|
||||
|
||||
return (
|
||||
<TableHoverCard
|
||||
value={record.data}
|
||||
extra={extra}
|
||||
title={t`Internal Units`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
accessor: 'template_detail.units',
|
||||
ordering: 'units',
|
||||
sortable: true
|
||||
},
|
||||
NoteColumn({}),
|
||||
DateColumn({
|
||||
accessor: 'updated',
|
||||
title: t`Last Updated`,
|
||||
sortable: true,
|
||||
switchable: true
|
||||
}),
|
||||
UserColumn({
|
||||
accessor: 'updated_by_detail',
|
||||
ordering: 'updated_by',
|
||||
title: t`Updated By`
|
||||
})
|
||||
];
|
||||
}, [user]);
|
||||
|
||||
const tableFilters: TableFilter[] = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
name: 'enabled',
|
||||
label: 'Enabled',
|
||||
description: t`Show parameters for enabled templates`,
|
||||
type: 'boolean'
|
||||
},
|
||||
UserFilter({
|
||||
name: 'updated_by',
|
||||
label: t`Updated By`,
|
||||
description: t`Filter by user who last updated the parameter`
|
||||
})
|
||||
];
|
||||
}, []);
|
||||
|
||||
const [selectedParameter, setSelectedParameter] = useState<any | undefined>(
|
||||
undefined
|
||||
);
|
||||
|
||||
const [importOpened, setImportOpened] = useState<boolean>(false);
|
||||
|
||||
const [selectedSession, setSelectedSession] = useState<number | undefined>(
|
||||
undefined
|
||||
);
|
||||
|
||||
const importSessionFields = useMemo(() => {
|
||||
const fields = dataImporterSessionFields({
|
||||
modelType: ModelType.parameter
|
||||
});
|
||||
|
||||
fields.field_overrides.value = {
|
||||
model_type: modelType,
|
||||
model_id: modelId
|
||||
};
|
||||
|
||||
return fields;
|
||||
}, [modelType, modelId]);
|
||||
|
||||
const importParameters = useCreateApiFormModal({
|
||||
url: ApiEndpoints.import_session_list,
|
||||
title: t`Import Parameters`,
|
||||
fields: importSessionFields,
|
||||
onFormSuccess: (response: any) => {
|
||||
setSelectedSession(response.pk);
|
||||
setImportOpened(true);
|
||||
}
|
||||
});
|
||||
|
||||
const newParameter = useCreateApiFormModal({
|
||||
url: ApiEndpoints.parameter_list,
|
||||
title: t`Add Parameter`,
|
||||
fields: useParameterFields({ modelType, modelId }),
|
||||
initialData: {
|
||||
data: ''
|
||||
},
|
||||
table: table
|
||||
});
|
||||
|
||||
const editParameter = useEditApiFormModal({
|
||||
url: ApiEndpoints.parameter_list,
|
||||
pk: selectedParameter?.pk,
|
||||
title: t`Edit Parameter`,
|
||||
fields: useParameterFields({ modelType, modelId }),
|
||||
table: table
|
||||
});
|
||||
|
||||
const deleteParameter = useDeleteApiFormModal({
|
||||
url: ApiEndpoints.parameter_list,
|
||||
pk: selectedParameter?.pk,
|
||||
title: t`Delete Parameter`,
|
||||
table: table
|
||||
});
|
||||
|
||||
const tableActions = useMemo(() => {
|
||||
return [
|
||||
<ActionDropdown
|
||||
key='add-parameter-actions'
|
||||
tooltip={t`Add Parameters`}
|
||||
position='bottom-start'
|
||||
icon={<IconPlus />}
|
||||
hidden={!user.hasAddPermission(modelType)}
|
||||
actions={[
|
||||
{
|
||||
name: t`Create Parameter`,
|
||||
icon: <IconPlus />,
|
||||
tooltip: t`Create a new parameter`,
|
||||
onClick: () => {
|
||||
setSelectedParameter(undefined);
|
||||
newParameter.open();
|
||||
}
|
||||
},
|
||||
{
|
||||
name: t`Import from File`,
|
||||
icon: <IconFileUpload />,
|
||||
tooltip: t`Import parameters from a file`,
|
||||
onClick: () => {
|
||||
importParameters.open();
|
||||
}
|
||||
}
|
||||
]}
|
||||
/>
|
||||
];
|
||||
}, [allowEdit, user]);
|
||||
|
||||
const rowActions = useCallback(
|
||||
(record: any) => {
|
||||
return [
|
||||
RowEditAction({
|
||||
tooltip: t`Edit Parameter`,
|
||||
onClick: () => {
|
||||
setSelectedParameter(record);
|
||||
editParameter.open();
|
||||
},
|
||||
hidden: !user.hasChangePermission(modelType)
|
||||
}),
|
||||
RowDeleteAction({
|
||||
tooltip: t`Delete Parameter`,
|
||||
onClick: () => {
|
||||
setSelectedParameter(record);
|
||||
deleteParameter.open();
|
||||
},
|
||||
hidden: !user.hasDeletePermission(modelType)
|
||||
})
|
||||
];
|
||||
},
|
||||
[user]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{newParameter.modal}
|
||||
{editParameter.modal}
|
||||
{deleteParameter.modal}
|
||||
{importParameters.modal}
|
||||
<InvenTreeTable
|
||||
url={apiUrl(ApiEndpoints.parameter_list)}
|
||||
tableState={table}
|
||||
columns={tableColumns}
|
||||
props={{
|
||||
enableDownload: true,
|
||||
enableBulkDelete: allowEdit != false,
|
||||
enableSelection: allowEdit != false,
|
||||
rowActions: allowEdit == false ? undefined : rowActions,
|
||||
tableActions: allowEdit == false ? undefined : tableActions,
|
||||
tableFilters: tableFilters,
|
||||
params: {
|
||||
model_type: modelType,
|
||||
model_id: modelId
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<ImporterDrawer
|
||||
sessionId={selectedSession ?? -1}
|
||||
opened={selectedSession !== undefined && importOpened}
|
||||
onClose={() => {
|
||||
setSelectedSession(undefined);
|
||||
setImportOpened(false);
|
||||
table.refreshTable();
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,19 +1,18 @@
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import { AddItemButton } from '@lib/components/AddItemButton';
|
||||
import {
|
||||
type RowAction,
|
||||
AddItemButton,
|
||||
ApiEndpoints,
|
||||
type ApiFormFieldSet,
|
||||
RowDeleteAction,
|
||||
RowDuplicateAction,
|
||||
RowEditAction
|
||||
} from '@lib/components/RowActions';
|
||||
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
|
||||
import { UserRoles } from '@lib/enums/Roles';
|
||||
import { apiUrl } from '@lib/functions/Api';
|
||||
RowEditAction,
|
||||
UserRoles,
|
||||
apiUrl
|
||||
} from '@lib/index';
|
||||
import type { TableFilter } from '@lib/types/Filters';
|
||||
import type { ApiFormFieldSet } from '@lib/types/Forms';
|
||||
import type { TableColumn } from '@lib/types/Tables';
|
||||
import type { RowAction, TableColumn } from '@lib/types/Tables';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useFilters } from '../../hooks/UseFilter';
|
||||
import {
|
||||
useCreateApiFormModal,
|
||||
useDeleteApiFormModal,
|
||||
@@ -24,11 +23,114 @@ import { useUserState } from '../../states/UserState';
|
||||
import { BooleanColumn, DescriptionColumn } from '../ColumnRenderers';
|
||||
import { InvenTreeTable } from '../InvenTreeTable';
|
||||
|
||||
export default function PartParameterTemplateTable() {
|
||||
const table = useTable('part-parameter-templates');
|
||||
|
||||
/**
|
||||
* Render a table of ParameterTemplate objects
|
||||
*/
|
||||
export default function ParameterTemplateTable() {
|
||||
const table = useTable('parameter-templates');
|
||||
const user = useUserState();
|
||||
|
||||
const parameterTemplateFields: ApiFormFieldSet = useMemo(() => {
|
||||
return {
|
||||
name: {},
|
||||
description: {},
|
||||
units: {},
|
||||
model_type: {},
|
||||
choices: {},
|
||||
checkbox: {},
|
||||
selectionlist: {},
|
||||
enabled: {}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const newTemplate = useCreateApiFormModal({
|
||||
url: ApiEndpoints.parameter_template_list,
|
||||
title: t`Add Parameter Template`,
|
||||
table: table,
|
||||
fields: useMemo(
|
||||
() => ({
|
||||
...parameterTemplateFields
|
||||
}),
|
||||
[parameterTemplateFields]
|
||||
)
|
||||
});
|
||||
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<any | undefined>(
|
||||
undefined
|
||||
);
|
||||
|
||||
const duplicateTemplate = useCreateApiFormModal({
|
||||
url: ApiEndpoints.parameter_template_list,
|
||||
title: t`Duplicate Parameter Template`,
|
||||
table: table,
|
||||
fields: useMemo(
|
||||
() => ({
|
||||
...parameterTemplateFields
|
||||
}),
|
||||
[parameterTemplateFields]
|
||||
),
|
||||
initialData: selectedTemplate
|
||||
});
|
||||
|
||||
const deleteTemplate = useDeleteApiFormModal({
|
||||
url: ApiEndpoints.parameter_template_list,
|
||||
pk: selectedTemplate?.pk,
|
||||
title: t`Delete Parameter Template`,
|
||||
table: table
|
||||
});
|
||||
|
||||
const editTemplate = useEditApiFormModal({
|
||||
url: ApiEndpoints.parameter_template_list,
|
||||
pk: selectedTemplate?.pk,
|
||||
title: t`Edit Parameter Template`,
|
||||
table: table,
|
||||
fields: useMemo(
|
||||
() => ({
|
||||
...parameterTemplateFields
|
||||
}),
|
||||
[parameterTemplateFields]
|
||||
)
|
||||
});
|
||||
|
||||
// Callback for row actions
|
||||
const rowActions = useCallback(
|
||||
(record: any): RowAction[] => {
|
||||
return [
|
||||
RowEditAction({
|
||||
onClick: () => {
|
||||
setSelectedTemplate(record);
|
||||
editTemplate.open();
|
||||
}
|
||||
}),
|
||||
RowDuplicateAction({
|
||||
onClick: () => {
|
||||
setSelectedTemplate(record);
|
||||
duplicateTemplate.open();
|
||||
}
|
||||
}),
|
||||
RowDeleteAction({
|
||||
onClick: () => {
|
||||
setSelectedTemplate(record);
|
||||
deleteTemplate.open();
|
||||
}
|
||||
})
|
||||
];
|
||||
},
|
||||
[user]
|
||||
);
|
||||
|
||||
const modelTypeFilters = useFilters({
|
||||
url: apiUrl(ApiEndpoints.parameter_template_list),
|
||||
method: 'OPTIONS',
|
||||
accessor: 'data.actions.POST.model_type.choices',
|
||||
transform: (item: any) => {
|
||||
return {
|
||||
value: item.value,
|
||||
label: item.display_name
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const tableFilters: TableFilter[] = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
@@ -45,9 +147,20 @@ export default function PartParameterTemplateTable() {
|
||||
name: 'has_units',
|
||||
label: t`Has Units`,
|
||||
description: t`Show templates with units`
|
||||
},
|
||||
{
|
||||
name: 'enabled',
|
||||
label: t`Enabled`,
|
||||
description: t`Show enabled templates`
|
||||
},
|
||||
{
|
||||
name: 'model_type',
|
||||
label: t`Model Type`,
|
||||
description: t`Filter by model type`,
|
||||
choices: modelTypeFilters.choices
|
||||
}
|
||||
];
|
||||
}, []);
|
||||
}, [modelTypeFilters.choices]);
|
||||
|
||||
const tableColumns: TableColumn[] = useMemo(() => {
|
||||
return [
|
||||
@@ -56,109 +169,27 @@ export default function PartParameterTemplateTable() {
|
||||
sortable: true,
|
||||
switchable: false
|
||||
},
|
||||
{
|
||||
accessor: 'parts',
|
||||
sortable: true,
|
||||
switchable: true
|
||||
},
|
||||
DescriptionColumn({}),
|
||||
{
|
||||
accessor: 'units',
|
||||
sortable: true
|
||||
},
|
||||
DescriptionColumn({}),
|
||||
{
|
||||
accessor: 'model_type'
|
||||
},
|
||||
BooleanColumn({
|
||||
accessor: 'checkbox'
|
||||
}),
|
||||
{
|
||||
accessor: 'choices'
|
||||
}
|
||||
},
|
||||
BooleanColumn({
|
||||
accessor: 'enabled',
|
||||
title: t`Enabled`
|
||||
})
|
||||
];
|
||||
}, []);
|
||||
|
||||
const partParameterTemplateFields: ApiFormFieldSet = useMemo(() => {
|
||||
return {
|
||||
name: {},
|
||||
description: {},
|
||||
units: {},
|
||||
choices: {},
|
||||
checkbox: {},
|
||||
selectionlist: {}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const newTemplate = useCreateApiFormModal({
|
||||
url: ApiEndpoints.part_parameter_template_list,
|
||||
title: t`Add Parameter Template`,
|
||||
table: table,
|
||||
fields: useMemo(
|
||||
() => ({ ...partParameterTemplateFields }),
|
||||
[partParameterTemplateFields]
|
||||
)
|
||||
});
|
||||
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<any | undefined>(
|
||||
undefined
|
||||
);
|
||||
|
||||
const duplicateTemplate = useCreateApiFormModal({
|
||||
url: ApiEndpoints.part_parameter_template_list,
|
||||
title: t`Duplicate Parameter Template`,
|
||||
table: table,
|
||||
fields: useMemo(
|
||||
() => ({ ...partParameterTemplateFields }),
|
||||
[partParameterTemplateFields]
|
||||
),
|
||||
initialData: selectedTemplate
|
||||
});
|
||||
|
||||
const editTemplate = useEditApiFormModal({
|
||||
url: ApiEndpoints.part_parameter_template_list,
|
||||
pk: selectedTemplate?.pk,
|
||||
title: t`Edit Parameter Template`,
|
||||
table: table,
|
||||
fields: useMemo(
|
||||
() => ({ ...partParameterTemplateFields }),
|
||||
[partParameterTemplateFields]
|
||||
)
|
||||
});
|
||||
|
||||
const deleteTemplate = useDeleteApiFormModal({
|
||||
url: ApiEndpoints.part_parameter_template_list,
|
||||
pk: selectedTemplate?.pk,
|
||||
title: t`Delete Parameter Template`,
|
||||
table: table
|
||||
});
|
||||
|
||||
// Callback for row actions
|
||||
const rowActions = useCallback(
|
||||
(record: any): RowAction[] => {
|
||||
return [
|
||||
RowEditAction({
|
||||
hidden: !user.hasChangeRole(UserRoles.part),
|
||||
onClick: () => {
|
||||
setSelectedTemplate(record);
|
||||
editTemplate.open();
|
||||
}
|
||||
}),
|
||||
RowDuplicateAction({
|
||||
hidden: !user.hasAddRole(UserRoles.part),
|
||||
onClick: () => {
|
||||
setSelectedTemplate(record);
|
||||
duplicateTemplate.open();
|
||||
}
|
||||
}),
|
||||
RowDeleteAction({
|
||||
hidden: !user.hasDeleteRole(UserRoles.part),
|
||||
onClick: () => {
|
||||
setSelectedTemplate(record);
|
||||
deleteTemplate.open();
|
||||
}
|
||||
})
|
||||
];
|
||||
},
|
||||
[user]
|
||||
);
|
||||
|
||||
const tableActions = useMemo(() => {
|
||||
return [
|
||||
<AddItemButton
|
||||
@@ -177,13 +208,13 @@ export default function PartParameterTemplateTable() {
|
||||
{duplicateTemplate.modal}
|
||||
{deleteTemplate.modal}
|
||||
<InvenTreeTable
|
||||
url={apiUrl(ApiEndpoints.part_parameter_template_list)}
|
||||
url={apiUrl(ApiEndpoints.parameter_template_list)}
|
||||
tableState={table}
|
||||
columns={tableColumns}
|
||||
props={{
|
||||
rowActions: rowActions,
|
||||
tableFilters: tableFilters,
|
||||
tableActions: tableActions,
|
||||
tableFilters: tableFilters,
|
||||
enableDownload: true
|
||||
}}
|
||||
/>
|
||||
442
src/frontend/src/tables/general/ParametricDataTable.tsx
Normal file
442
src/frontend/src/tables/general/ParametricDataTable.tsx
Normal file
@@ -0,0 +1,442 @@
|
||||
import { cancelEvent } from '@lib/functions/Events';
|
||||
import {
|
||||
ApiEndpoints,
|
||||
type ApiFormFieldSet,
|
||||
type ModelType,
|
||||
UserRoles,
|
||||
YesNoButton,
|
||||
apiUrl,
|
||||
formatDecimal,
|
||||
getDetailUrl,
|
||||
navigateToLink
|
||||
} from '@lib/index';
|
||||
import type { TableFilter } from '@lib/types/Filters';
|
||||
import type { TableColumn } from '@lib/types/Tables';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { Group } from '@mantine/core';
|
||||
import { useHover } from '@mantine/hooks';
|
||||
import { IconCirclePlus } from '@tabler/icons-react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { type ReactNode, useCallback, useMemo, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useApi } from '../../contexts/ApiContext';
|
||||
import { useParameterFields } from '../../forms/CommonForms';
|
||||
import {
|
||||
useCreateApiFormModal,
|
||||
useEditApiFormModal
|
||||
} from '../../hooks/UseForm';
|
||||
import { useTable } from '../../hooks/UseTable';
|
||||
import { useUserState } from '../../states/UserState';
|
||||
import { InvenTreeTable } from '../InvenTreeTable';
|
||||
import { TableHoverCard } from '../TableHoverCard';
|
||||
import {
|
||||
PARAMETER_FILTER_OPERATORS,
|
||||
ParameterFilter
|
||||
} from './ParametricDataTableFilters';
|
||||
|
||||
// Render an individual parameter cell
|
||||
function ParameterCell({
|
||||
record,
|
||||
template,
|
||||
canEdit
|
||||
}: Readonly<{
|
||||
record: any;
|
||||
template: any;
|
||||
canEdit: boolean;
|
||||
}>) {
|
||||
const { hovered, ref } = useHover();
|
||||
|
||||
// Find matching template parameter
|
||||
const parameter = useMemo(() => {
|
||||
return record.parameters?.find((p: any) => p.template == template.pk);
|
||||
}, [record, template]);
|
||||
|
||||
const extra: any[] = [];
|
||||
|
||||
// Format the value for display
|
||||
const value: ReactNode = useMemo(() => {
|
||||
let v: any = parameter?.data;
|
||||
|
||||
// Handle boolean values
|
||||
if (template?.checkbox && v != undefined) {
|
||||
v = <YesNoButton value={parameter.data} />;
|
||||
}
|
||||
|
||||
return v;
|
||||
}, [parameter, template]);
|
||||
|
||||
if (
|
||||
template.units &&
|
||||
parameter &&
|
||||
parameter.data_numeric &&
|
||||
parameter.data_numeric != parameter.data
|
||||
) {
|
||||
const numeric = formatDecimal(parameter.data_numeric, { digits: 15 });
|
||||
extra.push(`${numeric} [${template.units}]`);
|
||||
}
|
||||
|
||||
if (hovered && canEdit) {
|
||||
extra.push(t`Click to edit`);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Group grow ref={ref} justify='space-between'>
|
||||
<Group grow>
|
||||
<TableHoverCard
|
||||
value={value ?? '-'}
|
||||
extra={extra}
|
||||
icon={hovered && canEdit ? 'edit' : 'info'}
|
||||
title={template.name}
|
||||
/>
|
||||
</Group>
|
||||
</Group>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* A table which displays parametric data for generic model types.
|
||||
* The table can be extended by passing in additional column, filters, and actions.
|
||||
*/
|
||||
export default function ParametricDataTable({
|
||||
modelType,
|
||||
endpoint,
|
||||
queryParams,
|
||||
customFilters,
|
||||
customColumns
|
||||
}: {
|
||||
modelType: ModelType;
|
||||
endpoint: ApiEndpoints | string;
|
||||
queryParams?: Record<string, any>;
|
||||
customFilters?: TableFilter[];
|
||||
customColumns?: TableColumn[];
|
||||
}) {
|
||||
const api = useApi();
|
||||
const table = useTable(`parametric-data-${modelType}`);
|
||||
const user = useUserState();
|
||||
const navigate = useNavigate();
|
||||
|
||||
// Fetch all active parameter templates for the given model type
|
||||
const parameterTemplates = useQuery({
|
||||
queryKey: ['parameter-templates', modelType],
|
||||
queryFn: async () => {
|
||||
return api
|
||||
.get(apiUrl(ApiEndpoints.parameter_template_list), {
|
||||
params: {
|
||||
active: true,
|
||||
for_model: modelType,
|
||||
exists_for_model: modelType
|
||||
}
|
||||
})
|
||||
.then((response) => response.data);
|
||||
},
|
||||
refetchOnMount: true
|
||||
});
|
||||
|
||||
/* Store filters against selected part parameters.
|
||||
* These are stored in the format:
|
||||
* {
|
||||
* parameter_1: {
|
||||
* '=': 'value1',
|
||||
* '<': 'value2',
|
||||
* ...
|
||||
* },
|
||||
* parameter_2: {
|
||||
* '=': 'value3',
|
||||
* },
|
||||
* ...
|
||||
* }
|
||||
*
|
||||
* Which allows multiple filters to be applied against each parameter template.
|
||||
*/
|
||||
const [parameterFilters, setParameterFilters] = useState<any>({});
|
||||
|
||||
/* Remove filters for a specific parameter template
|
||||
* - If no operator is specified, remove all filters for this template
|
||||
* - If an operator is specified, remove filters for that operator only
|
||||
*/
|
||||
const clearParameterFilter = useCallback(
|
||||
(templateId: number, operator?: string) => {
|
||||
const filterName = `parameter_${templateId}`;
|
||||
|
||||
if (!operator) {
|
||||
// If no operator is specified, remove all filters for this template
|
||||
setParameterFilters((prev: any) => {
|
||||
const newFilters = { ...prev };
|
||||
// Remove any filters that match the template ID
|
||||
Object.keys(newFilters).forEach((key: string) => {
|
||||
if (key == filterName) {
|
||||
delete newFilters[key];
|
||||
}
|
||||
});
|
||||
return newFilters;
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// An operator is specified, so we remove filters for that operator only
|
||||
setParameterFilters((prev: any) => {
|
||||
const filters = { ...prev };
|
||||
|
||||
const paramFilters = filters[filterName] || {};
|
||||
|
||||
if (paramFilters[operator] !== undefined) {
|
||||
// Remove the specific operator filter
|
||||
delete paramFilters[operator];
|
||||
}
|
||||
|
||||
return {
|
||||
...filters,
|
||||
[filterName]: paramFilters
|
||||
};
|
||||
});
|
||||
|
||||
table.refreshTable();
|
||||
},
|
||||
[setParameterFilters, table.refreshTable]
|
||||
);
|
||||
|
||||
/**
|
||||
* Add (or update) a filter for a specific parameter template.
|
||||
* @param templateId - The ID of the parameter template to filter on.
|
||||
* @param value - The value to filter by.
|
||||
* @param operator - The operator to use for filtering (e.g., '=', '<', '>', etc.).
|
||||
*/
|
||||
const addParameterFilter = useCallback(
|
||||
(templateId: number, value: string, operator: string) => {
|
||||
const filterName = `parameter_${templateId}`;
|
||||
|
||||
const filterValue = value?.toString().trim() ?? '';
|
||||
|
||||
if (filterValue.length > 0) {
|
||||
setParameterFilters((prev: any) => {
|
||||
const filters = { ...prev };
|
||||
const paramFilters = filters[filterName] || {};
|
||||
|
||||
paramFilters[operator] = filterValue;
|
||||
|
||||
return {
|
||||
...filters,
|
||||
[filterName]: paramFilters
|
||||
};
|
||||
});
|
||||
|
||||
table.refreshTable();
|
||||
}
|
||||
},
|
||||
[setParameterFilters, clearParameterFilter, table.refreshTable]
|
||||
);
|
||||
|
||||
// Construct the query filters for the table based on the parameter filters
|
||||
const parametricQueryFilters = useMemo(() => {
|
||||
const filters: Record<string, string> = {};
|
||||
|
||||
Object.keys(parameterFilters).forEach((key: string) => {
|
||||
const paramFilters: any = parameterFilters[key];
|
||||
|
||||
Object.keys(paramFilters).forEach((operator: string) => {
|
||||
const name = `${key}${PARAMETER_FILTER_OPERATORS[operator] || ''}`;
|
||||
const value = paramFilters[operator];
|
||||
|
||||
filters[name] = value;
|
||||
});
|
||||
});
|
||||
|
||||
return filters;
|
||||
}, [parameterFilters]);
|
||||
|
||||
const [selectedInstance, setSelectedInstance] = useState<number>(-1);
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<number | null>(null);
|
||||
const [selectedParameter, setSelectedParameter] = useState<number>(-1);
|
||||
|
||||
const parameterFields: ApiFormFieldSet = useParameterFields({
|
||||
modelType: modelType,
|
||||
modelId: selectedInstance
|
||||
});
|
||||
|
||||
const addParameter = useCreateApiFormModal({
|
||||
url: ApiEndpoints.parameter_list,
|
||||
title: t`Add Parameter`,
|
||||
fields: useMemo(() => ({ ...parameterFields }), [parameterFields]),
|
||||
focus: 'data',
|
||||
onFormSuccess: (parameter: any) => {
|
||||
updateParameterRecord(selectedInstance, parameter);
|
||||
|
||||
// Ensure that the parameter template is included in the table
|
||||
const template = parameterTemplates.data.find(
|
||||
(t: any) => t.pk == parameter.template
|
||||
);
|
||||
|
||||
if (!template) {
|
||||
// Reload the parameter templates
|
||||
parameterTemplates.refetch();
|
||||
}
|
||||
},
|
||||
initialData: {
|
||||
part: selectedInstance,
|
||||
template: selectedTemplate
|
||||
}
|
||||
});
|
||||
|
||||
const editParameter = useEditApiFormModal({
|
||||
url: ApiEndpoints.parameter_list,
|
||||
title: t`Edit Parameter`,
|
||||
pk: selectedParameter,
|
||||
fields: useMemo(() => ({ ...parameterFields }), [parameterFields]),
|
||||
focus: 'data',
|
||||
onFormSuccess: (parameter: any) => {
|
||||
updateParameterRecord(selectedInstance, parameter);
|
||||
}
|
||||
});
|
||||
|
||||
// Update a single parameter record in the table
|
||||
const updateParameterRecord = useCallback(
|
||||
(part: number, parameter: any) => {
|
||||
const records = table.records;
|
||||
const recordIndex = records.findIndex((record: any) => record.pk == part);
|
||||
|
||||
if (recordIndex < 0) {
|
||||
// No matching part: reload the entire table
|
||||
table.refreshTable();
|
||||
return;
|
||||
}
|
||||
|
||||
const parameterIndex = records[recordIndex].parameters.findIndex(
|
||||
(p: any) => p.pk == parameter.pk
|
||||
);
|
||||
|
||||
if (parameterIndex < 0) {
|
||||
// No matching parameter - append new parameter
|
||||
records[recordIndex].parameters.push(parameter);
|
||||
} else {
|
||||
records[recordIndex].parameters[parameterIndex] = parameter;
|
||||
}
|
||||
|
||||
table.updateRecord(records[recordIndex]);
|
||||
},
|
||||
[table.records, table.updateRecord]
|
||||
);
|
||||
|
||||
const parameterColumns: TableColumn[] = useMemo(() => {
|
||||
const data = parameterTemplates?.data || [];
|
||||
|
||||
return data.map((template: any) => {
|
||||
let title = template.name;
|
||||
|
||||
if (template.units) {
|
||||
title += ` [${template.units}]`;
|
||||
}
|
||||
|
||||
const filters = parameterFilters[`parameter_${template.pk}`] || {};
|
||||
|
||||
return {
|
||||
accessor: `parameter_${template.pk}`,
|
||||
title: title,
|
||||
sortable: true,
|
||||
extra: {
|
||||
template: template.pk
|
||||
},
|
||||
render: (record: any) => (
|
||||
<ParameterCell
|
||||
record={record}
|
||||
template={template}
|
||||
canEdit={user.hasChangeRole(UserRoles.part)}
|
||||
/>
|
||||
),
|
||||
filtering: Object.keys(filters).length > 0,
|
||||
filter: ({ close }: { close: () => void }) => {
|
||||
return (
|
||||
<ParameterFilter
|
||||
template={template}
|
||||
filters={parameterFilters[`parameter_${template.pk}`] || {}}
|
||||
setFilter={addParameterFilter}
|
||||
clearFilter={clearParameterFilter}
|
||||
closeFilter={close}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
});
|
||||
}, [user, parameterTemplates.data, parameterFilters]);
|
||||
|
||||
// Callback function when a parameter cell is clicked
|
||||
const onParameterClick = useCallback((template: number, instance: any) => {
|
||||
setSelectedTemplate(template);
|
||||
setSelectedInstance(instance.pk);
|
||||
const parameter = instance.parameters?.find(
|
||||
(p: any) => p.template == template
|
||||
);
|
||||
|
||||
if (parameter) {
|
||||
setSelectedParameter(parameter.pk);
|
||||
editParameter.open();
|
||||
} else {
|
||||
addParameter.open();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const tableFilters: TableFilter[] = useMemo(() => {
|
||||
return [...(customFilters || [])];
|
||||
}, [customFilters]);
|
||||
|
||||
const tableColumns: TableColumn[] = useMemo(() => {
|
||||
return [...(customColumns || []), ...parameterColumns];
|
||||
}, [customColumns, parameterColumns]);
|
||||
|
||||
const rowActions = useCallback(
|
||||
(record: any) => {
|
||||
return [
|
||||
{
|
||||
title: t`Add Parameter`,
|
||||
icon: <IconCirclePlus />,
|
||||
color: 'green',
|
||||
hidden: !user.hasAddPermission(modelType),
|
||||
onClick: () => {
|
||||
setSelectedInstance(record.pk);
|
||||
setSelectedTemplate(null);
|
||||
addParameter.open();
|
||||
}
|
||||
}
|
||||
];
|
||||
},
|
||||
[modelType, user]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{addParameter.modal}
|
||||
{editParameter.modal}
|
||||
<InvenTreeTable
|
||||
url={apiUrl(endpoint)}
|
||||
tableState={table}
|
||||
columns={tableColumns}
|
||||
props={{
|
||||
enableDownload: true,
|
||||
rowActions: rowActions,
|
||||
tableFilters: tableFilters,
|
||||
params: {
|
||||
...queryParams,
|
||||
parameters: true,
|
||||
...parametricQueryFilters
|
||||
},
|
||||
modelType: modelType,
|
||||
onCellClick: ({ event, record, index, column, columnIndex }) => {
|
||||
cancelEvent(event);
|
||||
|
||||
// Is this a "parameter" cell?
|
||||
if (column?.accessor?.toString()?.startsWith('parameter_')) {
|
||||
const col = column as any;
|
||||
onParameterClick(col.extra.template, record);
|
||||
} else if (record?.pk) {
|
||||
// Navigate through to the detail page
|
||||
const url = getDetailUrl(modelType, record.pk);
|
||||
navigateToLink(url, navigate, event);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,353 +1,18 @@
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { Group } from '@mantine/core';
|
||||
import { useHover } from '@mantine/hooks';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { type ReactNode, useCallback, useMemo, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { YesNoButton } from '@lib/components/YesNoButton';
|
||||
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
|
||||
import { ModelType } from '@lib/enums/ModelType';
|
||||
import { UserRoles } from '@lib/enums/Roles';
|
||||
import { apiUrl } from '@lib/functions/Api';
|
||||
import { cancelEvent } from '@lib/functions/Events';
|
||||
import { getDetailUrl } from '@lib/functions/Navigation';
|
||||
import { navigateToLink } from '@lib/functions/Navigation';
|
||||
import type { TableFilter } from '@lib/types/Filters';
|
||||
import type { ApiFormFieldSet } from '@lib/types/Forms';
|
||||
import type { TableColumn } from '@lib/types/Tables';
|
||||
import { useApi } from '../../contexts/ApiContext';
|
||||
import { formatDecimal } from '../../defaults/formatters';
|
||||
import { usePartParameterFields } from '../../forms/PartForms';
|
||||
import {
|
||||
useCreateApiFormModal,
|
||||
useEditApiFormModal
|
||||
} from '../../hooks/UseForm';
|
||||
import { useTable } from '../../hooks/UseTable';
|
||||
import { useUserState } from '../../states/UserState';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useMemo } from 'react';
|
||||
import { DescriptionColumn, PartColumn } from '../ColumnRenderers';
|
||||
import { InvenTreeTable } from '../InvenTreeTable';
|
||||
import { TableHoverCard } from '../TableHoverCard';
|
||||
import {
|
||||
PARAMETER_FILTER_OPERATORS,
|
||||
ParameterFilter
|
||||
} from './ParametricPartTableFilters';
|
||||
|
||||
// Render an individual parameter cell
|
||||
function ParameterCell({
|
||||
record,
|
||||
template,
|
||||
canEdit
|
||||
}: Readonly<{
|
||||
record: any;
|
||||
template: any;
|
||||
canEdit: boolean;
|
||||
}>) {
|
||||
const { hovered, ref } = useHover();
|
||||
|
||||
// Find matching template parameter
|
||||
const parameter = useMemo(() => {
|
||||
return record.parameters?.find((p: any) => p.template == template.pk);
|
||||
}, [record, template]);
|
||||
|
||||
const extra: any[] = [];
|
||||
|
||||
// Format the value for display
|
||||
const value: ReactNode = useMemo(() => {
|
||||
let v: any = parameter?.data;
|
||||
|
||||
// Handle boolean values
|
||||
if (template?.checkbox && v != undefined) {
|
||||
v = <YesNoButton value={parameter.data} />;
|
||||
}
|
||||
|
||||
return v;
|
||||
}, [parameter, template]);
|
||||
|
||||
if (
|
||||
template.units &&
|
||||
parameter &&
|
||||
parameter.data_numeric &&
|
||||
parameter.data_numeric != parameter.data
|
||||
) {
|
||||
const numeric = formatDecimal(parameter.data_numeric, { digits: 15 });
|
||||
extra.push(`${numeric} [${template.units}]`);
|
||||
}
|
||||
|
||||
if (hovered && canEdit) {
|
||||
extra.push(t`Click to edit`);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Group grow ref={ref} justify='space-between'>
|
||||
<Group grow>
|
||||
<TableHoverCard
|
||||
value={value ?? '-'}
|
||||
extra={extra}
|
||||
icon={hovered && canEdit ? 'edit' : 'info'}
|
||||
title={template.name}
|
||||
/>
|
||||
</Group>
|
||||
</Group>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
import ParametricDataTable from '../general/ParametricDataTable';
|
||||
|
||||
export default function ParametricPartTable({
|
||||
categoryId
|
||||
}: Readonly<{
|
||||
categoryId?: any;
|
||||
}>) {
|
||||
const api = useApi();
|
||||
const table = useTable('parametric-parts');
|
||||
const user = useUserState();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const categoryParameters = useQuery({
|
||||
queryKey: ['category-parameters', categoryId],
|
||||
queryFn: async () => {
|
||||
return api
|
||||
.get(apiUrl(ApiEndpoints.part_parameter_template_list), {
|
||||
params: {
|
||||
category: categoryId
|
||||
}
|
||||
})
|
||||
.then((response) => response.data);
|
||||
},
|
||||
refetchOnMount: true
|
||||
});
|
||||
|
||||
/* Store filters against selected part parameters.
|
||||
* These are stored in the format:
|
||||
* {
|
||||
* parameter_1: {
|
||||
* '=': 'value1',
|
||||
* '<': 'value2',
|
||||
* ...
|
||||
* },
|
||||
* parameter_2: {
|
||||
* '=': 'value3',
|
||||
* },
|
||||
* ...
|
||||
* }
|
||||
*
|
||||
* Which allows multiple filters to be applied against each parameter template.
|
||||
*/
|
||||
const [parameterFilters, setParameterFilters] = useState<any>({});
|
||||
|
||||
/* Remove filters for a specific parameter template
|
||||
* - If no operator is specified, remove all filters for this template
|
||||
* - If an operator is specified, remove filters for that operator only
|
||||
*/
|
||||
const clearParameterFilter = useCallback(
|
||||
(templateId: number, operator?: string) => {
|
||||
const filterName = `parameter_${templateId}`;
|
||||
|
||||
if (!operator) {
|
||||
// If no operator is specified, remove all filters for this template
|
||||
setParameterFilters((prev: any) => {
|
||||
const newFilters = { ...prev };
|
||||
// Remove any filters that match the template ID
|
||||
Object.keys(newFilters).forEach((key: string) => {
|
||||
if (key == filterName) {
|
||||
delete newFilters[key];
|
||||
}
|
||||
});
|
||||
return newFilters;
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// An operator is specified, so we remove filters for that operator only
|
||||
setParameterFilters((prev: any) => {
|
||||
const filters = { ...prev };
|
||||
|
||||
const paramFilters = filters[filterName] || {};
|
||||
|
||||
if (paramFilters[operator] !== undefined) {
|
||||
// Remove the specific operator filter
|
||||
delete paramFilters[operator];
|
||||
}
|
||||
|
||||
return {
|
||||
...filters,
|
||||
[filterName]: paramFilters
|
||||
};
|
||||
});
|
||||
|
||||
table.refreshTable();
|
||||
},
|
||||
[setParameterFilters, table.refreshTable]
|
||||
);
|
||||
|
||||
/**
|
||||
* Add (or update) a filter for a specific parameter template.
|
||||
* @param templateId - The ID of the parameter template to filter on.
|
||||
* @param value - The value to filter by.
|
||||
* @param operator - The operator to use for filtering (e.g., '=', '<', '>', etc.).
|
||||
*/
|
||||
const addParameterFilter = useCallback(
|
||||
(templateId: number, value: string, operator: string) => {
|
||||
const filterName = `parameter_${templateId}`;
|
||||
|
||||
const filterValue = value?.toString().trim() ?? '';
|
||||
|
||||
if (filterValue.length > 0) {
|
||||
setParameterFilters((prev: any) => {
|
||||
const filters = { ...prev };
|
||||
const paramFilters = filters[filterName] || {};
|
||||
|
||||
paramFilters[operator] = filterValue;
|
||||
|
||||
return {
|
||||
...filters,
|
||||
[filterName]: paramFilters
|
||||
};
|
||||
});
|
||||
|
||||
table.refreshTable();
|
||||
}
|
||||
},
|
||||
[setParameterFilters, clearParameterFilter, table.refreshTable]
|
||||
);
|
||||
|
||||
// Construct the query filters for the table based on the parameter filters
|
||||
const parametricQueryFilters = useMemo(() => {
|
||||
const filters: Record<string, string> = {};
|
||||
|
||||
Object.keys(parameterFilters).forEach((key: string) => {
|
||||
const paramFilters: any = parameterFilters[key];
|
||||
|
||||
Object.keys(paramFilters).forEach((operator: string) => {
|
||||
const name = `${key}${PARAMETER_FILTER_OPERATORS[operator] || ''}`;
|
||||
const value = paramFilters[operator];
|
||||
|
||||
filters[name] = value;
|
||||
});
|
||||
});
|
||||
|
||||
return filters;
|
||||
}, [parameterFilters]);
|
||||
|
||||
const [selectedPart, setSelectedPart] = useState<number>(0);
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<number>(0);
|
||||
const [selectedParameter, setSelectedParameter] = useState<number>(0);
|
||||
|
||||
const partParameterFields: ApiFormFieldSet = usePartParameterFields({
|
||||
editTemplate: false
|
||||
});
|
||||
|
||||
const addParameter = useCreateApiFormModal({
|
||||
url: ApiEndpoints.part_parameter_list,
|
||||
title: t`Add Part Parameter`,
|
||||
fields: useMemo(() => ({ ...partParameterFields }), [partParameterFields]),
|
||||
focus: 'data',
|
||||
onFormSuccess: (parameter: any) => {
|
||||
updateParameterRecord(selectedPart, parameter);
|
||||
},
|
||||
initialData: {
|
||||
part: selectedPart,
|
||||
template: selectedTemplate
|
||||
}
|
||||
});
|
||||
|
||||
const editParameter = useEditApiFormModal({
|
||||
url: ApiEndpoints.part_parameter_list,
|
||||
title: t`Edit Part Parameter`,
|
||||
pk: selectedParameter,
|
||||
fields: useMemo(() => ({ ...partParameterFields }), [partParameterFields]),
|
||||
focus: 'data',
|
||||
onFormSuccess: (parameter: any) => {
|
||||
updateParameterRecord(selectedPart, parameter);
|
||||
}
|
||||
});
|
||||
|
||||
// Update a single parameter record in the table
|
||||
const updateParameterRecord = useCallback(
|
||||
(part: number, parameter: any) => {
|
||||
const records = table.records;
|
||||
const partIndex = records.findIndex((record: any) => record.pk == part);
|
||||
|
||||
if (partIndex < 0) {
|
||||
// No matching part: reload the entire table
|
||||
table.refreshTable();
|
||||
return;
|
||||
}
|
||||
|
||||
const parameterIndex = records[partIndex].parameters.findIndex(
|
||||
(p: any) => p.pk == parameter.pk
|
||||
);
|
||||
|
||||
if (parameterIndex < 0) {
|
||||
// No matching parameter - append new parameter
|
||||
records[partIndex].parameters.push(parameter);
|
||||
} else {
|
||||
records[partIndex].parameters[parameterIndex] = parameter;
|
||||
}
|
||||
|
||||
table.updateRecord(records[partIndex]);
|
||||
},
|
||||
[table.records, table.updateRecord]
|
||||
);
|
||||
|
||||
const parameterColumns: TableColumn[] = useMemo(() => {
|
||||
const data = categoryParameters?.data || [];
|
||||
|
||||
return data.map((template: any) => {
|
||||
let title = template.name;
|
||||
|
||||
if (template.units) {
|
||||
title += ` [${template.units}]`;
|
||||
}
|
||||
|
||||
const filters = parameterFilters[`parameter_${template.pk}`] || {};
|
||||
|
||||
return {
|
||||
accessor: `parameter_${template.pk}`,
|
||||
title: title,
|
||||
sortable: true,
|
||||
extra: {
|
||||
template: template.pk
|
||||
},
|
||||
render: (record: any) => (
|
||||
<ParameterCell
|
||||
record={record}
|
||||
template={template}
|
||||
canEdit={user.hasChangeRole(UserRoles.part)}
|
||||
/>
|
||||
),
|
||||
filtering: Object.keys(filters).length > 0,
|
||||
filter: ({ close }: { close: () => void }) => {
|
||||
return (
|
||||
<ParameterFilter
|
||||
template={template}
|
||||
filters={parameterFilters[`parameter_${template.pk}`] || {}}
|
||||
setFilter={addParameterFilter}
|
||||
clearFilter={clearParameterFilter}
|
||||
closeFilter={close}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
});
|
||||
}, [user, categoryParameters.data, parameterFilters]);
|
||||
|
||||
const onParameterClick = useCallback((template: number, part: any) => {
|
||||
setSelectedTemplate(template);
|
||||
setSelectedPart(part.pk);
|
||||
const parameter = part.parameters?.find((p: any) => p.template == template);
|
||||
|
||||
if (parameter) {
|
||||
setSelectedParameter(parameter.pk);
|
||||
editParameter.open();
|
||||
} else {
|
||||
addParameter.open();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const tableFilters: TableFilter[] = useMemo(() => {
|
||||
const customFilters: TableFilter[] = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
name: 'active',
|
||||
@@ -367,8 +32,8 @@ export default function ParametricPartTable({
|
||||
];
|
||||
}, []);
|
||||
|
||||
const tableColumns: TableColumn[] = useMemo(() => {
|
||||
const partColumns: TableColumn[] = [
|
||||
const customColumns: TableColumn[] = useMemo(() => {
|
||||
return [
|
||||
PartColumn({
|
||||
part: '',
|
||||
switchable: false
|
||||
@@ -386,43 +51,19 @@ export default function ParametricPartTable({
|
||||
sortable: true
|
||||
}
|
||||
];
|
||||
|
||||
return [...partColumns, ...parameterColumns];
|
||||
}, [parameterColumns]);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
{addParameter.modal}
|
||||
{editParameter.modal}
|
||||
<InvenTreeTable
|
||||
url={apiUrl(ApiEndpoints.part_list)}
|
||||
tableState={table}
|
||||
columns={tableColumns}
|
||||
props={{
|
||||
enableDownload: true,
|
||||
tableFilters: tableFilters,
|
||||
params: {
|
||||
category: categoryId,
|
||||
cascade: true,
|
||||
category_detail: true,
|
||||
parameters: true,
|
||||
...parametricQueryFilters
|
||||
},
|
||||
onCellClick: ({ event, record, index, column, columnIndex }) => {
|
||||
cancelEvent(event);
|
||||
|
||||
// Is this a "parameter" cell?
|
||||
if (column?.accessor?.toString()?.startsWith('parameter_')) {
|
||||
const col = column as any;
|
||||
onParameterClick(col.extra.template, record);
|
||||
} else if (record?.pk) {
|
||||
// Navigate through to the part detail page
|
||||
const url = getDetailUrl(ModelType.part, record.pk);
|
||||
navigateToLink(url, navigate, event);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
<ParametricDataTable
|
||||
modelType={ModelType.part}
|
||||
endpoint={ApiEndpoints.part_list}
|
||||
customColumns={customColumns}
|
||||
customFilters={customFilters}
|
||||
queryParams={{
|
||||
category: categoryId,
|
||||
cascade: true,
|
||||
category_detail: true
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ export default function PartCategoryTemplateTable({
|
||||
value: categoryId,
|
||||
disabled: categoryId !== undefined
|
||||
},
|
||||
parameter_template: {},
|
||||
template: {},
|
||||
default_value: {}
|
||||
};
|
||||
}, [categoryId]);
|
||||
@@ -83,7 +83,7 @@ export default function PartCategoryTemplateTable({
|
||||
accessor: 'category_detail.pathstring'
|
||||
},
|
||||
{
|
||||
accessor: 'parameter_template_detail.name',
|
||||
accessor: 'template_detail.name',
|
||||
title: t`Parameter Template`,
|
||||
sortable: true,
|
||||
switchable: false
|
||||
@@ -99,8 +99,8 @@ export default function PartCategoryTemplateTable({
|
||||
|
||||
let units = '';
|
||||
|
||||
if (record?.parameter_template_detail?.units) {
|
||||
units = `[${record.parameter_template_detail.units}]`;
|
||||
if (record?.template_detail?.units) {
|
||||
units = `[${record.template_detail.units}]`;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -162,7 +162,9 @@ export default function PartCategoryTemplateTable({
|
||||
tableActions: tableActions,
|
||||
enableDownload: true,
|
||||
params: {
|
||||
category: categoryId
|
||||
category: categoryId,
|
||||
template_detail: true,
|
||||
category_detail: true
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -1,254 +0,0 @@
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { Alert, Stack, Text } from '@mantine/core';
|
||||
import { IconLock } from '@tabler/icons-react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import { AddItemButton } from '@lib/components/AddItemButton';
|
||||
import {
|
||||
type RowAction,
|
||||
RowDeleteAction,
|
||||
RowEditAction
|
||||
} from '@lib/components/RowActions';
|
||||
import { YesNoButton } from '@lib/components/YesNoButton';
|
||||
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
|
||||
import { UserRoles } from '@lib/enums/Roles';
|
||||
import { apiUrl } from '@lib/functions/Api';
|
||||
import type { TableFilter } from '@lib/types/Filters';
|
||||
import type { ApiFormFieldSet } from '@lib/types/Forms';
|
||||
import type { TableColumn } from '@lib/types/Tables';
|
||||
import { formatDecimal } from '../../defaults/formatters';
|
||||
import { usePartParameterFields } from '../../forms/PartForms';
|
||||
import {
|
||||
useCreateApiFormModal,
|
||||
useDeleteApiFormModal,
|
||||
useEditApiFormModal
|
||||
} from '../../hooks/UseForm';
|
||||
import { useTable } from '../../hooks/UseTable';
|
||||
import { useUserState } from '../../states/UserState';
|
||||
import {
|
||||
DateColumn,
|
||||
DescriptionColumn,
|
||||
NoteColumn,
|
||||
PartColumn,
|
||||
UserColumn
|
||||
} from '../ColumnRenderers';
|
||||
import { IncludeVariantsFilter, UserFilter } from '../Filter';
|
||||
import { InvenTreeTable } from '../InvenTreeTable';
|
||||
import { TableHoverCard } from '../TableHoverCard';
|
||||
|
||||
/**
|
||||
* Construct a table listing parameters for a given part
|
||||
*/
|
||||
export function PartParameterTable({
|
||||
partId,
|
||||
partLocked
|
||||
}: Readonly<{
|
||||
partId: any;
|
||||
partLocked?: boolean;
|
||||
}>) {
|
||||
const table = useTable('part-parameters');
|
||||
|
||||
const user = useUserState();
|
||||
|
||||
const tableColumns: TableColumn[] = useMemo(() => {
|
||||
return [
|
||||
PartColumn({
|
||||
part: 'part_detail'
|
||||
}),
|
||||
{
|
||||
accessor: 'part_detail.IPN',
|
||||
sortable: false,
|
||||
switchable: true,
|
||||
defaultVisible: false
|
||||
},
|
||||
{
|
||||
accessor: 'template_detail.name',
|
||||
switchable: false,
|
||||
sortable: true,
|
||||
ordering: 'name',
|
||||
render: (record) => {
|
||||
const variant = String(partId) != String(record.part);
|
||||
|
||||
return (
|
||||
<Text style={{ fontStyle: variant ? 'italic' : 'inherit' }}>
|
||||
{record.template_detail?.name}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
},
|
||||
DescriptionColumn({
|
||||
accessor: 'template_detail.description'
|
||||
}),
|
||||
{
|
||||
accessor: 'data',
|
||||
switchable: false,
|
||||
sortable: true,
|
||||
render: (record) => {
|
||||
const template = record.template_detail;
|
||||
|
||||
if (template?.checkbox) {
|
||||
return <YesNoButton value={record.data} />;
|
||||
}
|
||||
|
||||
const extra: any[] = [];
|
||||
|
||||
if (
|
||||
template.units &&
|
||||
record.data_numeric &&
|
||||
record.data_numeric != record.data
|
||||
) {
|
||||
const numeric = formatDecimal(record.data_numeric, { digits: 15 });
|
||||
extra.push(`${numeric} [${template.units}]`);
|
||||
}
|
||||
|
||||
return (
|
||||
<TableHoverCard
|
||||
value={record.data}
|
||||
extra={extra}
|
||||
title={t`Internal Units`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
accessor: 'template_detail.units',
|
||||
ordering: 'units',
|
||||
sortable: true
|
||||
},
|
||||
NoteColumn({}),
|
||||
DateColumn({
|
||||
accessor: 'updated',
|
||||
title: t`Last Updated`,
|
||||
sortable: true,
|
||||
switchable: true
|
||||
}),
|
||||
UserColumn({
|
||||
accessor: 'updated_by_detail',
|
||||
ordering: 'updated_by',
|
||||
title: t`Updated By`
|
||||
})
|
||||
];
|
||||
}, [partId]);
|
||||
|
||||
const tableFilters: TableFilter[] = useMemo(() => {
|
||||
return [
|
||||
IncludeVariantsFilter(),
|
||||
UserFilter({
|
||||
name: 'updated_by',
|
||||
label: t`Updated By`,
|
||||
description: t`Filter by user who last updated the parameter`
|
||||
})
|
||||
];
|
||||
}, []);
|
||||
|
||||
const partParameterFields: ApiFormFieldSet = usePartParameterFields({});
|
||||
|
||||
const newParameter = useCreateApiFormModal({
|
||||
url: ApiEndpoints.part_parameter_list,
|
||||
title: t`New Part Parameter`,
|
||||
fields: useMemo(() => ({ ...partParameterFields }), [partParameterFields]),
|
||||
focus: 'template',
|
||||
initialData: {
|
||||
part: partId
|
||||
},
|
||||
table: table
|
||||
});
|
||||
|
||||
const [selectedParameter, setSelectedParameter] = useState<
|
||||
number | undefined
|
||||
>(undefined);
|
||||
|
||||
const editParameter = useEditApiFormModal({
|
||||
url: ApiEndpoints.part_parameter_list,
|
||||
pk: selectedParameter,
|
||||
title: t`Edit Part Parameter`,
|
||||
focus: 'data',
|
||||
fields: useMemo(() => ({ ...partParameterFields }), [partParameterFields]),
|
||||
table: table
|
||||
});
|
||||
|
||||
const deleteParameter = useDeleteApiFormModal({
|
||||
url: ApiEndpoints.part_parameter_list,
|
||||
pk: selectedParameter,
|
||||
title: t`Delete Part Parameter`,
|
||||
table: table
|
||||
});
|
||||
|
||||
// Callback for row actions
|
||||
const rowActions = useCallback(
|
||||
(record: any): RowAction[] => {
|
||||
// Actions not allowed for "variant" rows
|
||||
if (String(partId) != String(record.part)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
RowEditAction({
|
||||
tooltip: t`Edit Part Parameter`,
|
||||
hidden: partLocked || !user.hasChangeRole(UserRoles.part),
|
||||
onClick: () => {
|
||||
setSelectedParameter(record.pk);
|
||||
editParameter.open();
|
||||
}
|
||||
}),
|
||||
RowDeleteAction({
|
||||
tooltip: t`Delete Part Parameter`,
|
||||
hidden: partLocked || !user.hasDeleteRole(UserRoles.part),
|
||||
onClick: () => {
|
||||
setSelectedParameter(record.pk);
|
||||
deleteParameter.open();
|
||||
}
|
||||
})
|
||||
];
|
||||
},
|
||||
[partId, partLocked, user]
|
||||
);
|
||||
|
||||
// Custom table actions
|
||||
const tableActions = useMemo(() => {
|
||||
return [
|
||||
<AddItemButton
|
||||
key='add-parameter'
|
||||
hidden={partLocked || !user.hasAddRole(UserRoles.part)}
|
||||
tooltip={t`Add parameter`}
|
||||
onClick={() => newParameter.open()}
|
||||
/>
|
||||
];
|
||||
}, [partLocked, user]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{newParameter.modal}
|
||||
{editParameter.modal}
|
||||
{deleteParameter.modal}
|
||||
<Stack gap='xs'>
|
||||
{partLocked && (
|
||||
<Alert
|
||||
title={t`Part is Locked`}
|
||||
color='orange'
|
||||
icon={<IconLock />}
|
||||
p='xs'
|
||||
>
|
||||
<Text>{t`Part parameters cannot be edited, as the part is locked`}</Text>
|
||||
</Alert>
|
||||
)}
|
||||
<InvenTreeTable
|
||||
url={apiUrl(ApiEndpoints.part_parameter_list)}
|
||||
tableState={table}
|
||||
columns={tableColumns}
|
||||
props={{
|
||||
rowActions: rowActions,
|
||||
enableDownload: true,
|
||||
tableActions: tableActions,
|
||||
tableFilters: tableFilters,
|
||||
params: {
|
||||
part: partId,
|
||||
template_detail: true,
|
||||
part_detail: true
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,140 +0,0 @@
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import { AddItemButton } from '@lib/components/AddItemButton';
|
||||
import {
|
||||
type RowAction,
|
||||
RowDeleteAction,
|
||||
RowEditAction
|
||||
} from '@lib/components/RowActions';
|
||||
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
|
||||
import { UserRoles } from '@lib/enums/Roles';
|
||||
import { apiUrl } from '@lib/functions/Api';
|
||||
import type { TableColumn } from '@lib/types/Tables';
|
||||
import { useManufacturerPartParameterFields } from '../../forms/CompanyForms';
|
||||
import {
|
||||
useCreateApiFormModal,
|
||||
useDeleteApiFormModal,
|
||||
useEditApiFormModal
|
||||
} from '../../hooks/UseForm';
|
||||
import { useTable } from '../../hooks/UseTable';
|
||||
import { useUserState } from '../../states/UserState';
|
||||
import { InvenTreeTable } from '../InvenTreeTable';
|
||||
|
||||
export default function ManufacturerPartParameterTable({
|
||||
params
|
||||
}: Readonly<{
|
||||
params: any;
|
||||
}>) {
|
||||
const table = useTable('manufacturer-part-parameter');
|
||||
const user = useUserState();
|
||||
|
||||
const tableColumns: TableColumn[] = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
accessor: 'name',
|
||||
title: t`Name`,
|
||||
sortable: true,
|
||||
switchable: false
|
||||
},
|
||||
{
|
||||
accessor: 'value',
|
||||
title: t`Value`,
|
||||
sortable: true,
|
||||
switchable: false
|
||||
},
|
||||
{
|
||||
accessor: 'units',
|
||||
title: t`Units`,
|
||||
sortable: false,
|
||||
switchable: true
|
||||
}
|
||||
];
|
||||
}, []);
|
||||
|
||||
const fields = useManufacturerPartParameterFields();
|
||||
|
||||
const [selectedParameter, setSelectedParameter] = useState<
|
||||
number | undefined
|
||||
>(undefined);
|
||||
|
||||
const createParameter = useCreateApiFormModal({
|
||||
url: ApiEndpoints.manufacturer_part_parameter_list,
|
||||
title: t`Add Parameter`,
|
||||
fields: fields,
|
||||
table: table,
|
||||
initialData: {
|
||||
manufacturer_part: params.manufacturer_part
|
||||
}
|
||||
});
|
||||
|
||||
const editParameter = useEditApiFormModal({
|
||||
url: ApiEndpoints.manufacturer_part_parameter_list,
|
||||
pk: selectedParameter,
|
||||
title: t`Edit Parameter`,
|
||||
fields: fields,
|
||||
table: table
|
||||
});
|
||||
|
||||
const deleteParameter = useDeleteApiFormModal({
|
||||
url: ApiEndpoints.manufacturer_part_parameter_list,
|
||||
pk: selectedParameter,
|
||||
title: t`Delete Parameter`,
|
||||
table: table
|
||||
});
|
||||
|
||||
const rowActions = useCallback(
|
||||
(record: any): RowAction[] => {
|
||||
return [
|
||||
RowEditAction({
|
||||
hidden: !user.hasChangeRole(UserRoles.purchase_order),
|
||||
onClick: () => {
|
||||
setSelectedParameter(record.pk);
|
||||
editParameter.open();
|
||||
}
|
||||
}),
|
||||
RowDeleteAction({
|
||||
hidden: !user.hasDeleteRole(UserRoles.purchase_order),
|
||||
onClick: () => {
|
||||
setSelectedParameter(record.pk);
|
||||
deleteParameter.open();
|
||||
}
|
||||
})
|
||||
];
|
||||
},
|
||||
[user]
|
||||
);
|
||||
|
||||
const tableActions = useMemo(() => {
|
||||
return [
|
||||
<AddItemButton
|
||||
key='add-parameter'
|
||||
tooltip={t`Add Parameter`}
|
||||
onClick={() => {
|
||||
createParameter.open();
|
||||
}}
|
||||
hidden={!user.hasAddRole(UserRoles.purchase_order)}
|
||||
/>
|
||||
];
|
||||
}, [user]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{createParameter.modal}
|
||||
{editParameter.modal}
|
||||
{deleteParameter.modal}
|
||||
<InvenTreeTable
|
||||
url={apiUrl(ApiEndpoints.manufacturer_part_parameter_list)}
|
||||
tableState={table}
|
||||
columns={tableColumns}
|
||||
props={{
|
||||
params: {
|
||||
...params
|
||||
},
|
||||
rowActions: rowActions,
|
||||
tableActions: tableActions
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { ApiEndpoints, ModelType } from '@lib/index';
|
||||
import type { TableFilter } from '@lib/types/Filters';
|
||||
import type { TableColumn } from '@lib/types/Tables';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { type ReactNode, useMemo } from 'react';
|
||||
import { CompanyColumn, PartColumn } from '../ColumnRenderers';
|
||||
import ParametricDataTable from '../general/ParametricDataTable';
|
||||
|
||||
export default function ManufacturerPartParametricTable({
|
||||
queryParams
|
||||
}: {
|
||||
queryParams?: Record<string, any>;
|
||||
}): ReactNode {
|
||||
const customColumns: TableColumn[] = useMemo(() => {
|
||||
return [
|
||||
PartColumn({
|
||||
switchable: false
|
||||
}),
|
||||
{
|
||||
accessor: 'part_detail.IPN',
|
||||
title: t`IPN`,
|
||||
sortable: false,
|
||||
switchable: true
|
||||
},
|
||||
{
|
||||
accessor: 'manufacturer',
|
||||
sortable: true,
|
||||
render: (record: any) => (
|
||||
<CompanyColumn company={record?.manufacturer_detail} />
|
||||
)
|
||||
},
|
||||
{
|
||||
accessor: 'MPN',
|
||||
title: t`MPN`,
|
||||
sortable: true
|
||||
}
|
||||
];
|
||||
}, []);
|
||||
|
||||
const customFilters: TableFilter[] = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
name: 'part_active',
|
||||
label: t`Active Part`,
|
||||
description: t`Show manufacturer parts for active internal parts.`,
|
||||
type: 'boolean'
|
||||
},
|
||||
{
|
||||
name: 'manufacturer_active',
|
||||
label: t`Active Manufacturer`,
|
||||
description: t`Show manufacturer parts for active manufacturers.`,
|
||||
type: 'boolean'
|
||||
}
|
||||
];
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ParametricDataTable
|
||||
modelType={ModelType.manufacturerpart}
|
||||
endpoint={ApiEndpoints.manufacturer_part_list}
|
||||
customColumns={customColumns}
|
||||
customFilters={customFilters}
|
||||
queryParams={{
|
||||
...queryParams,
|
||||
part_detail: true,
|
||||
manufacturer_detail: true
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
|
||||
import { ModelType } from '@lib/enums/ModelType';
|
||||
import type { TableFilter } from '@lib/types/Filters';
|
||||
import type { TableColumn } from '@lib/types/Tables';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { type ReactNode, useMemo } from 'react';
|
||||
import {
|
||||
CompanyColumn,
|
||||
DescriptionColumn,
|
||||
ReferenceColumn
|
||||
} from '../ColumnRenderers';
|
||||
import {
|
||||
AssignedToMeFilter,
|
||||
OrderStatusFilter,
|
||||
OutstandingFilter,
|
||||
OverdueFilter,
|
||||
ProjectCodeFilter,
|
||||
ResponsibleFilter
|
||||
} from '../Filter';
|
||||
import ParametricDataTable from '../general/ParametricDataTable';
|
||||
|
||||
export default function PurchaseOrderParametricTable({
|
||||
queryParams
|
||||
}: {
|
||||
queryParams?: Record<string, any>;
|
||||
}): ReactNode {
|
||||
const customColumns: TableColumn[] = useMemo(() => {
|
||||
return [
|
||||
ReferenceColumn({
|
||||
switchable: false
|
||||
}),
|
||||
{
|
||||
accessor: 'supplier__name',
|
||||
title: t`Supplier`,
|
||||
sortable: true,
|
||||
render: (record: any) => (
|
||||
<CompanyColumn company={record.supplier_detail} />
|
||||
)
|
||||
},
|
||||
DescriptionColumn({})
|
||||
];
|
||||
}, []);
|
||||
|
||||
const customFilters: TableFilter[] = useMemo(() => {
|
||||
return [
|
||||
OrderStatusFilter({ model: ModelType.purchaseorder }),
|
||||
OutstandingFilter(),
|
||||
OverdueFilter(),
|
||||
AssignedToMeFilter(),
|
||||
ProjectCodeFilter(),
|
||||
ResponsibleFilter()
|
||||
];
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ParametricDataTable
|
||||
modelType={ModelType.purchaseorder}
|
||||
endpoint={ApiEndpoints.purchase_order_list}
|
||||
customColumns={customColumns}
|
||||
customFilters={customFilters}
|
||||
queryParams={{
|
||||
...queryParams,
|
||||
supplier_detail: true
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { ApiEndpoints, ModelType } from '@lib/index';
|
||||
import type { TableFilter } from '@lib/types/Filters';
|
||||
import type { TableColumn } from '@lib/types/Tables';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { type ReactNode, useMemo } from 'react';
|
||||
import { CompanyColumn, PartColumn } from '../ColumnRenderers';
|
||||
import ParametricDataTable from '../general/ParametricDataTable';
|
||||
|
||||
export default function SupplierPartParametricTable({
|
||||
queryParams
|
||||
}: {
|
||||
queryParams?: Record<string, any>;
|
||||
}): ReactNode {
|
||||
const customColumns: TableColumn[] = useMemo(() => {
|
||||
return [
|
||||
PartColumn({
|
||||
switchable: false,
|
||||
part: 'part_detail'
|
||||
}),
|
||||
{
|
||||
accessor: 'supplier',
|
||||
sortable: true,
|
||||
render: (record: any) => (
|
||||
<CompanyColumn company={record?.supplier_detail} />
|
||||
)
|
||||
},
|
||||
{
|
||||
accessor: 'SKU',
|
||||
title: t`Supplier Part`,
|
||||
sortable: true
|
||||
}
|
||||
];
|
||||
}, []);
|
||||
|
||||
const customFilters: TableFilter[] = useMemo(() => {
|
||||
return [];
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ParametricDataTable
|
||||
modelType={ModelType.supplierpart}
|
||||
endpoint={ApiEndpoints.supplier_part_list}
|
||||
customColumns={customColumns}
|
||||
customFilters={customFilters}
|
||||
queryParams={{
|
||||
...queryParams,
|
||||
part_detail: true,
|
||||
supplier_detail: true
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
65
src/frontend/src/tables/sales/ReturnOrderParametricTable.tsx
Normal file
65
src/frontend/src/tables/sales/ReturnOrderParametricTable.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
|
||||
import { ModelType } from '@lib/enums/ModelType';
|
||||
import type { TableFilter } from '@lib/types/Filters';
|
||||
import type { TableColumn } from '@lib/types/Tables';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { type ReactNode, useMemo } from 'react';
|
||||
import {
|
||||
CompanyColumn,
|
||||
DescriptionColumn,
|
||||
ReferenceColumn
|
||||
} from '../ColumnRenderers';
|
||||
import {
|
||||
AssignedToMeFilter,
|
||||
OrderStatusFilter,
|
||||
OutstandingFilter,
|
||||
OverdueFilter,
|
||||
ProjectCodeFilter,
|
||||
ResponsibleFilter
|
||||
} from '../Filter';
|
||||
import ParametricDataTable from '../general/ParametricDataTable';
|
||||
|
||||
export default function ReturnOrderParametricTable({
|
||||
queryParams
|
||||
}: {
|
||||
queryParams?: Record<string, any>;
|
||||
}): ReactNode {
|
||||
const customColumns: TableColumn[] = useMemo(() => {
|
||||
return [
|
||||
ReferenceColumn({ switchable: false }),
|
||||
{
|
||||
accessor: 'customer__name',
|
||||
title: t`Customer`,
|
||||
sortable: true,
|
||||
render: (record: any) => (
|
||||
<CompanyColumn company={record.customer_detail} />
|
||||
)
|
||||
},
|
||||
DescriptionColumn({})
|
||||
];
|
||||
}, []);
|
||||
|
||||
const customFilters: TableFilter[] = useMemo(() => {
|
||||
return [
|
||||
OrderStatusFilter({ model: ModelType.returnorder }),
|
||||
OutstandingFilter(),
|
||||
OverdueFilter(),
|
||||
AssignedToMeFilter(),
|
||||
ProjectCodeFilter(),
|
||||
ResponsibleFilter()
|
||||
];
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ParametricDataTable
|
||||
modelType={ModelType.returnorder}
|
||||
endpoint={ApiEndpoints.return_order_list}
|
||||
customColumns={customColumns}
|
||||
customFilters={customFilters}
|
||||
queryParams={{
|
||||
...queryParams,
|
||||
customer_detail: true
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
65
src/frontend/src/tables/sales/SalesOrderParametricTable.tsx
Normal file
65
src/frontend/src/tables/sales/SalesOrderParametricTable.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
|
||||
import { ModelType } from '@lib/enums/ModelType';
|
||||
import type { TableFilter } from '@lib/types/Filters';
|
||||
import type { TableColumn } from '@lib/types/Tables';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { type ReactNode, useMemo } from 'react';
|
||||
import {
|
||||
CompanyColumn,
|
||||
DescriptionColumn,
|
||||
ReferenceColumn
|
||||
} from '../ColumnRenderers';
|
||||
import {
|
||||
AssignedToMeFilter,
|
||||
OrderStatusFilter,
|
||||
OutstandingFilter,
|
||||
OverdueFilter,
|
||||
ProjectCodeFilter,
|
||||
ResponsibleFilter
|
||||
} from '../Filter';
|
||||
import ParametricDataTable from '../general/ParametricDataTable';
|
||||
|
||||
export default function SalesOrderParametricTable({
|
||||
queryParams
|
||||
}: {
|
||||
queryParams?: Record<string, any>;
|
||||
}): ReactNode {
|
||||
const customColumns: TableColumn[] = useMemo(() => {
|
||||
return [
|
||||
ReferenceColumn({ switchable: false }),
|
||||
{
|
||||
accessor: 'customer__name',
|
||||
title: t`Customer`,
|
||||
sortable: true,
|
||||
render: (record: any) => (
|
||||
<CompanyColumn company={record.customer_detail} />
|
||||
)
|
||||
},
|
||||
DescriptionColumn({})
|
||||
];
|
||||
}, []);
|
||||
|
||||
const customFilters: TableFilter[] = useMemo(() => {
|
||||
return [
|
||||
OrderStatusFilter({ model: ModelType.salesorder }),
|
||||
OutstandingFilter(),
|
||||
OverdueFilter(),
|
||||
AssignedToMeFilter(),
|
||||
ProjectCodeFilter(),
|
||||
ResponsibleFilter()
|
||||
];
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ParametricDataTable
|
||||
modelType={ModelType.salesorder}
|
||||
endpoint={ApiEndpoints.sales_order_list}
|
||||
customColumns={customColumns}
|
||||
customFilters={customFilters}
|
||||
queryParams={{
|
||||
...queryParams,
|
||||
customer_detail: true
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,11 +1,20 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { type Page, expect } from '@playwright/test';
|
||||
import { createApi } from './api';
|
||||
|
||||
export const clickOnParamFilter = async (page: Page, name: string) => {
|
||||
const button = await page
|
||||
.getByRole('button', { name: `${name} Not sorted` })
|
||||
.getByRole('button')
|
||||
.first();
|
||||
await button.scrollIntoViewIfNeeded();
|
||||
await button.click();
|
||||
};
|
||||
|
||||
/**
|
||||
* Open the filter drawer for the currently visible table
|
||||
* @param page - The page object
|
||||
*/
|
||||
export const openFilterDrawer = async (page) => {
|
||||
export const openFilterDrawer = async (page: Page) => {
|
||||
await page.getByLabel('table-select-filters').click();
|
||||
};
|
||||
|
||||
@@ -13,7 +22,7 @@ export const openFilterDrawer = async (page) => {
|
||||
* Close the filter drawer for the currently visible table
|
||||
* @param page - The page object
|
||||
*/
|
||||
export const closeFilterDrawer = async (page) => {
|
||||
export const closeFilterDrawer = async (page: Page) => {
|
||||
await page.getByLabel('filter-drawer-close').click();
|
||||
};
|
||||
|
||||
@@ -22,7 +31,11 @@ export const closeFilterDrawer = async (page) => {
|
||||
* @param page - The page object
|
||||
* @param name - The name of the button to click
|
||||
*/
|
||||
export const clickButtonIfVisible = async (page, name, timeout = 500) => {
|
||||
export const clickButtonIfVisible = async (
|
||||
page: Page,
|
||||
name: string,
|
||||
timeout = 500
|
||||
) => {
|
||||
await page.waitForTimeout(timeout);
|
||||
|
||||
if (await page.getByRole('button', { name }).isVisible()) {
|
||||
@@ -34,14 +47,14 @@ export const clickButtonIfVisible = async (page, name, timeout = 500) => {
|
||||
* Clear all filters from the currently visible table
|
||||
* @param page - The page object
|
||||
*/
|
||||
export const clearTableFilters = async (page) => {
|
||||
export const clearTableFilters = async (page: Page) => {
|
||||
await openFilterDrawer(page);
|
||||
await clickButtonIfVisible(page, 'Clear Filters', 250);
|
||||
await closeFilterDrawer(page);
|
||||
await page.waitForLoadState('networkidle');
|
||||
};
|
||||
|
||||
export const setTableChoiceFilter = async (page, filter, value) => {
|
||||
export const setTableChoiceFilter = async (page: Page, filter, value) => {
|
||||
await openFilterDrawer(page);
|
||||
|
||||
await page.getByRole('button', { name: 'Add Filter' }).click();
|
||||
@@ -103,7 +116,7 @@ export const navigate = async (
|
||||
/**
|
||||
* CLick on the 'tab' element with the provided name
|
||||
*/
|
||||
export const loadTab = async (page, tabName, exact?) => {
|
||||
export const loadTab = async (page: Page, tabName, exact?) => {
|
||||
await page
|
||||
.getByLabel(/panel-tabs-/)
|
||||
.getByRole('tab', { name: tabName, exact: exact ?? false })
|
||||
@@ -113,13 +126,13 @@ export const loadTab = async (page, tabName, exact?) => {
|
||||
};
|
||||
|
||||
// Activate "table" view in certain contexts
|
||||
export const activateTableView = async (page) => {
|
||||
export const activateTableView = async (page: Page) => {
|
||||
await page.getByLabel('segmented-icon-control-table').click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
};
|
||||
|
||||
// Activate "calendar" view in certain contexts
|
||||
export const activateCalendarView = async (page) => {
|
||||
export const activateCalendarView = async (page: Page) => {
|
||||
await page.getByLabel('segmented-icon-control-calendar').click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
};
|
||||
@@ -127,7 +140,7 @@ export const activateCalendarView = async (page) => {
|
||||
/**
|
||||
* Perform a 'global search' on the provided page, for the provided query text
|
||||
*/
|
||||
export const globalSearch = async (page, query) => {
|
||||
export const globalSearch = async (page: Page, query) => {
|
||||
await page.getByLabel('open-search').click();
|
||||
await page.getByLabel('global-search-input').clear();
|
||||
await page.getByPlaceholder('Enter search text').fill(query);
|
||||
|
||||
@@ -11,6 +11,26 @@ import {
|
||||
} from '../helpers.ts';
|
||||
import { doCachedLogin } from '../login.ts';
|
||||
|
||||
test('Build - Index', async ({ browser }) => {
|
||||
const page = await doCachedLogin(browser, { url: 'manufacturing/index/' });
|
||||
|
||||
await loadTab(page, 'Build Orders');
|
||||
|
||||
// Ensure all data views are available
|
||||
await page
|
||||
.getByRole('button', { name: 'segmented-icon-control-parametric' })
|
||||
.click();
|
||||
|
||||
await page
|
||||
.getByRole('button', { name: 'segmented-icon-control-calendar' })
|
||||
.click();
|
||||
await page.getByRole('button', { name: 'action-button-next-month' }).click();
|
||||
|
||||
await page
|
||||
.getByRole('button', { name: 'segmented-icon-control-table' })
|
||||
.click();
|
||||
});
|
||||
|
||||
test('Build Order - Basic Tests', async ({ browser }) => {
|
||||
const page = await doCachedLogin(browser);
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { test } from '../baseFixtures.js';
|
||||
import { loadTab, navigate } from '../helpers.js';
|
||||
import { clickOnParamFilter, loadTab, navigate } from '../helpers.js';
|
||||
import { doCachedLogin } from '../login.js';
|
||||
|
||||
test('Company', async ({ browser }) => {
|
||||
@@ -40,3 +40,23 @@ test('Company', async ({ browser }) => {
|
||||
await page.getByText('Enter a valid URL.').waitFor();
|
||||
await page.getByRole('button', { name: 'Cancel' }).click();
|
||||
});
|
||||
|
||||
test('Company - Parameters', async ({ browser }) => {
|
||||
const page = await doCachedLogin(browser, {
|
||||
username: 'steven',
|
||||
password: 'wizardstaff',
|
||||
url: 'purchasing/index/suppliers'
|
||||
});
|
||||
|
||||
// Show parametric view
|
||||
await page
|
||||
.getByRole('button', { name: 'segmented-icon-control-parametric' })
|
||||
.click();
|
||||
|
||||
// Filter by "payment terms" parameter value
|
||||
await clickOnParamFilter(page, 'Payment Terms');
|
||||
await page.getByRole('option', { name: 'NET-30' }).click();
|
||||
|
||||
await page.getByRole('cell', { name: 'Arrow Electronics' }).waitFor();
|
||||
await page.getByRole('cell', { name: 'PCB assembly house' }).waitFor();
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { test } from '../baseFixtures';
|
||||
import {
|
||||
clearTableFilters,
|
||||
clickOnParamFilter,
|
||||
clickOnRowMenu,
|
||||
deletePart,
|
||||
getRowFromCell,
|
||||
@@ -182,7 +183,14 @@ test('Parts - Locking', async ({ browser }) => {
|
||||
.waitFor();
|
||||
|
||||
await loadTab(page, 'Parameters');
|
||||
await page.getByLabel('action-button-add-parameter').waitFor();
|
||||
await page
|
||||
.getByRole('button', { name: 'action-menu-add-parameters' })
|
||||
.click();
|
||||
await page
|
||||
.getByRole('menuitem', {
|
||||
name: 'action-menu-add-parameters-create-parameter'
|
||||
})
|
||||
.click();
|
||||
|
||||
// Navigate to a known assembly which *is* locked
|
||||
await navigate(page, 'part/100/bom');
|
||||
@@ -495,7 +503,14 @@ test('Parts - Parameters', async ({ browser }) => {
|
||||
const page = await doCachedLogin(browser, { url: 'part/69/parameters' });
|
||||
|
||||
// Create a new template
|
||||
await page.getByLabel('action-button-add-parameter').click();
|
||||
await page
|
||||
.getByRole('button', { name: 'action-menu-add-parameters' })
|
||||
.click();
|
||||
await page
|
||||
.getByRole('menuitem', {
|
||||
name: 'action-menu-add-parameters-create-parameter'
|
||||
})
|
||||
.click();
|
||||
|
||||
// Select the "Color" parameter template (should create a "choice" field)
|
||||
await page.getByLabel('related-field-template').fill('Color');
|
||||
@@ -509,7 +524,7 @@ test('Parts - Parameters', async ({ browser }) => {
|
||||
|
||||
// Select the "polarized" parameter template (should create a "checkbox" field)
|
||||
await page.getByLabel('related-field-template').fill('Polarized');
|
||||
await page.getByText('Is this part polarized?').click();
|
||||
await page.getByRole('option', { name: 'Polarized Is this part' }).click();
|
||||
|
||||
// Submit with "false" value
|
||||
await page.getByRole('button', { name: 'Submit' }).click();
|
||||
@@ -538,38 +553,33 @@ test('Parts - Parameters', async ({ browser }) => {
|
||||
// Finally, delete the parameter
|
||||
await row.getByLabel(/row-action-menu-/i).click();
|
||||
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
||||
await page.getByRole('button', { name: 'Delete' }).click();
|
||||
|
||||
await page.getByRole('button', { name: 'Delete', exact: true }).click();
|
||||
await page.getByText('No records found').first().waitFor();
|
||||
});
|
||||
|
||||
test('Parts - Parameter Filtering', async ({ browser }) => {
|
||||
const page = await doCachedLogin(browser, { url: 'part/' });
|
||||
|
||||
await loadTab(page, 'Part Parameters');
|
||||
await loadTab(page, 'Parts', true);
|
||||
await page
|
||||
.getByRole('button', { name: 'segmented-icon-control-parametric' })
|
||||
.click();
|
||||
|
||||
await clearTableFilters(page);
|
||||
|
||||
// All parts should be available (no filters applied)
|
||||
await page.getByText(/\/ 42\d/).waitFor();
|
||||
|
||||
const clickOnParamFilter = async (name: string) => {
|
||||
const button = await page
|
||||
.getByRole('button', { name: `${name} Not sorted` })
|
||||
.getByRole('button')
|
||||
.first();
|
||||
await button.scrollIntoViewIfNeeded();
|
||||
await button.click();
|
||||
};
|
||||
|
||||
const clearParamFilter = async (name: string) => {
|
||||
await clickOnParamFilter(name);
|
||||
await clickOnParamFilter(page, name);
|
||||
await page.getByLabel(`clear-filter-${name}`).waitFor();
|
||||
await page.getByLabel(`clear-filter-${name}`).click();
|
||||
// await page.getByLabel(`clear-filter-${name}`).click();
|
||||
};
|
||||
|
||||
// Let's filter by color
|
||||
await clickOnParamFilter('Color');
|
||||
await clickOnParamFilter(page, 'Color');
|
||||
await page.getByRole('option', { name: 'Red' }).click();
|
||||
|
||||
// Only 10 parts available
|
||||
|
||||
@@ -13,6 +13,117 @@ import {
|
||||
} from '../helpers.ts';
|
||||
import { doCachedLogin } from '../login.ts';
|
||||
|
||||
test('Purchasing - Index', async ({ browser }) => {
|
||||
const page = await doCachedLogin(browser, { url: 'purchasing/index/' });
|
||||
|
||||
// Purchase Orders tab
|
||||
await loadTab(page, 'Purchase Orders');
|
||||
await page
|
||||
.getByRole('button', { name: 'segmented-icon-control-parametric' })
|
||||
.click();
|
||||
|
||||
await page
|
||||
.getByRole('button', { name: 'segmented-icon-control-calendar' })
|
||||
.click();
|
||||
await page.getByRole('button', { name: 'calendar-select-month' }).waitFor();
|
||||
await page
|
||||
.getByRole('button', { name: 'segmented-icon-control-table' })
|
||||
.click();
|
||||
|
||||
// Suppliers tab
|
||||
await loadTab(page, 'Suppliers');
|
||||
await page
|
||||
.getByRole('button', { name: 'segmented-icon-control-parametric' })
|
||||
.click();
|
||||
await page
|
||||
.getByRole('button', { name: 'segmented-icon-control-table' })
|
||||
.click();
|
||||
|
||||
// Supplier parts tab
|
||||
await loadTab(page, 'Supplier Parts');
|
||||
await page
|
||||
.getByRole('button', { name: 'segmented-icon-control-parametric' })
|
||||
.click();
|
||||
await page
|
||||
.getByRole('button', { name: 'segmented-icon-control-table' })
|
||||
.click();
|
||||
|
||||
// Manufacturers tab
|
||||
await loadTab(page, 'Manufacturers');
|
||||
await page
|
||||
.getByRole('button', { name: 'segmented-icon-control-parametric' })
|
||||
.click();
|
||||
await page
|
||||
.getByRole('button', { name: 'segmented-icon-control-table' })
|
||||
.click();
|
||||
|
||||
// Manufacturer parts tab
|
||||
await loadTab(page, 'Manufacturer Parts');
|
||||
await page
|
||||
.getByRole('button', { name: 'segmented-icon-control-parametric' })
|
||||
.click();
|
||||
await page
|
||||
.getByRole('button', { name: 'segmented-icon-control-table' })
|
||||
.click();
|
||||
});
|
||||
|
||||
test('Purchase Orders - General', async ({ browser }) => {
|
||||
const page = await doCachedLogin(browser);
|
||||
|
||||
await page.getByRole('tab', { name: 'Purchasing' }).click();
|
||||
await page.waitForURL('**/purchasing/index/**');
|
||||
|
||||
await page.getByRole('cell', { name: 'PO0012' }).click();
|
||||
await page.waitForTimeout(200);
|
||||
|
||||
await loadTab(page, 'Line Items');
|
||||
await loadTab(page, 'Received Stock');
|
||||
await loadTab(page, 'Parameters');
|
||||
await loadTab(page, 'Attachments');
|
||||
|
||||
await page.getByRole('tab', { name: 'Purchasing' }).click();
|
||||
await loadTab(page, 'Suppliers');
|
||||
await page.getByText('Arrow', { exact: true }).click();
|
||||
await page.waitForTimeout(200);
|
||||
|
||||
await loadTab(page, 'Supplied Parts');
|
||||
await loadTab(page, 'Purchase Orders');
|
||||
await loadTab(page, 'Stock Items');
|
||||
await loadTab(page, 'Contacts');
|
||||
await loadTab(page, 'Addresses');
|
||||
await loadTab(page, 'Attachments');
|
||||
|
||||
await page.getByRole('tab', { name: 'Purchasing' }).click();
|
||||
await loadTab(page, 'Manufacturers');
|
||||
await page.getByText('AVX Corporation').click();
|
||||
await page.waitForTimeout(200);
|
||||
|
||||
await loadTab(page, 'Addresses');
|
||||
await page.getByRole('cell', { name: 'West Branch' }).click();
|
||||
await page.locator('.mantine-ScrollArea-root').click();
|
||||
await page
|
||||
.getByRole('row', { name: 'West Branch Yes Surf Avenue 9' })
|
||||
.getByRole('button')
|
||||
.click();
|
||||
await page.getByRole('menuitem', { name: 'Edit' }).click();
|
||||
|
||||
await page.getByLabel('text-field-title', { exact: true }).waitFor();
|
||||
await page.getByLabel('text-field-line2', { exact: true }).waitFor();
|
||||
|
||||
// Read the current value of the cell, to ensure we always *change* it!
|
||||
const value = await page
|
||||
.getByLabel('text-field-line2', { exact: true })
|
||||
.inputValue();
|
||||
await page
|
||||
.getByLabel('text-field-line2', { exact: true })
|
||||
.fill(value == 'old' ? 'new' : 'old');
|
||||
|
||||
await page.getByRole('button', { name: 'Submit' }).isEnabled();
|
||||
|
||||
await page.getByRole('button', { name: 'Submit' }).click();
|
||||
await page.getByRole('tab', { name: 'Details' }).waitFor();
|
||||
});
|
||||
|
||||
test('Purchase Orders - Table', async ({ browser }) => {
|
||||
const page = await doCachedLogin(browser);
|
||||
|
||||
@@ -130,62 +241,6 @@ test('Purchase Orders - Barcodes', async ({ browser }) => {
|
||||
await page.getByRole('button', { name: 'Issue Order' }).waitFor();
|
||||
});
|
||||
|
||||
test('Purchase Orders - General', async ({ browser }) => {
|
||||
const page = await doCachedLogin(browser);
|
||||
|
||||
await page.getByRole('tab', { name: 'Purchasing' }).click();
|
||||
await page.waitForURL('**/purchasing/index/**');
|
||||
|
||||
await page.getByRole('cell', { name: 'PO0012' }).click();
|
||||
await page.waitForTimeout(200);
|
||||
|
||||
await loadTab(page, 'Line Items');
|
||||
await loadTab(page, 'Received Stock');
|
||||
await loadTab(page, 'Attachments');
|
||||
|
||||
await page.getByRole('tab', { name: 'Purchasing' }).click();
|
||||
await loadTab(page, 'Suppliers');
|
||||
await page.getByText('Arrow', { exact: true }).click();
|
||||
await page.waitForTimeout(200);
|
||||
|
||||
await loadTab(page, 'Supplied Parts');
|
||||
await loadTab(page, 'Purchase Orders');
|
||||
await loadTab(page, 'Stock Items');
|
||||
await loadTab(page, 'Contacts');
|
||||
await loadTab(page, 'Addresses');
|
||||
await loadTab(page, 'Attachments');
|
||||
|
||||
await page.getByRole('tab', { name: 'Purchasing' }).click();
|
||||
await loadTab(page, 'Manufacturers');
|
||||
await page.getByText('AVX Corporation').click();
|
||||
await page.waitForTimeout(200);
|
||||
|
||||
await loadTab(page, 'Addresses');
|
||||
await page.getByRole('cell', { name: 'West Branch' }).click();
|
||||
await page.locator('.mantine-ScrollArea-root').click();
|
||||
await page
|
||||
.getByRole('row', { name: 'West Branch Yes Surf Avenue 9' })
|
||||
.getByRole('button')
|
||||
.click();
|
||||
await page.getByRole('menuitem', { name: 'Edit' }).click();
|
||||
|
||||
await page.getByLabel('text-field-title', { exact: true }).waitFor();
|
||||
await page.getByLabel('text-field-line2', { exact: true }).waitFor();
|
||||
|
||||
// Read the current value of the cell, to ensure we always *change* it!
|
||||
const value = await page
|
||||
.getByLabel('text-field-line2', { exact: true })
|
||||
.inputValue();
|
||||
await page
|
||||
.getByLabel('text-field-line2', { exact: true })
|
||||
.fill(value == 'old' ? 'new' : 'old');
|
||||
|
||||
await page.getByRole('button', { name: 'Submit' }).isEnabled();
|
||||
|
||||
await page.getByRole('button', { name: 'Submit' }).click();
|
||||
await page.getByRole('tab', { name: 'Details' }).waitFor();
|
||||
});
|
||||
|
||||
test('Purchase Orders - Filters', async ({ browser }) => {
|
||||
const page = await doCachedLogin(browser, {
|
||||
username: 'reader',
|
||||
|
||||
@@ -18,6 +18,16 @@ test('Sales Orders - Tabs', async ({ browser }) => {
|
||||
await loadTab(page, 'Sales Orders');
|
||||
await page.waitForURL('**/web/sales/index/salesorders');
|
||||
|
||||
await page
|
||||
.getByRole('button', { name: 'segmented-icon-control-parametric' })
|
||||
.click();
|
||||
await page
|
||||
.getByRole('button', { name: 'segmented-icon-control-calendar' })
|
||||
.click();
|
||||
await page
|
||||
.getByRole('button', { name: 'segmented-icon-control-table' })
|
||||
.click();
|
||||
|
||||
// Pending Shipments panel
|
||||
await loadTab(page, 'Pending Shipments');
|
||||
await page.getByRole('cell', { name: 'SO0007' }).waitFor();
|
||||
@@ -27,8 +37,26 @@ test('Sales Orders - Tabs', async ({ browser }) => {
|
||||
await loadTab(page, 'Return Orders');
|
||||
await page.getByRole('cell', { name: 'NOISE-COMPLAINT' }).waitFor();
|
||||
|
||||
await page
|
||||
.getByRole('button', { name: 'segmented-icon-control-parametric' })
|
||||
.click();
|
||||
await page
|
||||
.getByRole('button', { name: 'segmented-icon-control-calendar' })
|
||||
.click();
|
||||
await page
|
||||
.getByRole('button', { name: 'segmented-icon-control-table' })
|
||||
.click();
|
||||
|
||||
// Customers
|
||||
await loadTab(page, 'Customers');
|
||||
|
||||
await page
|
||||
.getByRole('button', { name: 'segmented-icon-control-parametric' })
|
||||
.click();
|
||||
await page
|
||||
.getByRole('button', { name: 'segmented-icon-control-table' })
|
||||
.click();
|
||||
|
||||
await page.getByText('Customer A').click();
|
||||
await loadTab(page, 'Notes');
|
||||
await loadTab(page, 'Attachments');
|
||||
|
||||
@@ -254,7 +254,7 @@ test('Settings - Admin', async ({ browser }) => {
|
||||
await loadTab(page, 'Currencies');
|
||||
await loadTab(page, 'Project Codes');
|
||||
await loadTab(page, 'Custom Units');
|
||||
await loadTab(page, 'Part Parameters');
|
||||
await loadTab(page, 'Parameters', true);
|
||||
await loadTab(page, 'Category Parameters');
|
||||
await loadTab(page, 'Label Templates');
|
||||
await loadTab(page, 'Report Templates');
|
||||
@@ -373,6 +373,115 @@ test('Settings - Admin - Barcode History', async ({ browser }) => {
|
||||
});
|
||||
});
|
||||
|
||||
test('Settings - Admin - Parameter', async ({ browser }) => {
|
||||
const page = await doCachedLogin(browser, {
|
||||
username: 'admin',
|
||||
password: 'inventree'
|
||||
});
|
||||
await page.getByRole('button', { name: 'admin' }).click();
|
||||
await page.getByRole('menuitem', { name: 'Admin Center' }).click();
|
||||
|
||||
await loadTab(page, 'Parameters', true);
|
||||
|
||||
await page.waitForTimeout(1000);
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Clean old template data if exists
|
||||
await page
|
||||
.getByRole('cell', { name: 'my custom parameter' })
|
||||
.waitFor({ timeout: 500 })
|
||||
.then(async (cell) => {
|
||||
await page
|
||||
.getByRole('cell', { name: 'my custom parameter' })
|
||||
.locator('..')
|
||||
.getByLabel('row-action-menu-')
|
||||
.click();
|
||||
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
||||
await page.getByRole('button', { name: 'Delete' }).click();
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
await page.getByRole('button', { name: 'Selection Lists' }).click();
|
||||
// Allow time for the table to load
|
||||
await page.waitForTimeout(1000);
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Clean old list data if exists
|
||||
await page
|
||||
.getByRole('cell', { name: 'some list' })
|
||||
.waitFor({ timeout: 500 })
|
||||
.then(async (cell) => {
|
||||
await page
|
||||
.getByRole('cell', { name: 'some list' })
|
||||
.locator('..')
|
||||
.getByLabel('row-action-menu-')
|
||||
.click();
|
||||
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
||||
await page.getByRole('button', { name: 'Delete' }).click();
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
// Add selection list
|
||||
await page.getByLabel('action-button-add-selection-').waitFor();
|
||||
await page.getByLabel('action-button-add-selection-').click();
|
||||
await page.getByLabel('text-field-name').fill('some list');
|
||||
await page.getByLabel('text-field-description').fill('Listdescription');
|
||||
await page.getByRole('button', { name: 'Submit' }).click();
|
||||
await page.getByRole('cell', { name: 'some list' }).waitFor();
|
||||
|
||||
await page.getByLabel('action-button-add-parameter').waitFor();
|
||||
await page.getByLabel('action-button-add-parameter').click();
|
||||
await page.getByLabel('text-field-name').fill('my custom parameter');
|
||||
await page.getByLabel('text-field-description').fill('description');
|
||||
await page
|
||||
.locator('div')
|
||||
.filter({ hasText: /^Search\.\.\.$/ })
|
||||
.nth(2)
|
||||
.click();
|
||||
await page
|
||||
.getByRole('option', { name: 'some list' })
|
||||
.locator('div')
|
||||
.first()
|
||||
.click();
|
||||
await page.getByRole('button', { name: 'Submit' }).click();
|
||||
await page.getByRole('cell', { name: 'my custom parameter' }).click();
|
||||
|
||||
// Fill parameter
|
||||
await navigate(page, 'part/104/parameters/');
|
||||
await page.getByLabel('Parameters').getByText('Parameters').waitFor();
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page
|
||||
.getByRole('button', { name: 'action-menu-add-parameters' })
|
||||
.click();
|
||||
|
||||
await page
|
||||
.getByRole('menuitem', {
|
||||
name: 'action-menu-add-parameters-create-parameter'
|
||||
})
|
||||
.click();
|
||||
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
await page.getByText('Add Parameter').waitFor();
|
||||
await page
|
||||
.getByText('Template *Parameter')
|
||||
.locator('div')
|
||||
.filter({ hasText: /^Search\.\.\.$/ })
|
||||
.first()
|
||||
.click();
|
||||
await page
|
||||
.getByText('Template *Parameter')
|
||||
.locator('div')
|
||||
.filter({ hasText: /^Search\.\.\.$/ })
|
||||
.locator('input')
|
||||
.fill('my custom parameter');
|
||||
await page.getByRole('option', { name: 'my custom parameter' }).click();
|
||||
await page.getByLabel('choice-field-data').fill('2');
|
||||
await page.getByRole('button', { name: 'Submit' }).click();
|
||||
|
||||
await page.waitForTimeout(2500);
|
||||
});
|
||||
|
||||
test('Settings - Admin - Unauthorized', async ({ browser }) => {
|
||||
// Try to access "admin" page with a non-staff user
|
||||
const page = await doCachedLogin(browser, {
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
import { test } from '../baseFixtures';
|
||||
import { navigate } from '../helpers';
|
||||
import { doCachedLogin } from '../login';
|
||||
|
||||
test('PUI - Admin - Parameter', async ({ browser }) => {
|
||||
const page = await doCachedLogin(browser, {
|
||||
username: 'admin',
|
||||
password: 'inventree'
|
||||
});
|
||||
await page.getByRole('button', { name: 'admin' }).click();
|
||||
await page.getByRole('menuitem', { name: 'Admin Center' }).click();
|
||||
await page.getByRole('tab', { name: 'Part Parameters' }).click();
|
||||
|
||||
await page.getByRole('button', { name: 'Selection Lists' }).click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// clean old data if exists
|
||||
await page
|
||||
.getByRole('cell', { name: 'some list' })
|
||||
.waitFor({ timeout: 200 })
|
||||
.then(async (cell) => {
|
||||
await page
|
||||
.getByRole('cell', { name: 'some list' })
|
||||
.locator('..')
|
||||
.getByLabel('row-action-menu-')
|
||||
.click();
|
||||
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
||||
await page.getByRole('button', { name: 'Delete' }).click();
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
// clean old data if exists
|
||||
await page.getByRole('button', { name: 'Part Parameter Template' }).click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page
|
||||
.getByRole('cell', { name: 'my custom parameter' })
|
||||
.waitFor({ timeout: 200 })
|
||||
.then(async (cell) => {
|
||||
await page
|
||||
.getByRole('cell', { name: 'my custom parameter' })
|
||||
.locator('..')
|
||||
.getByLabel('row-action-menu-')
|
||||
.click();
|
||||
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
||||
await page.getByRole('button', { name: 'Delete' }).click();
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
// Add selection list
|
||||
await page.getByRole('button', { name: 'Selection Lists' }).click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.getByLabel('action-button-add-selection-').waitFor();
|
||||
await page.getByLabel('action-button-add-selection-').click();
|
||||
await page.getByLabel('text-field-name').fill('some list');
|
||||
await page.getByLabel('text-field-description').fill('Listdescription');
|
||||
await page.getByRole('button', { name: 'Submit' }).click();
|
||||
await page.getByRole('cell', { name: 'some list' }).waitFor();
|
||||
await page.waitForTimeout(200);
|
||||
|
||||
// Add parameter
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.getByRole('button', { name: 'Part Parameter Template' }).click();
|
||||
await page.getByLabel('action-button-add-parameter').waitFor();
|
||||
await page.getByLabel('action-button-add-parameter').click();
|
||||
await page.getByLabel('text-field-name').fill('my custom parameter');
|
||||
await page.getByLabel('text-field-description').fill('description');
|
||||
await page
|
||||
.locator('div')
|
||||
.filter({ hasText: /^Search\.\.\.$/ })
|
||||
.nth(2)
|
||||
.click();
|
||||
await page
|
||||
.getByRole('option', { name: 'some list' })
|
||||
.locator('div')
|
||||
.first()
|
||||
.click();
|
||||
await page.getByRole('button', { name: 'Submit' }).click();
|
||||
await page.getByRole('cell', { name: 'my custom parameter' }).click();
|
||||
|
||||
// Fill parameter
|
||||
await navigate(page, 'part/104/parameters/');
|
||||
await page.getByLabel('Parameters').getByText('Parameters').waitFor();
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.getByLabel('action-button-add-parameter').waitFor();
|
||||
await page.getByLabel('action-button-add-parameter').click();
|
||||
await page.waitForTimeout(200);
|
||||
await page.getByText('New Part Parameter').waitFor();
|
||||
await page
|
||||
.getByText('Template *Parameter')
|
||||
.locator('div')
|
||||
.filter({ hasText: /^Search\.\.\.$/ })
|
||||
.first()
|
||||
.click();
|
||||
await page
|
||||
.getByText('Template *Parameter')
|
||||
.locator('div')
|
||||
.filter({ hasText: /^Search\.\.\.$/ })
|
||||
.locator('input')
|
||||
.fill('my custom parameter');
|
||||
await page.getByRole('option', { name: 'my custom parameter' }).click();
|
||||
await page.getByLabel('choice-field-data').fill('2');
|
||||
await page.getByRole('button', { name: 'Submit' }).click();
|
||||
});
|
||||
Reference in New Issue
Block a user