2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-06-18 04:55:44 +00:00

Single table for file attachments (#7420)

* Add basic model for handling generic attachments

* Refactor migration

* Data migration to convert old files across

* Admin updates

* Increase comment field max_length

* Adjust field name

* Remove legacy serializer classes / endpoints

* Expose new model to API

* Admin site list filters

* Remove legacy attachment models

- Add new mixin class to designate which models can have attachments

* Update data migration

- Ensure other apps are at the correct migration state beforehand

* Add migrations to remove legacy attachment tables

* Fix for "rename_attachment" callback

* Refactor model_type field

- ContentType does not allow easy API serialization

* Set allowed options for admin

* Update model verbose names

* Fix logic for file upload

* Add choices for serializer

* Add API filtering

* Fix for API filter

* Fix for attachment tables in PUI

- Still not solved permission issues

* Bump API version

* Record user when uploading attachment via API

* Refactor <AttachmentTable /> for PUI

* Display 'file_size' in PUI attachment table

* Fix company migrations

* Include permission informtion in roles API endpoint

* Read user permissions in PUI

* Simplify permission checks for <AttachmentTable />

* Automatically clean up old content types

* Cleanup PUI

* Fix typo in data migration

* Add reverse data migration

* Update unit tests

* Use InMemoryStorage for media files in test mode

* Data migration unit test

* Fix "model_type" field

- It is a required field after all

* Add permission check for serializer

* Fix permission check for CUI

* Fix PUI import

* Test python lib against specific branch

- Will be reverted once code is merged

* Revert STORAGES setting

- Might be worth looking into again

* Fix part unit test

* Fix unit test for sales order

* Use 'get_global_setting'

* Use 'get_global_setting'

* Update setting getter

* Unit tests

* Tweaks

* Revert change to settings.py

* More updates for get_global_setting

* Relax API query count requirement

* remove illegal chars and add unit tests

* Fix unit tests

* Fix frontend unit tests

* settings management updates

* Prevent db write under more conditions

* Simplify settings code

* Pop values before creating filters

* Prevent settings write under certain conditions

* Add debug msg

* Clear db on record import

* Refactor permissions checks

- Allows extension / customization of permission checks at a later date

* Unit test updates

* Prevent delete of attachment without correct permissions

* Adjust odcker.yaml

* Cleanup data migrations

* Tweak migration tests for build app

* Update data migration

- Handle case with missing data

* Prevent debug shell in TESTING mode

* Update migration dependencies

- Ensure all apps are "up to date" before removing legacy tables

* add file size test

* Update migration tests

* Revert some settings caching changes

* Fix incorrect logic in migration

* Update unit tests

* Prevent create on CURRENCY_CODES

- Seems to play havoc with bootup sequence

* Fix unit test

* Some refactoring

- Use get_global_setting

* Fix typo

* Revert change

* Add "tags" and "metadata"

* Include "tags" field in API serializer

* add "metadata" endpoint for attachments
This commit is contained in:
Oliver
2024-06-19 14:38:46 +10:00
committed by GitHub
parent b8b79b2b2d
commit 432e0c622c
111 changed files with 1549 additions and 1232 deletions

View File

@ -117,7 +117,23 @@ export function formatPriceRange(
)}`;
}
interface RenderDateOptionsInterface {
/*
* Format a file size (in bytes) into a human-readable format
*/
export function formatFileSize(size: number) {
const suffixes: string[] = ['B', 'KB', 'MB', 'GB'];
let idx = 0;
while (size > 1024 && idx < suffixes.length) {
size /= 1024;
idx++;
}
return `${size.toFixed(2)} ${suffixes[idx]}`;
}
interface FormatDateOptionsInterface {
showTime?: boolean;
showSeconds?: boolean;
}
@ -128,9 +144,9 @@ interface RenderDateOptionsInterface {
* The provided "date" variable is a string, nominally ISO format e.g. 2022-02-22
* The user-configured setting DATE_DISPLAY_FORMAT determines how the date should be displayed.
*/
export function renderDate(
export function formatDate(
date: string,
options: RenderDateOptionsInterface = {}
options: FormatDateOptionsInterface = {}
) {
if (!date) {
return '-';

View File

@ -57,7 +57,6 @@ export enum ApiEndpoints {
build_output_complete = 'build/:id/complete/',
build_output_scrap = 'build/:id/scrap-outputs/',
build_output_delete = 'build/:id/delete-outputs/',
build_order_attachment_list = 'build/attachment/',
build_line_list = 'build/line/',
bom_list = 'bom/',
@ -76,18 +75,15 @@ export enum ApiEndpoints {
category_tree = 'part/category/tree/',
category_parameter_list = 'part/category/parameters/',
related_part_list = 'part/related/',
part_attachment_list = 'part/attachment/',
part_test_template_list = 'part/test-template/',
// Company API endpoints
company_list = 'company/',
contact_list = 'company/contact/',
address_list = 'company/address/',
company_attachment_list = 'company/attachment/',
supplier_part_list = 'company/part/',
supplier_part_pricing_list = 'company/price-break/',
manufacturer_part_list = 'company/part/manufacturer/',
manufacturer_part_attachment_list = 'company/part/manufacturer/attachment/',
manufacturer_part_parameter_list = 'company/part/manufacturer/parameter/',
// Stock API endpoints
@ -96,7 +92,6 @@ export enum ApiEndpoints {
stock_location_list = 'stock/location/',
stock_location_type_list = 'stock/location-type/',
stock_location_tree = 'stock/location/tree/',
stock_attachment_list = 'stock/attachment/',
stock_test_result_list = 'stock/test/',
stock_transfer = 'stock/transfer/',
stock_remove = 'stock/remove/',
@ -115,16 +110,13 @@ export enum ApiEndpoints {
// Order API endpoints
purchase_order_list = 'order/po/',
purchase_order_line_list = 'order/po-line/',
purchase_order_attachment_list = 'order/po/attachment/',
purchase_order_receive = 'order/po/:id/receive/',
sales_order_list = 'order/so/',
sales_order_line_list = 'order/so-line/',
sales_order_attachment_list = 'order/so/attachment/',
sales_order_shipment_list = 'order/so/shipment/',
return_order_list = 'order/ro/',
return_order_attachment_list = 'order/ro/attachment/',
// Template API endpoints
label_list = 'label/template/',
@ -155,6 +147,7 @@ export enum ApiEndpoints {
machine_setting_detail = 'machine/:machine/settings/:config_type/',
// Miscellaneous API endpoints
attachment_list = 'attachment/',
error_report_list = 'error-report/',
project_code_list = 'project-code/',
custom_unit_list = 'units/',

View File

@ -56,20 +56,6 @@ function ApiFormsPlayground() {
fields: editPartFields
});
const newAttachment = useCreateApiFormModal({
url: ApiEndpoints.part_attachment_list,
title: 'Create Attachment',
fields: {
part: {},
attachment: {},
comment: {}
},
initialData: {
part: 1
},
successMessage: 'Attachment uploaded'
});
const [active, setActive] = useState(true);
const [name, setName] = useState('Hello');
@ -130,9 +116,6 @@ function ApiFormsPlayground() {
<Button onClick={() => editCategory.open()}>Edit Category</Button>
{editCategory.modal}
<Button onClick={() => newAttachment.open()}>Create Attachment</Button>
{newAttachment.modal}
<Button onClick={() => openCreatePart()}>Create Part new Modal</Button>
{createPartModal}
</Group>

View File

@ -295,11 +295,7 @@ export default function BuildDetail() {
label: t`Attachments`,
icon: <IconPaperclip />,
content: (
<AttachmentTable
endpoint={ApiEndpoints.build_order_attachment_list}
model="build"
pk={Number(id)}
/>
<AttachmentTable model_type={ModelType.build} model_id={Number(id)} />
)
},
{

View File

@ -256,9 +256,8 @@ export default function CompanyDetail(props: Readonly<CompanyDetailProps>) {
icon: <IconPaperclip />,
content: (
<AttachmentTable
endpoint={ApiEndpoints.company_attachment_list}
model="company"
pk={company.pk ?? -1}
model_type={ModelType.company}
model_id={company.pk}
/>
)
},

View File

@ -173,9 +173,8 @@ export default function ManufacturerPartDetail() {
icon: <IconPaperclip />,
content: (
<AttachmentTable
endpoint={ApiEndpoints.manufacturer_part_attachment_list}
model="manufacturer_part"
pk={manufacturerPart?.pk}
model_type={ModelType.manufacturerpart}
model_id={manufacturerPart?.pk}
/>
)
}

View File

@ -618,11 +618,7 @@ export default function PartDetail() {
label: t`Attachments`,
icon: <IconPaperclip />,
content: (
<AttachmentTable
endpoint={ApiEndpoints.part_attachment_list}
model="part"
pk={part.pk ?? -1}
/>
<AttachmentTable model_type={ModelType.part} model_id={part?.pk} />
)
},
{

View File

@ -22,7 +22,7 @@ import { DataTable } from 'mantine-datatable';
import { ReactNode, useMemo } from 'react';
import { tooltipFormatter } from '../../../components/charts/tooltipFormatter';
import { formatCurrency, renderDate } from '../../../defaults/formatters';
import { formatCurrency, formatDate } from '../../../defaults/formatters';
import { panelOptions } from '../PartPricingPanel';
interface PricingOverviewEntry {
@ -173,7 +173,7 @@ export default function PricingOverviewPanel({
{pricing?.updated && (
<Paper p="xs">
<Alert color="blue" title={t`Last Updated`}>
<Text>{renderDate(pricing.updated)}</Text>
<Text>{formatDate(pricing.updated)}</Text>
</Alert>
</Paper>
)}

View File

@ -3,7 +3,7 @@ import { BarChart } from '@mantine/charts';
import { Group, SimpleGrid, Text } from '@mantine/core';
import { ReactNode, useCallback, useMemo } from 'react';
import { formatCurrency, renderDate } from '../../../defaults/formatters';
import { formatCurrency, formatDate } from '../../../defaults/formatters';
import { ApiEndpoints } from '../../../enums/ApiEndpoints';
import { useTable } from '../../../hooks/UseTable';
import { apiUrl } from '../../../states/ApiState';
@ -40,7 +40,7 @@ export default function PurchaseHistoryPanel({
title: t`Date`,
sortable: true,
switchable: true,
render: (record: any) => renderDate(record.order_detail.complete_date)
render: (record: any) => formatDate(record.order_detail.complete_date)
},
{
accessor: 'purchase_price',

View File

@ -279,9 +279,8 @@ export default function PurchaseOrderDetail() {
icon: <IconPaperclip />,
content: (
<AttachmentTable
endpoint={ApiEndpoints.purchase_order_attachment_list}
model="order"
pk={Number(id)}
model_type={ModelType.purchaseorder}
model_id={order.pk}
/>
)
},

View File

@ -230,9 +230,8 @@ export default function ReturnOrderDetail() {
icon: <IconPaperclip />,
content: (
<AttachmentTable
endpoint={ApiEndpoints.return_order_attachment_list}
model="order"
pk={Number(id)}
model_type={ModelType.returnorder}
model_id={order.pk}
/>
)
},

View File

@ -280,9 +280,8 @@ export default function SalesOrderDetail() {
icon: <IconPaperclip />,
content: (
<AttachmentTable
endpoint={ApiEndpoints.sales_order_attachment_list}
model="order"
pk={Number(id)}
model_type={ModelType.salesorder}
model_id={order.pk}
/>
)
},

View File

@ -329,9 +329,8 @@ export default function StockDetail() {
icon: <IconPaperclip />,
content: (
<AttachmentTable
endpoint={ApiEndpoints.stock_attachment_list}
model="stock_item"
pk={Number(id)}
model_type={ModelType.stockitem}
model_id={stockitem.pk}
/>
)
},

View File

@ -2,6 +2,7 @@ import { create } from 'zustand';
import { api, setApiDefaults } from '../App';
import { ApiEndpoints } from '../enums/ApiEndpoints';
import { ModelType } from '../enums/ModelType';
import { UserPermissions, UserRoles } from '../enums/Roles';
import { clearCsrfCookie } from '../functions/auth';
import { apiUrl } from './ApiState';
@ -22,6 +23,14 @@ interface UserStateProps {
hasChangeRole: (role: UserRoles) => boolean;
hasAddRole: (role: UserRoles) => boolean;
hasViewRole: (role: UserRoles) => boolean;
checkUserPermission: (
model: ModelType,
permission: UserPermissions
) => boolean;
hasDeletePermission: (model: ModelType) => boolean;
hasChangePermission: (model: ModelType) => boolean;
hasAddPermission: (model: ModelType) => boolean;
hasViewPermission: (model: ModelType) => boolean;
isLoggedIn: () => boolean;
isStaff: () => boolean;
isSuperuser: () => boolean;
@ -113,6 +122,7 @@ export const useUserState = create<UserStateProps>((set, get) => ({
// Update user with role data
if (user) {
user.roles = response.data?.roles ?? {};
user.permissions = response.data?.permissions ?? {};
user.is_staff = response.data?.is_staff ?? false;
user.is_superuser = response.data?.is_superuser ?? false;
set({ user: user });
@ -126,21 +136,6 @@ export const useUserState = create<UserStateProps>((set, get) => ({
get().clearUserState();
});
},
checkUserRole: (role: UserRoles, permission: UserPermissions) => {
// Check if the user has the specified permission for the specified role
const user: UserProps = get().user as UserProps;
if (!user) {
return false;
}
if (user?.is_superuser) return true;
if (user?.roles === undefined) return false;
if (user?.roles[role] === undefined) return false;
if (user?.roles[role] === null) return false;
return user?.roles[role]?.includes(permission) ?? false;
},
isLoggedIn: () => {
if (!get().token) {
return false;
@ -156,6 +151,21 @@ export const useUserState = create<UserStateProps>((set, get) => ({
const user: UserProps = get().user as UserProps;
return user?.is_superuser ?? false;
},
checkUserRole: (role: UserRoles, permission: UserPermissions) => {
// Check if the user has the specified permission for the specified role
const user: UserProps = get().user as UserProps;
if (!user) {
return false;
}
if (user?.is_superuser) return true;
if (user?.roles === undefined) return false;
if (user?.roles[role] === undefined) return false;
if (user?.roles[role] === null) return false;
return user?.roles[role]?.includes(permission) ?? false;
},
hasDeleteRole: (role: UserRoles) => {
return get().checkUserRole(role, UserPermissions.delete);
},
@ -167,5 +177,33 @@ export const useUserState = create<UserStateProps>((set, get) => ({
},
hasViewRole: (role: UserRoles) => {
return get().checkUserRole(role, UserPermissions.view);
},
checkUserPermission: (model: ModelType, permission: UserPermissions) => {
// Check if the user has the specified permission for the specified model
const user: UserProps = get().user as UserProps;
if (!user) {
return false;
}
if (user?.is_superuser) return true;
if (user?.permissions === undefined) return false;
if (user?.permissions[model] === undefined) return false;
if (user?.permissions[model] === null) return false;
return user?.permissions[model]?.includes(permission) ?? false;
},
hasDeletePermission: (model: ModelType) => {
return get().checkUserPermission(model, UserPermissions.delete);
},
hasChangePermission: (model: ModelType) => {
return get().checkUserPermission(model, UserPermissions.change);
},
hasAddPermission: (model: ModelType) => {
return get().checkUserPermission(model, UserPermissions.add);
},
hasViewPermission: (model: ModelType) => {
return get().checkUserPermission(model, UserPermissions.view);
}
}));

View File

@ -23,6 +23,7 @@ export interface UserProps {
is_staff?: boolean;
is_superuser?: boolean;
roles?: Record<string, string[]>;
permissions?: Record<string, string[]>;
}
// Type interface fully defining the current server

View File

@ -9,7 +9,7 @@ import { Thumbnail } from '../components/images/Thumbnail';
import { ProgressBar } from '../components/items/ProgressBar';
import { TableStatusRenderer } from '../components/render/StatusRenderer';
import { RenderOwner } from '../components/render/User';
import { formatCurrency, renderDate } from '../defaults/formatters';
import { formatCurrency, formatDate } from '../defaults/formatters';
import { ModelType } from '../enums/ModelType';
import { resolveItem } from '../functions/conversion';
import { cancelEvent } from '../functions/events';
@ -180,7 +180,7 @@ export function DateColumn(props: TableColumnProps): TableColumn {
title: t`Date`,
switchable: true,
render: (record: any) =>
renderDate(resolveItem(record, props.accessor ?? 'date')),
formatDate(resolveItem(record, props.accessor ?? 'date')),
...props
};
}

View File

@ -14,7 +14,9 @@ import { api } from '../../App';
import { ActionButton } from '../../components/buttons/ActionButton';
import { ApiFormFieldSet } from '../../components/forms/fields/ApiFormField';
import { AttachmentLink } from '../../components/items/AttachmentLink';
import { formatFileSize } from '../../defaults/formatters';
import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType';
import {
useCreateApiFormModal,
useDeleteApiFormModal,
@ -22,7 +24,9 @@ import {
} from '../../hooks/UseForm';
import { useTable } from '../../hooks/UseTable';
import { apiUrl } from '../../states/ApiState';
import { useUserState } from '../../states/UserState';
import { TableColumn } from '../Column';
import { TableFilter } from '../Filter';
import { InvenTreeTable } from '../InvenTreeTable';
import { RowAction, RowDeleteAction, RowEditAction } from '../RowActions';
@ -36,7 +40,7 @@ function attachmentTableColumns(): TableColumn[] {
sortable: false,
switchable: false,
noWrap: true,
render: function (record: any) {
render: (record: any) => {
if (record.attachment) {
return <AttachmentLink attachment={record.attachment} />;
} else if (record.link) {
@ -50,7 +54,7 @@ function attachmentTableColumns(): TableColumn[] {
accessor: 'comment',
sortable: false,
render: function (record: any) {
render: (record: any) => {
return record.comment;
}
},
@ -58,7 +62,7 @@ function attachmentTableColumns(): TableColumn[] {
accessor: 'upload_date',
sortable: true,
render: function (record: any) {
render: (record: any) => {
return (
<Group justify="space-between">
<Text>{record.upload_date}</Text>
@ -68,6 +72,18 @@ function attachmentTableColumns(): TableColumn[] {
</Group>
);
}
},
{
accessor: 'file_size',
sortable: true,
switchable: true,
render: (record: any) => {
if (!record.attachment) {
return '-';
} else {
return formatFileSize(record.file_size);
}
}
}
];
}
@ -76,50 +92,34 @@ function attachmentTableColumns(): TableColumn[] {
* Construct a table for displaying uploaded attachments
*/
export function AttachmentTable({
endpoint,
model,
pk
model_type,
model_id
}: {
endpoint: ApiEndpoints;
pk: number;
model: string;
model_type: ModelType;
model_id: number;
}): ReactNode {
const table = useTable(`${model}-attachments`);
const user = useUserState();
const table = useTable(`${model_type}-attachments`);
const tableColumns = useMemo(() => attachmentTableColumns(), []);
const [allowEdit, setAllowEdit] = useState<boolean>(false);
const [allowDelete, setAllowDelete] = useState<boolean>(false);
const url = apiUrl(ApiEndpoints.attachment_list);
const url = useMemo(() => apiUrl(endpoint), [endpoint]);
const validPk = useMemo(() => pk > 0, [pk]);
// Determine which permissions are available for this URL
useEffect(() => {
api
.options(url)
.then((response) => {
let actions: any = response.data?.actions ?? {};
setAllowEdit('POST' in actions);
setAllowDelete('DELETE' in actions);
return response;
})
.catch((error) => {
return error;
});
}, [url]);
const validPk = useMemo(() => model_id > 0, [model_id]);
const [isUploading, setIsUploading] = useState<boolean>(false);
const allowDragAndDrop: boolean = useMemo(() => {
return user.hasAddPermission(model_type);
}, [user, model_type]);
// Callback to upload file attachment(s)
function uploadFiles(files: File[]) {
files.forEach((file) => {
let formData = new FormData();
formData.append('attachment', file);
formData.append(model, pk.toString());
formData.append('model_type', model_type);
formData.append('model_id', model_id.toString());
setIsUploading(true);
@ -161,8 +161,12 @@ export function AttachmentTable({
const uploadFields: ApiFormFieldSet = useMemo(() => {
let fields: ApiFormFieldSet = {
[model]: {
value: pk,
model_type: {
value: model_type,
hidden: true
},
model_id: {
value: model_id,
hidden: true
},
attachment: {},
@ -180,10 +184,10 @@ export function AttachmentTable({
}
return fields;
}, [endpoint, model, pk, attachmentType, selectedAttachment]);
}, [model_type, model_id, attachmentType, selectedAttachment]);
const uploadAttachment = useCreateApiFormModal({
url: endpoint,
url: url,
title: t`Upload Attachment`,
fields: uploadFields,
onFormSuccess: () => {
@ -192,7 +196,7 @@ export function AttachmentTable({
});
const editAttachment = useEditApiFormModal({
url: endpoint,
url: url,
pk: selectedAttachment,
title: t`Edit Attachment`,
fields: uploadFields,
@ -206,7 +210,7 @@ export function AttachmentTable({
});
const deleteAttachment = useDeleteApiFormModal({
url: endpoint,
url: url,
pk: selectedAttachment,
title: t`Delete Attachment`,
onFormSuccess: () => {
@ -214,12 +218,27 @@ export function AttachmentTable({
}
});
const tableFilters: TableFilter[] = useMemo(() => {
return [
{
name: 'is_link',
label: t`Is Link`,
description: t`Show link attachments`
},
{
name: 'is_file',
label: t`Is File`,
description: t`Show file attachments`
}
];
}, []);
const tableActions: ReactNode[] = useMemo(() => {
return [
<ActionButton
key="add-attachment"
tooltip={t`Add attachment`}
hidden={!allowEdit}
hidden={!user.hasAddPermission(model_type)}
icon={<IconFileUpload />}
onClick={() => {
setAttachmentType('attachment');
@ -230,7 +249,7 @@ export function AttachmentTable({
<ActionButton
key="add-external-link"
tooltip={t`Add external link`}
hidden={!allowEdit}
hidden={!user.hasAddPermission(model_type)}
icon={<IconExternalLink />}
onClick={() => {
setAttachmentType('link');
@ -239,38 +258,29 @@ export function AttachmentTable({
}}
/>
];
}, [allowEdit]);
}, [user, model_type]);
// 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();
}
})
);
}
if (allowDelete) {
actions.push(
RowDeleteAction({
onClick: () => {
setSelectedAttachment(record.pk);
deleteAttachment.open();
}
})
);
}
return actions;
return [
RowEditAction({
hidden: !user.hasChangePermission(model_type),
onClick: () => {
setSelectedAttachment(record.pk);
editAttachment.open();
}
}),
RowDeleteAction({
hidden: !user.hasDeletePermission(model_type),
onClick: () => {
setSelectedAttachment(record.pk);
deleteAttachment.open();
}
})
];
},
[allowEdit, allowDelete]
[user, model_type]
);
return (
@ -279,7 +289,7 @@ export function AttachmentTable({
{editAttachment.modal}
{deleteAttachment.modal}
<Stack gap="xs">
{pk && pk > 0 && (
{validPk && (
<InvenTreeTable
key="attachment-table"
url={url}
@ -289,14 +299,16 @@ export function AttachmentTable({
noRecordsText: t`No attachments found`,
enableSelection: true,
tableActions: tableActions,
rowActions: allowEdit && allowDelete ? rowActions : undefined,
tableFilters: tableFilters,
rowActions: rowActions,
params: {
[model]: pk
model_type: model_type,
model_id: model_id
}
}}
/>
)}
{allowEdit && validPk && (
{allowDragAndDrop && validPk && (
<Paper p="md" shadow="xs" radius="md">
<Dropzone
onDrop={uploadFiles}

View File

@ -16,7 +16,7 @@ import { PassFailButton } from '../../components/buttons/YesNoButton';
import { ApiFormFieldSet } from '../../components/forms/fields/ApiFormField';
import { AttachmentLink } from '../../components/items/AttachmentLink';
import { RenderUser } from '../../components/render/User';
import { renderDate } from '../../defaults/formatters';
import { formatDate } from '../../defaults/formatters';
import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { UserRoles } from '../../enums/Roles';
import { useTestResultFields } from '../../forms/StockForms';
@ -207,7 +207,7 @@ export default function StockItemTestResultTable({
render: (record: any) => {
return (
<Group justify="space-between">
{renderDate(record.started_datetime, {
{formatDate(record.started_datetime, {
showTime: true,
showSeconds: true
})}
@ -222,7 +222,7 @@ export default function StockItemTestResultTable({
render: (record: any) => {
return (
<Group justify="space-between">
{renderDate(record.finished_datetime, {
{formatDate(record.finished_datetime, {
showTime: true,
showSeconds: true
})}

View File

@ -56,12 +56,6 @@ test('PUI - Pages - Index - Playground', async ({ page }) => {
.getByRole('button', { name: 'Cancel' })
.click();
// Create Attachment
await page.getByRole('button', { name: 'Create Attachment' }).click();
await page.getByLabel('Attachment *').waitFor();
await page.getByRole('button', { name: 'Cancel' }).click();
// TODO: actually create an attachment
// Create Part new Modal
await page.getByRole('button', { name: 'Create Part new Modal' }).click();
await page.locator('.css-fehojk-Input2').first().click();