mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
[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:
parent
e8f8f3b3ec
commit
2a83c19208
@ -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?`
|
|
||||||
});
|
|
||||||
}
|
|
@ -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">
|
||||||
|
@ -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>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user