Skip to content

Commit 052c549

Browse files
feat: Add non-navigable alert modals
1 parent 5176156 commit 052c549

File tree

7 files changed

+307
-34
lines changed

7 files changed

+307
-34
lines changed

ui/components/app/alert-system/multiple-alert-modal/multiple-alert-modal.test.tsx

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ describe('MultipleAlertModal', () => {
2323
const FROM_ALERT_KEY_MOCK = 'from';
2424
const CONTRACT_ALERT_KEY_MOCK = 'contract';
2525
const DATA_ALERT_KEY_MOCK = 'data';
26+
const HIDDEN_ALERT_KEY_MOCK = 'hidden';
2627
const onAcknowledgeClickMock = jest.fn();
2728
const onCloseMock = jest.fn();
2829

@@ -282,4 +283,98 @@ describe('MultipleAlertModal', () => {
282283
expect(getByText(alertsMock[1].message)).toBeInTheDocument();
283284
});
284285
});
286+
287+
describe('Alerts hidden from navigation', () => {
288+
const alertsWithHiddenNavigationMock = [
289+
{
290+
key: FROM_ALERT_KEY_MOCK,
291+
field: FROM_ALERT_KEY_MOCK,
292+
severity: Severity.Warning,
293+
message: 'Alert 1',
294+
reason: 'Reason 1',
295+
alertDetails: ['Detail 1', 'Detail 2'],
296+
},
297+
{
298+
key: HIDDEN_ALERT_KEY_MOCK,
299+
field: HIDDEN_ALERT_KEY_MOCK,
300+
severity: Severity.Danger,
301+
message: 'Hidden Alert',
302+
hideFromAlertNavigation: true,
303+
},
304+
{
305+
key: CONTRACT_ALERT_KEY_MOCK,
306+
field: CONTRACT_ALERT_KEY_MOCK,
307+
severity: Severity.Info,
308+
message: 'Alert 3',
309+
},
310+
];
311+
312+
const mockStoreWithHiddenNavigation = configureMockStore([])({
313+
...STATE_MOCK,
314+
confirmAlerts: {
315+
alerts: { [OWNER_ID_MOCK]: alertsWithHiddenNavigationMock },
316+
confirmed: {
317+
[OWNER_ID_MOCK]: {
318+
[FROM_ALERT_KEY_MOCK]: false,
319+
[HIDDEN_ALERT_KEY_MOCK]: false,
320+
[CONTRACT_ALERT_KEY_MOCK]: false,
321+
},
322+
},
323+
},
324+
});
325+
326+
const mockStoreWithHiddenNavigationConfirmed = configureMockStore([])({
327+
...STATE_MOCK,
328+
confirmAlerts: {
329+
alerts: { [OWNER_ID_MOCK]: alertsWithHiddenNavigationMock },
330+
confirmed: {
331+
[OWNER_ID_MOCK]: {
332+
[FROM_ALERT_KEY_MOCK]: true,
333+
[HIDDEN_ALERT_KEY_MOCK]: true,
334+
[CONTRACT_ALERT_KEY_MOCK]: true,
335+
},
336+
},
337+
},
338+
});
339+
340+
it('does not render navigation controls when the selected alert hides navigation', () => {
341+
const { queryByTestId } = renderWithProvider(
342+
<MultipleAlertModal
343+
{...defaultProps}
344+
alertKey={HIDDEN_ALERT_KEY_MOCK}
345+
/>,
346+
mockStoreWithHiddenNavigation,
347+
);
348+
349+
expect(queryByTestId('alert-modal-next-button')).toBeNull();
350+
expect(queryByTestId('alert-modal-back-button')).toBeNull();
351+
});
352+
353+
it('skips alerts hidden from navigation when cycling forward', () => {
354+
const { getByTestId, getByText, queryByText } = renderWithProvider(
355+
<MultipleAlertModal {...defaultProps} />,
356+
mockStoreWithHiddenNavigation,
357+
);
358+
359+
fireEvent.click(getByTestId('alert-modal-next-button'));
360+
361+
expect(queryByText('Hidden Alert')).not.toBeInTheDocument();
362+
expect(getByText('Alert 3')).toBeInTheDocument();
363+
});
364+
365+
it('acknowledges an alert that hides navigation without cycling', () => {
366+
onAcknowledgeClickMock.mockReset();
367+
const { getByTestId } = renderWithProvider(
368+
<MultipleAlertModal
369+
{...defaultProps}
370+
alertKey={HIDDEN_ALERT_KEY_MOCK}
371+
/>,
372+
mockStoreWithHiddenNavigationConfirmed,
373+
);
374+
375+
fireEvent.click(getByTestId('alert-modal-button'));
376+
377+
expect(onAcknowledgeClickMock).toHaveBeenCalledTimes(1);
378+
});
379+
});
285380
});

ui/components/app/alert-system/multiple-alert-modal/multiple-alert-modal.tsx

