Merge commit '0b5db2f16a9dee9a6127c9757cc7f843dbe45f57' into block-notes

This commit is contained in:
Oliver Walters
2026-06-14 11:27:47 +00:00
112 changed files with 20694 additions and 18051 deletions
@@ -41,6 +41,7 @@ import {
useCallback,
useEffect,
useMemo,
useRef,
useState
} from 'react';
import { useShallow } from 'zustand/react/shallow';
@@ -168,8 +169,8 @@ export default function Calendar({
return (
<HoverCard
openDelay={300}
closeDelay={100}
openDelay={1000}
closeDelay={50}
shadow='md'
position='top-start'
>
@@ -183,6 +184,40 @@ export default function Calendar({
[calendarProps.eventContent, eventTooltipContent]
);
const scrollBoxRef = useRef<HTMLDivElement>(null);
const updateMonthFromScroll = useCallback(() => {
if (!scrollBoxRef.current) return;
const container = scrollBoxRef.current;
const containerTop = container.getBoundingClientRect().top;
const cells = Array.from(
container.querySelectorAll<HTMLElement>(
'.fc-daygrid-day[data-date$="-01"]'
)
);
let dateStr: string | null = null;
for (const cell of cells) {
if (cell.getBoundingClientRect().top <= containerTop + 1) {
dateStr = cell.getAttribute('data-date');
} else {
break;
}
}
if (!dateStr) dateStr = cells[0]?.getAttribute('data-date') ?? null;
if (dateStr) {
const date = new Date(`${dateStr}T12:00:00`);
state.setMonthName(
new Intl.DateTimeFormat(calendarLocale, {
month: 'long',
year: 'numeric'
}).format(date)
);
}
}, [calendarLocale, state.setMonthName]);
const monthDayCellClassNames = useCallback(
(arg: DayCellContentArg): string[] => {
const monthClass =
@@ -303,7 +338,19 @@ export default function Calendar({
)}
</Group>
</Group>
<Box pos='relative'>
<Box
ref={scrollBoxRef}
pos='relative'
onScroll={isScrollView ? updateMonthFromScroll : undefined}
{...(isScrollView && {
style: {
height: 'calc(100vh - 160px)',
overflowY: 'scroll',
scrollbarGutter: 'stable',
paddingRight: '12px'
}
})}
>
<LoadingOverlay visible={state.query.isFetching} />
<FullCalendar
ref={state.ref}
@@ -316,7 +363,7 @@ export default function Calendar({
duration: { months: horizonMonths }
}
},
height: 'calc(100vh - 160px)'
height: 'auto'
})}
locales={allLocales}
locale={calendarLocale}
@@ -189,7 +189,7 @@ export default function OrderCalendar({
}
return (
<Group gap='xs' wrap='nowrap'>
<Group gap='xs' wrap='nowrap' style={{ paddingLeft: 5 }}>
{order.overdue && (
<ActionIcon
color='orange-7'
@@ -10,10 +10,13 @@ import { RenderOwner } from '../render/User';
export default function OrderCalendarToolTip({
event,
instanceLookup,
extraEntries,
modelType
}: {
event: EventContentArg;
prefix?: string | React.ReactNode;
instanceLookup: string;
extraEntries?: { label: string; value: string | React.ReactNode }[];
modelType: ModelType;
}) {
// Extract the order instance from the event
@@ -23,24 +26,41 @@ export default function OrderCalendarToolTip({
if (!order) return null;
const entries = extraEntries || [];
if (order.project_code_detail?.code) {
entries.push({
label: t`Project Code`,
value: order.project_code_detail.code
});
}
return (
<Stack gap='xs'>
<Stack gap='xs' style={{ minWidth: 250 }}>
<RenderInstance model={modelType} instance={instance} />
<Divider />
<Group gap='xs'>
<Group grow gap='xs' justify='space-between'>
<Text size='sm' fw='bold'>
{order.reference}
</Text>
<Text size='xs'>{order.description || order.title}</Text>
</Group>
{entries.map((entry, index) => (
<Group key={index} grow gap='xs' justify='space-between'>
<Text size='sm' fw='bold'>
{entry.label}
</Text>
<Text size='xs'>{entry.value}</Text>
</Group>
))}
{order.start_date && (
<Group gap='xs'>
<Group grow gap='xs' justify='space-between'>
<Text size='sm' fw='bold'>{t`Start Date`}</Text>
<Text size='xs'>{formatDate(order.start_date)}</Text>
</Group>
)}
{order.target_date && (
<Group gap='xs'>
<Group grow gap='xs' justify='space-between'>
<Text size='sm' fw='bold'>{t`Target Date`}</Text>
<Text size='xs'>{formatDate(order.target_date)}</Text>
{order.overdue && (
@@ -51,7 +71,7 @@ export default function OrderCalendarToolTip({
</Group>
)}
{order.responsible && (
<Group gap='xs'>
<Group grow gap='xs' justify='space-between'>
<Text size='sm' fw='bold'>{t`Responsible`}</Text>
<RenderOwner instance={order.responsible_detail} />
</Group>
+19
View File
@@ -372,3 +372,22 @@ export function useNoteFields({
};
}, [modelType, modelId, title, description, content, fetchTemplate]);
}
export function selectionListFields(): ApiFormFieldSet {
return {
name: {},
description: {},
active: {},
source_plugin: {},
source_string: {}
};
}
export function selectionEntryFields(): ApiFormFieldSet {
return {
value: {},
label: {},
description: {},
active: {}
};
}
@@ -1,118 +0,0 @@
import type { ApiFormFieldSet, ApiFormFieldType } from '@lib/types/Forms';
import { t } from '@lingui/core/macro';
import { Table } from '@mantine/core';
import { useMemo } from 'react';
import RemoveRowButton from '../components/buttons/RemoveRowButton';
import { StandaloneField } from '../components/forms/StandaloneField';
import type { TableFieldRowProps } from '../components/forms/fields/TableField';
function BuildAllocateLineRow({
props
}: Readonly<{
props: TableFieldRowProps;
}>) {
const valueField: ApiFormFieldType = useMemo(() => {
return {
field_type: 'string',
name: 'value',
required: true,
value: props.item.value,
onValueChange: (value: any) => {
props.changeFn(props.idx, 'value', value);
}
};
}, [props]);
const labelField: ApiFormFieldType = useMemo(() => {
return {
field_type: 'string',
name: 'label',
required: true,
value: props.item.label,
onValueChange: (value: any) => {
props.changeFn(props.idx, 'label', value);
}
};
}, [props]);
const descriptionField: ApiFormFieldType = useMemo(() => {
return {
field_type: 'string',
name: 'description',
required: true,
value: props.item.description,
onValueChange: (value: any) => {
props.changeFn(props.idx, 'description', value);
}
};
}, [props]);
const activeField: ApiFormFieldType = useMemo(() => {
return {
field_type: 'boolean',
name: 'active',
required: true,
value: props.item.active,
onValueChange: (value: any) => {
props.changeFn(props.idx, 'active', value);
}
};
}, [props]);
return (
<Table.Tr key={`table-row-${props.item.id ?? props.idx}`}>
<Table.Td>
<StandaloneField fieldName='value' fieldDefinition={valueField} />
</Table.Td>
<Table.Td>
<StandaloneField fieldName='label' fieldDefinition={labelField} />
</Table.Td>
<Table.Td>
<StandaloneField
fieldName='description'
fieldDefinition={descriptionField}
/>
</Table.Td>
<Table.Td>
<StandaloneField fieldName='active' fieldDefinition={activeField} />
</Table.Td>
<Table.Td>
<RemoveRowButton onClick={() => props.removeFn(props.idx)} />
</Table.Td>
</Table.Tr>
);
}
export function selectionListFields(): ApiFormFieldSet {
return {
name: {},
description: {},
active: {},
locked: {},
source_plugin: {},
source_string: {},
choices: {
label: t`Entries`,
description: t`List of entries to choose from`,
field_type: 'table',
value: [],
headers: [
{ title: t`Value` },
{ title: t`Label` },
{ title: t`Description` },
{ title: t`Active` }
],
modelRenderer: (row: TableFieldRowProps) => (
<BuildAllocateLineRow props={row} />
),
addRow: () => {
return {
value: '',
label: '',
description: '',
active: true
};
}
}
};
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -33,6 +33,8 @@ import { PanelGroup } from '../../../../components/panels/PanelGroup';
import { GlobalSettingList } from '../../../../components/settings/SettingList';
import { Loadable } from '../../../../functions/loading';
import { useUserState } from '../../../../states/UserState';
import ParameterTemplateTable from '../../../../tables/general/ParameterTemplateTable';
import SelectionListTable from '../../../../tables/settings/SelectionListTable';
const ReportTemplatePanel = Loadable(
lazy(() => import('./ReportTemplatePanel'))
@@ -70,8 +72,6 @@ const MachineManagementPanel = Loadable(
lazy(() => import('./MachineManagementPanel'))
);
const ParameterPanel = Loadable(lazy(() => import('./ParameterPanel')));
const NoteTemplatePanel = Loadable(lazy(() => import('./NoteTemplatePanel')));
const ErrorReportTable = Loadable(
@@ -195,7 +195,14 @@ export default function AdminCenter() {
name: 'parameters',
label: t`Parameters`,
icon: <IconList />,
content: <ParameterPanel />,
content: <ParameterTemplateTable />,
hidden: !user.hasViewRole(UserRoles.part)
},
{
name: 'selection-lists',
label: t`Selection Lists`,
icon: <IconList />,
content: <SelectionListTable />,
hidden: !user.hasViewRole(UserRoles.part)
},
{
@@ -282,6 +289,7 @@ export default function AdminCenter() {
id: 'plm',
label: t`PLM`,
panelIDs: [
'selection-lists',
'parameters',
'category-parameters',
'notes',
@@ -1,29 +0,0 @@
import { t } from '@lingui/core/macro';
import { Accordion } from '@mantine/core';
import { StylishText } from '@lib/components/StylishText';
import ParameterTemplateTable from '../../../../tables/general/ParameterTemplateTable';
import SelectionListTable from '../../../../tables/part/SelectionListTable';
export default function ParameterPanel() {
return (
<Accordion multiple defaultValue={['parameter-templates']}>
<Accordion.Item value='parameter-templates' key='parameter-templates'>
<Accordion.Control>
<StylishText size='lg'>{t`Parameter Templates`}</StylishText>
</Accordion.Control>
<Accordion.Panel>
<ParameterTemplateTable />
</Accordion.Panel>
</Accordion.Item>
<Accordion.Item value='selection-lists' key='selection-lists'>
<Accordion.Control>
<StylishText size='lg'>{t`Selection Lists`}</StylishText>
</Accordion.Control>
<Accordion.Panel>
<SelectionListTable />
</Accordion.Panel>
</Accordion.Item>
</Accordion>
);
}
@@ -199,6 +199,7 @@ export default function SystemSettings() {
'REPORT_ENABLE',
'REPORT_DEFAULT_PAGE_SIZE',
'REPORT_DEBUG_MODE',
'REPORT_FETCH_URLS',
'REPORT_LOG_ERRORS',
'LABEL_ENABLE',
'LABEL_DPI'
+14 -1
View File
@@ -37,10 +37,23 @@ function BuildOrderCalendar() {
}, [globalSettings]);
const renderTooltip = useCallback((event: EventContentArg) => {
const order = event?.event?._def?.extendedProps?.order;
const extraEntries: { label: string; value: string | React.ReactNode }[] =
[];
if (order?.quantity) {
extraEntries.push({
label: t`Quantity`,
value: order.quantity
});
}
return OrderCalendarToolTip({
event: event,
modelType: ModelType.part,
instanceLookup: 'part_detail'
instanceLookup: 'part_detail',
extraEntries: extraEntries
});
}, []);
@@ -0,0 +1,216 @@
import { t } from '@lingui/core/macro';
import { Accordion, Alert, LoadingOverlay, Stack } from '@mantine/core';
import { useCallback, useMemo, useState } from 'react';
import { AddItemButton } from '@lib/components/AddItemButton';
import { RowDeleteAction, RowEditAction } from '@lib/components/RowActions';
import { StylishText } from '@lib/components/StylishText';
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
import { apiUrl } from '@lib/functions/Api';
import useTable from '@lib/hooks/UseTable';
import type { ApiFormFieldSet } from '@lib/types/Forms';
import type { TableColumn } from '@lib/types/Tables';
import { IconLock } from '@tabler/icons-react';
import { EditApiForm } from '../../components/forms/ApiForm';
import {
selectionEntryFields,
selectionListFields
} from '../../forms/CommonForms';
import {
useCreateApiFormModal,
useDeleteApiFormModal,
useEditApiFormModal
} from '../../hooks/UseForm';
import { useInstance } from '../../hooks/UseInstance';
import { BooleanColumn, DescriptionColumn } from '../ColumnRenderers';
import { InvenTreeTable } from '../InvenTreeTable';
function SelectionListEntriesTable({
id,
locked
}: Readonly<{ id: string; locked: boolean }>) {
const table = useTable('selectionlist-entries');
const columns: TableColumn[] = useMemo(
() => [
{ accessor: 'value', sortable: true },
{ accessor: 'label', sortable: true },
DescriptionColumn({}),
BooleanColumn({ accessor: 'active' })
],
[]
);
const entryFields: ApiFormFieldSet = selectionEntryFields();
const [selectedEntry, setSelectedEntry] = useState<number | undefined>(
undefined
);
// Construct the dynamic URL to edit (or delete) the selected entry
const selectedEntryUrl: string = useMemo(() => {
let url = apiUrl(ApiEndpoints.selectionentry_list, undefined, { id });
if (selectedEntry) {
url += `${selectedEntry}/`;
}
return url;
}, [id, selectedEntry]);
const createEntry = useCreateApiFormModal({
url: ApiEndpoints.selectionentry_list,
pathParams: { id },
title: t`Add Selection Entry`,
fields: {
...entryFields,
list: {
value: id,
hidden: true
}
},
table: table
});
const editEntry = useEditApiFormModal({
url: selectedEntryUrl,
title: t`Edit Selection Entry`,
fields: entryFields,
table: table
});
const deleteEntry = useDeleteApiFormModal({
url: selectedEntryUrl,
title: t`Delete Selection Entry`,
table: table
});
const tableActions = useMemo(() => {
if (locked) return [];
return [
<AddItemButton
key='add-entry'
onClick={() => createEntry.open()}
tooltip={t`Add Entry`}
/>
];
}, [locked, createEntry]);
const rowActions = useCallback(
(record: any) => {
if (locked) {
return [];
}
return [
RowEditAction({
onClick: () => {
console.log('record:', record);
setSelectedEntry(record.id);
editEntry.open();
}
}),
RowDeleteAction({
onClick: () => {
console.log('record:', record);
setSelectedEntry(record.id);
deleteEntry.open();
}
})
];
},
[editEntry, deleteEntry, locked]
);
return (
<>
{createEntry.modal}
{editEntry.modal}
{deleteEntry.modal}
<InvenTreeTable
url={apiUrl(ApiEndpoints.selectionentry_list, undefined, { id })}
tableState={table}
columns={columns}
props={{
enableSearch: true,
enableSelection: !locked,
enableBulkDelete: !locked,
tableActions: tableActions,
rowActions: locked ? undefined : rowActions
}}
/>
</>
);
}
export default function SelectionListDrawer({
id,
refreshTable
}: Readonly<{
id: string;
refreshTable: () => void;
}>) {
const {
instance,
refreshInstance,
instanceQuery: { isFetching }
} = useInstance({
endpoint: ApiEndpoints.selectionlist_list,
pk: id
});
const selectionFields: ApiFormFieldSet = useMemo(() => {
return selectionListFields();
}, []);
if (isFetching) {
return <LoadingOverlay visible />;
}
return (
<Stack>
{instance.locked && (
<Alert color='red' icon={<IconLock />} title={t`Locked`}>
{t`This selection list is locked and cannot be edited.`}
</Alert>
)}
<Accordion defaultValue={['details', 'entries']} multiple>
<Accordion.Item key='details' value='details'>
<Accordion.Control>
<StylishText size='lg'>{t`Selection List Details`}</StylishText>
</Accordion.Control>
<Accordion.Panel>
<fieldset
disabled={instance.locked}
style={{ border: 'none', padding: 0, margin: 0 }}
>
<EditApiForm
props={{
url: ApiEndpoints.selectionlist_list,
pk: id,
fields: selectionFields,
onFormSuccess: () => {
refreshTable();
refreshInstance();
}
}}
id={`selection-list-drawer-${id}`}
/>
</fieldset>
</Accordion.Panel>
</Accordion.Item>
<Accordion.Item key='entries' value='entries'>
<Accordion.Control>
<StylishText size='lg'>{t`Selection List Entries`}</StylishText>
</Accordion.Control>
<Accordion.Panel>
<SelectionListEntriesTable
id={id}
locked={instance?.locked ?? false}
/>
</Accordion.Panel>
</Accordion.Item>
</Accordion>
</Stack>
);
}
@@ -1,5 +1,6 @@
import { t } from '@lingui/core/macro';
import { useCallback, useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { AddItemButton } from '@lib/components/AddItemButton';
import {
@@ -7,26 +8,28 @@ import {
RowDeleteAction,
RowEditAction
} from '@lib/components/RowActions';
import { DetailDrawer } from '@lib/components/nav/DetailDrawer';
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
import { UserRoles } from '@lib/enums/Roles';
import { apiUrl } from '@lib/functions/Api';
import useTable from '@lib/hooks/UseTable';
import type { TableColumn } from '@lib/types/Tables';
import { selectionListFields } from '../../forms/selectionListFields';
import { selectionListFields } from '../../forms/CommonForms';
import {
useCreateApiFormModal,
useDeleteApiFormModal,
useEditApiFormModal
useDeleteApiFormModal
} from '../../hooks/UseForm';
import { useUserState } from '../../states/UserState';
import { BooleanColumn, DescriptionColumn } from '../ColumnRenderers';
import { InvenTreeTable } from '../InvenTreeTable';
import SelectionListDrawer from './SelectionListDrawer';
/**
* Table for displaying list of selectionlist items
*/
export default function SelectionListTable() {
const table = useTable('selectionlist');
const navigate = useNavigate();
const user = useUserState();
@@ -70,14 +73,6 @@ export default function SelectionListTable() {
number | undefined
>(undefined);
const editSelectionList = useEditApiFormModal({
url: ApiEndpoints.selectionlist_list,
pk: selectedSelectionList,
title: t`Edit Selection List`,
fields: selectionListFields(),
table: table
});
const deleteSelectionList = useDeleteApiFormModal({
url: ApiEndpoints.selectionlist_list,
pk: selectedSelectionList,
@@ -91,8 +86,7 @@ export default function SelectionListTable() {
RowEditAction({
hidden: !user.hasChangeRole(UserRoles.admin),
onClick: () => {
setSelectedSelectionList(record.pk);
editSelectionList.open();
navigate(`${record.pk}/`);
}
}),
RowDeleteAction({
@@ -120,8 +114,16 @@ export default function SelectionListTable() {
return (
<>
{newSelectionList.modal}
{editSelectionList.modal}
{deleteSelectionList.modal}
<DetailDrawer
title={t`Selection List`}
size='xl'
renderContent={(id) =>
id ? (
<SelectionListDrawer id={id} refreshTable={table.refreshTable} />
) : null
}
/>
<InvenTreeTable
url={apiUrl(ApiEndpoints.selectionlist_list)}
tableState={table}
@@ -129,7 +131,10 @@ export default function SelectionListTable() {
props={{
rowActions: rowActions,
tableActions: tableActions,
enableDownload: true
enableDownload: true,
onRowClick: (record) => {
navigate(`${record.pk}/`);
}
}}
/>
</>
@@ -77,6 +77,7 @@ test('Parts - Image Selection', async ({ browser }) => {
await page
.getByRole('tabpanel', { name: 'Part Details' })
.locator('img')
.first()
.hover();
await page
.getByRole('button', { name: 'action-button-select-from-' })
@@ -94,6 +95,7 @@ test('Parts - Image Selection', async ({ browser }) => {
await page
.getByRole('tabpanel', { name: 'Part Details' })
.locator('img')
.first()
.hover();
await page
.getByRole('button', { name: 'action-button-delete-image' })
+46 -40
View File
@@ -408,29 +408,8 @@ test('Settings - Admin - Parameter', async ({ browser }) => {
await page.getByRole('button', { name: 'admin' }).click();
await page.getByRole('menuitem', { name: 'Admin Center' }).click();
await loadTab(page, 'Parameters', true);
await page.waitForLoadState('networkidle');
await page.waitForTimeout(500);
// Clean old template data if exists
await page
.getByRole('cell', { name: 'my custom parameter', exact: true })
.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(() => {});
// Allow time for the table to load
await page.getByRole('button', { name: 'Selection Lists' }).click();
await page.waitForLoadState('networkidle');
await loadTab(page, 'Selection Lists');
// Check for expected entry
await page.getByRole('cell', { name: 'Animals', exact: true }).waitFor();
@@ -456,38 +435,65 @@ test('Settings - Admin - Parameter', async ({ browser }) => {
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();
// Select the new list to edit entries
await page.getByRole('cell', { name: 'some list' }).click();
await page.getByRole('button', { name: 'Selection List Entries' }).waitFor();
await page.getByRole('button', { name: 'Selection List Details' }).click();
// Add an entry to the selection list
await page.getByRole('button', { name: 'action-button-add-new-row' }).click();
await page.getByRole('button', { name: 'action-button-add-entry' }).click();
await page.getByRole('textbox', { name: 'text-field-value' }).fill('HW');
await page
.getByRole('textbox', { name: 'text-field-label' })
.fill('Hardwood');
await page
.getByRole('row', { name: 'boolean-field-active action-' })
.getByLabel('text-field-description')
.getByRole('textbox', { name: 'text-field-description' })
.fill('Hardwood materials');
await page.getByRole('cell', { name: 'boolean-field-active' }).click();
await page.waitForTimeout(100);
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.getByRole('cell', { name: 'HW' }).waitFor();
// Next, navigate to the "Parameters" tab
await navigate(page, 'settings/admin/parameters/');
await loadTab(page, 'Parameters', true);
await page.waitForLoadState('networkidle');
await page.waitForTimeout(500);
// Clean old template data if exists
await page
.locator('div')
.filter({ hasText: /^Search\.\.\.$/ })
.nth(2)
.getByRole('cell', { name: 'my custom parameter', exact: true })
.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(() => {});
// Create a new parameter, using the "Hardwood" selection list entry as the value
await page
.getByRole('button', { name: 'action-button-add-parameter-' })
.click();
await page
.getByRole('option', { name: 'some list' })
.locator('div')
.first()
.click();
.getByRole('textbox', { name: 'text-field-name' })
.fill('my custom parameter');
await page
.getByRole('combobox', { name: 'related-field-selectionlist' })
.fill('some');
await page.getByRole('option', { name: 'some list' }).click();
await page.getByRole('button', { name: 'Submit' }).click();
await page.getByRole('cell', { name: 'my custom parameter' }).click();
await page.waitForLoadState('networkidle');
// Fill parameter
await navigate(page, 'part/104/parameters/');