[PUI] Attachment table fix (#7232)

* Use modal hook for creating new attachments

* Add "edit" and "delete" modals for attachment table

* Fix for drag-and-drop zone

- Update to match mantine v7

* Fix link clicking

* Fix call to cancelEvent

* Add placeholder for more unit tests
This commit is contained in:
Oliver 2024-05-16 12:34:44 +10:00 committed by GitHub
parent e8f8f3b3ec
commit 2a83c19208
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 201 additions and 229 deletions

View File

@ -1,130 +0,0 @@
import { t } from '@lingui/macro';
import { ApiFormFieldSet } from '../components/forms/fields/ApiFormField';
import { ApiEndpoints } from '../enums/ApiEndpoints';
import {
openCreateApiForm,
openDeleteApiForm,
openEditApiForm
} from '../functions/forms';
export function attachmentFields(editing: boolean): ApiFormFieldSet {
let fields: ApiFormFieldSet = {
attachment: {},
comment: {}
};
if (editing) {
delete fields['attachment'];
}
return fields;
}
/**
* Add a new attachment (either a file or a link)
*/
export function addAttachment({
endpoint,
model,
pk,
attachmentType,
callback
}: {
endpoint: ApiEndpoints;
model: string;
pk: number;
attachmentType: 'file' | 'link';
callback?: () => void;
}) {
let formFields: ApiFormFieldSet = {
attachment: {},
link: {},
comment: {}
};
if (attachmentType === 'link') {
delete formFields['attachment'];
} else {
delete formFields['link'];
}
formFields[model] = {
value: pk,
hidden: true
};
let title = attachmentType === 'file' ? t`Add File` : t`Add Link`;
let message = attachmentType === 'file' ? t`File added` : t`Link added`;
openCreateApiForm({
title: title,
url: endpoint,
successMessage: message,
fields: formFields,
onFormSuccess: callback
});
}
/**
* Edit an existing attachment (either a file or a link)
*/
export function editAttachment({
endpoint,
model,
pk,
attachmentType,
callback
}: {
endpoint: ApiEndpoints;
model: string;
pk: number;
attachmentType: 'file' | 'link';
callback?: (record: any) => void;
}) {
let formFields: ApiFormFieldSet = {
link: {},
comment: {}
};
if (attachmentType === 'file') {
delete formFields['link'];
}
formFields[model] = {
value: pk,
hidden: true
};
let title = attachmentType === 'file' ? t`Edit File` : t`Edit Link`;
let message = attachmentType === 'file' ? t`File updated` : t`Link updated`;
openEditApiForm({
title: title,
url: endpoint,
pk: pk,
successMessage: message,
fields: formFields,
onFormSuccess: callback
});
}
export function deleteAttachment({
endpoint,
pk,
callback
}: {
endpoint: ApiEndpoints;
pk: number;
callback: () => void;
}) {
openDeleteApiForm({
url: endpoint,
pk: pk,
title: t`Delete Attachment`,
successMessage: t`Attachment deleted`,
onFormSuccess: callback,
fields: {},
preFormWarning: t`Are you sure you want to delete this attachment?`
});
}

View File

@ -542,8 +542,6 @@ export function InvenTreeTable<T = any>({
record: any;
index: number;
}) => {
cancelEvent(event);
if (props.onRowClick) {
// If a custom row click handler is provided, use that
props.onRowClick(record, index, event);
@ -552,6 +550,7 @@ export function InvenTreeTable<T = any>({
const pk = resolveItem(record, accessor);
if (pk) {
cancelEvent(event);
// If a model type is provided, navigate to the detail view for that model
let url = getDetailUrl(tableProps.modelType, pk);
navigateToLink(url, navigate, event);
@ -564,12 +563,14 @@ export function InvenTreeTable<T = any>({
return (
<>
{tableProps.enableFilters && (filters.length ?? 0) > 0 && (
<FilterSelectDrawer
availableFilters={filters}
tableState={tableState}
opened={filtersVisible}
onClose={() => setFiltersVisible(false)}
/>
<Boundary label="table-filter-drawer">
<FilterSelectDrawer
availableFilters={filters}
tableState={tableState}
opened={filtersVisible}
onClose={() => setFiltersVisible(false)}
/>
</Boundary>
)}
<Boundary label="inventreetable">
<Stack gap="sm">

View File

@ -1,18 +1,33 @@
import { t } from '@lingui/macro';
import { ActionIcon, Badge, Group, Stack, Text, Tooltip } from '@mantine/core';
import {
ActionIcon,
Badge,
Group,
Paper,
Stack,
Text,
Tooltip,
rem
} from '@mantine/core';
import { Dropzone } from '@mantine/dropzone';
import { notifications } from '@mantine/notifications';
import { IconExternalLink, IconFileUpload } from '@tabler/icons-react';
import {
IconExternalLink,
IconFileUpload,
IconUpload,
IconX
} from '@tabler/icons-react';
import { ReactNode, useCallback, useEffect, useMemo, useState } from 'react';
import { api } from '../../App';
import { ApiFormFieldSet } from '../../components/forms/fields/ApiFormField';
import { AttachmentLink } from '../../components/items/AttachmentLink';
import { ApiEndpoints } from '../../enums/ApiEndpoints';
import {
addAttachment,
deleteAttachment,
editAttachment
} from '../../forms/AttachmentForms';
useCreateApiFormModal,
useDeleteApiFormModal,
useEditApiFormModal
} from '../../hooks/UseForm';
import { useTable } from '../../hooks/UseTable';
import { apiUrl } from '../../states/ApiState';
import { TableColumn } from '../Column';
@ -103,49 +118,9 @@ export function AttachmentTable({
.catch((error) => {
return error;
});
}, []);
}, [url]);
// Construct row actions for the attachment table
const rowActions = useCallback(
(record: any) => {
let actions: RowAction[] = [];
if (allowEdit) {
actions.push(
RowEditAction({
onClick: () => {
editAttachment({
endpoint: endpoint,
model: model,
pk: record.pk,
attachmentType: record.attachment ? 'file' : 'link',
callback: (record: any) => {
table.updateRecord(record);
}
});
}
})
);
}
if (allowDelete) {
actions.push(
RowDeleteAction({
onClick: () => {
deleteAttachment({
endpoint: endpoint,
pk: record.pk,
callback: table.refreshTable
});
}
})
);
}
return actions;
},
[allowEdit, allowDelete]
);
const [isUploading, setIsUploading] = useState<boolean>(false);
// Callback to upload file attachment(s)
function uploadFiles(files: File[]) {
@ -154,6 +129,8 @@ export function AttachmentTable({
formData.append('attachment', file);
formData.append(model, pk.toString());
setIsUploading(true);
api
.post(url, formData)
.then((response) => {
@ -175,10 +152,76 @@ export function AttachmentTable({
color: 'red'
});
return error;
})
.finally(() => {
setIsUploading(false);
});
});
}
const [attachmentType, setAttachmentType] = useState<'attachment' | 'link'>(
'attachment'
);
const [selectedAttachment, setSelectedAttachment] = useState<
number | undefined
>(undefined);
const uploadFields: ApiFormFieldSet = useMemo(() => {
let fields: ApiFormFieldSet = {
[model]: {
value: pk,
hidden: true
},
attachment: {},
link: {},
comment: {}
};
if (attachmentType != 'link') {
delete fields['link'];
}
// Remove the 'attachment' field if we are editing an existing attachment, or uploading a link
if (attachmentType != 'attachment' || !!selectedAttachment) {
delete fields['attachment'];
}
return fields;
}, [endpoint, model, pk, attachmentType, selectedAttachment]);
const uploadAttachment = useCreateApiFormModal({
url: endpoint,
title: t`Upload Attachment`,
fields: uploadFields,
onFormSuccess: () => {
table.refreshTable();
}
});
const editAttachment = useEditApiFormModal({
url: endpoint,
pk: selectedAttachment,
title: t`Edit Attachment`,
fields: uploadFields,
onFormSuccess: (record: any) => {
if (record.pk) {
table.updateRecord(record);
} else {
table.refreshTable();
}
}
});
const deleteAttachment = useDeleteApiFormModal({
url: endpoint,
pk: selectedAttachment,
title: t`Delete Attachment`,
onFormSuccess: () => {
table.refreshTable();
}
});
const tableActions: ReactNode[] = useMemo(() => {
let actions = [];
@ -188,13 +231,9 @@ export function AttachmentTable({
<ActionIcon
radius="sm"
onClick={() => {
addAttachment({
endpoint: endpoint,
model: model,
pk: pk,
attachmentType: 'file',
callback: table.refreshTable
});
setAttachmentType('attachment');
setSelectedAttachment(undefined);
uploadAttachment.open();
}}
variant="transparent"
>
@ -208,13 +247,9 @@ export function AttachmentTable({
<ActionIcon
radius="sm"
onClick={() => {
addAttachment({
endpoint: endpoint,
model: model,
pk: pk,
attachmentType: 'link',
callback: table.refreshTable
});
setAttachmentType('link');
setSelectedAttachment(undefined);
uploadAttachment.open();
}}
variant="transparent"
>
@ -227,35 +262,93 @@ export function AttachmentTable({
return actions;
}, [allowEdit]);
return (
<Stack gap="xs">
{pk && pk > 0 && (
<InvenTreeTable
key="attachment-table"
url={url}
tableState={table}
columns={tableColumns}
props={{
noRecordsText: t`No attachments found`,
enableSelection: true,
tableActions: tableActions,
rowActions: allowEdit && allowDelete ? rowActions : undefined,
params: {
[model]: pk
// Construct row actions for the attachment table
const rowActions = useCallback(
(record: any) => {
let actions: RowAction[] = [];
if (allowEdit) {
actions.push(
RowEditAction({
onClick: () => {
setSelectedAttachment(record.pk);
editAttachment.open();
}
}}
/>
)}
{allowEdit && validPk && (
<Dropzone onDrop={uploadFiles} key="attachment-dropzone">
<Dropzone.Idle>
<Group justify="center">
<IconFileUpload size={24} />
<Text size="sm">{t`Upload attachment`}</Text>
</Group>
</Dropzone.Idle>
</Dropzone>
)}
</Stack>
})
);
}
if (allowDelete) {
actions.push(
RowDeleteAction({
onClick: () => {
setSelectedAttachment(record.pk);
deleteAttachment.open();
}
})
);
}
return actions;
},
[allowEdit, allowDelete]
);
return (
<>
{uploadAttachment.modal}
{editAttachment.modal}
{deleteAttachment.modal}
<Stack gap="xs">
{pk && pk > 0 && (
<InvenTreeTable
key="attachment-table"
url={url}
tableState={table}
columns={tableColumns}
props={{
noRecordsText: t`No attachments found`,
enableSelection: true,
tableActions: tableActions,
rowActions: allowEdit && allowDelete ? rowActions : undefined,
params: {
[model]: pk
}
}}
/>
)}
{allowEdit && validPk && (
<Paper p="md" shadow="xs" radius="md">
<Dropzone
onDrop={uploadFiles}
loading={isUploading}
key="attachment-dropzone"
>
<Group justify="center" gap="lg" mih={100}>
<Dropzone.Accept>
<IconUpload
style={{ color: 'var(--mantine-color-blue-6)' }}
stroke={1.5}
/>
</Dropzone.Accept>
<Dropzone.Reject>
<IconX
style={{ color: 'var(--mantine-color-red-6)' }}
stroke={1.5}
/>
</Dropzone.Reject>
<Dropzone.Idle>
<IconUpload
style={{ color: 'var(--mantine-color-dimmed)' }}
stroke={1.5}
/>
</Dropzone.Idle>
<Text size="sm">{t`Drag attachment file here to upload`}</Text>
</Group>
</Dropzone>
</Paper>
)}
</Stack>
</>
);
}

View File

@ -151,3 +151,11 @@ test('PUI - Pages - Part - Pricing (Purchase)', async ({ page }) => {
.waitFor();
await page.getByText('2022-04-29').waitFor();
});
test('PUI - Pages - Part - Attachments', async ({ page }) => {
await doQuickLogin(page);
await page.goto(`${baseUrl}/part/69/attachments`);
await page.waitForTimeout(5000);
});