mirror of
https://github.com/inventree/InvenTree.git
synced 2025-06-16 12:05:53 +00:00
Bulk update mixin (#9313)
* Refactor BulkDeleteMixin * Implement BulkUpdateMixin class * Refactor NotificationsTable - Use common bulkdelete operation * Update successMessage * Update metadata constructs * Add bulk-edit support for PartList endpoint * Implement set-category for part table * Cleanup old endpoint * Improve form error handling * Simplify translated text * Add playwright tests * Bump API version * Fix unit tests * Further test updates
This commit is contained in:
@ -505,7 +505,22 @@ export function ApiForm({
|
||||
case 400:
|
||||
// Data validation errors
|
||||
const _nonFieldErrors: string[] = [];
|
||||
|
||||
const processErrors = (errors: any, _path?: string) => {
|
||||
// Handle an array of errors
|
||||
if (Array.isArray(errors)) {
|
||||
errors.forEach((error: any) => {
|
||||
_nonFieldErrors.push(error.toString());
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle simple string
|
||||
if (typeof errors === 'string') {
|
||||
_nonFieldErrors.push(errors);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const [k, v] of Object.entries(errors)) {
|
||||
const path = _path ? `${_path}.${k}` : k;
|
||||
|
||||
@ -513,10 +528,8 @@ export function ApiForm({
|
||||
const field = fields[k];
|
||||
const valid = field && !field.hidden;
|
||||
|
||||
if (!valid || k === 'non_field_errors' || k === '__all__') {
|
||||
if (Array.isArray(v)) {
|
||||
_nonFieldErrors.push(...v);
|
||||
}
|
||||
if (!valid || k == 'non_field_errors' || k == '__all__') {
|
||||
processErrors(v);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -88,7 +88,7 @@ export function useCreateApiFormModal(props: ApiFormModalProps) {
|
||||
props.successMessage === null
|
||||
? null
|
||||
: (props.successMessage ?? t`Item Created`),
|
||||
method: 'POST'
|
||||
method: props.method ?? 'POST'
|
||||
}),
|
||||
[props]
|
||||
);
|
||||
@ -116,6 +116,41 @@ export function useEditApiFormModal(props: ApiFormModalProps) {
|
||||
return useApiFormModal(editProps);
|
||||
}
|
||||
|
||||
interface BulkEditApiFormModalProps extends ApiFormModalProps {
|
||||
items: number[];
|
||||
}
|
||||
|
||||
export function useBulkEditApiFormModal({
|
||||
items,
|
||||
...props
|
||||
}: BulkEditApiFormModalProps) {
|
||||
const bulkEditProps = useMemo<ApiFormModalProps>(
|
||||
() => ({
|
||||
...props,
|
||||
method: 'PATCH',
|
||||
submitText: props.submitText ?? t`Update`,
|
||||
successMessage:
|
||||
props.successMessage === null
|
||||
? null
|
||||
: (props.successMessage ?? t`Items Updated`),
|
||||
preFormContent: props.preFormContent ?? (
|
||||
<Alert color={'blue'}>{t`Update multiple items`}</Alert>
|
||||
),
|
||||
fields: {
|
||||
...props.fields,
|
||||
items: {
|
||||
hidden: true,
|
||||
field_type: 'number',
|
||||
value: items
|
||||
}
|
||||
}
|
||||
}),
|
||||
[props, items]
|
||||
);
|
||||
|
||||
return useApiFormModal(bulkEditProps);
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a modal form to delete a model instance
|
||||
*/
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { t } from '@lingui/macro';
|
||||
import { Stack } from '@mantine/core';
|
||||
import { modals } from '@mantine/modals';
|
||||
import {
|
||||
IconBellCheck,
|
||||
IconBellExclamation,
|
||||
@ -39,26 +38,6 @@ export default function NotificationsPage() {
|
||||
.catch((_error) => {});
|
||||
}, []);
|
||||
|
||||
const deleteNotifications = useCallback(() => {
|
||||
modals.openConfirmModal({
|
||||
title: t`Delete Notifications`,
|
||||
onConfirm: () => {
|
||||
api
|
||||
.delete(apiUrl(ApiEndpoints.notifications_list), {
|
||||
data: {
|
||||
filters: {
|
||||
read: true
|
||||
}
|
||||
}
|
||||
})
|
||||
.then((_response) => {
|
||||
readTable.refreshTable();
|
||||
})
|
||||
.catch((_error) => {});
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
const notificationPanels = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
@ -139,14 +118,7 @@ export default function NotificationsPage() {
|
||||
}
|
||||
}
|
||||
]}
|
||||
tableActions={[
|
||||
<ActionButton
|
||||
color='red'
|
||||
icon={<IconTrash />}
|
||||
tooltip={t`Delete notifications`}
|
||||
onClick={deleteNotifications}
|
||||
/>
|
||||
]}
|
||||
tableActions={[]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ import {
|
||||
} from '@mantine/core';
|
||||
import {
|
||||
IconBarcode,
|
||||
IconExclamationCircle,
|
||||
IconFilter,
|
||||
IconRefresh,
|
||||
IconTrash
|
||||
@ -16,6 +17,7 @@ import {
|
||||
import { useMemo, useState } from 'react';
|
||||
import { Fragment } from 'react/jsx-runtime';
|
||||
|
||||
import { showNotification } from '@mantine/notifications';
|
||||
import { Boundary } from '../components/Boundary';
|
||||
import { ActionButton } from '../components/buttons/ActionButton';
|
||||
import { ButtonMenu } from '../components/buttons/ButtonMenu';
|
||||
@ -112,6 +114,17 @@ export default function InvenTreeTableHeader({
|
||||
hidden: true
|
||||
}
|
||||
},
|
||||
successMessage: t`Items deleted`,
|
||||
onFormError: (response) => {
|
||||
showNotification({
|
||||
id: 'bulk-delete-error',
|
||||
title: t`Error`,
|
||||
message: t`Failed to delete items`,
|
||||
color: 'red',
|
||||
icon: <IconExclamationCircle />,
|
||||
autoClose: 5000
|
||||
});
|
||||
},
|
||||
onFormSuccess: () => {
|
||||
tableState.clearSelectedRecords();
|
||||
tableState.refreshTable();
|
||||
|
@ -52,6 +52,7 @@ export function NotificationTable({
|
||||
rowActions: actions,
|
||||
tableActions: tableActions,
|
||||
enableSelection: true,
|
||||
enableBulkDelete: true,
|
||||
params: params
|
||||
}}
|
||||
/>
|
||||
|
@ -12,7 +12,10 @@ import { ModelType } from '../../enums/ModelType';
|
||||
import { UserRoles } from '../../enums/Roles';
|
||||
import { usePartFields } from '../../forms/PartForms';
|
||||
import { InvenTreeIcon } from '../../functions/icons';
|
||||
import { useCreateApiFormModal } from '../../hooks/UseForm';
|
||||
import {
|
||||
useBulkEditApiFormModal,
|
||||
useCreateApiFormModal
|
||||
} from '../../hooks/UseForm';
|
||||
import { useTable } from '../../hooks/UseTable';
|
||||
import { apiUrl } from '../../states/ApiState';
|
||||
import { useUserState } from '../../states/UserState';
|
||||
@ -341,6 +344,16 @@ export function PartListTable({
|
||||
modelType: ModelType.part
|
||||
});
|
||||
|
||||
const setCategory = useBulkEditApiFormModal({
|
||||
url: ApiEndpoints.part_list,
|
||||
items: table.selectedIds,
|
||||
title: t`Set Category`,
|
||||
fields: {
|
||||
category: {}
|
||||
},
|
||||
onFormSuccess: table.refreshTable
|
||||
});
|
||||
|
||||
const orderPartsWizard = OrderPartsWizard({ parts: table.selectedRecords });
|
||||
|
||||
const tableActions = useMemo(() => {
|
||||
@ -350,10 +363,21 @@ export function PartListTable({
|
||||
icon={<InvenTreeIcon icon='part' />}
|
||||
disabled={!table.hasSelectedRecords}
|
||||
actions={[
|
||||
{
|
||||
name: t`Set Category`,
|
||||
icon: <InvenTreeIcon icon='category' />,
|
||||
tooltip: t`Set category for selected parts`,
|
||||
hidden: !user.hasChangeRole(UserRoles.part),
|
||||
disabled: !table.hasSelectedRecords,
|
||||
onClick: () => {
|
||||
setCategory.open();
|
||||
}
|
||||
},
|
||||
{
|
||||
name: t`Order Parts`,
|
||||
icon: <IconShoppingCart color='blue' />,
|
||||
tooltip: t`Order selected parts`,
|
||||
hidden: !user.hasAddRole(UserRoles.purchase_order),
|
||||
onClick: () => {
|
||||
orderPartsWizard.openWizard();
|
||||
}
|
||||
@ -372,6 +396,7 @@ export function PartListTable({
|
||||
return (
|
||||
<>
|
||||
{newPart.modal}
|
||||
{setCategory.modal}
|
||||
{orderPartsWizard.wizard}
|
||||
<InvenTreeTable
|
||||
url={apiUrl(ApiEndpoints.part_list)}
|
||||
|
@ -419,3 +419,23 @@ test('Parts - Revision', async ({ page }) => {
|
||||
await page.waitForURL('**/platform/part/101/**');
|
||||
await page.getByText('Select Part Revision').waitFor();
|
||||
});
|
||||
|
||||
test('Parts - Bulk Edit', async ({ page }) => {
|
||||
await doQuickLogin(page);
|
||||
|
||||
await navigate(page, 'part/category/index/parts');
|
||||
|
||||
// Edit the category for multiple parts
|
||||
await page.getByLabel('Select record 1', { exact: true }).click();
|
||||
await page.getByLabel('Select record 2', { exact: true }).click();
|
||||
await page.getByLabel('action-menu-part-actions').click();
|
||||
await page.getByLabel('action-menu-part-actions-set-category').click();
|
||||
await page.getByLabel('related-field-category').fill('rnitu');
|
||||
await page
|
||||
.getByRole('option', { name: '- Furniture/Chairs' })
|
||||
.getByRole('paragraph')
|
||||
.click();
|
||||
|
||||
await page.getByRole('button', { name: 'Update' }).click();
|
||||
await page.getByText('Items Updated').waitFor();
|
||||
});
|
||||
|
Reference in New Issue
Block a user