Skip to content

Commit cb0e157

Browse files
author
Pavel Fokin
committed
Add notification channel deletion functionality
- Add deletePartnerConnection API function - Refactor ChannelCard component into modular structure with separate files - Add ChannelDeleteDialog component for confirmation - Add useDeleteNotificationChannel hook - Create reusable Dialog UI component with styling - Move NOTIFICATION_CHANNEL_TYPES from constants.js to notificationChannels.js - Update imports to use consolidated notificationChannels module
1 parent 71d9768 commit cb0e157

File tree

12 files changed

+477
-115
lines changed

12 files changed

+477
-115
lines changed

api/partner.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,9 @@ export const getPartnerConnectionListeners = async (connectionId) => {
1313
export const createPartnerConnection = async (data) => {
1414
const response = await axiosAdmin.post('/partner/connections', data)
1515
return response.data
16+
}
17+
18+
export const deletePartnerConnection = async (connectionId) => {
19+
const response = await axiosAdmin.delete(`/partner/connection/${connectionId}`)
20+
return response.data
1621
}

components/Admin/notifications/ChannelCard.js

Lines changed: 0 additions & 105 deletions
This file was deleted.
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import React, { useState } from 'react'
2+
3+
import { FaDiscord, FaEnvelope, FaSlack } from 'react-icons/fa'
4+
import { FaXTwitter } from 'react-icons/fa6'
5+
6+
import Card from '@/components/UI/Card'
7+
import { useDeleteNotificationChannel } from '@/hooks/useNotifications'
8+
import { NOTIFICATION_CHANNEL_TYPES } from '@/lib/notificationChannels'
9+
10+
import ChannelDeleteDialog from './ChannelDeleteDialog'
11+
import ChannelSpecificDetails from './ChannelSpecificDetails'
12+
13+
const iconMap = {
14+
[NOTIFICATION_CHANNEL_TYPES.SLACK]: FaSlack,
15+
[NOTIFICATION_CHANNEL_TYPES.DISCORD]: FaDiscord,
16+
[NOTIFICATION_CHANNEL_TYPES.TWITTER]: FaXTwitter,
17+
[NOTIFICATION_CHANNEL_TYPES.EMAIL]: FaEnvelope
18+
}
19+
20+
export default function ChannelCard({ channel }) {
21+
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false)
22+
const deleteChannel = useDeleteNotificationChannel()
23+
24+
const handleDelete = async () => {
25+
await deleteChannel.mutate(channel.id)
26+
setIsDeleteDialogOpen(false)
27+
}
28+
29+
return (
30+
<Card className="flex flex-col justify-between">
31+
<div className="font-bold text-lg mb-2 capitalize flex items-center gap-2 w-full">
32+
{channel.type &&
33+
iconMap[channel.type] &&
34+
React.createElement(iconMap[channel.type], {
35+
className: 'inline-block w-4 h-4 text-gray-600 dark:text-gray-400'
36+
})}{' '}
37+
{channel.name || `Channel #${channel.id}`}
38+
</div>
39+
<div className="flex justify-end">
40+
<button className="btn btn-error" onClick={() => setIsDeleteDialogOpen(true)}>
41+
Delete
42+
</button>
43+
</div>
44+
<ChannelSpecificDetails channel={channel} />
45+
<div className="text-sm text-gray-600 dark:text-gray-400 mb-2 w-full">
46+
Used in {channel.rules.length} {channel.rules.length === 1 ? 'rule' : 'rules'}
47+
</div>
48+
<ChannelDeleteDialog
49+
isOpen={isDeleteDialogOpen}
50+
onClose={() => setIsDeleteDialogOpen(false)}
51+
onDelete={handleDelete}
52+
/>
53+
</Card>
54+
)
55+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import Dialog from '@/components/UI/Dialog'
2+
3+
export default function ChannelDeleteDialog({ isOpen, onClose, onDelete }) {
4+
return (
5+
<Dialog
6+
isOpen={isOpen}
7+
onClose={onClose}
8+
title="Delete channel"
9+
>
10+
<div className="text-gray-600 dark:text-gray-400 mb-4">
11+
Are you sure you want to delete this channel? This action cannot be undone.
12+
</div>
13+
<div className="flex justify-end gap-2">
14+
<button className="btn btn-secondary" onClick={onClose}>
15+
Cancel
16+
</button>
17+
<button className="btn btn-error" onClick={onDelete}>
18+
Delete
19+
</button>
20+
</div>
21+
</Dialog>
22+
)
23+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { NOTIFICATION_CHANNEL_TYPES } from '@/lib/notificationChannels'
2+
3+
export default function ChannelSpecificDetails({ channel }) {
4+
if (!channel.settings) {
5+
return null
6+
}
7+
switch (channel.type) {
8+
case NOTIFICATION_CHANNEL_TYPES.SLACK:
9+
return (
10+
<div className="text-sm text-gray-600 dark:text-gray-400 mb-2 flex items-center gap-1 w-full">
11+
<span className="inline-block text-xs font-bold truncate max-w-xs">{channel.settings.webhook || 'N/A'}</span>
12+
</div>
13+
)
14+
case NOTIFICATION_CHANNEL_TYPES.DISCORD:
15+
return (
16+
<div className="text-sm text-gray-600 dark:text-gray-400 mb-2 w-full">
17+
<div className="flex items-center gap-1">
18+
<span className="inline-block text-xs font-bold truncate max-w-xs">
19+
{channel.settings.webhook || 'N/A'}
20+
</span>
21+
</div>
22+
<div className="flex items-center gap-1">
23+
Username:{' '}
24+
<span className="inline-block text-xs font-bold truncate max-w-xs">
25+
{channel.settings.username || 'N/A'}
26+
</span>
27+
</div>
28+
</div>
29+
)
30+
case NOTIFICATION_CHANNEL_TYPES.EMAIL:
31+
return (
32+
<div className="text-sm text-gray-600 dark:text-gray-400 mb-2">
33+
<div className="flex items-center gap-1">
34+
Webhook:{' '}
35+
<span className="inline-block text-xs font-bold truncate max-w-xs">
36+
{channel.settings.webhook || 'N/A'}
37+
</span>
38+
</div>
39+
</div>
40+
)
41+
case NOTIFICATION_CHANNEL_TYPES.TWITTER:
42+
return (
43+
<div className="text-sm text-gray-600 dark:text-gray-400 mb-2 w-full">
44+
<div className="flex items-center gap-1">
45+
Consumer key:{' '}
46+
<span className="inline-block text-xs font-mono truncate max-w-xs">
47+
{channel.settings.consumer_key ? channel.settings.consumer_key.slice(-8) : 'N/A'}
48+
</span>
49+
</div>
50+
<div className="flex items-center gap-1">
51+
Consumer secret:{' '}
52+
<span className="inline-block text-xs font-mono truncate max-w-xs">
53+
{channel.settings.consumer_secret ? channel.settings.consumer_secret.slice(-8) : 'N/A'}
54+
</span>
55+
</div>
56+
<div className="flex items-center gap-1">
57+
Access token key:{' '}
58+
<span className="inline-block text-xs font-mono truncate max-w-xs">
59+
{channel.settings.access_token_key ? channel.settings.access_token_key.slice(-8) : 'N/A'}
60+
</span>
61+
</div>
62+
<div className="flex items-center gap-1">
63+
<span>Access token secret:</span>
64+
<span className="inline-block text-xs font-mono truncate max-w-xs">
65+
{channel.settings.access_token_secret ? channel.settings.access_token_secret.slice(-8) : 'N/A'}
66+
</span>
67+
</div>
68+
</div>
69+
)
70+
default:
71+
return null
72+
}
73+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default } from './ChannelCard'

components/UI/Dialog.js

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import React, { useEffect, useRef } from 'react';
2+
import styles from '@/styles/components/dialog.module.scss';
3+
4+
const Dialog = ({
5+
isOpen,
6+
onClose,
7+
title,
8+
children,
9+
size = 'medium',
10+
showCloseButton = true,
11+
closeOnBackdropClick = true,
12+
closeOnEscape = true
13+
}) => {
14+
const dialogRef = useRef(null);
15+
const previousActiveElement = useRef(null);
16+
17+
useEffect(() => {
18+
if (isOpen) {
19+
// Store the currently focused element
20+
previousActiveElement.current = document.activeElement;
21+
22+
// Focus the dialog
23+
if (dialogRef.current) {
24+
dialogRef.current.focus();
25+
}
26+
27+
// Prevent body scroll
28+
document.body.style.overflow = 'hidden';
29+
} else {
30+
// Restore body scroll
31+
document.body.style.overflow = 'unset';
32+
33+
// Restore focus to the previous element
34+
if (previousActiveElement.current) {
35+
previousActiveElement.current.focus();
36+
}
37+
}
38+
39+
return () => {
40+
document.body.style.overflow = 'unset';
41+
};
42+
}, [isOpen]);
43+
44+
useEffect(() => {
45+
const handleEscape = (event) => {
46+
if (event.key === 'Escape' && isOpen && closeOnEscape) {
47+
onClose();
48+
}
49+
};
50+
51+
if (isOpen) {
52+
document.addEventListener('keydown', handleEscape);
53+
}
54+
55+
return () => {
56+
document.removeEventListener('keydown', handleEscape);
57+
};
58+
}, [isOpen, onClose, closeOnEscape]);
59+
60+
const handleBackdropClick = (event) => {
61+
if (event.target === event.currentTarget && closeOnBackdropClick) {
62+
onClose();
63+
}
64+
};
65+
66+
if (!isOpen) return null;
67+
68+
return (
69+
<div
70+
className={styles.backdrop}
71+
onClick={handleBackdropClick}
72+
role="presentation"
73+
>
74+
<div
75+
ref={dialogRef}
76+
className={`${styles.dialog} ${styles[size]}`}
77+
role="dialog"
78+
aria-modal="true"
79+
aria-labelledby={title ? 'dialog-title' : undefined}
80+
tabIndex={-1}
81+
>
82+
<div className={styles.header}>
83+
{title && (
84+
<h2 id="dialog-title" className={styles.title}>
85+
{title}
86+
</h2>
87+
)}
88+
{showCloseButton && (
89+
<button
90+
type="button"
91+
className={styles.closeButton}
92+
onClick={onClose}
93+
aria-label="Close dialog"
94+
>
95+
<svg
96+
width="24"
97+
height="24"
98+
viewBox="0 0 24 24"
99+
fill="none"
100+
stroke="currentColor"
101+
strokeWidth="2"
102+
strokeLinecap="round"
103+
strokeLinejoin="round"
104+
>
105+
<line x1="18" y1="6" x2="6" y2="18"></line>
106+
<line x1="6" y1="6" x2="18" y2="18"></line>
107+
</svg>
108+
</button>
109+
)}
110+
</div>
111+
<div className={styles.content}>
112+
{children}
113+
</div>
114+
</div>
115+
</div>
116+
);
117+
};
118+
119+
export default Dialog;

0 commit comments

Comments
 (0)