mirror of
https://github.com/inventree/InvenTree.git
synced 2025-06-18 04:55:44 +00:00
Report printing refactor (#7074)
* Adds a new "generic" ReportTemplate model * expose API endpoints * Update model / migrations / serializer * Add new mixin class to existing database models * - Add detail view for report template - Revert filters field behaviour * Filter report list by provided item IDs - Greatly simplify filtering logic compared to existing implemetation - Expose to API schema * Create data migration for converting *old* report templates * Ignore internal reports for data migration * Add report mixin to StockLocation model * Provide model choices in admin interface * Offload context data generation to the model classes * Remove old report template models * Refactor JS code in CUI * Fix for API filtering * Add data migration to delete old models * Remove dead URL * Updates * Construct sample report templates on app start * Bump API version * Typo fix * Fix incorrect context calls * Add new LabelTemplate model - ReportTemplate and LabelTemplate share common base - Refactor previous migration * Expose to admin interface * Add in extra context from existing label models * Add migration to create LabelTemplate instances from existing labels * Add API endpoints for listing and updating LabelTemplate objects * Adjust 'upload_to' path * Refactor label printing * Move default label templates * Update API endpoints * Update migrations * Handle LookupError in migration * Redirect the "label" API endpoint * Add new model for handling result of template printing * Refactor LabelPrinting mixin * Unlink "labels" app entirely * Fix typo * Record 'plugin' used to generate a particular output * Fix imports * Generate label print response - Still not good yet * Refactoring label printing in CUI * add "items" count to TemplateOutput model * Fix for InvenTreeLabelSheetPlugin * Remove old "label" app * Make request object optional * Fix filename generation * Add help text for "model_type" * Simplify TemplateTable * Tweak TemplateTable * Get template editor to display template data again * Stringify template name - Important, otherwise you get a TypeError instead of TemplateDoesNotExist * Add hooks to reset plugin state * fix context for StockLocation model * Tweak log messages * Fix incorrect serializer * Cleanup TemplateTable * Fix broken import * Filter by target model type * Remove manual file operations * Update old migrations - Remove references to functions that no longer exist * Refactor asset / snippet uploading * Update comments * Retain original filename when editing templatese * Cleanup * Refactor model type filter to use new hook * Add placeholder actions for printing labels and reports * Improve hookiness * Add new ReportOutput class * Report printing works from PUI now! * More inspired filename pattern for generated reports * Fix template preview window - Use new "output" response field across the board * Remove outdated task * Update data migration to use raw SQL - If the 'labels' app is no longer available, this will fail - So, use raw SQL instead * Add more API endpoint defs * Adds placeholder API endpoint for label printing * Expose plugin field to the printing endpoint * Adds plugin model type * Hook to print labels * Refactor action dropdown items * Refactor report printing for CUI * Refactor label print for CUI - Still needs to handle custom printing options for plugin * Fix migration * Update ModelType dict * playwright test fix * Unit test fixes * Fix model ruleset associations * Fix for report.js * Add support for "dynamic" fields in metadata.py * Add in custom fields based on plugin * Refactoring * Reset plugin on form close * Set custom timeout values * Update migration - Not atomic * Cleanup * Implement more printing actions * Reduce timeout * Unit test updates * Fix part serializers * Label printing works in CUI again * js linting * Update <ActionDropdown> * Fix for label printing API endpoint * Fix filterselectdrawer * Improve button rendering * Allow printing from StockLocationTable * Add aria-labels to modal form fields * Add test for printing stock item labels from table * Add test for report printing * Add unit testing for report template editing / preview * Message refactor * Refactor InvenTreeReportMixin class * Update playwright test * Update 'verbose_name' for a number of models * Additional admin filtering * Playwright test updates * Run checks against new python lib branch (temporary, will be reverted) * remove old app reference * fix testing ref * fix app init * remove old tests * Revert custom target branch * Expose label and report output objects to API * refactor * fix a few tests * factor plugin_ref out * fix options testing * Update table field header * re-enable full options testing * fix missing plugin matching * disable call assert * Add custom related field for PluginConfig - Uses 'key' rather than 'pk' - Revert label print plugin to use slug * Add support for custom pk field in metadata * switch to labels for testing * re-align report testing code * disable version check * fix url * Implement lazy loading * Allow blank plugin for printing - Uses the builtin label printer if not specified * Add printing actions for StockItem * Fix for metadata helper * Use key instead of pk in printing actions * Support non-standard pk values in RelatedModelField * pass context data to report serializers * disable template / item discovery * fix call * Tweak unit test * Run python checks against specific branch * Add task for running docs server - Option to compile schema as part of task * Custom branch no longer needed * Starting on documentation updates * fix tests for reports * fix label testing * Update template context variables * Refactor report context documentation * Documentation cleanup * Docs cleanup * Include sample report files * Fix links * Link cleanup * Integrate plugin example code into docs * Code cleanup * Fix type annotation * Revert deleted variable * remove templatetype * remove unused imports * extend context testing * test if plg can print * re-enable version check * Update unit tests * Fix test * Adjust unit test * Add debug statement to test * Fix unit test - Labels get printed against LabelTemplate items, duh * Unit test update * Unit test updates * Test update * Patch fix for <PartColumn> component * Fix ReportSerialierBase class - Re-initialize field options if not already set * Fix unit test for sqlite * Fix kwargs for non-blocking label printing * Update playwright tests * Tweak unit test --------- Co-authored-by: Matthias Mair <code@mjmair.com>
This commit is contained in:
191
src/frontend/src/components/buttons/PrintingActions.tsx
Normal file
191
src/frontend/src/components/buttons/PrintingActions.tsx
Normal file
@ -0,0 +1,191 @@
|
||||
import { t } from '@lingui/macro';
|
||||
import { notifications } from '@mantine/notifications';
|
||||
import { IconPrinter, IconReport, IconTags } from '@tabler/icons-react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { api } from '../../App';
|
||||
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
||||
import { ModelType } from '../../enums/ModelType';
|
||||
import { extractAvailableFields } from '../../functions/forms';
|
||||
import { useCreateApiFormModal } from '../../hooks/UseForm';
|
||||
import { apiUrl } from '../../states/ApiState';
|
||||
import { useLocalState } from '../../states/LocalState';
|
||||
import { ApiFormFieldSet } from '../forms/fields/ApiFormField';
|
||||
import { ActionDropdown } from '../items/ActionDropdown';
|
||||
|
||||
export function PrintingActions({
|
||||
items,
|
||||
enableLabels,
|
||||
enableReports,
|
||||
modelType
|
||||
}: {
|
||||
items: number[];
|
||||
enableLabels?: boolean;
|
||||
enableReports?: boolean;
|
||||
modelType?: ModelType;
|
||||
}) {
|
||||
const { host } = useLocalState.getState();
|
||||
|
||||
const enabled = useMemo(() => items.length > 0, [items]);
|
||||
|
||||
const [pluginKey, setPluginKey] = useState<string>('');
|
||||
|
||||
const loadFields = useCallback(() => {
|
||||
if (!enableLabels) {
|
||||
return;
|
||||
}
|
||||
|
||||
api
|
||||
.options(apiUrl(ApiEndpoints.label_print), {
|
||||
params: {
|
||||
plugin: pluginKey || undefined
|
||||
}
|
||||
})
|
||||
.then((response: any) => {
|
||||
setExtraFields(extractAvailableFields(response, 'POST') || {});
|
||||
})
|
||||
.catch(() => {});
|
||||
}, [enableLabels, pluginKey]);
|
||||
|
||||
useEffect(() => {
|
||||
loadFields();
|
||||
}, [loadFields, pluginKey]);
|
||||
|
||||
const [extraFields, setExtraFields] = useState<ApiFormFieldSet>({});
|
||||
|
||||
const labelFields: ApiFormFieldSet = useMemo(() => {
|
||||
let fields: ApiFormFieldSet = extraFields;
|
||||
|
||||
// Override field values
|
||||
fields['template'] = {
|
||||
...fields['template'],
|
||||
filters: {
|
||||
enabled: true,
|
||||
model_type: modelType,
|
||||
items: items.join(',')
|
||||
}
|
||||
};
|
||||
|
||||
fields['items'] = {
|
||||
...fields['items'],
|
||||
value: items,
|
||||
hidden: true
|
||||
};
|
||||
|
||||
fields['plugin'] = {
|
||||
...fields['plugin'],
|
||||
filters: {
|
||||
active: true,
|
||||
mixin: 'labels'
|
||||
},
|
||||
onValueChange: (value: string, record?: any) => {
|
||||
console.log('onValueChange:', value, record);
|
||||
|
||||
if (record?.key && record?.key != pluginKey) {
|
||||
setPluginKey(record.key);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return fields;
|
||||
}, [extraFields, items, loadFields]);
|
||||
|
||||
const labelModal = useCreateApiFormModal({
|
||||
url: apiUrl(ApiEndpoints.label_print),
|
||||
title: t`Print Label`,
|
||||
fields: labelFields,
|
||||
timeout: (items.length + 1) * 1000,
|
||||
onClose: () => {
|
||||
setPluginKey('');
|
||||
},
|
||||
successMessage: t`Label printing completed successfully`,
|
||||
onFormSuccess: (response: any) => {
|
||||
if (!response.complete) {
|
||||
// TODO: Periodically check for completion (requires server-side changes)
|
||||
notifications.show({
|
||||
title: t`Error`,
|
||||
message: t`The label could not be generated`,
|
||||
color: 'red'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.output) {
|
||||
// An output file was generated
|
||||
const url = `${host}${response.output}`;
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const reportModal = useCreateApiFormModal({
|
||||
title: t`Print Report`,
|
||||
url: apiUrl(ApiEndpoints.report_print),
|
||||
timeout: (items.length + 1) * 1000,
|
||||
fields: {
|
||||
template: {
|
||||
filters: {
|
||||
enabled: true,
|
||||
model_type: modelType,
|
||||
items: items.join(',')
|
||||
}
|
||||
},
|
||||
items: {
|
||||
hidden: true,
|
||||
value: items
|
||||
}
|
||||
},
|
||||
successMessage: t`Report printing completed successfully`,
|
||||
onFormSuccess: (response: any) => {
|
||||
if (!response.complete) {
|
||||
// TODO: Periodically check for completion (requires server-side changes)
|
||||
notifications.show({
|
||||
title: t`Error`,
|
||||
message: t`The report could not be generated`,
|
||||
color: 'red'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.output) {
|
||||
// An output file was generated
|
||||
const url = `${host}${response.output}`;
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!modelType) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!enableLabels && !enableReports) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{reportModal.modal}
|
||||
{labelModal.modal}
|
||||
<ActionDropdown
|
||||
tooltip={t`Printing Actions`}
|
||||
icon={<IconPrinter />}
|
||||
disabled={!enabled}
|
||||
actions={[
|
||||
{
|
||||
name: t`Print Labels`,
|
||||
icon: <IconTags />,
|
||||
onClick: () => labelModal.open(),
|
||||
hidden: !enableLabels
|
||||
},
|
||||
{
|
||||
name: t`Print Reports`,
|
||||
icon: <IconReport />,
|
||||
onClick: () => reportModal.open(),
|
||||
hidden: !enableReports
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
@ -10,6 +10,7 @@ import {
|
||||
import { IconChevronDown } from '@tabler/icons-react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { identifierString } from '../../functions/conversion';
|
||||
import { TablerIconType } from '../../functions/icons';
|
||||
import * as classes from './SplitButton.css';
|
||||
|
||||
@ -25,6 +26,7 @@ interface SplitButtonOption {
|
||||
interface SplitButtonProps {
|
||||
options: SplitButtonOption[];
|
||||
defaultSelected: string;
|
||||
name: string;
|
||||
selected?: string;
|
||||
setSelected?: (value: string) => void;
|
||||
loading?: boolean;
|
||||
@ -34,6 +36,7 @@ export function SplitButton({
|
||||
options,
|
||||
defaultSelected,
|
||||
selected,
|
||||
name,
|
||||
setSelected,
|
||||
loading
|
||||
}: Readonly<SplitButtonProps>) {
|
||||
@ -61,6 +64,7 @@ export function SplitButton({
|
||||
disabled={loading ? false : currentOption?.disabled}
|
||||
className={classes.button}
|
||||
loading={loading}
|
||||
aria-label={`split-button-${name}`}
|
||||
>
|
||||
{currentOption?.name}
|
||||
</Button>
|
||||
@ -75,6 +79,7 @@ export function SplitButton({
|
||||
color={theme.primaryColor}
|
||||
size={36}
|
||||
className={classes.icon}
|
||||
aria-label={`split-button-${name}-action`}
|
||||
>
|
||||
<IconChevronDown size={16} />
|
||||
</ActionIcon>
|
||||
@ -88,6 +93,9 @@ export function SplitButton({
|
||||
setCurrent(option.key);
|
||||
option.onClick();
|
||||
}}
|
||||
aria-label={`split-button-${name}-item-${identifierString(
|
||||
option.key
|
||||
)}`}
|
||||
disabled={option.disabled}
|
||||
leftSection={<option.icon />}
|
||||
>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Trans, t } from '@lingui/macro';
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { forwardRef, useImperativeHandle, useState } from 'react';
|
||||
|
||||
import { api } from '../../../../App';
|
||||
@ -13,54 +13,53 @@ export const PdfPreviewComponent: PreviewAreaComponent = forwardRef(
|
||||
code,
|
||||
previewItem,
|
||||
saveTemplate,
|
||||
{ uploadKey, uploadUrl, preview: { itemKey }, templateType }
|
||||
{ templateUrl, printingUrl, template }
|
||||
) => {
|
||||
if (saveTemplate) {
|
||||
const formData = new FormData();
|
||||
formData.append(uploadKey, new File([code], 'template.html'));
|
||||
|
||||
const res = await api.patch(uploadUrl, formData);
|
||||
const filename =
|
||||
template.template?.split('/').pop() ?? 'template.html';
|
||||
|
||||
formData.append('template', new File([code], filename));
|
||||
|
||||
const res = await api.patch(templateUrl, formData);
|
||||
if (res.status !== 200) {
|
||||
throw new Error(res.data);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- TODO: Fix this when implementing the new API ----
|
||||
let preview = await api.get(
|
||||
uploadUrl + `print/?plugin=inventreelabel&${itemKey}=${previewItem}`,
|
||||
let preview = await api.post(
|
||||
printingUrl,
|
||||
{
|
||||
responseType: templateType === 'label' ? 'json' : 'blob',
|
||||
items: [previewItem],
|
||||
template: template.pk
|
||||
},
|
||||
{
|
||||
responseType: 'json',
|
||||
timeout: 30000,
|
||||
validateStatus: () => true
|
||||
}
|
||||
);
|
||||
|
||||
if (preview.status !== 200) {
|
||||
if (templateType === 'report') {
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(await preview.data.text());
|
||||
} catch (err) {
|
||||
throw new Error(t`Failed to parse error response from server.`);
|
||||
}
|
||||
|
||||
throw new Error(data.detail?.join(', '));
|
||||
} else if (preview.data?.non_field_errors) {
|
||||
if (preview.status !== 200 && preview.status !== 201) {
|
||||
if (preview.data?.non_field_errors) {
|
||||
throw new Error(preview.data?.non_field_errors.join(', '));
|
||||
}
|
||||
|
||||
throw new Error(preview.data);
|
||||
}
|
||||
|
||||
if (templateType === 'label') {
|
||||
preview = await api.get(preview.data.file, {
|
||||
if (preview?.data?.output) {
|
||||
preview = await api.get(preview.data.output, {
|
||||
responseType: 'blob'
|
||||
});
|
||||
}
|
||||
// ----
|
||||
|
||||
let pdf = new Blob([preview.data], {
|
||||
type: preview.headers['content-type']
|
||||
});
|
||||
|
||||
let srcUrl = URL.createObjectURL(pdf);
|
||||
|
||||
setPdfUrl(srcUrl + '#view=fitH');
|
||||
|
@ -9,7 +9,7 @@ import {
|
||||
Tabs
|
||||
} from '@mantine/core';
|
||||
import { openConfirmModal } from '@mantine/modals';
|
||||
import { showNotification } from '@mantine/notifications';
|
||||
import { notifications, showNotification } from '@mantine/notifications';
|
||||
import {
|
||||
IconAlertTriangle,
|
||||
IconDeviceFloppy,
|
||||
@ -70,25 +70,16 @@ export type PreviewArea = {
|
||||
component: PreviewAreaComponent;
|
||||
};
|
||||
|
||||
export type TemplatePreviewProps = {
|
||||
itemKey: string;
|
||||
model: ModelType;
|
||||
filters?: Record<string, any>;
|
||||
};
|
||||
|
||||
type TemplateEditorProps = {
|
||||
downloadUrl: string;
|
||||
uploadUrl: string;
|
||||
uploadKey: string;
|
||||
preview: TemplatePreviewProps;
|
||||
templateType: 'label' | 'report';
|
||||
export type TemplateEditorProps = {
|
||||
templateUrl: string;
|
||||
printingUrl: string;
|
||||
editors: Editor[];
|
||||
previewAreas: PreviewArea[];
|
||||
template: TemplateI;
|
||||
};
|
||||
|
||||
export function TemplateEditor(props: Readonly<TemplateEditorProps>) {
|
||||
const { downloadUrl, editors, previewAreas, preview } = props;
|
||||
const { templateUrl, editors, previewAreas, template } = props;
|
||||
const editorRef = useRef<EditorRef>();
|
||||
const previewRef = useRef<PreviewAreaRef>();
|
||||
|
||||
@ -131,13 +122,17 @@ export function TemplateEditor(props: Readonly<TemplateEditorProps>) {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!downloadUrl) return;
|
||||
if (!templateUrl) return;
|
||||
|
||||
api.get(downloadUrl).then((res) => {
|
||||
codeRef.current = res.data;
|
||||
loadCodeToEditor(res.data);
|
||||
api.get(templateUrl).then((response: any) => {
|
||||
if (response.data?.template) {
|
||||
api.get(response.data.template).then((res) => {
|
||||
codeRef.current = res.data;
|
||||
loadCodeToEditor(res.data);
|
||||
});
|
||||
}
|
||||
});
|
||||
}, [downloadUrl]);
|
||||
}, [templateUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
if (codeRef.current === undefined) return;
|
||||
@ -148,7 +143,7 @@ export function TemplateEditor(props: Readonly<TemplateEditorProps>) {
|
||||
async (confirmed: boolean, saveTemplate: boolean = true) => {
|
||||
if (!confirmed) {
|
||||
openConfirmModal({
|
||||
title: t`Save & Reload preview?`,
|
||||
title: t`Save & Reload Preview`,
|
||||
children: (
|
||||
<Alert
|
||||
color="yellow"
|
||||
@ -187,10 +182,14 @@ export function TemplateEditor(props: Readonly<TemplateEditorProps>) {
|
||||
)
|
||||
.then(() => {
|
||||
setErrorOverlay(null);
|
||||
|
||||
notifications.hide('template-preview');
|
||||
|
||||
showNotification({
|
||||
title: t`Preview updated`,
|
||||
message: t`The preview has been updated successfully.`,
|
||||
color: 'green'
|
||||
color: 'green',
|
||||
id: 'template-preview'
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
@ -204,18 +203,25 @@ export function TemplateEditor(props: Readonly<TemplateEditorProps>) {
|
||||
);
|
||||
|
||||
const previewApiUrl = useMemo(
|
||||
() => ModelInformationDict[preview.model].api_endpoint,
|
||||
[preview.model]
|
||||
() =>
|
||||
ModelInformationDict[template.model_type ?? ModelType.stockitem]
|
||||
.api_endpoint,
|
||||
[template]
|
||||
);
|
||||
|
||||
const templateFilters: Record<string, string> = useMemo(() => {
|
||||
// TODO: Extract custom filters from template
|
||||
return {};
|
||||
}, [template]);
|
||||
|
||||
useEffect(() => {
|
||||
api
|
||||
.get(apiUrl(previewApiUrl), { params: { limit: 1, ...preview.filters } })
|
||||
.get(apiUrl(previewApiUrl), { params: { limit: 1, ...templateFilters } })
|
||||
.then((res) => {
|
||||
if (res.data.results.length === 0) return;
|
||||
setPreviewItem(res.data.results[0].pk);
|
||||
});
|
||||
}, [previewApiUrl, preview.filters]);
|
||||
}, [previewApiUrl, templateFilters]);
|
||||
|
||||
return (
|
||||
<Stack style={{ height: '100%', flex: '1' }}>
|
||||
@ -249,6 +255,7 @@ export function TemplateEditor(props: Readonly<TemplateEditorProps>) {
|
||||
<SplitButton
|
||||
loading={isPreviewLoading}
|
||||
defaultSelected="preview_save"
|
||||
name="preview-options"
|
||||
options={[
|
||||
{
|
||||
key: 'preview',
|
||||
@ -260,7 +267,7 @@ export function TemplateEditor(props: Readonly<TemplateEditorProps>) {
|
||||
},
|
||||
{
|
||||
key: 'preview_save',
|
||||
name: t`Save & Reload preview`,
|
||||
name: t`Save & Reload Preview`,
|
||||
tooltip: t`Save the current template and reload the preview`,
|
||||
icon: IconDeviceFloppy,
|
||||
onClick: () => updatePreview(hasSaveConfirmed),
|
||||
@ -319,10 +326,10 @@ export function TemplateEditor(props: Readonly<TemplateEditorProps>) {
|
||||
field_type: 'related field',
|
||||
api_url: apiUrl(previewApiUrl),
|
||||
description: '',
|
||||
label: t`Select` + ' ' + preview.model + ' ' + t`to preview`,
|
||||
model: preview.model,
|
||||
label: t`Select instance to preview`,
|
||||
model: template.model_type,
|
||||
value: previewItem,
|
||||
filters: preview.filters,
|
||||
filters: templateFilters,
|
||||
onValueChange: (value) => setPreviewItem(value)
|
||||
}}
|
||||
/>
|
||||
|
@ -40,6 +40,7 @@ export type ApiFormAdjustFilterType = {
|
||||
* @param icon : An icon to display next to the field
|
||||
* @param field_type : The type of field to render
|
||||
* @param api_url : The API endpoint to fetch data from (for related fields)
|
||||
* @param pk_field : The primary key field for the related field (default = "pk")
|
||||
* @param model : The model to use for related fields
|
||||
* @param filters : Optional API filters to apply to related fields
|
||||
* @param required : Whether the field is required
|
||||
@ -74,6 +75,7 @@ export type ApiFormFieldType = {
|
||||
| 'nested object'
|
||||
| 'table';
|
||||
api_url?: string;
|
||||
pk_field?: string;
|
||||
model?: ModelType;
|
||||
modelRenderer?: (instance: any) => ReactNode;
|
||||
filters?: any;
|
||||
@ -190,6 +192,7 @@ export function ApiFormField({
|
||||
{...reducedDefinition}
|
||||
ref={field.ref}
|
||||
id={fieldId}
|
||||
aria-label={`text-field-${field.name}`}
|
||||
type={definition.field_type}
|
||||
value={value || ''}
|
||||
error={error?.message}
|
||||
@ -208,6 +211,7 @@ export function ApiFormField({
|
||||
{...reducedDefinition}
|
||||
ref={ref}
|
||||
id={fieldId}
|
||||
aria-label={`boolean-field-${field.name}`}
|
||||
radius="lg"
|
||||
size="sm"
|
||||
checked={isTrue(value)}
|
||||
@ -228,6 +232,7 @@ export function ApiFormField({
|
||||
radius="sm"
|
||||
ref={field.ref}
|
||||
id={fieldId}
|
||||
aria-label={`number-field-${field.name}`}
|
||||
value={numericalValue}
|
||||
error={error?.message}
|
||||
decimalScale={definition.field_type == 'integer' ? 0 : 10}
|
||||
|
@ -51,6 +51,7 @@ export function ChoiceField({
|
||||
return (
|
||||
<Select
|
||||
id={fieldId}
|
||||
aria-label={`choice-field-${field.name}`}
|
||||
error={error?.message}
|
||||
radius="sm"
|
||||
{...field}
|
||||
|
@ -50,6 +50,7 @@ export default function DateField({
|
||||
return (
|
||||
<DateInput
|
||||
id={fieldId}
|
||||
aria-label={`date-field-${field.name}`}
|
||||
radius="sm"
|
||||
ref={field.ref}
|
||||
type={undefined}
|
||||
|
@ -64,22 +64,24 @@ export function RelatedModelField({
|
||||
field.value !== ''
|
||||
) {
|
||||
const url = `${definition.api_url}${field.value}/`;
|
||||
|
||||
api.get(url).then((response) => {
|
||||
if (response.data && response.data.pk) {
|
||||
let pk_field = definition.pk_field ?? 'pk';
|
||||
if (response.data && response.data[pk_field]) {
|
||||
const value = {
|
||||
value: response.data.pk,
|
||||
value: response.data[pk_field],
|
||||
data: response.data
|
||||
};
|
||||
|
||||
setInitialData(value);
|
||||
dataRef.current = [value];
|
||||
setPk(response.data.pk);
|
||||
setPk(response.data[pk_field]);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
setPk(null);
|
||||
}
|
||||
}, [definition.api_url, field.value]);
|
||||
}, [definition.api_url, definition.pk_field, field.value]);
|
||||
|
||||
// Search input query
|
||||
const [value, setValue] = useState<string>('');
|
||||
@ -146,13 +148,15 @@ export function RelatedModelField({
|
||||
const results = response.data?.results ?? response.data ?? [];
|
||||
|
||||
results.forEach((item: any) => {
|
||||
// do not push already existing items into the values array
|
||||
if (alreadyPresentPks.includes(item.pk)) return;
|
||||
let pk_field = definition.pk_field ?? 'pk';
|
||||
let pk = item[pk_field];
|
||||
|
||||
values.push({
|
||||
value: item.pk ?? -1,
|
||||
data: item
|
||||
});
|
||||
if (pk && !alreadyPresentPks.includes(pk)) {
|
||||
values.push({
|
||||
value: pk,
|
||||
data: item
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
setData(values);
|
||||
@ -276,6 +280,7 @@ export function RelatedModelField({
|
||||
>
|
||||
<Select
|
||||
id={fieldId}
|
||||
aria-label={`related-field-${field.name}`}
|
||||
value={currentValue}
|
||||
ref={field.ref}
|
||||
options={data}
|
||||
|
@ -33,11 +33,11 @@ export function TableField({
|
||||
};
|
||||
|
||||
return (
|
||||
<Table highlightOnHover striped>
|
||||
<Table highlightOnHover striped aria-label={`table-field-${field.name}`}>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
{definition.headers?.map((header) => {
|
||||
return <th key={header}>{header}</th>;
|
||||
return <Table.Th key={header}>{header}</Table.Th>;
|
||||
})}
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
|
@ -14,9 +14,9 @@ import {
|
||||
IconTrash,
|
||||
IconUnlink
|
||||
} from '@tabler/icons-react';
|
||||
import { color } from '@uiw/react-codemirror';
|
||||
import { ReactNode, useMemo } from 'react';
|
||||
|
||||
import { identifierString } from '../../functions/conversion';
|
||||
import { InvenTreeIcon } from '../../functions/icons';
|
||||
import { notYetImplemented } from '../../functions/notifications';
|
||||
|
||||
@ -42,19 +42,24 @@ export function ActionDropdown({
|
||||
disabled = false
|
||||
}: {
|
||||
icon: ReactNode;
|
||||
tooltip?: string;
|
||||
tooltip: string;
|
||||
actions: ActionDropdownItem[];
|
||||
disabled?: boolean;
|
||||
}) {
|
||||
const hasActions = useMemo(() => {
|
||||
return actions.some((action) => !action.hidden);
|
||||
}, [actions]);
|
||||
|
||||
const indicatorProps = useMemo(() => {
|
||||
return actions.find((action) => action.indicator);
|
||||
}, [actions]);
|
||||
|
||||
const menuName: string = useMemo(() => {
|
||||
return identifierString(`action-menu-${tooltip}`);
|
||||
}, [tooltip]);
|
||||
|
||||
return hasActions ? (
|
||||
<Menu position="bottom-end">
|
||||
<Menu position="bottom-end" key={menuName}>
|
||||
<Indicator disabled={!indicatorProps} {...indicatorProps?.indicator}>
|
||||
<Menu.Target>
|
||||
<Tooltip label={tooltip} hidden={!tooltip}>
|
||||
@ -63,6 +68,7 @@ export function ActionDropdown({
|
||||
radius="sm"
|
||||
variant="outline"
|
||||
disabled={disabled}
|
||||
aria-label={menuName}
|
||||
>
|
||||
{icon}
|
||||
</ActionIcon>
|
||||
@ -70,15 +76,17 @@ export function ActionDropdown({
|
||||
</Menu.Target>
|
||||
</Indicator>
|
||||
<Menu.Dropdown>
|
||||
{actions.map((action) =>
|
||||
action.hidden ? null : (
|
||||
{actions.map((action) => {
|
||||
const id: string = identifierString(`${menuName}-${action.name}`);
|
||||
return action.hidden ? null : (
|
||||
<Indicator
|
||||
disabled={!action.indicator}
|
||||
{...action.indicator}
|
||||
key={action.name}
|
||||
>
|
||||
<Tooltip label={action.tooltip}>
|
||||
<Tooltip label={action.tooltip} hidden={!action.tooltip}>
|
||||
<Menu.Item
|
||||
aria-label={id}
|
||||
leftSection={action.icon}
|
||||
onClick={() => {
|
||||
if (action.onClick != undefined) {
|
||||
@ -93,8 +101,8 @@ export function ActionDropdown({
|
||||
</Menu.Item>
|
||||
</Tooltip>
|
||||
</Indicator>
|
||||
)
|
||||
)}
|
||||
);
|
||||
})}
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
) : null;
|
||||
@ -108,7 +116,6 @@ export function BarcodeActionDropdown({
|
||||
}) {
|
||||
return (
|
||||
<ActionDropdown
|
||||
key="barcode-actions"
|
||||
tooltip={t`Barcode Actions`}
|
||||
icon={<IconQrcode />}
|
||||
actions={actions}
|
||||
|
@ -26,6 +26,8 @@ import {
|
||||
RenderPartParameterTemplate,
|
||||
RenderPartTestTemplate
|
||||
} from './Part';
|
||||
import { RenderPlugin } from './Plugin';
|
||||
import { RenderLabelTemplate, RenderReportTemplate } from './Report';
|
||||
import {
|
||||
RenderStockItem,
|
||||
RenderStockLocation,
|
||||
@ -72,7 +74,10 @@ const RendererLookup: EnumDictionary<
|
||||
[ModelType.stockitem]: RenderStockItem,
|
||||
[ModelType.stockhistory]: RenderStockItem,
|
||||
[ModelType.supplierpart]: RenderSupplierPart,
|
||||
[ModelType.user]: RenderUser
|
||||
[ModelType.user]: RenderUser,
|
||||
[ModelType.reporttemplate]: RenderReportTemplate,
|
||||
[ModelType.labeltemplate]: RenderLabelTemplate,
|
||||
[ModelType.pluginconfig]: RenderPlugin
|
||||
};
|
||||
|
||||
export type RenderInstanceProps = {
|
||||
|
@ -195,6 +195,27 @@ export const ModelInformationDict: ModelDict = {
|
||||
url_overview: '/user',
|
||||
url_detail: '/user/:pk/',
|
||||
api_endpoint: ApiEndpoints.user_list
|
||||
},
|
||||
labeltemplate: {
|
||||
label: t`Label Template`,
|
||||
label_multiple: t`Label Templates`,
|
||||
url_overview: '/labeltemplate',
|
||||
url_detail: '/labeltemplate/:pk/',
|
||||
api_endpoint: ApiEndpoints.label_list
|
||||
},
|
||||
reporttemplate: {
|
||||
label: t`Report Template`,
|
||||
label_multiple: t`Report Templates`,
|
||||
url_overview: '/reporttemplate',
|
||||
url_detail: '/reporttemplate/:pk/',
|
||||
api_endpoint: ApiEndpoints.report_list
|
||||
},
|
||||
pluginconfig: {
|
||||
label: t`Plugin Configuration`,
|
||||
label_multiple: t`Plugin Configurations`,
|
||||
url_overview: '/pluginconfig',
|
||||
url_detail: '/pluginconfig/:pk/',
|
||||
api_endpoint: ApiEndpoints.plugin_list
|
||||
}
|
||||
};
|
||||
|
||||
|
21
src/frontend/src/components/render/Plugin.tsx
Normal file
21
src/frontend/src/components/render/Plugin.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import { t } from '@lingui/macro';
|
||||
import { Badge } from '@mantine/core';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
import { RenderInlineModel } from './Instance';
|
||||
|
||||
export function RenderPlugin({
|
||||
instance
|
||||
}: {
|
||||
instance: Readonly<any>;
|
||||
}): ReactNode {
|
||||
return (
|
||||
<RenderInlineModel
|
||||
primary={instance.name}
|
||||
secondary={instance.meta?.description}
|
||||
suffix={
|
||||
!instance.active && <Badge size="sm" color="red">{t`Inactive`}</Badge>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
29
src/frontend/src/components/render/Report.tsx
Normal file
29
src/frontend/src/components/render/Report.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
import { RenderInlineModel } from './Instance';
|
||||
|
||||
export function RenderReportTemplate({
|
||||
instance
|
||||
}: {
|
||||
instance: any;
|
||||
}): ReactNode {
|
||||
return (
|
||||
<RenderInlineModel
|
||||
primary={instance.name}
|
||||
secondary={instance.description}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function RenderLabelTemplate({
|
||||
instance
|
||||
}: {
|
||||
instance: any;
|
||||
}): ReactNode {
|
||||
return (
|
||||
<RenderInlineModel
|
||||
primary={instance.name}
|
||||
secondary={instance.description}
|
||||
/>
|
||||
);
|
||||
}
|
@ -80,7 +80,7 @@ export const StatusRenderer = ({
|
||||
|
||||
const statusCodes = statusCodeList[type];
|
||||
if (statusCodes === undefined) {
|
||||
console.log('StatusRenderer: statusCodes is undefined');
|
||||
console.warn('StatusRenderer: statusCodes is undefined');
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -127,8 +127,14 @@ export enum ApiEndpoints {
|
||||
return_order_attachment_list = 'order/ro/attachment/',
|
||||
|
||||
// Template API endpoints
|
||||
label_list = 'label/:variant/',
|
||||
report_list = 'report/:variant/',
|
||||
label_list = 'label/template/',
|
||||
label_print = 'label/print/',
|
||||
label_output = 'label/output/',
|
||||
report_list = 'report/template/',
|
||||
report_print = 'report/print/',
|
||||
report_output = 'report/output/',
|
||||
report_snippet = 'report/snippet/',
|
||||
report_asset = 'report/asset/',
|
||||
|
||||
// Plugin API endpoints
|
||||
plugin_list = 'plugins/',
|
||||
|
@ -24,5 +24,8 @@ export enum ModelType {
|
||||
address = 'address',
|
||||
contact = 'contact',
|
||||
owner = 'owner',
|
||||
user = 'user'
|
||||
user = 'user',
|
||||
reporttemplate = 'reporttemplate',
|
||||
labeltemplate = 'labeltemplate',
|
||||
pluginconfig = 'pluginconfig'
|
||||
}
|
||||
|
@ -32,3 +32,8 @@ export function resolveItem(obj: any, path: string): any {
|
||||
let properties = path.split('.');
|
||||
return properties.reduce((prev, curr) => prev?.[curr], obj);
|
||||
}
|
||||
|
||||
export function identifierString(value: string): string {
|
||||
// Convert an input string e.g. "Hello World" into a string that can be used as an identifier, e.g. "hello-world"
|
||||
return value.toLowerCase().replace(/[^a-z0-9]/g, '-');
|
||||
}
|
||||
|
@ -27,7 +27,7 @@ export function useInstance<T = any>({
|
||||
updateInterval
|
||||
}: {
|
||||
endpoint: ApiEndpoints;
|
||||
pk?: string | undefined;
|
||||
pk?: string | number | undefined;
|
||||
hasPrimaryKey?: boolean;
|
||||
params?: any;
|
||||
pathParams?: PathParams;
|
||||
@ -43,7 +43,12 @@ export function useInstance<T = any>({
|
||||
queryKey: ['instance', endpoint, pk, params, pathParams],
|
||||
queryFn: async () => {
|
||||
if (hasPrimaryKey) {
|
||||
if (pk == null || pk == undefined || pk.length == 0 || pk == '-1') {
|
||||
if (
|
||||
pk == null ||
|
||||
pk == undefined ||
|
||||
pk.toString().length == 0 ||
|
||||
pk == '-1'
|
||||
) {
|
||||
setInstance(defaultValue);
|
||||
return defaultValue;
|
||||
}
|
||||
|
@ -24,6 +24,7 @@ export type TableState = {
|
||||
expandedRecords: any[];
|
||||
setExpandedRecords: (records: any[]) => void;
|
||||
selectedRecords: any[];
|
||||
selectedIds: number[];
|
||||
hasSelectedRecords: boolean;
|
||||
setSelectedRecords: (records: any[]) => void;
|
||||
clearSelectedRecords: () => void;
|
||||
@ -77,6 +78,12 @@ export function useTable(tableName: string): TableState {
|
||||
// Array of selected records
|
||||
const [selectedRecords, setSelectedRecords] = useState<any[]>([]);
|
||||
|
||||
// Array of selected primary key values
|
||||
const selectedIds = useMemo(
|
||||
() => selectedRecords.map((r) => r.pk ?? r.id),
|
||||
[selectedRecords]
|
||||
);
|
||||
|
||||
const clearSelectedRecords = useCallback(() => {
|
||||
setSelectedRecords([]);
|
||||
}, []);
|
||||
@ -135,6 +142,7 @@ export function useTable(tableName: string): TableState {
|
||||
expandedRecords,
|
||||
setExpandedRecords,
|
||||
selectedRecords,
|
||||
selectedIds,
|
||||
setSelectedRecords,
|
||||
clearSelectedRecords,
|
||||
hasSelectedRecords,
|
||||
|
@ -202,7 +202,6 @@ function SpotlighPlayground() {
|
||||
onClick: () => console.log('Secret')
|
||||
}
|
||||
]);
|
||||
console.log('registed');
|
||||
firstSpotlight.open();
|
||||
}}
|
||||
>
|
||||
|
@ -684,7 +684,6 @@ function InputImageBarcode({ action }: Readonly<ScanInputInterface>) {
|
||||
useEffect(() => {
|
||||
if (cameraValue === null) return;
|
||||
if (cameraValue === camId?.id) {
|
||||
console.log('matching value and id');
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -9,9 +9,10 @@ import {
|
||||
IconListDetails,
|
||||
IconPackages,
|
||||
IconPlugConnected,
|
||||
IconReport,
|
||||
IconScale,
|
||||
IconSitemap,
|
||||
IconTemplate,
|
||||
IconTags,
|
||||
IconUsersGroup
|
||||
} from '@tabler/icons-react';
|
||||
import { lazy, useMemo } from 'react';
|
||||
@ -22,6 +23,12 @@ import { SettingsHeader } from '../../../../components/nav/SettingsHeader';
|
||||
import { GlobalSettingList } from '../../../../components/settings/SettingList';
|
||||
import { Loadable } from '../../../../functions/loading';
|
||||
|
||||
const ReportTemplatePanel = Loadable(
|
||||
lazy(() => import('./ReportTemplatePanel'))
|
||||
);
|
||||
|
||||
const LabelTemplatePanel = Loadable(lazy(() => import('./LabelTemplatePanel')));
|
||||
|
||||
const UserManagementPanel = Loadable(
|
||||
lazy(() => import('./UserManagementPanel'))
|
||||
);
|
||||
@ -66,10 +73,6 @@ const CurrencyTable = Loadable(
|
||||
lazy(() => import('../../../../tables/settings/CurrencyTable'))
|
||||
);
|
||||
|
||||
const TemplateManagementPanel = Loadable(
|
||||
lazy(() => import('./TemplateManagementPanel'))
|
||||
);
|
||||
|
||||
export default function AdminCenter() {
|
||||
const adminCenterPanels: PanelType[] = useMemo(() => {
|
||||
return [
|
||||
@ -127,18 +130,24 @@ export default function AdminCenter() {
|
||||
icon: <IconSitemap />,
|
||||
content: <PartCategoryTemplateTable />
|
||||
},
|
||||
{
|
||||
name: 'labels',
|
||||
label: t`Label Templates`,
|
||||
icon: <IconTags />,
|
||||
content: <LabelTemplatePanel />
|
||||
},
|
||||
{
|
||||
name: 'reports',
|
||||
label: t`Report Templates`,
|
||||
icon: <IconReport />,
|
||||
content: <ReportTemplatePanel />
|
||||
},
|
||||
{
|
||||
name: 'location-types',
|
||||
label: t`Location types`,
|
||||
icon: <IconPackages />,
|
||||
content: <LocationTypesTable />
|
||||
},
|
||||
{
|
||||
name: 'templates',
|
||||
label: t`Templates`,
|
||||
icon: <IconTemplate />,
|
||||
content: <TemplateManagementPanel />
|
||||
},
|
||||
{
|
||||
name: 'plugin',
|
||||
label: t`Plugins`,
|
||||
|
@ -0,0 +1,17 @@
|
||||
import { ApiEndpoints } from '../../../../enums/ApiEndpoints';
|
||||
import { TemplateTable } from '../../../../tables/settings/TemplateTable';
|
||||
|
||||
export default function LabelTemplatePanel() {
|
||||
return (
|
||||
<TemplateTable
|
||||
templateProps={{
|
||||
templateEndpoint: ApiEndpoints.label_list,
|
||||
printingEndpoint: ApiEndpoints.label_print,
|
||||
additionalFormFields: {
|
||||
width: {},
|
||||
height: {}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
import { ApiEndpoints } from '../../../../enums/ApiEndpoints';
|
||||
import { TemplateTable } from '../../../../tables/settings/TemplateTable';
|
||||
|
||||
export default function ReportTemplateTable() {
|
||||
return (
|
||||
<TemplateTable
|
||||
templateProps={{
|
||||
templateEndpoint: ApiEndpoints.report_list,
|
||||
printingEndpoint: ApiEndpoints.report_print,
|
||||
additionalFormFields: {
|
||||
page_size: {},
|
||||
landscape: {}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
@ -1,211 +0,0 @@
|
||||
import { t } from '@lingui/macro';
|
||||
import { Stack } from '@mantine/core';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { TemplatePreviewProps } from '../../../../components/editors/TemplateEditor/TemplateEditor';
|
||||
import { ApiFormFieldSet } from '../../../../components/forms/fields/ApiFormField';
|
||||
import { PanelGroup } from '../../../../components/nav/PanelGroup';
|
||||
import {
|
||||
defaultLabelTemplate,
|
||||
defaultReportTemplate
|
||||
} from '../../../../defaults/templates';
|
||||
import { ApiEndpoints } from '../../../../enums/ApiEndpoints';
|
||||
import { ModelType } from '../../../../enums/ModelType';
|
||||
import { InvenTreeIcon, InvenTreeIconType } from '../../../../functions/icons';
|
||||
import { TemplateTable } from '../../../../tables/settings/TemplateTable';
|
||||
|
||||
type TemplateType = {
|
||||
type: 'label' | 'report';
|
||||
name: string;
|
||||
singularName: string;
|
||||
apiEndpoints: ApiEndpoints;
|
||||
templateKey: string;
|
||||
additionalFormFields?: ApiFormFieldSet;
|
||||
defaultTemplate: string;
|
||||
variants: {
|
||||
name: string;
|
||||
key: string;
|
||||
icon: InvenTreeIconType;
|
||||
preview: TemplatePreviewProps;
|
||||
}[];
|
||||
};
|
||||
|
||||
export default function TemplateManagementPanel() {
|
||||
const templateTypes = useMemo(() => {
|
||||
const templateTypes: TemplateType[] = [
|
||||
{
|
||||
type: 'label',
|
||||
name: t`Labels`,
|
||||
singularName: t`Label`,
|
||||
apiEndpoints: ApiEndpoints.label_list,
|
||||
templateKey: 'label',
|
||||
additionalFormFields: {
|
||||
width: {},
|
||||
height: {}
|
||||
},
|
||||
defaultTemplate: defaultLabelTemplate,
|
||||
variants: [
|
||||
{
|
||||
name: t`Part`,
|
||||
key: 'part',
|
||||
icon: 'part',
|
||||
preview: {
|
||||
itemKey: 'part',
|
||||
model: ModelType.part
|
||||
}
|
||||
},
|
||||
{
|
||||
name: t`Location`,
|
||||
key: 'location',
|
||||
icon: 'location',
|
||||
preview: {
|
||||
itemKey: 'location',
|
||||
model: ModelType.stocklocation
|
||||
}
|
||||
},
|
||||
{
|
||||
name: t`Stock Item`,
|
||||
key: 'stock',
|
||||
icon: 'stock',
|
||||
preview: {
|
||||
itemKey: 'item',
|
||||
model: ModelType.stockitem
|
||||
}
|
||||
},
|
||||
{
|
||||
name: t`Build Line`,
|
||||
key: 'buildline',
|
||||
icon: 'builds',
|
||||
preview: {
|
||||
itemKey: 'line',
|
||||
model: ModelType.buildline
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'report',
|
||||
name: t`Reports`,
|
||||
singularName: t`Report`,
|
||||
apiEndpoints: ApiEndpoints.report_list,
|
||||
templateKey: 'template',
|
||||
additionalFormFields: {
|
||||
page_size: {},
|
||||
landscape: {}
|
||||
},
|
||||
defaultTemplate: defaultReportTemplate,
|
||||
variants: [
|
||||
{
|
||||
name: t`Purchase Order`,
|
||||
key: 'po',
|
||||
icon: 'purchase_orders',
|
||||
preview: {
|
||||
itemKey: 'order',
|
||||
model: ModelType.purchaseorder
|
||||
}
|
||||
},
|
||||
{
|
||||
name: t`Sales Order`,
|
||||
key: 'so',
|
||||
icon: 'sales_orders',
|
||||
preview: {
|
||||
itemKey: 'order',
|
||||
model: ModelType.salesorder
|
||||
}
|
||||
},
|
||||
{
|
||||
name: t`Return Order`,
|
||||
key: 'ro',
|
||||
icon: 'return_orders',
|
||||
preview: {
|
||||
itemKey: 'order',
|
||||
model: ModelType.returnorder
|
||||
}
|
||||
},
|
||||
{
|
||||
name: t`Build`,
|
||||
key: 'build',
|
||||
icon: 'builds',
|
||||
preview: {
|
||||
itemKey: 'build',
|
||||
model: ModelType.build
|
||||
}
|
||||
},
|
||||
{
|
||||
name: t`Bill of Materials`,
|
||||
key: 'bom',
|
||||
icon: 'bom',
|
||||
preview: {
|
||||
itemKey: 'part',
|
||||
model: ModelType.part,
|
||||
filters: { assembly: true }
|
||||
}
|
||||
},
|
||||
{
|
||||
name: t`Tests`,
|
||||
key: 'test',
|
||||
icon: 'test_templates',
|
||||
preview: {
|
||||
itemKey: 'item',
|
||||
model: ModelType.stockitem
|
||||
}
|
||||
},
|
||||
{
|
||||
name: t`Stock Location`,
|
||||
key: 'slr',
|
||||
icon: 'default_location',
|
||||
preview: {
|
||||
itemKey: 'location',
|
||||
model: ModelType.stocklocation
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
return templateTypes;
|
||||
}, []);
|
||||
|
||||
const panels = useMemo(() => {
|
||||
return templateTypes.flatMap((templateType) => {
|
||||
return [
|
||||
// Add panel headline
|
||||
{ name: templateType.type, label: templateType.name, disabled: true },
|
||||
|
||||
// Add panel for each variant
|
||||
...templateType.variants.map((variant) => {
|
||||
return {
|
||||
name: variant.key,
|
||||
label: variant.name,
|
||||
content: (
|
||||
<TemplateTable
|
||||
templateProps={{
|
||||
apiEndpoint: templateType.apiEndpoints,
|
||||
templateType: templateType.type as 'label' | 'report',
|
||||
templateTypeTranslation: templateType.singularName,
|
||||
variant: variant.key,
|
||||
templateKey: templateType.templateKey,
|
||||
preview: variant.preview,
|
||||
additionalFormFields: templateType.additionalFormFields,
|
||||
defaultTemplate: templateType.defaultTemplate
|
||||
}}
|
||||
/>
|
||||
),
|
||||
icon: <InvenTreeIcon icon={variant.icon} />,
|
||||
showHeadline: false
|
||||
};
|
||||
})
|
||||
];
|
||||
});
|
||||
}, [templateTypes]);
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<PanelGroup
|
||||
pageKey="admin-center-templates"
|
||||
panels={panels}
|
||||
collapsible={false}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
}
|
@ -4,13 +4,11 @@ import {
|
||||
IconClipboardCheck,
|
||||
IconClipboardList,
|
||||
IconDots,
|
||||
IconFileTypePdf,
|
||||
IconInfoCircle,
|
||||
IconList,
|
||||
IconListCheck,
|
||||
IconNotes,
|
||||
IconPaperclip,
|
||||
IconPrinter,
|
||||
IconQrcode,
|
||||
IconSitemap
|
||||
} from '@tabler/icons-react';
|
||||
@ -18,6 +16,7 @@ import { useMemo } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import AdminButton from '../../components/buttons/AdminButton';
|
||||
import { PrintingActions } from '../../components/buttons/PrintingActions';
|
||||
import { DetailsField, DetailsTable } from '../../components/details/Details';
|
||||
import { DetailsImage } from '../../components/details/DetailsImage';
|
||||
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
|
||||
@ -358,7 +357,6 @@ export default function BuildDetail() {
|
||||
return [
|
||||
<AdminButton model={ModelType.build} pk={build.pk} />,
|
||||
<ActionDropdown
|
||||
key="barcode"
|
||||
tooltip={t`Barcode Actions`}
|
||||
icon={<IconQrcode />}
|
||||
actions={[
|
||||
@ -371,20 +369,12 @@ export default function BuildDetail() {
|
||||
})
|
||||
]}
|
||||
/>,
|
||||
<ActionDropdown
|
||||
key="report"
|
||||
tooltip={t`Reporting Actions`}
|
||||
icon={<IconPrinter />}
|
||||
actions={[
|
||||
{
|
||||
icon: <IconFileTypePdf />,
|
||||
name: t`Report`,
|
||||
tooltip: t`Print build report`
|
||||
}
|
||||
]}
|
||||
<PrintingActions
|
||||
modelType={ModelType.build}
|
||||
items={[build.pk]}
|
||||
enableReports
|
||||
/>,
|
||||
<ActionDropdown
|
||||
key="build"
|
||||
tooltip={t`Build Order Actions`}
|
||||
icon={<IconDots />}
|
||||
actions={[
|
||||
|
@ -289,7 +289,6 @@ export default function CompanyDetail(props: Readonly<CompanyDetailProps>) {
|
||||
return [
|
||||
<AdminButton model={ModelType.company} pk={company.pk} />,
|
||||
<ActionDropdown
|
||||
key="company"
|
||||
tooltip={t`Company Actions`}
|
||||
icon={<IconDots />}
|
||||
actions={[
|
||||
|
@ -210,7 +210,6 @@ export default function ManufacturerPartDetail() {
|
||||
pk={manufacturerPart.pk}
|
||||
/>,
|
||||
<ActionDropdown
|
||||
key="part"
|
||||
tooltip={t`Manufacturer Part Actions`}
|
||||
icon={<IconDots />}
|
||||
actions={[
|
||||
|
@ -246,7 +246,6 @@ export default function SupplierPartDetail() {
|
||||
return [
|
||||
<AdminButton model={ModelType.supplierpart} pk={supplierPart.pk} />,
|
||||
<ActionDropdown
|
||||
key="part"
|
||||
tooltip={t`Supplier Part Actions`}
|
||||
icon={<IconDots />}
|
||||
actions={[
|
||||
|
@ -204,7 +204,6 @@ export default function CategoryDetail({}: {}) {
|
||||
return [
|
||||
<AdminButton model={ModelType.partcategory} pk={category.pk} />,
|
||||
<ActionDropdown
|
||||
key="category"
|
||||
tooltip={t`Category Actions`}
|
||||
icon={<IconDots />}
|
||||
actions={[
|
||||
|
@ -761,7 +761,6 @@ export default function PartDetail() {
|
||||
key="action_dropdown"
|
||||
/>,
|
||||
<ActionDropdown
|
||||
key="stock"
|
||||
tooltip={t`Stock Actions`}
|
||||
icon={<IconPackages />}
|
||||
actions={[
|
||||
@ -790,7 +789,6 @@ export default function PartDetail() {
|
||||
]}
|
||||
/>,
|
||||
<ActionDropdown
|
||||
key="part"
|
||||
tooltip={t`Part Actions`}
|
||||
icon={<IconDots />}
|
||||
actions={[
|
||||
|
@ -12,6 +12,7 @@ import { ReactNode, useMemo } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import AdminButton from '../../components/buttons/AdminButton';
|
||||
import { PrintingActions } from '../../components/buttons/PrintingActions';
|
||||
import { DetailsField, DetailsTable } from '../../components/details/Details';
|
||||
import { DetailsImage } from '../../components/details/DetailsImage';
|
||||
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
|
||||
@ -313,8 +314,12 @@ export default function PurchaseOrderDetail() {
|
||||
})
|
||||
]}
|
||||
/>,
|
||||
<PrintingActions
|
||||
modelType={ModelType.purchaseorder}
|
||||
items={[order.pk]}
|
||||
enableReports
|
||||
/>,
|
||||
<ActionDropdown
|
||||
key="order-actions"
|
||||
tooltip={t`Order Actions`}
|
||||
icon={<IconDots />}
|
||||
actions={[
|
||||
|
@ -11,6 +11,7 @@ import { ReactNode, useMemo } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import AdminButton from '../../components/buttons/AdminButton';
|
||||
import { PrintingActions } from '../../components/buttons/PrintingActions';
|
||||
import { DetailsField, DetailsTable } from '../../components/details/Details';
|
||||
import { DetailsImage } from '../../components/details/DetailsImage';
|
||||
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
|
||||
@ -287,8 +288,12 @@ export default function ReturnOrderDetail() {
|
||||
const orderActions = useMemo(() => {
|
||||
return [
|
||||
<AdminButton model={ModelType.returnorder} pk={order.pk} />,
|
||||
<PrintingActions
|
||||
modelType={ModelType.returnorder}
|
||||
items={[order.pk]}
|
||||
enableReports
|
||||
/>,
|
||||
<ActionDropdown
|
||||
key="order-actions"
|
||||
tooltip={t`Order Actions`}
|
||||
icon={<IconDots />}
|
||||
actions={[
|
||||
|
@ -14,6 +14,7 @@ import { ReactNode, useMemo } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import AdminButton from '../../components/buttons/AdminButton';
|
||||
import { PrintingActions } from '../../components/buttons/PrintingActions';
|
||||
import { DetailsField, DetailsTable } from '../../components/details/Details';
|
||||
import { DetailsImage } from '../../components/details/DetailsImage';
|
||||
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
|
||||
@ -299,8 +300,12 @@ export default function SalesOrderDetail() {
|
||||
const soActions = useMemo(() => {
|
||||
return [
|
||||
<AdminButton model={ModelType.salesorder} pk={order.pk} />,
|
||||
<PrintingActions
|
||||
modelType={ModelType.salesorder}
|
||||
items={[order.pk]}
|
||||
enableReports
|
||||
/>,
|
||||
<ActionDropdown
|
||||
key="order-actions"
|
||||
tooltip={t`Order Actions`}
|
||||
icon={<IconDots />}
|
||||
actions={[
|
||||
|
@ -11,6 +11,7 @@ import { useNavigate, useParams } from 'react-router-dom';
|
||||
|
||||
import { ActionButton } from '../../components/buttons/ActionButton';
|
||||
import AdminButton from '../../components/buttons/AdminButton';
|
||||
import { PrintingActions } from '../../components/buttons/PrintingActions';
|
||||
import { DetailsField, DetailsTable } from '../../components/details/Details';
|
||||
import { ItemDetailsGrid } from '../../components/details/ItemDetails';
|
||||
import {
|
||||
@ -290,24 +291,14 @@ export default function Stock() {
|
||||
}
|
||||
]}
|
||||
/>,
|
||||
<ActionDropdown
|
||||
key="reports"
|
||||
icon={<InvenTreeIcon icon="reports" />}
|
||||
actions={[
|
||||
{
|
||||
name: 'Print Label',
|
||||
icon: '',
|
||||
tooltip: 'Print label'
|
||||
},
|
||||
{
|
||||
name: 'Print Location Report',
|
||||
icon: '',
|
||||
tooltip: 'Print Report'
|
||||
}
|
||||
]}
|
||||
<PrintingActions
|
||||
modelType={ModelType.stocklocation}
|
||||
items={[location.pk ?? 0]}
|
||||
enableLabels
|
||||
enableReports
|
||||
/>,
|
||||
<ActionDropdown
|
||||
key="operations"
|
||||
tooltip={t`Stock Actions`}
|
||||
icon={<InvenTreeIcon icon="stock" />}
|
||||
actions={[
|
||||
{
|
||||
@ -329,7 +320,6 @@ export default function Stock() {
|
||||
]}
|
||||
/>,
|
||||
<ActionDropdown
|
||||
key="location"
|
||||
tooltip={t`Location Actions`}
|
||||
icon={<IconDots />}
|
||||
actions={[
|
||||
|
@ -16,6 +16,7 @@ import { ReactNode, useMemo, useState } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
|
||||
import AdminButton from '../../components/buttons/AdminButton';
|
||||
import { PrintingActions } from '../../components/buttons/PrintingActions';
|
||||
import { DetailsField, DetailsTable } from '../../components/details/Details';
|
||||
import DetailsBadge from '../../components/details/DetailsBadge';
|
||||
import { DetailsImage } from '../../components/details/DetailsImage';
|
||||
@ -429,8 +430,13 @@ export default function StockDetail() {
|
||||
})
|
||||
]}
|
||||
/>,
|
||||
<PrintingActions
|
||||
modelType={ModelType.stockitem}
|
||||
items={[stockitem.pk]}
|
||||
enableReports
|
||||
enableLabels
|
||||
/>,
|
||||
<ActionDropdown
|
||||
key="operations"
|
||||
tooltip={t`Stock Operations`}
|
||||
icon={<IconPackages />}
|
||||
actions={[
|
||||
@ -473,7 +479,6 @@ export default function StockDetail() {
|
||||
]}
|
||||
/>,
|
||||
<ActionDropdown
|
||||
key="stock"
|
||||
tooltip={t`Stock Item Actions`}
|
||||
icon={<IconDots />}
|
||||
actions={[
|
||||
|
@ -2,7 +2,7 @@
|
||||
* Common rendering functions for table column data.
|
||||
*/
|
||||
import { t } from '@lingui/macro';
|
||||
import { Anchor, Text } from '@mantine/core';
|
||||
import { Anchor, Skeleton, Text } from '@mantine/core';
|
||||
|
||||
import { YesNoButton } from '../components/buttons/YesNoButton';
|
||||
import { Thumbnail } from '../components/images/Thumbnail';
|
||||
@ -18,11 +18,13 @@ import { ProjectCodeHoverCard } from './TableHoverCard';
|
||||
|
||||
// Render a Part instance within a table
|
||||
export function PartColumn(part: any, full_name?: boolean) {
|
||||
return (
|
||||
return part ? (
|
||||
<Thumbnail
|
||||
src={part?.thumbnail ?? part.image}
|
||||
text={full_name ? part.full_name : part.name}
|
||||
src={part?.thumbnail ?? part?.image}
|
||||
text={full_name ? part?.full_name : part?.name}
|
||||
/>
|
||||
) : (
|
||||
<Skeleton />
|
||||
);
|
||||
}
|
||||
|
||||
@ -226,8 +228,8 @@ export function CurrencyColumn({
|
||||
sortable: sortable ?? true,
|
||||
render: (record: any) => {
|
||||
let currency_key = currency_accessor ?? `${accessor}_currency`;
|
||||
return formatCurrency(record[accessor], {
|
||||
currency: currency ?? record[currency_key]
|
||||
return formatCurrency(resolveItem(record, accessor), {
|
||||
currency: currency ?? resolveItem(record, currency_key)
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -1,6 +1,17 @@
|
||||
import { t } from '@lingui/macro';
|
||||
import { ActionIcon, Menu, Tooltip } from '@mantine/core';
|
||||
import { IconDownload } from '@tabler/icons-react';
|
||||
import {
|
||||
IconDownload,
|
||||
IconFileSpreadsheet,
|
||||
IconFileText,
|
||||
IconFileTypeCsv
|
||||
} from '@tabler/icons-react';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import {
|
||||
ActionDropdown,
|
||||
ActionDropdownItem
|
||||
} from '../components/items/ActionDropdown';
|
||||
|
||||
export function DownloadAction({
|
||||
downloadCallback
|
||||
@ -8,34 +19,27 @@ export function DownloadAction({
|
||||
downloadCallback: (fileFormat: string) => void;
|
||||
}) {
|
||||
const formatOptions = [
|
||||
{ value: 'csv', label: t`CSV` },
|
||||
{ value: 'tsv', label: t`TSV` },
|
||||
{ value: 'xlsx', label: t`Excel` }
|
||||
{ value: 'csv', label: t`CSV`, icon: <IconFileTypeCsv /> },
|
||||
{ value: 'tsv', label: t`TSV`, icon: <IconFileText /> },
|
||||
{ value: 'xls', label: t`Excel (.xls)`, icon: <IconFileSpreadsheet /> },
|
||||
{ value: 'xlsx', label: t`Excel (.xlsx)`, icon: <IconFileSpreadsheet /> }
|
||||
];
|
||||
|
||||
const actions: ActionDropdownItem[] = useMemo(() => {
|
||||
return formatOptions.map((format) => ({
|
||||
name: format.label,
|
||||
icon: format.icon,
|
||||
onClick: () => downloadCallback(format.value)
|
||||
}));
|
||||
}, [formatOptions, downloadCallback]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Menu>
|
||||
<Menu.Target>
|
||||
<ActionIcon variant="transparent">
|
||||
<Tooltip label={t`Download selected data`}>
|
||||
<IconDownload />
|
||||
</Tooltip>
|
||||
</ActionIcon>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
{formatOptions.map((format) => (
|
||||
<Menu.Item
|
||||
key={format.value}
|
||||
onClick={() => {
|
||||
downloadCallback(format.value);
|
||||
}}
|
||||
>
|
||||
{format.label}
|
||||
</Menu.Item>
|
||||
))}
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
<ActionDropdown
|
||||
tooltip={t`Download Data`}
|
||||
icon={<IconDownload />}
|
||||
actions={actions}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ import {
|
||||
Text,
|
||||
Tooltip
|
||||
} from '@mantine/core';
|
||||
import { forwardRef, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { StylishText } from '../components/items/StylishText';
|
||||
import { TableState } from '../hooks/UseTable';
|
||||
@ -63,18 +63,6 @@ interface FilterProps extends React.ComponentPropsWithoutRef<'div'> {
|
||||
description?: string;
|
||||
}
|
||||
|
||||
/*
|
||||
* Custom component for the filter select
|
||||
*/
|
||||
const FilterSelectItem = forwardRef<HTMLDivElement, FilterProps>(
|
||||
({ label, description, ...others }, ref) => (
|
||||
<div ref={ref} {...others}>
|
||||
<Text size="sm">{label}</Text>
|
||||
<Text size="xs">{description}</Text>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
|
||||
function FilterAddGroup({
|
||||
tableState,
|
||||
availableFilters
|
||||
@ -144,7 +132,6 @@ function FilterAddGroup({
|
||||
<Divider />
|
||||
<Select
|
||||
data={filterOptions}
|
||||
component={FilterSelectItem}
|
||||
searchable={true}
|
||||
placeholder={t`Select filter`}
|
||||
label={t`Filter`}
|
||||
|
@ -15,7 +15,6 @@ import { showNotification } from '@mantine/notifications';
|
||||
import {
|
||||
IconBarcode,
|
||||
IconFilter,
|
||||
IconPrinter,
|
||||
IconRefresh,
|
||||
IconTrash
|
||||
} from '@tabler/icons-react';
|
||||
@ -38,6 +37,7 @@ import { api } from '../App';
|
||||
import { Boundary } from '../components/Boundary';
|
||||
import { ActionButton } from '../components/buttons/ActionButton';
|
||||
import { ButtonMenu } from '../components/buttons/ButtonMenu';
|
||||
import { PrintingActions } from '../components/buttons/PrintingActions';
|
||||
import { ApiFormFieldSet } from '../components/forms/fields/ApiFormField';
|
||||
import { ModelType } from '../enums/ModelType';
|
||||
import { resolveItem } from '../functions/conversion';
|
||||
@ -46,7 +46,6 @@ import { extractAvailableFields, mapFields } from '../functions/forms';
|
||||
import { navigateToLink } from '../functions/navigation';
|
||||
import { getDetailUrl } from '../functions/urls';
|
||||
import { TableState } from '../hooks/UseTable';
|
||||
import { base_url } from '../main';
|
||||
import { useLocalState } from '../states/LocalState';
|
||||
import { TableColumn } from './Column';
|
||||
import { TableColumnSelect } from './ColumnSelect';
|
||||
@ -70,13 +69,14 @@ const defaultPageSize: number = 25;
|
||||
* @param enableFilters : boolean - Enable filter actions
|
||||
* @param enableSelection : boolean - Enable row selection
|
||||
* @param enableSearch : boolean - Enable search actions
|
||||
* @param enableLabels : boolean - Enable printing of labels against selected items
|
||||
* @param enableReports : boolean - Enable printing of reports against selected items
|
||||
* @param enablePagination : boolean - Enable pagination
|
||||
* @param enableRefresh : boolean - Enable refresh actions
|
||||
* @param pageSize : number - Number of records per page
|
||||
* @param barcodeActions : any[] - List of barcode actions
|
||||
* @param tableFilters : TableFilter[] - List of custom filters
|
||||
* @param tableActions : any[] - List of custom action groups
|
||||
* @param printingActions : any[] - List of printing actions
|
||||
* @param dataFormatter : (data: any) => any - Callback function to reformat data returned by server (if not in default format)
|
||||
* @param rowActions : (record: any) => RowAction[] - Callback function to generate row actions
|
||||
* @param onRowClick : (record: any, index: number, event: any) => void - Callback function when a row is clicked
|
||||
@ -94,11 +94,12 @@ export type InvenTreeTableProps<T = any> = {
|
||||
enableSearch?: boolean;
|
||||
enablePagination?: boolean;
|
||||
enableRefresh?: boolean;
|
||||
enableLabels?: boolean;
|
||||
enableReports?: boolean;
|
||||
pageSize?: number;
|
||||
barcodeActions?: any[];
|
||||
tableFilters?: TableFilter[];
|
||||
tableActions?: React.ReactNode[];
|
||||
printingActions?: any[];
|
||||
rowExpansion?: any;
|
||||
idAccessor?: string;
|
||||
dataFormatter?: (data: any) => any;
|
||||
@ -117,6 +118,8 @@ const defaultInvenTreeTableProps: InvenTreeTableProps = {
|
||||
params: {},
|
||||
noRecordsText: t`No records found`,
|
||||
enableDownload: false,
|
||||
enableLabels: false,
|
||||
enableReports: false,
|
||||
enableFilters: true,
|
||||
enablePagination: true,
|
||||
enableRefresh: true,
|
||||
@ -124,7 +127,6 @@ const defaultInvenTreeTableProps: InvenTreeTableProps = {
|
||||
enableSelection: false,
|
||||
pageSize: defaultPageSize,
|
||||
defaultSortColumn: '',
|
||||
printingActions: [],
|
||||
barcodeActions: [],
|
||||
tableFilters: [],
|
||||
tableActions: [],
|
||||
@ -226,13 +228,16 @@ export function InvenTreeTable<T = any>({
|
||||
}, [props]);
|
||||
|
||||
// Check if any columns are switchable (can be hidden)
|
||||
const hasSwitchableColumns = columns.some(
|
||||
(col: TableColumn) => col.switchable ?? true
|
||||
);
|
||||
const hasSwitchableColumns: boolean = useMemo(() => {
|
||||
return columns.some((col: TableColumn) => col.switchable ?? true);
|
||||
}, [columns]);
|
||||
|
||||
const onSelectedRecordsChange = useCallback((records: any[]) => {
|
||||
tableState.setSelectedRecords(records);
|
||||
}, []);
|
||||
const onSelectedRecordsChange = useCallback(
|
||||
(records: any[]) => {
|
||||
tableState.setSelectedRecords(records);
|
||||
},
|
||||
[tableState.setSelectedRecords]
|
||||
);
|
||||
|
||||
// Update column visibility when hiddenColumns change
|
||||
const dataColumns: any = useMemo(() => {
|
||||
@ -572,13 +577,22 @@ export function InvenTreeTable<T = any>({
|
||||
/>
|
||||
</Boundary>
|
||||
)}
|
||||
<Boundary label="inventreetable">
|
||||
<Boundary label={`InvenTreeTable-${tableState.tableKey}`}>
|
||||
<Stack gap="sm">
|
||||
<Group justify="space-between">
|
||||
<Group justify="apart" grow>
|
||||
<Group justify="left" key="custom-actions" gap={5}>
|
||||
{tableProps.tableActions?.map((group, idx) => (
|
||||
<Fragment key={idx}>{group}</Fragment>
|
||||
))}
|
||||
{tableProps.enableDownload && (
|
||||
<DownloadAction
|
||||
key="download-action"
|
||||
downloadCallback={downloadData}
|
||||
/>
|
||||
)}
|
||||
<PrintingActions
|
||||
items={tableState.selectedIds}
|
||||
modelType={tableProps.modelType}
|
||||
enableLabels={tableProps.enableLabels}
|
||||
enableReports={tableProps.enableReports}
|
||||
/>
|
||||
{(tableProps.barcodeActions?.length ?? 0 > 0) && (
|
||||
<ButtonMenu
|
||||
key="barcode-actions"
|
||||
@ -588,24 +602,18 @@ export function InvenTreeTable<T = any>({
|
||||
actions={tableProps.barcodeActions ?? []}
|
||||
/>
|
||||
)}
|
||||
{(tableProps.printingActions?.length ?? 0 > 0) && (
|
||||
<ButtonMenu
|
||||
key="printing-actions"
|
||||
icon={<IconPrinter />}
|
||||
label={t`Print actions`}
|
||||
tooltip={t`Print actions`}
|
||||
actions={tableProps.printingActions ?? []}
|
||||
/>
|
||||
)}
|
||||
{(tableProps.enableBulkDelete ?? false) && (
|
||||
<ActionButton
|
||||
disabled={tableState.selectedRecords.length == 0}
|
||||
disabled={!tableState.hasSelectedRecords}
|
||||
icon={<IconTrash />}
|
||||
color="red"
|
||||
tooltip={t`Delete selected records`}
|
||||
onClick={deleteSelectedRecords}
|
||||
/>
|
||||
)}
|
||||
{tableProps.tableActions?.map((group, idx) => (
|
||||
<Fragment key={idx}>{group}</Fragment>
|
||||
))}
|
||||
</Group>
|
||||
<Space />
|
||||
<Group justify="right" gap={5}>
|
||||
@ -644,12 +652,6 @@ export function InvenTreeTable<T = any>({
|
||||
</ActionIcon>
|
||||
</Indicator>
|
||||
)}
|
||||
{tableProps.enableDownload && (
|
||||
<DownloadAction
|
||||
key="download-action"
|
||||
downloadCallback={downloadData}
|
||||
/>
|
||||
)}
|
||||
</Group>
|
||||
</Group>
|
||||
<Box pos="relative">
|
||||
|
@ -194,7 +194,6 @@ export function BuildOrderTable({
|
||||
tableState={table}
|
||||
columns={tableColumns}
|
||||
props={{
|
||||
enableDownload: true,
|
||||
params: {
|
||||
part: partId,
|
||||
sales_order: salesOrderId,
|
||||
@ -203,7 +202,10 @@ export function BuildOrderTable({
|
||||
},
|
||||
tableActions: tableActions,
|
||||
tableFilters: tableFilters,
|
||||
modelType: ModelType.build
|
||||
modelType: ModelType.build,
|
||||
enableSelection: true,
|
||||
enableReports: true,
|
||||
enableDownload: true
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
|
@ -159,7 +159,10 @@ export function PurchaseOrderTable({
|
||||
},
|
||||
tableFilters: tableFilters,
|
||||
tableActions: tableActions,
|
||||
modelType: ModelType.purchaseorder
|
||||
modelType: ModelType.purchaseorder,
|
||||
enableSelection: true,
|
||||
enableDownload: true,
|
||||
enableReports: true
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
|
@ -146,7 +146,10 @@ export function ReturnOrderTable({ params }: { params?: any }) {
|
||||
},
|
||||
tableFilters: tableFilters,
|
||||
tableActions: tableActions,
|
||||
modelType: ModelType.returnorder
|
||||
modelType: ModelType.returnorder,
|
||||
enableSelection: true,
|
||||
enableDownload: true,
|
||||
enableReports: true
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
|
@ -157,7 +157,10 @@ export function SalesOrderTable({
|
||||
},
|
||||
tableFilters: tableFilters,
|
||||
tableActions: tableActions,
|
||||
modelType: ModelType.salesorder
|
||||
modelType: ModelType.salesorder,
|
||||
enableSelection: true,
|
||||
enableDownload: true,
|
||||
enableReports: true
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Trans, t } from '@lingui/macro';
|
||||
import { Box, Group, LoadingOverlay, Stack, Text, Title } from '@mantine/core';
|
||||
import { IconDots } from '@tabler/icons-react';
|
||||
import { Group, LoadingOverlay, Stack, Text, Title } from '@mantine/core';
|
||||
import { IconFileCode } from '@tabler/icons-react';
|
||||
import { ReactNode, useCallback, useMemo, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
@ -10,15 +10,11 @@ import {
|
||||
PdfPreview,
|
||||
TemplateEditor
|
||||
} from '../../components/editors/TemplateEditor';
|
||||
import { TemplatePreviewProps } from '../../components/editors/TemplateEditor/TemplateEditor';
|
||||
import { ApiFormFieldSet } from '../../components/forms/fields/ApiFormField';
|
||||
import {
|
||||
ActionDropdown,
|
||||
DeleteItemAction,
|
||||
EditItemAction
|
||||
} from '../../components/items/ActionDropdown';
|
||||
import { DetailDrawer } from '../../components/nav/DetailDrawer';
|
||||
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
||||
import { ModelType } from '../../enums/ModelType';
|
||||
import { useFilters } from '../../hooks/UseFilter';
|
||||
import {
|
||||
useCreateApiFormModal,
|
||||
useDeleteApiFormModal,
|
||||
@ -27,88 +23,54 @@ import {
|
||||
import { useInstance } from '../../hooks/UseInstance';
|
||||
import { useTable } from '../../hooks/UseTable';
|
||||
import { apiUrl } from '../../states/ApiState';
|
||||
import { useUserState } from '../../states/UserState';
|
||||
import { TableColumn } from '../Column';
|
||||
import { BooleanColumn } from '../ColumnRenderers';
|
||||
import { TableFilter } from '../Filter';
|
||||
import { InvenTreeTable } from '../InvenTreeTable';
|
||||
import { RowAction, RowDeleteAction, RowEditAction } from '../RowActions';
|
||||
import {
|
||||
RowAction,
|
||||
RowDeleteAction,
|
||||
RowDuplicateAction,
|
||||
RowEditAction
|
||||
} from '../RowActions';
|
||||
|
||||
export type TemplateI = {
|
||||
pk: number;
|
||||
name: string;
|
||||
description: string;
|
||||
model_type: ModelType;
|
||||
filters: string;
|
||||
filename_pattern: string;
|
||||
enabled: boolean;
|
||||
template: string;
|
||||
};
|
||||
|
||||
export interface TemplateProps {
|
||||
apiEndpoint: ApiEndpoints;
|
||||
templateType: 'label' | 'report';
|
||||
templateTypeTranslation: string;
|
||||
variant: string;
|
||||
templateKey: string;
|
||||
templateEndpoint: ApiEndpoints;
|
||||
printingEndpoint: ApiEndpoints;
|
||||
additionalFormFields?: ApiFormFieldSet;
|
||||
preview: TemplatePreviewProps;
|
||||
defaultTemplate: string;
|
||||
}
|
||||
|
||||
export function TemplateDrawer({
|
||||
id,
|
||||
refreshTable,
|
||||
templateProps
|
||||
}: {
|
||||
id: string;
|
||||
refreshTable: () => void;
|
||||
id: string | number;
|
||||
templateProps: TemplateProps;
|
||||
}) {
|
||||
const {
|
||||
apiEndpoint,
|
||||
templateType,
|
||||
templateTypeTranslation,
|
||||
variant,
|
||||
additionalFormFields
|
||||
} = templateProps;
|
||||
const navigate = useNavigate();
|
||||
const { templateEndpoint, printingEndpoint } = templateProps;
|
||||
|
||||
const {
|
||||
instance: template,
|
||||
refreshInstance,
|
||||
instanceQuery: { isFetching, error }
|
||||
} = useInstance<TemplateI>({
|
||||
endpoint: apiEndpoint,
|
||||
pathParams: { variant },
|
||||
endpoint: templateEndpoint,
|
||||
hasPrimaryKey: true,
|
||||
pk: id,
|
||||
throwError: true
|
||||
});
|
||||
|
||||
const editTemplate = useEditApiFormModal({
|
||||
url: apiEndpoint,
|
||||
pathParams: { variant },
|
||||
pk: id,
|
||||
title: t`Edit` + ' ' + templateTypeTranslation,
|
||||
fields: {
|
||||
name: {},
|
||||
description: {},
|
||||
filters: {},
|
||||
enabled: {},
|
||||
...additionalFormFields
|
||||
},
|
||||
onFormSuccess: (data) => {
|
||||
refreshInstance();
|
||||
refreshTable();
|
||||
}
|
||||
});
|
||||
|
||||
const deleteTemplate = useDeleteApiFormModal({
|
||||
url: apiEndpoint,
|
||||
pathParams: { variant },
|
||||
pk: id,
|
||||
title: t`Delete` + ' ' + templateTypeTranslation,
|
||||
onFormSuccess: () => {
|
||||
refreshTable();
|
||||
navigate('../');
|
||||
}
|
||||
});
|
||||
|
||||
if (isFetching) {
|
||||
return <LoadingOverlay visible={true} />;
|
||||
}
|
||||
@ -117,13 +79,9 @@ export function TemplateDrawer({
|
||||
return (
|
||||
<Text>
|
||||
{(error as any)?.response?.status === 404 ? (
|
||||
<Trans>
|
||||
{templateTypeTranslation} with id {id} not found
|
||||
</Trans>
|
||||
<Trans>Template not found</Trans>
|
||||
) : (
|
||||
<Trans>
|
||||
An error occurred while fetching {templateTypeTranslation} details
|
||||
</Trans>
|
||||
<Trans>An error occurred while fetching template details</Trans>
|
||||
)}
|
||||
</Text>
|
||||
);
|
||||
@ -131,40 +89,13 @@ export function TemplateDrawer({
|
||||
|
||||
return (
|
||||
<Stack gap="xs" style={{ display: 'flex', flex: '1' }}>
|
||||
{editTemplate.modal}
|
||||
{deleteTemplate.modal}
|
||||
|
||||
<Group justify="space-between">
|
||||
<Box></Box>
|
||||
|
||||
<Group>
|
||||
<Title order={4}>{template?.name}</Title>
|
||||
</Group>
|
||||
|
||||
<Group>
|
||||
<ActionDropdown
|
||||
tooltip={templateTypeTranslation + ' ' + t`actions`}
|
||||
icon={<IconDots />}
|
||||
actions={[
|
||||
EditItemAction({
|
||||
tooltip: t`Edit` + ' ' + templateTypeTranslation,
|
||||
onClick: editTemplate.open
|
||||
}),
|
||||
DeleteItemAction({
|
||||
tooltip: t`Delete` + ' ' + templateTypeTranslation,
|
||||
onClick: deleteTemplate.open
|
||||
})
|
||||
]}
|
||||
/>
|
||||
</Group>
|
||||
<Group justify="left">
|
||||
<Title order={4}>{template?.name}</Title>
|
||||
</Group>
|
||||
|
||||
<TemplateEditor
|
||||
downloadUrl={(template as any)[templateProps.templateKey]}
|
||||
uploadUrl={apiUrl(apiEndpoint, id, { variant })}
|
||||
uploadKey={templateProps.templateKey}
|
||||
preview={templateProps.preview}
|
||||
templateType={templateType}
|
||||
templateUrl={apiUrl(templateEndpoint, id)}
|
||||
printingUrl={apiUrl(printingEndpoint)}
|
||||
template={template}
|
||||
editors={[CodeEditor]}
|
||||
previewAreas={[PdfPreview]}
|
||||
@ -178,17 +109,11 @@ export function TemplateTable({
|
||||
}: {
|
||||
templateProps: TemplateProps;
|
||||
}) {
|
||||
const {
|
||||
apiEndpoint,
|
||||
templateType,
|
||||
templateTypeTranslation,
|
||||
variant,
|
||||
templateKey,
|
||||
additionalFormFields,
|
||||
defaultTemplate
|
||||
} = templateProps;
|
||||
const table = useTable(`${templateType}-${variant}`);
|
||||
const { templateEndpoint, additionalFormFields } = templateProps;
|
||||
|
||||
const table = useTable(`${templateEndpoint}-template`);
|
||||
const navigate = useNavigate();
|
||||
const user = useUserState();
|
||||
|
||||
const openDetailDrawer = useCallback((pk: number) => navigate(`${pk}/`), []);
|
||||
|
||||
@ -196,62 +121,120 @@ export function TemplateTable({
|
||||
return [
|
||||
{
|
||||
accessor: 'name',
|
||||
sortable: true
|
||||
sortable: true,
|
||||
switchable: false
|
||||
},
|
||||
{
|
||||
accessor: 'description',
|
||||
sortable: false
|
||||
sortable: false,
|
||||
switchable: true
|
||||
},
|
||||
{
|
||||
accessor: 'template',
|
||||
sortable: false,
|
||||
switchable: true,
|
||||
render: (record: any) => {
|
||||
return record.template?.split('/')?.pop() ?? '-';
|
||||
}
|
||||
},
|
||||
{
|
||||
accessor: 'model_type',
|
||||
sortable: true,
|
||||
switchable: false
|
||||
},
|
||||
{
|
||||
accessor: 'revision',
|
||||
sortable: false,
|
||||
switchable: true
|
||||
},
|
||||
{
|
||||
accessor: 'filters',
|
||||
sortable: false
|
||||
sortable: false,
|
||||
switchable: true
|
||||
},
|
||||
...Object.entries(additionalFormFields || {})?.map(([key, field]) => ({
|
||||
accessor: key,
|
||||
sortable: false
|
||||
sortable: false,
|
||||
switchable: true
|
||||
})),
|
||||
BooleanColumn({ accessor: 'enabled', title: t`Enabled` })
|
||||
];
|
||||
}, []);
|
||||
}, [additionalFormFields]);
|
||||
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<number>(-1);
|
||||
|
||||
const rowActions = useCallback((record: TemplateI): RowAction[] => {
|
||||
return [
|
||||
RowEditAction({
|
||||
onClick: () => openDetailDrawer(record.pk)
|
||||
}),
|
||||
RowDeleteAction({
|
||||
onClick: () => {
|
||||
setSelectedTemplate(record.pk), deleteTemplate.open();
|
||||
}
|
||||
})
|
||||
];
|
||||
}, []);
|
||||
const rowActions = useCallback(
|
||||
(record: TemplateI): RowAction[] => {
|
||||
return [
|
||||
{
|
||||
title: t`Modify`,
|
||||
tooltip: t`Modify template file`,
|
||||
icon: <IconFileCode />,
|
||||
onClick: () => openDetailDrawer(record.pk)
|
||||
},
|
||||
RowEditAction({
|
||||
onClick: () => {
|
||||
setSelectedTemplate(record.pk);
|
||||
editTemplate.open();
|
||||
}
|
||||
}),
|
||||
RowDuplicateAction({
|
||||
// TODO: Duplicate selected template
|
||||
}),
|
||||
RowDeleteAction({
|
||||
onClick: () => {
|
||||
setSelectedTemplate(record.pk);
|
||||
deleteTemplate.open();
|
||||
}
|
||||
})
|
||||
];
|
||||
},
|
||||
[user]
|
||||
);
|
||||
|
||||
const templateFields: ApiFormFieldSet = {
|
||||
name: {},
|
||||
description: {},
|
||||
model_type: {},
|
||||
filters: {},
|
||||
filename_pattern: {},
|
||||
enabled: {}
|
||||
};
|
||||
|
||||
const editTemplateFields: ApiFormFieldSet = useMemo(() => {
|
||||
return {
|
||||
...templateFields,
|
||||
...additionalFormFields
|
||||
};
|
||||
}, [additionalFormFields]);
|
||||
|
||||
const newTemplateFields: ApiFormFieldSet = useMemo(() => {
|
||||
return {
|
||||
template: {},
|
||||
...templateFields,
|
||||
...additionalFormFields
|
||||
};
|
||||
}, [additionalFormFields]);
|
||||
|
||||
const editTemplate = useEditApiFormModal({
|
||||
url: templateEndpoint,
|
||||
pk: selectedTemplate,
|
||||
title: t`Edit Template`,
|
||||
fields: editTemplateFields,
|
||||
onFormSuccess: (record: any) => table.updateRecord(record)
|
||||
});
|
||||
|
||||
const deleteTemplate = useDeleteApiFormModal({
|
||||
url: apiEndpoint,
|
||||
pathParams: { variant },
|
||||
url: templateEndpoint,
|
||||
pk: selectedTemplate,
|
||||
title: t`Delete` + ' ' + templateTypeTranslation,
|
||||
title: t`Delete template`,
|
||||
table: table
|
||||
});
|
||||
|
||||
const newTemplate = useCreateApiFormModal({
|
||||
url: apiEndpoint,
|
||||
pathParams: { variant },
|
||||
title: t`Add new` + ' ' + templateTypeTranslation,
|
||||
fields: {
|
||||
name: {},
|
||||
description: {},
|
||||
filters: {},
|
||||
enabled: {},
|
||||
[templateKey]: {
|
||||
hidden: true,
|
||||
value: new File([defaultTemplate], 'template.html')
|
||||
},
|
||||
...additionalFormFields
|
||||
},
|
||||
url: templateEndpoint,
|
||||
title: t`Add Template`,
|
||||
fields: newTemplateFields,
|
||||
onFormSuccess: (data) => {
|
||||
table.refreshTable();
|
||||
openDetailDrawer(data.pk);
|
||||
@ -261,13 +244,25 @@ export function TemplateTable({
|
||||
const tableActions: ReactNode[] = useMemo(() => {
|
||||
return [
|
||||
<AddItemButton
|
||||
key={`add-${templateType}`}
|
||||
key="add-template"
|
||||
onClick={() => newTemplate.open()}
|
||||
tooltip={t`Add` + ' ' + templateTypeTranslation}
|
||||
tooltip={t`Add template`}
|
||||
/>
|
||||
];
|
||||
}, []);
|
||||
|
||||
const modelTypeFilters = useFilters({
|
||||
url: apiUrl(templateEndpoint),
|
||||
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 [
|
||||
{
|
||||
@ -275,30 +270,31 @@ export function TemplateTable({
|
||||
label: t`Enabled`,
|
||||
description: t`Filter by enabled status`,
|
||||
type: 'checkbox'
|
||||
},
|
||||
{
|
||||
name: 'model_type',
|
||||
label: t`Model Type`,
|
||||
description: t`Filter by target model type`,
|
||||
choices: modelTypeFilters.choices
|
||||
}
|
||||
];
|
||||
}, []);
|
||||
}, [modelTypeFilters.choices]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{newTemplate.modal}
|
||||
{editTemplate.modal}
|
||||
{deleteTemplate.modal}
|
||||
<DetailDrawer
|
||||
title={t`Edit` + ' ' + templateTypeTranslation}
|
||||
title={t`Edit Template`}
|
||||
size={'90%'}
|
||||
closeOnEscape={false}
|
||||
renderContent={(id) => {
|
||||
return (
|
||||
<TemplateDrawer
|
||||
id={id ?? ''}
|
||||
refreshTable={table.refreshTable}
|
||||
templateProps={templateProps}
|
||||
/>
|
||||
);
|
||||
return <TemplateDrawer id={id ?? ''} templateProps={templateProps} />;
|
||||
}}
|
||||
/>
|
||||
<InvenTreeTable
|
||||
url={apiUrl(apiEndpoint, undefined, { variant })}
|
||||
url={apiUrl(templateEndpoint)}
|
||||
tableState={table}
|
||||
columns={columns}
|
||||
props={{
|
||||
|
@ -406,7 +406,7 @@ export function StockItemTable({
|
||||
let can_change_order = user.hasChangeRole(UserRoles.purchase_order);
|
||||
return [
|
||||
<ActionDropdown
|
||||
key="stockoperations"
|
||||
tooltip={t`Stock Actions`}
|
||||
icon={<InvenTreeIcon icon="stock" />}
|
||||
disabled={table.selectedRecords.length === 0}
|
||||
actions={[
|
||||
@ -520,6 +520,8 @@ export function StockItemTable({
|
||||
props={{
|
||||
enableDownload: true,
|
||||
enableSelection: true,
|
||||
enableLabels: true,
|
||||
enableReports: true,
|
||||
tableFilters: tableFilters,
|
||||
tableActions: tableActions,
|
||||
modelType: ModelType.stockitem,
|
||||
|
@ -140,7 +140,10 @@ export function StockLocationTable({ parentId }: { parentId?: any }) {
|
||||
tableState={table}
|
||||
columns={tableColumns}
|
||||
props={{
|
||||
enableSelection: true,
|
||||
enableDownload: true,
|
||||
enableLabels: true,
|
||||
enableReports: true,
|
||||
params: {
|
||||
parent: parentId
|
||||
},
|
||||
|
@ -19,8 +19,11 @@ test('PUI - Pages - Index - Playground', async ({ page }) => {
|
||||
.locator('div')
|
||||
.first()
|
||||
.click();
|
||||
await page.getByLabel('Name *').fill(newPartName);
|
||||
await page.getByLabel('Initial Stock Quantity *').fill('1');
|
||||
|
||||
// Set the "name"
|
||||
await page.getByLabel('text-field-name').fill(newPartName);
|
||||
await page.getByLabel('number-field-initial_stock.').fill('1');
|
||||
|
||||
await page
|
||||
.getByLabel('Create Part')
|
||||
.getByRole('button', { name: 'Cancel' })
|
||||
@ -37,7 +40,7 @@ test('PUI - Pages - Index - Playground', async ({ page }) => {
|
||||
|
||||
// Create Stock Item
|
||||
await page.getByRole('button', { name: 'Create Stock Item' }).click();
|
||||
await page.locator('#react-select-25-input').fill('R_1K_0402_1');
|
||||
await page.getByLabel('related-field-part').fill('R_1K_0402_1');
|
||||
await page.getByText('R_1K_0402_1%').click();
|
||||
await page
|
||||
.getByLabel('Add Stock Item')
|
||||
|
@ -186,7 +186,8 @@ test('PUI - Admin', async ({ page }) => {
|
||||
await page.getByRole('tab', { name: 'Custom Units' }).click();
|
||||
await page.getByRole('tab', { name: 'Part Parameters' }).click();
|
||||
await page.getByRole('tab', { name: 'Category Parameters' }).click();
|
||||
await page.getByRole('tab', { name: 'Templates' }).click();
|
||||
await page.getByRole('tab', { name: 'Label Templates' }).click();
|
||||
await page.getByRole('tab', { name: 'Report Templates' }).click();
|
||||
await page.getByRole('tab', { name: 'Plugins' }).click();
|
||||
await page.getByRole('tab', { name: 'Machines' }).click();
|
||||
});
|
||||
|
108
src/frontend/tests/pui_printing.spec.ts
Normal file
108
src/frontend/tests/pui_printing.spec.ts
Normal file
@ -0,0 +1,108 @@
|
||||
import { test } from './baseFixtures.js';
|
||||
import { baseUrl } from './defaults.js';
|
||||
import { doQuickLogin } from './login.js';
|
||||
|
||||
/*
|
||||
* Test for label printing.
|
||||
* Select a number of stock items from the table,
|
||||
* and print labels against them
|
||||
*/
|
||||
test('PUI - Label Printing', async ({ page }) => {
|
||||
await doQuickLogin(page);
|
||||
|
||||
await page.goto(`${baseUrl}/stock/location/index/`);
|
||||
await page.waitForURL('**/platform/stock/location/**');
|
||||
|
||||
await page.getByRole('tab', { name: 'Stock Items' }).click();
|
||||
|
||||
// Select some labels
|
||||
await page.getByLabel('Select record 1', { exact: true }).click();
|
||||
await page.getByLabel('Select record 2', { exact: true }).click();
|
||||
await page.getByLabel('Select record 3', { exact: true }).click();
|
||||
|
||||
await page
|
||||
.getByLabel('Stock Items')
|
||||
.getByLabel('action-menu-printing-actions')
|
||||
.click();
|
||||
await page.getByLabel('action-menu-printing-actions-print-labels').click();
|
||||
|
||||
// Select plugin
|
||||
await page.getByLabel('related-field-plugin').click();
|
||||
await page.getByText('InvenTreeLabelSheet').click();
|
||||
|
||||
// Select label template
|
||||
await page.getByLabel('related-field-template').click();
|
||||
await page.getByText('InvenTree Stock Item Label (').click();
|
||||
|
||||
await page.waitForTimeout(100);
|
||||
|
||||
// Submit the form (second time should result in success)
|
||||
await page.getByRole('button', { name: 'Submit' }).isEnabled();
|
||||
await page.getByRole('button', { name: 'Submit' }).click();
|
||||
|
||||
await page.locator('#form-success').waitFor();
|
||||
await page.getByText('Label printing completed').waitFor();
|
||||
|
||||
await page.context().close();
|
||||
});
|
||||
|
||||
/*
|
||||
* Test for report printing
|
||||
* Navigate to a PurchaseOrder detail page,
|
||||
* and print a report against it.
|
||||
*/
|
||||
test('PUI - Report Printing', async ({ page }) => {
|
||||
await doQuickLogin(page);
|
||||
|
||||
await page.goto(`${baseUrl}/stock/location/index/`);
|
||||
await page.waitForURL('**/platform/stock/location/**');
|
||||
|
||||
// Navigate to a specific PurchaseOrder
|
||||
await page.getByRole('tab', { name: 'Purchasing' }).click();
|
||||
await page.getByRole('cell', { name: 'PO0009' }).click();
|
||||
|
||||
// Select "print report"
|
||||
await page.getByLabel('action-menu-printing-actions').click();
|
||||
await page.getByLabel('action-menu-printing-actions-print-reports').click();
|
||||
|
||||
// Select template
|
||||
await page.getByLabel('related-field-template').click();
|
||||
await page.getByText('InvenTree Purchase Order').click();
|
||||
|
||||
await page.waitForTimeout(100);
|
||||
|
||||
// Submit the form (should result in success)
|
||||
await page.getByRole('button', { name: 'Submit' }).isEnabled();
|
||||
await page.getByRole('button', { name: 'Submit' }).click();
|
||||
|
||||
await page.locator('#form-success').waitFor();
|
||||
await page.getByText('Report printing completed').waitFor();
|
||||
|
||||
await page.context().close();
|
||||
});
|
||||
|
||||
test('PUI - Report Editing', async ({ page }) => {
|
||||
await doQuickLogin(page, 'admin', 'inventree');
|
||||
|
||||
// Navigate to the admin center
|
||||
await page.getByRole('button', { name: 'admin' }).click();
|
||||
await page.getByRole('menuitem', { name: 'Admin Center' }).click();
|
||||
await page.getByRole('tab', { name: 'Label Templates' }).click();
|
||||
await page
|
||||
.getByRole('cell', { name: 'InvenTree Stock Item Label (' })
|
||||
.click();
|
||||
|
||||
// Generate preview
|
||||
await page.getByLabel('split-button-preview-options-action').click();
|
||||
await page
|
||||
.getByLabel('split-button-preview-options-item-preview-save', {
|
||||
exact: true
|
||||
})
|
||||
.click();
|
||||
|
||||
await page.getByRole('button', { name: 'Save & Reload' }).click();
|
||||
|
||||
await page.getByText('The preview has been updated').waitFor();
|
||||
|
||||
await page.context().close();
|
||||
});
|
@ -13,7 +13,9 @@ test('PUI - Stock', async ({ page }) => {
|
||||
|
||||
await page.getByRole('tab', { name: 'Stock Items' }).click();
|
||||
await page.getByRole('cell', { name: '1551ABK' }).click();
|
||||
|
||||
await page.getByRole('tab', { name: 'Stock', exact: true }).click();
|
||||
await page.waitForURL('**/platform/stock/**');
|
||||
await page.getByRole('tab', { name: 'Stock Locations' }).click();
|
||||
await page.getByRole('cell', { name: 'Electronics Lab' }).first().click();
|
||||
await page.getByRole('tab', { name: 'Default Parts' }).click();
|
||||
@ -78,11 +80,15 @@ test('PUI - Purchasing', async ({ page }) => {
|
||||
.getByRole('button')
|
||||
.click();
|
||||
await page.getByRole('menuitem', { name: 'Edit' }).click();
|
||||
await page.getByLabel('Address title *').waitFor();
|
||||
|
||||
await page.getByLabel('text-field-title').waitFor();
|
||||
await page.getByLabel('text-field-line2').waitFor();
|
||||
|
||||
// Read the current value of the cell, to ensure we always *change* it!
|
||||
const value = await page.getByLabel('Line 2').inputValue();
|
||||
await page.getByLabel('Line 2').fill(value == 'old' ? 'new' : 'old');
|
||||
const value = await page.getByLabel('text-field-line2').inputValue();
|
||||
await page
|
||||
.getByLabel('text-field-line2')
|
||||
.fill(value == 'old' ? 'new' : 'old');
|
||||
|
||||
await page.getByRole('button', { name: 'Submit' }).isEnabled();
|
||||
|
||||
|
Reference in New Issue
Block a user