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

View File

@ -1,18 +1,33 @@
import { t } from '@lingui/macro'; 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 { Dropzone } from '@mantine/dropzone';
import { notifications } from '@mantine/notifications'; 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 { ReactNode, useCallback, useEffect, useMemo, useState } from 'react';
import { api } from '../../App'; import { api } from '../../App';
import { ApiFormFieldSet } from '../../components/forms/fields/ApiFormField';
import { AttachmentLink } from '../../components/items/AttachmentLink'; import { AttachmentLink } from '../../components/items/AttachmentLink';
import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { import {
addAttachment, useCreateApiFormModal,
deleteAttachment, useDeleteApiFormModal,
editAttachment useEditApiFormModal
} from '../../forms/AttachmentForms'; } from '../../hooks/UseForm';
import { useTable } from '../../hooks/UseTable'; import { useTable } from '../../hooks/UseTable';
import { apiUrl } from '../../states/ApiState'; import { apiUrl } from '../../states/ApiState';
import { TableColumn } from '../Column'; import { TableColumn } from '../Column';
@ -103,49 +118,9 @@ export function AttachmentTable({
.catch((error) => { .catch((error) => {
return error; return error;
}); });
}, []); }, [url]);
// Construct row actions for the attachment table const [isUploading, setIsUploading] = useState<boolean>(false);
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]
);
// Callback to upload file attachment(s) // Callback to upload file attachment(s)
function uploadFiles(files: File[]) { function uploadFiles(files: File[]) {
@ -154,6 +129,8 @@ export function AttachmentTable({
formData.append('attachment', file); formData.append('attachment', file);
formData.append(model, pk.toString()); formData.append(model, pk.toString());
setIsUploading(true);
api api
.post(url, formData) .post(url, formData)
.then((response) => { .then((response) => {
@ -175,10 +152,76 @@ export function AttachmentTable({
color: 'red' color: 'red'
}); });
return error; 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(() => { const tableActions: ReactNode[] = useMemo(() => {
let actions = []; let actions = [];
@ -188,13 +231,9 @@ export function AttachmentTable({
<ActionIcon <ActionIcon
radius="sm" radius="sm"
onClick={() => { onClick={() => {
addAttachment({ setAttachmentType('attachment');
endpoint: endpoint, setSelectedAttachment(undefined);
model: model, uploadAttachment.open();
pk: pk,
attachmentType: 'file',
callback: table.refreshTable
});
}} }}
variant="transparent" variant="transparent"
> >
@ -208,13 +247,9 @@ export function AttachmentTable({
<ActionIcon <ActionIcon
radius="sm" radius="sm"
onClick={() => { onClick={() => {
addAttachment({ setAttachmentType('link');
endpoint: endpoint, setSelectedAttachment(undefined);
model: model, uploadAttachment.open();
pk: pk,
attachmentType: 'link',
callback: table.refreshTable
});
}} }}
variant="transparent" variant="transparent"
> >
@ -227,35 +262,93 @@ export function AttachmentTable({
return actions; return actions;
}, [allowEdit]); }, [allowEdit]);
return ( // Construct row actions for the attachment table
<Stack gap="xs"> const rowActions = useCallback(
{pk && pk > 0 && ( (record: any) => {
<InvenTreeTable let actions: RowAction[] = [];
key="attachment-table"
url={url} if (allowEdit) {
tableState={table} actions.push(
columns={tableColumns} RowEditAction({
props={{ onClick: () => {
noRecordsText: t`No attachments found`, setSelectedAttachment(record.pk);
enableSelection: true, editAttachment.open();
tableActions: tableActions,
rowActions: allowEdit && allowDelete ? rowActions : undefined,
params: {
[model]: pk
} }
}} })
/> );
)} }
{allowEdit && validPk && (
<Dropzone onDrop={uploadFiles} key="attachment-dropzone"> if (allowDelete) {
<Dropzone.Idle> actions.push(
<Group justify="center"> RowDeleteAction({
<IconFileUpload size={24} /> onClick: () => {
<Text size="sm">{t`Upload attachment`}</Text> setSelectedAttachment(record.pk);
</Group> deleteAttachment.open();
</Dropzone.Idle> }
</Dropzone> })
)} );
</Stack> }
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(); .waitFor();
await page.getByText('2022-04-29').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);
});