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;
|
||||
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">
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -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);
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user