Skip to content

Commit 79ca374

Browse files
FEATURE (notifiers): Add mobile adaptivity for notifiers
1 parent b3f1a6f commit 79ca374

13 files changed

+365
-298
lines changed

frontend/src/features/notifiers/ui/NotifiersComponent.tsx

Lines changed: 115 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { useEffect, useState } from 'react';
44
import { notifierApi } from '../../../entity/notifiers';
55
import type { Notifier } from '../../../entity/notifiers';
66
import type { WorkspaceResponse } from '../../../entity/workspaces';
7+
import { useIsMobile } from '../../../shared/hooks';
78
import { NotifierCardComponent } from './NotifierCardComponent';
89
import { NotifierComponent } from './NotifierComponent';
910
import { EditNotifierComponent } from './edit/EditNotifierComponent';
@@ -14,21 +15,47 @@ interface Props {
1415
isCanManageNotifiers: boolean;
1516
}
1617

18+
const SELECTED_NOTIFIER_STORAGE_KEY = 'selectedNotifierId';
19+
1720
export const NotifiersComponent = ({ contentHeight, workspace, isCanManageNotifiers }: Props) => {
21+
const isMobile = useIsMobile();
1822
const [isLoading, setIsLoading] = useState(true);
1923
const [notifiers, setNotifiers] = useState<Notifier[]>([]);
24+
const [searchQuery, setSearchQuery] = useState('');
2025

2126
const [isShowAddNotifier, setIsShowAddNotifier] = useState(false);
2227
const [selectedNotifierId, setSelectedNotifierId] = useState<string | undefined>(undefined);
23-
const loadNotifiers = () => {
24-
setIsLoading(true);
28+
29+
const updateSelectedNotifierId = (notifierId: string | undefined) => {
30+
setSelectedNotifierId(notifierId);
31+
if (notifierId) {
32+
localStorage.setItem(`${SELECTED_NOTIFIER_STORAGE_KEY}_${workspace.id}`, notifierId);
33+
} else {
34+
localStorage.removeItem(`${SELECTED_NOTIFIER_STORAGE_KEY}_${workspace.id}`);
35+
}
36+
};
37+
38+
const loadNotifiers = (isSilent = false, selectNotifierId?: string) => {
39+
if (!isSilent) {
40+
setIsLoading(true);
41+
}
2542

2643
notifierApi
2744
.getNotifiers(workspace.id)
2845
.then((notifiers) => {
2946
setNotifiers(notifiers);
30-
if (!selectedNotifierId) {
31-
setSelectedNotifierId(notifiers[0]?.id);
47+
if (selectNotifierId) {
48+
updateSelectedNotifierId(selectNotifierId);
49+
} else if (!selectedNotifierId && !isSilent && !isMobile) {
50+
// On desktop, auto-select a notifier; on mobile, keep it unselected
51+
const savedNotifierId = localStorage.getItem(
52+
`${SELECTED_NOTIFIER_STORAGE_KEY}_${workspace.id}`,
53+
);
54+
const notifierToSelect =
55+
savedNotifierId && notifiers.some((n) => n.id === savedNotifierId)
56+
? savedNotifierId
57+
: notifiers[0]?.id;
58+
updateSelectedNotifierId(notifierToSelect);
3259
}
3360
})
3461
.catch((e) => alert(e.message))
@@ -37,6 +64,12 @@ export const NotifiersComponent = ({ contentHeight, workspace, isCanManageNotifi
3764

3865
useEffect(() => {
3966
loadNotifiers();
67+
68+
const interval = setInterval(() => {
69+
loadNotifiers(true);
70+
}, 5 * 60_000);
71+
72+
return () => clearInterval(interval);
4073
}, []);
4174

4275
if (isLoading) {
@@ -53,45 +86,89 @@ export const NotifiersComponent = ({ contentHeight, workspace, isCanManageNotifi
5386
</Button>
5487
);
5588

89+
const filteredNotifiers = notifiers.filter((notifier) =>
90+
notifier.name.toLowerCase().includes(searchQuery.toLowerCase()),
91+
);
92+
93+
// On mobile, show either the list or the notifier details
94+
const showNotifierList = !isMobile || !selectedNotifierId;
95+
const showNotifierDetails = selectedNotifierId && (!isMobile || selectedNotifierId);
96+
5697
return (
5798
<>
5899
<div className="flex grow">
59-
<div
60-
className="mx-3 w-[250px] min-w-[250px] overflow-y-auto"
61-
style={{ height: contentHeight }}
62-
>
63-
{notifiers.length >= 5 && isCanManageNotifiers && addNotifierButton}
64-
65-
{notifiers.map((notifier) => (
66-
<NotifierCardComponent
67-
key={notifier.id}
68-
notifier={notifier}
69-
selectedNotifierId={selectedNotifierId}
70-
setSelectedNotifierId={setSelectedNotifierId}
71-
/>
72-
))}
100+
{showNotifierList && (
101+
<div
102+
className="w-full overflow-y-auto md:mx-3 md:w-[250px] md:min-w-[250px] md:pr-2"
103+
style={{ height: contentHeight }}
104+
>
105+
{notifiers.length >= 5 && (
106+
<>
107+
{isCanManageNotifiers && addNotifierButton}
73108

74-
{notifiers.length < 5 && isCanManageNotifiers && addNotifierButton}
109+
<div className="mb-2">
110+
<input
111+
placeholder="Search notifier"
112+
value={searchQuery}
113+
onChange={(e) => setSearchQuery(e.target.value)}
114+
className="w-full border-b border-gray-300 p-1 text-gray-500 outline-none"
115+
/>
116+
</div>
117+
</>
118+
)}
75119

76-
<div className="mx-3 text-center text-xs text-gray-500">
77-
Notifier - is a place where notifications will be sent (email, Slack, Telegram, etc.)
120+
{filteredNotifiers.length > 0
121+
? filteredNotifiers.map((notifier) => (
122+
<NotifierCardComponent
123+
key={notifier.id}
124+
notifier={notifier}
125+
selectedNotifierId={selectedNotifierId}
126+
setSelectedNotifierId={updateSelectedNotifierId}
127+
/>
128+
))
129+
: searchQuery && (
130+
<div className="mb-4 text-center text-sm text-gray-500">
131+
No notifiers found matching &quot;{searchQuery}&quot;
132+
</div>
133+
)}
134+
135+
{notifiers.length < 5 && isCanManageNotifiers && addNotifierButton}
136+
137+
<div className="mx-3 text-center text-xs text-gray-500">
138+
Notifier - is a place where notifications will be sent (email, Slack, Telegram, etc.)
139+
</div>
78140
</div>
79-
</div>
141+
)}
80142

81-
{selectedNotifierId && (
82-
<NotifierComponent
83-
notifierId={selectedNotifierId}
84-
onNotifierChanged={() => {
85-
loadNotifiers();
86-
}}
87-
onNotifierDeleted={() => {
88-
loadNotifiers();
89-
setSelectedNotifierId(
90-
notifiers.filter((notifier) => notifier.id !== selectedNotifierId)[0]?.id,
91-
);
92-
}}
93-
isCanManageNotifiers={isCanManageNotifiers}
94-
/>
143+
{showNotifierDetails && (
144+
<div className="flex w-full flex-col md:flex-1">
145+
{isMobile && (
146+
<div className="mb-2">
147+
<Button
148+
type="default"
149+
onClick={() => updateSelectedNotifierId(undefined)}
150+
className="w-full"
151+
>
152+
← Back to notifiers
153+
</Button>
154+
</div>
155+
)}
156+
157+
<NotifierComponent
158+
notifierId={selectedNotifierId}
159+
onNotifierChanged={() => {
160+
loadNotifiers();
161+
}}
162+
onNotifierDeleted={() => {
163+
const remainingNotifiers = notifiers.filter(
164+
(notifier) => notifier.id !== selectedNotifierId,
165+
);
166+
updateSelectedNotifierId(remainingNotifiers[0]?.id);
167+
loadNotifiers();
168+
}}
169+
isCanManageNotifiers={isCanManageNotifiers}
170+
/>
171+
</div>
95172
)}
96173
</div>
97174

@@ -111,8 +188,8 @@ export const NotifiersComponent = ({ contentHeight, workspace, isCanManageNotifi
111188
isShowName
112189
isShowClose={false}
113190
onClose={() => setIsShowAddNotifier(false)}
114-
onChanged={() => {
115-
loadNotifiers();
191+
onChanged={(notifier) => {
192+
loadNotifiers(false, notifier.id);
116193
setIsShowAddNotifier(false);
117194
}}
118195
/>

frontend/src/features/notifiers/ui/edit/EditNotifierComponent.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ export function EditNotifierComponent({
225225
)}
226226

227227
<div className="mb-1 flex items-center">
228-
<div className="w-[130px] min-w-[130px]">Type</div>
228+
<div className="w-[150px] min-w-[150px]">Type</div>
229229

230230
<Select
231231
value={notifier?.notifierType}

frontend/src/features/notifiers/ui/edit/notifiers/EditDiscordNotifierComponent.tsx

Lines changed: 20 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -11,31 +11,28 @@ interface Props {
1111
export function EditDiscordNotifierComponent({ notifier, setNotifier, setUnsaved }: Props) {
1212
return (
1313
<>
14-
<div className="flex">
15-
<div className="w-[130px] max-w-[130px] min-w-[130px] pr-3">Channel webhook URL</div>
16-
17-
<div className="w-[250px]">
18-
<Input
19-
value={notifier?.discordNotifier?.channelWebhookUrl || ''}
20-
onChange={(e) => {
21-
if (!notifier?.discordNotifier) return;
22-
setNotifier({
23-
...notifier,
24-
discordNotifier: {
25-
...notifier.discordNotifier,
26-
channelWebhookUrl: e.target.value.trim(),
27-
},
28-
});
29-
setUnsaved();
30-
}}
31-
size="small"
32-
className="w-full"
33-
placeholder="1234567890:ABCDEFGHIJKLMNOPQRSTUVWXYZ"
34-
/>
35-
</div>
14+
<div className="mb-1 flex w-full flex-col items-start sm:flex-row sm:items-center">
15+
<div className="mb-1 min-w-[150px] sm:mb-0">Channel webhook URL</div>
16+
<Input
17+
value={notifier?.discordNotifier?.channelWebhookUrl || ''}
18+
onChange={(e) => {
19+
if (!notifier?.discordNotifier) return;
20+
setNotifier({
21+
...notifier,
22+
discordNotifier: {
23+
...notifier.discordNotifier,
24+
channelWebhookUrl: e.target.value.trim(),
25+
},
26+
});
27+
setUnsaved();
28+
}}
29+
size="small"
30+
className="w-full max-w-[250px]"
31+
placeholder="1234567890:ABCDEFGHIJKLMNOPQRSTUVWXYZ"
32+
/>
3633
</div>
3734

38-
<div className="ml-[130px] max-w-[250px]">
35+
<div className="max-w-[250px] sm:ml-[150px]">
3936
<div className="mt-1 text-xs text-gray-500">
4037
<strong>How to get Discord webhook URL:</strong>
4138
<br />

0 commit comments

Comments
 (0)