Lines changed: 100 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -165,73 +165,147 @@ export function MultipleAlertModal({
165165
showCloseIcon = true,
166166
skipAlertNavigation = false,
167167
}: MultipleAlertModalProps) {
168-
const { isAlertConfirmed, fieldAlerts, alerts } = useAlerts(ownerId);
168+
const {
169+
alerts: alertsFromHook,
170+
fieldAlerts: fieldAlertsFromHook,
171+
isAlertConfirmed,
172+
navigableAlerts: navigableAlertsFromHook,
173+
navigableFieldAlerts: navigableFieldAlertsFromHook,
174+
} = useAlerts(ownerId);
175+
176+
const alerts = alertsFromHook ?? [];
177+
const fieldAlerts = fieldAlertsFromHook ?? [];
178+
const navigableAlerts = navigableAlertsFromHook ?? alerts;
179+
const navigableFieldAlerts = navigableFieldAlertsFromHook ?? fieldAlerts;
180+
169181
const alertsToDisplay = displayAllAlerts ? alerts : fieldAlerts;
182+
const navigableAlertsToDisplay = displayAllAlerts
183+
? navigableAlerts
184+
: navigableFieldAlerts;
170185

171-
const initialAlertIndex = alertsToDisplay.findIndex(
172-
(alert: Alert) => alert.key === alertKey,
173-
);
186+
const initialAlertKey =
187+
alertsToDisplay.find((alert) => alert.key === alertKey)?.key ??
188+
navigableAlertsToDisplay[0]?.key ??
189+
alertsToDisplay[0]?.key;
174190

175-
const [selectedIndex, setSelectedIndex] = useState(
176-
initialAlertIndex === -1 ? 0 : initialAlertIndex,
191+
const [currentAlertKey, setCurrentAlertKey] = useState<string | undefined>(
192+
initialAlertKey,
177193
);
178194

179-
// If the selected alert is not found, default to the first alert
180-
const selectedAlert = alertsToDisplay[selectedIndex] ?? alertsToDisplay[0];
195+
const selectedAlert =
196+
alertsToDisplay.find((alert) => alert.key === currentAlertKey) ??
197+
alertsToDisplay[0];
198+
199+
const currentNavigableIndex = navigableAlertsToDisplay.findIndex(
200+
(alert) => alert.key === (selectedAlert?.key ?? currentAlertKey),
201+
);
181202

182203
const hasUnconfirmedAlerts = alerts.some(
183204
(alert: Alert) =>
184205
!isAlertConfirmed(alert.key) && alert.severity === Severity.Danger,
185206
);
186207

187208
const handleBackButtonClick = useCallback(() => {
188-
setSelectedIndex((prevIndex: number) =>
189-
prevIndex > 0 ? prevIndex - 1 : prevIndex,
209+
const activeAlertKey = selectedAlert?.key ?? currentAlertKey;
210+
const newIndex = navigableAlertsToDisplay.findIndex(
211+
(alert) => alert.key === activeAlertKey,
190212
);
191-
}, []);
213+
214+
if (newIndex > 0) {
215+
setCurrentAlertKey(navigableAlertsToDisplay[newIndex - 1]?.key);
216+
}
217+
}, [currentAlertKey, navigableAlertsToDisplay, selectedAlert]);
192218

193219
const handleNextButtonClick = useCallback(() => {
194-
setSelectedIndex((prevIndex: number) => prevIndex + 1);
195-
}, []);
220+
const activeAlertKey = selectedAlert?.key ?? currentAlertKey;
221+
const newIndex = navigableAlertsToDisplay.findIndex(
222+
(alert) => alert.key === activeAlertKey,
223+
);
224+
225+
if (
226+
newIndex !== -1 &&
227+
newIndex + 1 < navigableAlertsToDisplay.length &&
228+
navigableAlertsToDisplay[newIndex + 1]?.key
229+
) {
230+
setCurrentAlertKey(navigableAlertsToDisplay[newIndex + 1]?.key);
231+
}
232+
}, [currentAlertKey, navigableAlertsToDisplay, selectedAlert]);
196233

