Skip to content

Commit 70ec5a8

Browse files
committed
feat: add delete confirm modal
1 parent b277c2e commit 70ec5a8

9 files changed

Lines changed: 346 additions & 186 deletions

File tree

src/taxonomy/tag-list/Actions.tsx

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import {
2+
Icon,
3+
IconButton,
4+
IconButtonWithTooltip,
5+
Dropdown,
6+
} from '@openedx/paragon';
7+
import {
8+
AddCircle,
9+
MoreVert,
10+
} from '@openedx/paragon/icons';
11+
import { useIntl } from '@edx/frontend-platform/i18n';
12+
import type { Row } from '@tanstack/react-table';
13+
14+
import type {
15+
RowId,
16+
TreeRowData,
17+
} from '@src/taxonomy/tree-table/types';
18+
import type { TagListRowData } from './types';
19+
import messages from './messages';
20+
21+
interface ActionsHeaderProps {
22+
onStartDraft: () => void;
23+
setDraftError: (error: string) => void;
24+
setIsCreatingTopRow: (isCreating: boolean) => void;
25+
setEditingRowId: (id: RowId | null) => void;
26+
setActiveActionMenuRowId: (id: RowId | null) => void;
27+
hasOpenDraft: boolean;
28+
draftInProgressHintId: string;
29+
canAddTag: boolean;
30+
}
31+
32+
const ActionsHeader = ({
33+
onStartDraft,
34+
setDraftError,
35+
setIsCreatingTopRow,
36+
setEditingRowId,
37+
setActiveActionMenuRowId,
38+
hasOpenDraft,
39+
canAddTag,
40+
draftInProgressHintId,
41+
}: ActionsHeaderProps) => {
42+
const intl = useIntl();
43+
return (
44+
<div className="d-flex justify-content-end">
45+
<IconButtonWithTooltip
46+
tooltipPlacement="top"
47+
tooltipContent={<div>{intl.formatMessage(messages.createNewTagTooltip)}</div>}
48+
src={AddCircle}
49+
alt={intl.formatMessage(messages.createTagButtonLabel)}
50+
size="inline"
51+
onClick={() => {
52+
onStartDraft();
53+
setDraftError('');
54+
setIsCreatingTopRow(true);
55+
setEditingRowId(null);
56+
setActiveActionMenuRowId(null);
57+
}}
58+
disabled={hasOpenDraft || !canAddTag}
59+
aria-describedby={hasOpenDraft ? draftInProgressHintId : undefined}
60+
/>
61+
</div>
62+
);
63+
};
64+
65+
interface ActionsMenuProps {
66+
rowData: TagListRowData;
67+
startSubtagDraft: () => void;
68+
disableAddSubtag: boolean;
69+
startEditTag: () => void;
70+
disableEditTag: boolean;
71+
reachedMaxDepth: (row: Row<TreeRowData>) => boolean;
72+
startDeleteTag: (row: Row<TreeRowData>) => void;
73+
disableDeleteTag: boolean;
74+
row: Row<TreeRowData>;
75+
}
76+
77+
const ActionsMenu = ({
78+
rowData,
79+
row,
80+
startSubtagDraft,
81+
disableAddSubtag,
82+
startEditTag,
83+
disableEditTag,
84+
reachedMaxDepth,
85+
startDeleteTag,
86+
disableDeleteTag,
87+
}: ActionsMenuProps) => {
88+
const intl = useIntl();
89+
90+
const deleteTagMenuItem = (
91+
<Dropdown.Item
92+
onClick={() => startDeleteTag(row)}
93+
disabled={disableDeleteTag}
94+
>
95+
{intl.formatMessage(messages.deleteTag)}
96+
</Dropdown.Item>
97+
);
98+
99+
const editTagMenuItem = (
100+
<Dropdown.Item
101+
onClick={startEditTag}
102+
disabled={disableEditTag}
103+
>
104+
{intl.formatMessage(messages.renameTag)}
105+
</Dropdown.Item>
106+
);
107+
108+
return (
109+
<Dropdown>
110+
<Dropdown.Toggle
111+
id={`dropdown-toggle-for-tag-${rowData.id}`}
112+
as={IconButton}
113+
src={MoreVert}
114+
iconAs={Icon}
115+
variant="primary"
116+
aria-label={intl.formatMessage(messages.moreActionsForTag, { tagName: rowData.value })}
117+
size="sm"
118+
/>
119+
<Dropdown.Menu>
120+
<Dropdown.Item
121+
onClick={startSubtagDraft}
122+
disabled={reachedMaxDepth(row) || disableAddSubtag}
123+
>
124+
{intl.formatMessage(messages.addSubtag)}
125+
</Dropdown.Item>
126+
{editTagMenuItem}
127+
{deleteTagMenuItem}
128+
</Dropdown.Menu>
129+
</Dropdown>
130+
);
131+
};
132+
133+
const Actions = {
134+
Header: ActionsHeader,
135+
Menu: ActionsMenu,
136+
};
137+
138+
export default Actions;

