Skip to content

Commit 9dc7542

Browse files
authored
Add bulk delete in list and relationships views (#6394)
* start add bulk delete * udpate fragment * add more function for 1 request with multiple mutations * improve modal description * create one requets with a mutation for each node to delete * update types and fix mutation to include objects kind * start fix errors handle in delete mutations * update types * 🔧 * add checkbox in skeleton * add fragment * add testid * add test for bulk delete * fix test action * rename file * fix refetch * lint * fix test * improve function * add on settled * fix string function * fix typo and remove unused param * add description * update types * remove util * update alias key * update type * remove comment * add check to deleted objects
1 parent 43e4cdb commit 9dc7542

File tree

12 files changed

+307
-21
lines changed

12 files changed

+307
-21
lines changed

changelog/+selection-table.added.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@ We added row selection functionality to the table view. Users can now select mul
22

33
- add them to groups via the new "Add to groups" button.
44
- remove them from groups via the new "Remove from groups" button.
5+
- delete them via the "Delete" button
56
- dissociate selected rows on relationship list view via the new "Dissociate" button.

changelog/2932.added.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Add bulk delete for objects and relationships
2+
Improve object list loader
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import graphqlClient from "@/shared/api/graphql/graphqlClientApollo";
2+
import { BranchContextParams } from "@/shared/api/types";
3+
import { gql } from "@apollo/client";
4+
import { jsonToGraphQLQuery } from "json-to-graphql-query";
5+
6+
export interface ObjectParam {
7+
id: string;
8+
kind: string;
9+
}
10+
11+
const getDeleteObjectsQuery = (objects: Array<ObjectParam>) => {
12+
// Creates dynamic mutations with aliases
13+
const mutations = objects.reduce((acc, { id, kind }, index) => {
14+
return {
15+
...acc,
16+
[`delete_${kind}_${index}`]: {
17+
__aliasFor: `${kind}Delete`,
18+
__args: {
19+
data: { id },
20+
},
21+
ok: true,
22+
},
23+
};
24+
}, {});
25+
26+
const query = {
27+
mutation: mutations,
28+
};
29+
30+
return jsonToGraphQLQuery(query);
31+
};
32+
33+
export interface DeleteObjectsFromApiParams extends BranchContextParams {
34+
objects: Array<ObjectParam>;
35+
context: Record<string, any>;
36+
}
37+
38+
export function deleteObjectsFromApi({ objects, branchName, context }: DeleteObjectsFromApiParams) {
39+
return graphqlClient.mutate({
40+
mutation: gql(getDeleteObjectsQuery(objects)),
41+
context: {
42+
branch: branchName,
43+
...context,
44+
},
45+
});
46+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { useCurrentBranch } from "@/entities/branches/ui/branches-provider";
2+
import { DeleteObjectsParams } from "@/entities/nodes/object/api/delete-objects-from-api";
3+
import { datetimeAtom } from "@/shared/stores/time.atom";
4+
import { DefaultContext } from "@apollo/client";
5+
import { useMutation } from "@tanstack/react-query";
6+
import { useAtomValue } from "jotai";
7+
import { deleteObjects } from "./delete-objects";
8+
9+
interface DeleteObjectsProps {
10+
context?: DefaultContext;
11+
onSuccess?: () => void;
12+
onError?: () => void;
13+
onSettled?: () => void;
14+
}
15+
16+
export function useDeleteObjects({ context, onSuccess, onError, onSettled }: DeleteObjectsProps) {
17+
const { currentBranch } = useCurrentBranch();
18+
const timeMachineDate = useAtomValue(datetimeAtom);
19+
20+
return useMutation({
21+
mutationFn: async ({ objects }: DeleteObjectsParams) => {
22+
await deleteObjects({
23+
objects,
24+
branchName: currentBranch.name,
25+
atDate: timeMachineDate,
26+
context,
27+
});
28+
29+
return { objects };
30+
},
31+
onSuccess,
32+
onError,
33+
onSettled,
34+
});
35+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import {
2+
DeleteObjectsFromApiParams,
3+
deleteObjectsFromApi,
4+
} from "@/entities/nodes/object/api/delete-objects-from-api";
5+
6+
export type DeleteObject = (data: DeleteObjectsFromApiParams) => Promise<void>;
7+
8+
export const deleteObjects: DeleteObject = async (data) => {
9+
await deleteObjectsFromApi(data);
10+
};

frontend/app/src/entities/nodes/object/ui/object-table/cells/table-identifier-cell.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,13 @@ export function TableIdentifierCell({
2222
const { isAuthenticated } = useAuth();
2323
return (
2424
<TableCell className="sticky left-0 bg-white" data-testid="identifier-cell">
25-
{isAuthenticated && <Checkbox isSelected={isSelected} onChange={onSelectionChange} />}
25+
{isAuthenticated && (
26+
<Checkbox
27+
isSelected={isSelected}
28+
onChange={onSelectionChange}
29+
data-testid="identifier-checkbox-cell"
30+
/>
31+
)}
2632

2733
<LinkButton
2834
variant="ghost"
Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { Checkbox } from "@/shared/components/aria/checkbox";
12
import { Skeleton } from "@/shared/components/skeleton";
23
import { TableCell } from "@/shared/components/table/table-cell";
34
import { classNames } from "@/shared/utils/common";
@@ -8,16 +9,21 @@ export interface ObjectsTableSkeletonProps {
89
}
910

1011
export function ObjectTableSkeleton({ headerCount }: ObjectsTableSkeletonProps) {
11-
return [...Array(20)].map((_, rowIndex) => (
12-
<React.Fragment key={`skeleton-row-${rowIndex}`}>
13-
{[...Array(headerCount)].map((_, colIndex) => (
14-
<TableCell
15-
key={`skeleton-${rowIndex}-${colIndex}`}
16-
className={classNames(colIndex === 0 && "sticky left-0")}
17-
>
18-
<Skeleton className="h-4 w-full" />
19-
</TableCell>
20-
))}
21-
</React.Fragment>
22-
));
12+
return [...Array(20)].map((_, rowIndex) => {
13+
return (
14+
<React.Fragment key={`skeleton-row-${rowIndex}`}>
15+
{[...Array(headerCount)].map((_, colIndex) => {
16+
return (
17+
<TableCell
18+
key={`skeleton-${rowIndex}-${colIndex}`}
19+
className={classNames(colIndex === 0 && "sticky left-0")}
20+
>
21+
{colIndex === 0 && <Checkbox isDisabled className={"mr-4"} />}
22+
<Skeleton className="h-4 w-full" />
23+
</TableCell>
24+
);
25+
})}
26+
</React.Fragment>
27+
);
28+
});
2329
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { useDeleteObjects } from "@/entities/nodes/object/domain/delete-objects.mutation";
2+
import { NodeObject } from "@/entities/nodes/types";
3+
import { queryClient } from "@/shared/api/rest/client";
4+
import ModalDelete from "@/shared/components/modals/modal-delete";
5+
import { ALERT_TYPES, Alert } from "@/shared/components/ui/alert";
6+
import { pluralize } from "@/shared/utils/string";
7+
import { toast } from "react-toastify";
8+
9+
export interface DeleteObjectModalProps {
10+
selectedRows: Array<NodeObject>;
11+
open: boolean;
12+
setOpen: (b: boolean) => void;
13+
}
14+
15+
export function DeleteObjectsModal({ selectedRows, open, setOpen }: DeleteObjectModalProps) {
16+
const { mutate, isPending } = useDeleteObjects({
17+
context: {
18+
processErrorMessage: (message: string) => {
19+
const regex = new RegExp(/Cannot delete \w* \'(\w|-)*\'\./g);
20+
const matches = message.match(regex);
21+
22+
const messageDisplay = matches?.[0];
23+
24+
toast(<Alert type={ALERT_TYPES.ERROR} message={messageDisplay} />);
25+
},
26+
},
27+
onSuccess: () => {
28+
setOpen(false);
29+
30+
toast(<Alert type={ALERT_TYPES.SUCCESS} message={"Objects deleted!"} />);
31+
},
32+
onSettled: () => {
33+
queryClient.invalidateQueries({
34+
predicate: (query) => query.queryKey.includes("objects"),
35+
});
36+
},
37+
});
38+
39+
const handleRemoveObjects = async () => {
40+
const objects = selectedRows.map(({ id, __typename }) => {
41+
return { id, kind: __typename };
42+
});
43+
44+
mutate({
45+
objects,
46+
});
47+
};
48+
49+
return (
50+
<ModalDelete
51+
title="Delete"
52+
description={
53+
<>
54+
Are you sure you want to delete{" "}
55+
<strong>{pluralize(selectedRows.length, "object")}</strong> ?
56+
</>
57+
}
58+
open={open}
59+
setOpen={setOpen}
60+
onCancel={() => setOpen(false)}
61+
onDelete={handleRemoveObjects}
62+
isLoading={isPending}
63+
/>
64+
);
65+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { ToolbarButton } from "@/entities/nodes/object/ui/object-table/toolbar/toolbar-button";
2+
import { NodeObject } from "@/entities/nodes/types";
3+
import { Icon } from "@iconify-icon/react";
4+
import React from "react";
5+
import { DeleteObjectsModal } from "./delete-objects-modal";
6+
7+
export interface ToolbarDeleteObjectProps {
8+
selectedRows: Array<NodeObject>;
9+
}
10+
11+
export function ToolbarDeleteObject({ selectedRows }: ToolbarDeleteObjectProps) {
12+
const [isOpen, setIsOpen] = React.useState(false);
13+
14+
return (
15+
<>
16+
<ToolbarButton variant="danger" onPress={() => setIsOpen((prev) => !prev)}>
17+
<Icon icon="mdi:trash-can-outline" />
18+
Delete
19+
</ToolbarButton>
20+
21+
<DeleteObjectsModal selectedRows={selectedRows} open={isOpen} setOpen={setIsOpen} />
22+
</>
23+
);
24+
}

frontend/app/src/entities/nodes/object/ui/object-table/toolbar/object-table-toolbar.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { ToolbarAddToGroupsAction } from "@/entities/nodes/object/ui/object-table/toolbar/actions/groups/toolbar-add-to-groups-action";
22
import { ToolBarRemoveFromGroupsAction } from "@/entities/nodes/object/ui/object-table/toolbar/actions/groups/toolbar-remove-from-groups-action";
3+
import { ToolbarDeleteObject } from "@/entities/nodes/object/ui/object-table/toolbar/actions/objects/toolbar-delete-action";
34
import { ToolbarButton } from "@/entities/nodes/object/ui/object-table/toolbar/toolbar-button";
45
import { ToolbarDivider } from "@/entities/nodes/object/ui/object-table/toolbar/toolbar-divider";
56
import { NodeObject } from "@/entities/nodes/types";
@@ -38,6 +39,7 @@ export function ObjectTableToolbar({
3839

3940
<ToolbarAddToGroupsAction selectedRows={selectedRows} />
4041
<ToolBarRemoveFromGroupsAction selectedRows={selectedRows} />
42+
<ToolbarDeleteObject selectedRows={selectedRows} />
4143

4244
{renderMore && (
4345
<>

0 commit comments

Comments
 (0)