197234
const handleAcknowledgeClick = useCallback(() => {
198-
if (skipAlertNavigation) {
235+
const activeAlertKey = selectedAlert?.key ?? currentAlertKey;
236+
const activeAlertHidesNavigation =
237+
selectedAlert?.hideFromAlertNavigation === true;
238+
239+
if (skipAlertNavigation || !activeAlertKey) {
199240
onFinalAcknowledgeClick();
200241
return;
201242
}
202243

203-
if (selectedIndex + 1 === alertsToDisplay.length) {
244+
if (activeAlertHidesNavigation) {
245+
onFinalAcknowledgeClick();
246+
return;
247+
}
248+
249+
const navigableIndex = navigableAlertsToDisplay.findIndex(
250+
(alert) => alert.key === activeAlertKey,
251+
);
252+
253+
if (navigableIndex === -1) {
254+
onFinalAcknowledgeClick();
255+
return;
256+
}
257+
258+
const isLastNavigableAlert =
259+
navigableIndex === navigableAlertsToDisplay.length - 1;
260+
261+
if (isLastNavigableAlert) {
204262
if (!hasUnconfirmedAlerts) {
205263
onFinalAcknowledgeClick();
206264
return;
207265
}
208266

209-
setSelectedIndex(0);
267+
const firstNavigableKey = navigableAlertsToDisplay[0]?.key;
268+
if (firstNavigableKey) {
269+
setCurrentAlertKey(firstNavigableKey);
270+
}
271+
onFinalAcknowledgeClick();
210272
return;
211273
}
212-
handleNextButtonClick();
274+
275+
const nextAlertKey = navigableAlertsToDisplay[navigableIndex + 1]?.key;
276+
if (nextAlertKey) {
277+
setCurrentAlertKey(nextAlertKey);
278+
}
213279
}, [
280+
currentAlertKey,
214281
onFinalAcknowledgeClick,
215-
handleNextButtonClick,
216-
selectedIndex,
217-
alertsToDisplay.length,
218282
hasUnconfirmedAlerts,
283+
navigableAlertsToDisplay,
284+
selectedAlert,
219285
skipAlertNavigation,
220286
]);
221287

288+
const showNavigationButtons =
289+
!skipAlertNavigation &&
290+
selectedAlert?.hideFromAlertNavigation !== true &&
291+
currentNavigableIndex !== -1 &&
292+
navigableAlertsToDisplay.length > 1;
293+
222294
return (
223295
<AlertModal
224296
ownerId={ownerId}
225297
onAcknowledgeClick={handleAcknowledgeClick}
226298
alertKey={selectedAlert?.key}
227299
onClose={onClose}
228300
headerStartAccessory={
229-
<PageNavigation
230-
alerts={alertsToDisplay}
231-
onBackButtonClick={handleBackButtonClick}
232-
onNextButtonClick={handleNextButtonClick}
233-
selectedIndex={selectedIndex}
234-
/>
301+
showNavigationButtons ? (
302+
<PageNavigation
303+
alerts={navigableAlertsToDisplay}
304+
onBackButtonClick={handleBackButtonClick}
305+
onNextButtonClick={handleNextButtonClick}
306+
selectedIndex={currentNavigableIndex}
307+
/>
308+
) : null
235309
}
236310
showCloseIcon={showCloseIcon}
237311
/>

ui/components/app/confirm/info/row/alert-row/alert-row.tsx

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,15 @@ export const ConfirmInfoAlertRow = ({
5454
const { getFieldAlerts } = useAlerts(ownerId);
5555
const fieldAlerts = getFieldAlerts(alertKey);
5656
const hasFieldAlert = fieldAlerts.length > 0;
57-
const selectedAlertSeverity = fieldAlerts[0]?.severity;
58-
const selectedAlertKey = fieldAlerts[0]?.key;
59-
const selectedAlertShowArrow = fieldAlerts[0]?.showArrow;
60-
const selectedAlertInlineAlertText = fieldAlerts[0]?.inlineAlertText;
57+
const selectedAlert = fieldAlerts[0];
58+
const selectedAlertSeverity = selectedAlert?.severity;
59+
const selectedAlertKey = selectedAlert?.key;
60+
const selectedAlertShowArrow = selectedAlert?.showArrow;
61+
const selectedAlertInlineAlertText = selectedAlert?.inlineAlertText;
6162
const selectedAlertIsOpenModalOnClick =
62-
fieldAlerts[0]?.isOpenModalOnClick ?? true;
63+
selectedAlert?.isOpenModalOnClick ?? true;
64+
const selectedAlertHideFromAlertNavigation =
65+
selectedAlert?.hideFromAlertNavigation ?? false;
6366

6467
const [alertModalVisible, setAlertModalVisible] = useState<boolean>(false);
6568

@@ -139,7 +142,7 @@ export const ConfirmInfoAlertRow = ({
139142
onFinalAcknowledgeClick={handleModalClose}
140143
onClose={handleModalClose}
141144
showCloseIcon={false}
142-
skipAlertNavigation={true}
145+
skipAlertNavigation={selectedAlertHideFromAlertNavigation}
143146
/>
144147
)}
145148
{confirmInfoRow}

ui/ducks/confirm-alerts/confirm-alerts.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,11 @@ export type Alert = {
6868
*/
6969
severity: AlertSeverity;
7070

71+
/**
72+
* Whether this alert should be excluded from navigation controls.
73+
*/
74+
hideFromAlertNavigation?: boolean;
75+
7176
/**
7277
* Whether to show the arrow icon on the inline alert.
7378
*/

0 commit comments

Comments
 (0)