mirror of
https://github.com/inventree/InvenTree.git
synced 2026-09-02 19:08:47 +00:00
Merge commit '0b5db2f16a9dee9a6127c9757cc7f843dbe45f57' into block-notes
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
+20
-15
@@ -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' })
|
||||
|
||||
@@ -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/');
|
||||
|
||||
Reference in New Issue
Block a user