11import { useAlerts } from '@dhis2/app-runtime'
2- import { AlertBar , AlertStack } from '@dhis2/ui'
3- import React , { useState , useEffect } from 'react'
2+ import { AlertStack , AlertBar } from '@dhis2/ui'
3+ import React , { useCallback , useState } from 'react'
44
5- /*
6- * The alert-manager which populates the `useAlerts` hook from `@dhis2/app-service-alerts`
7- * hook with alerts only supports simply adding and removing alerts. However, the
8- * `AlertBar` from `@dhis2/ui` should leave the screen with a hide-animation, so this
9- * requires an additional state. The `alertStackAlerts` state in the Alerts component
10- * provides this addional state:
11- * - It contains all alerts from the alert-manager, with `options.hidden` set to `false`
12- * - And also alerts which have been removed from the alert-manager, but still have their
13- * leave animation in progress, whtih `options.hidden` set to `true`)
14- * Alerts are removed from the `alertStackAlerts` state once the `onHidden` callback fires
15- */
5+ /* The alerts-manager which populates the `useAlerts` hook from
6+ * `@dhis2/app-service-alerts` hook with alerts only supports
7+ * simply adding and removing alerts. However, the `AlertBar`
8+ * from `@dhis2/ui` should leave the screen with a hide-animation.
9+ * This works well, for alerts that hide "naturally" (after the
10+ * timeout expires or when the close icon is clicked). In these
11+ * cases the component will request to be removed from the alerts-
12+ * manager after the animation completes. However, when
13+ * programatically hiding an alert this is the other way around:
14+ * the alert is removed from the alerts-manager straight away and
15+ * if we were to render the alerts from the `useAlerts` hook, these
16+ * alerts would be removed from the DOM abruptly without an animation.
17+ * To prevent this from happening, we have implemented the
18+ * `useAlertsWithHideCache` hook:
19+ * - It contains all alerts from the alert-manager, with
20+ * `options.hidden` set to `false`
21+ * - And also alerts which have been removed from the alert-manager,
22+ * but still have their leave animation in progress, with
23+ * `options.hidden` set to `true`
24+ * - Alerts are removed once the `onHidden` callback fires */
1625
17- const Alerts = ( ) => {
26+ const useAlertsWithHideCache = ( ) => {
27+ const [ alertsMap ] = useState ( new Map ( ) )
28+ /* We don't use this state value, it is used to trigger
29+ * a rerender to remove the hidden alert from the DOM */
30+ const [ , setLastRemovedId ] = useState ( null )
1831 const alertManagerAlerts = useAlerts ( )
19- const [ alertStackAlerts , setAlertStackAlerts ] = useState ( alertManagerAlerts )
20- const removeAlertStackAlert = ( id ) =>
21- setAlertStackAlerts (
22- alertStackAlerts . filter (
23- ( alertStackAlert ) => alertStackAlert . id !== id
24- )
25- )
32+ const updateAlertsFromManager = useCallback (
33+ ( newAlerts = [ ] ) => {
34+ const newAlertsIdLookup = new Set ( )
35+ newAlerts . forEach ( ( alert ) => {
36+ newAlertsIdLookup . add ( alert . id )
37+ if ( ! alertsMap . has ( alert . id ) ) {
38+ // new alerts, these are not hiding
39+ alertsMap . set ( alert . id , {
40+ ...alert ,
41+ options : {
42+ ...alert . options ,
43+ hidden : alert . options . hidden || false ,
44+ } ,
45+ } )
46+ }
47+ } )
48+ // alerts in alertsMap but not in newAlerts are hiding
49+ alertsMap . forEach ( ( alert ) => {
50+ if ( ! newAlertsIdLookup . has ( alert . id ) ) {
51+ alert . options . hidden = true
52+ }
53+ } )
54+ } ,
55+ [ alertsMap ]
56+ )
57+ const removeAlert = useCallback (
58+ ( id ) => {
59+ alertsMap . delete ( id )
60+ setLastRemovedId ( id )
61+ } ,
62+ [ alertsMap ]
63+ )
2664
27- useEffect ( ( ) => {
28- if ( alertManagerAlerts . length > 0 ) {
29- setAlertStackAlerts ( ( currentAlertStackAlerts ) =>
30- mergeAlertStackAlerts (
31- currentAlertStackAlerts ,
32- alertManagerAlerts
33- )
34- )
35- }
36- } , [ alertManagerAlerts ] )
65+ updateAlertsFromManager ( alertManagerAlerts )
66+
67+ return {
68+ alerts : Array . from ( alertsMap . values ( ) ) . sort ( ( a , b ) => a . id - b . id ) ,
69+ removeAlert ,
70+ }
71+ }
72+
73+ const Alerts = ( ) => {
74+ const { alerts , removeAlert } = useAlertsWithHideCache ( )
3775
3876 return (
3977 < AlertStack >
40- { alertStackAlerts . map (
78+ { alerts . map (
4179 ( { message, remove, id, options : { onHidden, ...props } } ) => (
4280 < AlertBar
4381 { ...props }
4482 key = { id }
4583 onHidden = { ( ) => {
4684 onHidden && onHidden ( )
47- removeAlertStackAlert ( id )
48- if ( alertManagerAlerts . some ( ( a ) => a . id === id ) ) {
49- remove ( )
50- }
85+ removeAlert ( id )
86+ remove ( )
5187 } }
5288 >
5389 { message }
@@ -58,34 +94,4 @@ const Alerts = () => {
5894 )
5995}
6096
61- function mergeAlertStackAlerts ( alertStackAlerts , alertManagerAlerts ) {
62- return Object . values ( {
63- /*
64- * Assume that all alerts in the alertStackAlerts array are hiding.
65- * After the object merge only the alerts not in the alertManagerAlerts
66- * array will have `options.hidden === true`.
67- */
68- ...toIdBasedObjectWithHiddenOption ( alertStackAlerts , true ) ,
69- /*
70- * All alertManagerAlerts should be showing. This object merge will
71- * overwrite any alertStackAlert by the alertManagerAlert with
72- * the same `id`, thus ensuring the alert is visible.
73- */
74- ...toIdBasedObjectWithHiddenOption ( alertManagerAlerts , false ) ,
75- } )
76- }
77-
78- function toIdBasedObjectWithHiddenOption ( arr , hidden ) {
79- return arr . reduce ( ( obj , item ) => {
80- obj [ item . id ] = {
81- ...item ,
82- options : {
83- ...item . options ,
84- hidden,
85- } ,
86- }
87- return obj
88- } , { } )
89- }
90-
91- export { Alerts , mergeAlertStackAlerts }
97+ export { Alerts }
0 commit comments