Skip to content

Commit 5de9bfd

Browse files
authored
Update action buttons on details views (#4362) (#4428)
* start extract action buttons and modals for object details * add relationship action button * fix test id * fix test id * fix refetch * udpate confirm label in modal delete * fix refresh * revert rename * fix test * fix refetch * improve on delete * improve delete label * add fragment * wait for ondelete * mark tests as slow
1 parent 90c0540 commit 5de9bfd

File tree

14 files changed

+386
-273
lines changed

14 files changed

+386
-273
lines changed

changelog/4362.added.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Update action buttons in details view and relatisonhips views
2+
* in the details view, we can edit / delete the object and manage its groups
3+
* in the relationships views, we can add new relationships (it replaces the "+" button at the bottom)
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import React, { Fragment, useState } from "react";
2+
import ModalDelete from "./modal-delete";
3+
import { deleteObject } from "@/graphql/mutations/objects/deleteObject";
4+
import { toast } from "react-toastify";
5+
import { Alert, ALERT_TYPES } from "../ui/alert";
6+
import graphqlClient from "@/graphql/graphqlClientApollo";
7+
import { ACCOUNT_TOKEN_OBJECT } from "@/config/constants";
8+
import { stringifyWithoutQuotes } from "@/utils/string";
9+
import { gql } from "@apollo/client";
10+
import { useAtomValue } from "jotai";
11+
import { currentBranchAtom } from "@/state/atoms/branches.atom";
12+
import { datetimeAtom } from "@/state/atoms/time.atom";
13+
import { useParams } from "react-router-dom";
14+
15+
interface iProps {
16+
label?: string | null;
17+
rowToDelete: any;
18+
isLoading?: boolean;
19+
open: boolean;
20+
close: () => void;
21+
onDelete?: () => void;
22+
}
23+
24+
export default function ModalDeleteObject({ label, rowToDelete, open, close, onDelete }: iProps) {
25+
const [isLoading, setIsLoading] = useState<boolean>(false);
26+
const branch = useAtomValue(currentBranchAtom);
27+
const date = useAtomValue(datetimeAtom);
28+
const { objectKind } = useParams();
29+
30+
const objectDisplay = rowToDelete?.display_label || rowToDelete?.name?.value || rowToDelete?.name;
31+
32+
const handleDeleteObject = async () => {
33+
if (!rowToDelete?.id) {
34+
return;
35+
}
36+
37+
setIsLoading(true);
38+
39+
try {
40+
const mutationString = deleteObject({
41+
kind:
42+
rowToDelete.__typename === "AccountTokenNode"
43+
? ACCOUNT_TOKEN_OBJECT
44+
: rowToDelete.__typename,
45+
data: stringifyWithoutQuotes({
46+
id: rowToDelete?.id,
47+
}),
48+
});
49+
50+
const mutation = gql`
51+
${mutationString}
52+
`;
53+
54+
await graphqlClient.mutate({
55+
mutation,
56+
context: { branch: branch?.name, date },
57+
});
58+
59+
if (objectKind) await graphqlClient.refetchQueries({ include: [objectKind!] });
60+
61+
if (onDelete) await onDelete();
62+
63+
close();
64+
65+
toast(<Alert type={ALERT_TYPES.SUCCESS} message={`Object ${objectDisplay} deleted`} />);
66+
} catch (error) {
67+
console.error("Error while deleting object: ", error);
68+
}
69+
70+
setIsLoading(false);
71+
};
72+
73+
return (
74+
<ModalDelete
75+
title="Delete"
76+
description={
77+
objectDisplay ? (
78+
<>
79+
Are you sure you want to remove the <i>{label}</i>
80+
<b className="ml-2">
81+
&quot;{objectDisplay}
82+
&quot;
83+
</b>
84+
?
85+
</>
86+
) : (
87+
<>
88+
Are you sure you want to remove this <i>{label}</i>?
89+
</>
90+
)
91+
}
92+
onCancel={close}
93+
onDelete={handleDeleteObject}
94+
open={!!open}
95+
setOpen={close}
96+
isLoading={isLoading}
97+
/>
98+
);
99+
}

frontend/app/src/components/modals/modal-delete.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ interface iProps {
1212
onDelete: () => void;
1313
onCancel: () => void;
1414
children?: ReactNode;
15+
confirmLabel?: string;
1516
}
1617

1718
export default function ModalDelete({
@@ -23,6 +24,7 @@ export default function ModalDelete({
2324
setOpen,
2425
isLoading,
2526
children,
27+
confirmLabel,
2628
}: iProps) {
2729
const cancelButtonRef = useRef(null);
2830

@@ -84,7 +86,7 @@ export default function ModalDelete({
8486
isLoading={isLoading}
8587
data-cy="modal-delete-confirm"
8688
data-testid="modal-delete-confirm">
87-
Delete
89+
{confirmLabel ?? "Delete"}
8890
</Button>
8991
<Button onClick={onCancel} ref={cancelButtonRef}>
9092
Cancel

frontend/app/src/components/search/search-anywhere.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ type SearchModalProps = {
5454
export function SearchAnywhere({ className = "" }: SearchModalProps) {
5555
let [isOpen, setIsOpen] = useState(false);
5656

57-
function closeModal() {
57+
function closeDrawer() {
5858
setIsOpen(false);
5959
}
6060

@@ -85,7 +85,7 @@ export function SearchAnywhere({ className = "" }: SearchModalProps) {
8585
</div>
8686

8787
<Transition appear show={isOpen} as={Fragment}>
88-
<Dialog onClose={closeModal}>
88+
<Dialog onClose={closeDrawer}>
8989
<Transition.Child
9090
as={Fragment}
9191
enter="ease-out duration-300"
@@ -107,7 +107,7 @@ export function SearchAnywhere({ className = "" }: SearchModalProps) {
107107
leave="ease-in duration-200"
108108
leaveFrom="opacity-100 scale-100"
109109
leaveTo="opacity-0 scale-95">
110-
<SearchAnywhereDialog onSelection={closeModal} />
110+
<SearchAnywhereDialog onSelection={closeDrawer} />
111111
</Transition.Child>
112112
</div>
113113
</div>
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { ButtonWithTooltip } from "@/components/buttons/button-primitive";
2+
import SlideOver, { SlideOverTitle } from "@/components/display/slide-over";
3+
import ModalDeleteObject from "@/components/modals/modal-delete-object";
4+
import { ARTIFACT_DEFINITION_OBJECT, GENERIC_REPOSITORY_KIND } from "@/config/constants";
5+
import graphqlClient from "@/graphql/graphqlClientApollo";
6+
import { usePermission } from "@/hooks/usePermission";
7+
import { Generate } from "@/screens/artifacts/generate";
8+
import { GroupsManagerTriggerButton } from "@/screens/groups/groups-manager-trigger-button";
9+
import ObjectItemEditComponent from "@/screens/object-item-edit/object-item-edit-paginated";
10+
import RepositoryActionMenu from "@/screens/repository/repository-action-menu";
11+
import { IModelSchema } from "@/state/atoms/schema.atom";
12+
import { isGeneric } from "@/utils/common";
13+
import { Icon } from "@iconify-icon/react";
14+
import { useState } from "react";
15+
import { useLocation, useNavigate, useParams } from "react-router-dom";
16+
17+
type DetailsButtonsProps = {
18+
schema: IModelSchema;
19+
objectDetailsData: any;
20+
};
21+
22+
export function DetailsButtons({ schema, objectDetailsData }: DetailsButtonsProps) {
23+
const permission = usePermission();
24+
const location = useLocation();
25+
const { objectid } = useParams();
26+
const navigate = useNavigate();
27+
28+
const redirect = location.pathname.replace(objectid ?? "", "");
29+
30+
const [showEditModal, setShowEditModal] = useState(false);
31+
const [showDeleteModal, setShowDeleteModal] = useState(false);
32+
33+
return (
34+
<>
35+
<div className="flex items-center gap-2">
36+
{schema.kind === ARTIFACT_DEFINITION_OBJECT && <Generate />}
37+
38+
<ButtonWithTooltip
39+
disabled={!permission.write.allow}
40+
tooltipEnabled
41+
tooltipContent={permission.write.message ?? "Edit object"}
42+
onClick={() => setShowEditModal(true)}
43+
data-testid="edit-button">
44+
<Icon icon="mdi:pencil" className="mr-1.5" aria-hidden="true" /> Edit {schema.label}
45+
</ButtonWithTooltip>
46+
47+
{!schema.kind?.match(/Core.*Group/g)?.length && ( // Hide group buttons on group list view
48+
<GroupsManagerTriggerButton
49+
schema={schema}
50+
objectId={objectDetailsData.id}
51+
className="text-custom-blue-600 p-4"
52+
/>
53+
)}
54+
55+
{!isGeneric(schema) && schema.inherit_from?.includes(GENERIC_REPOSITORY_KIND) && (
56+
<RepositoryActionMenu repositoryId={objectDetailsData.id} />
57+
)}
58+
59+
<ButtonWithTooltip
60+
disabled={!permission.write.allow}
61+
tooltipEnabled
62+
tooltipContent={permission.write.message ?? "Delete object"}
63+
data-testid="delete-button"
64+
variant={"danger"}
65+
size={"square"}
66+
onClick={() => setShowDeleteModal(true)}>
67+
<Icon icon="mdi:trash-can-outline" className="" aria-hidden="true" />
68+
</ButtonWithTooltip>
69+
</div>
70+
71+
<SlideOver
72+
title={
73+
<SlideOverTitle
74+
schema={schema}
75+
currentObjectLabel={objectDetailsData.display_label}
76+
title={`Edit ${objectDetailsData.display_label}`}
77+
subtitle={schema.description}
78+
/>
79+
}
80+
open={showEditModal}
81+
setOpen={setShowEditModal}>
82+
<ObjectItemEditComponent
83+
closeDrawer={() => setShowEditModal(false)}
84+
onUpdateComplete={() => graphqlClient.refetchQueries({ include: [schema.kind!] })}
85+
objectid={objectDetailsData.id!}
86+
objectname={schema.kind!}
87+
/>
88+
</SlideOver>
89+
90+
<ModalDeleteObject
91+
label={schema.label ?? schema.kind}
92+
rowToDelete={objectDetailsData}
93+
open={!!showDeleteModal}
94+
close={() => setShowDeleteModal(false)}
95+
onDelete={() => navigate(redirect)}
96+
/>
97+
</>
98+
);
99+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { StringParam, useQueryParam } from "use-query-params";
2+
import { DetailsButtons } from "./details-buttons";
3+
import { QSP } from "@/config/qsp";
4+
import { TASK_TAB } from "@/config/constants";
5+
import { RelationshipsButtons } from "./relationships-buttons";
6+
7+
export function ActionButtons(props: any) {
8+
const [qspTab] = useQueryParam(QSP.TAB, StringParam);
9+
10+
if (!qspTab) {
11+
return <DetailsButtons {...props} />;
12+
}
13+
14+
if (qspTab && qspTab !== TASK_TAB) {
15+
return <RelationshipsButtons {...props} />;
16+
}
17+
18+
return null;
19+
}

0 commit comments

Comments
 (0)