src/taxonomy/tag-list/TagListTable.tsx

Lines changed: 43 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
TABLE_MODES,
1717
} from './constants';
1818
import { useTableModes, useEditActions } from './hooks';
19+
import TypeXToConfirmPopup from './TypeXToConfirmPopup';
1920

2021
interface TagListTableProps {
2122
taxonomyId: number;
@@ -50,6 +51,8 @@ const TagListTable = ({ taxonomyId, maxDepth }: TagListTableProps) => {
5051
const [draftError, setDraftError] = useState('');
5152
const treeData = (tagTree?.getAllAsDeepCopy() || []) as unknown as TreeRowData[];
5253
const hasOpenDraft = isCreatingTopTag || creatingParentId !== null || editingRowId !== null;
54+
const [confirmDeleteDialogOpen, setConfirmDeleteDialogOpen] = useState(false);
55+
const [confirmDeleteDialogContext, setConfirmDeleteDialogContext] = useState<Row<TreeRowData> | null>(null);
5356

5457
// TABLE MODES
5558
const {
@@ -89,18 +92,25 @@ const TagListTable = ({ taxonomyId, maxDepth }: TagListTableProps) => {
8992

9093
// Custom Edit Actions Hook - handles table mode transitions, API calls,
9194
// and updating the table without a full data reload when creating or editing tags.
92-
const { handleCreateTag, handleUpdateTag, validate } = useEditActions({
93-
setTagTree,
94-
setDraftError,
95-
createTagMutation,
96-
updateTagMutation,
97-
enterPreviewMode,
98-
setToast,
99-
setIsCreatingTopTag,
100-
setCreatingParentId,
101-
exitDraftWithoutSave,
102-
setEditingRowId,
103-
});
95+
const { handleCreateTag, handleUpdateTag, validate, startSubtagDraft, startEditTag, startDeleteTag, handleDeleteTag } = useEditActions(
96+
{
97+
enterDraftMode,
98+
enterPreviewMode,
99+
enterViewMode,
100+
setTagTree,
101+
setDraftError,
102+
createTagMutation,
103+
updateTagMutation,
104+
setToast,
105+
setIsCreatingTopTag,
106+
setCreatingParentId,
107+
exitDraftWithoutSave,
108+
setEditingRowId,
109+
setActiveActionMenuRowId,
110+
setConfirmDeleteDialogOpen,
111+
setConfirmDeleteDialogContext,
112+
},
113+
);
104114

105115
// RELOAD DATA IN VIEW MODE
106116
useEffect(() => {
@@ -142,6 +152,13 @@ const TagListTable = ({ taxonomyId, maxDepth }: TagListTableProps) => {
142152
hasOpenDraft,
143153
canAddTag,
144154
maxDepth,
155+
startSubtagDraft,
156+
startEditTag,
157+
startDeleteTag,
158+
confirmDeleteDialogOpen,
159+
setConfirmDeleteDialogOpen,
160+
confirmDeleteDialogContext,
161+
setConfirmDeleteDialogContext,
145162
};
146163
const contextValue = {
147164
...contextValueArgs,
@@ -151,6 +168,20 @@ const TagListTable = ({ taxonomyId, maxDepth }: TagListTableProps) => {
151168
return (
152169
<TreeTableContext.Provider value={contextValue}>
153170
<TableView />
171+
<TypeXToConfirmPopup
172+
label="Confirm Delete"
173+
X="DELETE"
174+
bodyText="Are you sure you want to delete this tag?"
175+
confirmLabel="Delete"
176+
cancelLabel="Cancel"
177+
isOpen={confirmDeleteDialogOpen}
178+
context={confirmDeleteDialogContext}
179+
onConfirm={(row) => { handleDeleteTag(row); setConfirmDeleteDialogOpen(false); setConfirmDeleteDialogContext(null); }}
180+
onCancel={() => {
181+
setConfirmDeleteDialogOpen(false);
182+
setConfirmDeleteDialogContext(null);
183+
}}
184+
/>
154185
</TreeTableContext.Provider>
155186
);
156187
};
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import React from 'react';
2+
import PropTypes from 'prop-types';
3+
import { Button, Form, ModalDialog } from '@openedx/paragon';
4+
5+
const TypeXToConfirmPopup = ({
6+
label,
7+
X,
8+
bodyText,
9+
confirmLabel,
10+
cancelLabel,
11+
isOpen,
12+
context,
13+
onConfirm,
14+
onCancel,
15+
}) => {
16+
const [confirmedByTyping, setConfirmedByTyping] = React.useState(false);
17+
18+
return (
19+
<ModalDialog
20+
title={label}
21+
isOpen={isOpen}
22+
onClose={onCancel}
23+
isOverflowVisible
24+
>
25+
<ModalDialog.Header>
26+
<ModalDialog.Title>{label}</ModalDialog.Title>
27+
</ModalDialog.Header>
28+
<ModalDialog.Body>
29+
<div>{bodyText}</div>
30+
<div>
31+
<div>
32+
Type <span className="text-primary-500 font-weight-bold">{X}</span> to confirm
33+
</div>
34+
<Form.Control
35+
onChange={(e) => e.target.value === X ? setConfirmedByTyping(true) : setConfirmedByTyping(false)}
36+
/>
37+
</div>
38+
<ModalDialog.Footer>
39+
<Button variant="tertiary" onClick={onCancel}>
40+
{cancelLabel}
41+
</Button>
42+
<Button onClick={() => onConfirm(context)} disabled={!confirmedByTyping}>
43+
{confirmLabel}
44+
</Button>
45+
</ModalDialog.Footer>
46+
</ModalDialog.Body>
47+
</ModalDialog>
48+
);
49+
};
50+
51+
TypeXToConfirmPopup.propTypes = {
52+
label: PropTypes.string.isRequired,
53+
bodyText: PropTypes.string.isRequired,
54+
onConfirm: PropTypes.func.isRequired,
55+
onCancel: PropTypes.func.isRequired,
56+
confirmLabel: PropTypes.string.isRequired,
57+
cancelLabel: PropTypes.string.isRequired,
58+
X: PropTypes.string.isRequired,
59+
context: PropTypes.any,
60+
confirmButtonClass: PropTypes.string,
61+
cancelButtonClass: PropTypes.string,
62+
confirmVariant: PropTypes.string,
63+
};
64+
TypeXToConfirmPopup.defaultProps = {
65+
confirmVariant: 'outline-brand',
66+
confirmButtonClass: '',
67+
cancelButtonClass: '',
68+
};
69+
70+
export default React.memo(TypeXToConfirmPopup);

0 commit comments

Comments
 (0)