Skip to content

Commit c8331ac

Browse files
committed
Merge branch 'master' into alpha
2 parents 9cf797c + 82f05d5 commit c8331ac

File tree

17 files changed

+1577
-1454
lines changed

17 files changed

+1577
-1454
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939

4040
### Bug Fixes
4141

42-
* don't start plugins for apps without a plugin entrypoint ([#850](https://github.com/dhis2/app-platform/issues/850)) ([a89d4cf](https://github.com/dhis2/app-platform/commit/a89d4cf348f7edc0a52b8ab9aacf96f2de939de4))
42+
* do not start plugins for apps without a plugin entrypoint ([#850](https://github.com/dhis2/app-platform/issues/850)) ([a89d4cf](https://github.com/dhis2/app-platform/commit/a89d4cf348f7edc0a52b8ab9aacf96f2de939de4))
4343

4444
# [11.3.0](https://github.com/dhis2/app-platform/compare/v11.2.2...v11.3.0) (2024-05-30)
4545

adapter/i18n/en.pot

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ msgstr ""
55
"Content-Type: text/plain; charset=utf-8\n"
66
"Content-Transfer-Encoding: 8bit\n"
77
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
8-
"POT-Creation-Date: 2024-04-24T13:58:13.591Z\n"
9-
"PO-Revision-Date: 2024-04-24T13:58:13.591Z\n"
8+
"POT-Creation-Date: 2024-06-21T08:27:55.991Z\n"
9+
"PO-Revision-Date: 2024-06-21T08:27:55.991Z\n"
1010

1111
msgid "Save your data"
1212
msgstr "Save your data"
@@ -39,6 +39,12 @@ msgstr "An error occurred in the DHIS2 application."
3939
msgid "Technical details copied to clipboard"
4040
msgstr "Technical details copied to clipboard"
4141

42+
msgid "There was a problem loading this plugin"
43+
msgstr "There was a problem loading this plugin"
44+
45+
msgid "Copy debug info to clipboard"
46+
msgstr "Copy debug info to clipboard"
47+
4248
msgid "Try again"
4349
msgstr "Try again"
4450

@@ -48,9 +54,6 @@ msgstr "Something went wrong"
4854
msgid "Redirect to safe login mode"
4955
msgstr "Redirect to safe login mode"
5056

51-
msgid "Redirect to safe login mode"
52-
msgstr "Redirect to safe login mode"
53-
5457
msgid "Hide technical details"
5558
msgstr "Hide technical details"
5659

adapter/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
"peerDependencies": {
4141
"@dhis2/app-runtime": "^3.10.4",
4242
"@dhis2/d2-i18n": "^1",
43-
"@dhis2/ui": ">=9.4.4",
43+
"@dhis2/ui": ">=9.8.9",
4444
"classnames": "^2",
4545
"moment": "^2",
4646
"prop-types": "^15",

adapter/src/components/Alerts.js

Lines changed: 73 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,89 @@
11
import { 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 }

adapter/src/components/ErrorBoundary.js

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,35 @@ import styles from './styles/ErrorBoundary.style.js'
88
// In order to avoid using @dhis2/ui components in the error boundary - as anything
99
// that breaks within it will not be caught properly - we define a component
1010
// with the same styles as Button
11-
const UIButton = ({ children, onClick }) => (
11+
const UIButton = ({ children, onClick, plugin }) => (
1212
<>
1313
<style jsx>{buttonStyles}</style>
14-
<button onClick={onClick}>{children}</button>
14+
<button className={plugin ? 'pluginButton' : null} onClick={onClick}>
15+
{children}
16+
</button>
1517
</>
1618
)
1719

1820
UIButton.propTypes = {
1921
children: PropTypes.node.isRequired,
2022
onClick: PropTypes.func.isRequired,
23+
plugin: PropTypes.bool,
2124
}
2225

26+
const InfoIcon24 = () => (
27+
<svg
28+
height="24"
29+
viewBox="0 0 24 24"
30+
width="24"
31+
xmlns="http://www.w3.org/2000/svg"
32+
>
33+
<path
34+
d="m12 2c5.5228475 0 10 4.4771525 10 10s-4.4771525 10-10 10-10-4.4771525-10-10 4.4771525-10 10-10zm0 2c-4.418278 0-8 3.581722-8 8s3.581722 8 8 8 8-3.581722 8-8-3.581722-8-8-8zm1 7v6h-2v-6zm-1-4c.5522847 0 1 .44771525 1 1s-.4477153 1-1 1-1-.44771525-1-1 .4477153-1 1-1z"
35+
fill="#A0ADBA"
36+
></path>
37+
</svg>
38+
)
39+
2340
const translatedErrorHeading = i18n.t(
2441
'An error occurred in the DHIS2 application.'
2542
)
@@ -61,6 +78,13 @@ export class ErrorBoundary extends Component {
6178
})
6279
}
6380

81+
handleCopyErrorDetailsPlugin = ({ error, errorInfo }) => {
82+
const errorDetails = `${error}\n${error?.stack}\n${errorInfo?.componentStack}`
83+
navigator.clipboard.writeText(errorDetails).then(() => {
84+
alert(i18n.t('Technical details copied to clipboard'))
85+
})
86+
}
87+
6488
handleSafeLoginRedirect = () => {
6589
window.location.href =
6690
this.props.baseURL +
@@ -77,10 +101,26 @@ export class ErrorBoundary extends Component {
77101
<>
78102
<style jsx>{styles}</style>
79103
<div className="pluginBoundary">
80-
<span>I am the default plugin boundary</span>
104+
<InfoIcon24 />
105+
<div className="pluginErrorMessage">
106+
{i18n.t(
107+
'There was a problem loading this plugin'
108+
)}
109+
</div>
110+
<div
111+
className="pluginErrorCopy"
112+
onClick={() => {
113+
this.handleCopyErrorDetailsPlugin({
114+
error: this.state.error,
115+
errorInfo: this.state.errorInfo,
116+
})
117+
}}
118+
>
119+
{i18n.t('Copy debug info to clipboard')}
120+
</div>
81121
{onRetry && (
82-
<div className="retry">
83-
<UIButton onClick={onRetry}>
122+
<div className="pluginRetry">
123+
<UIButton onClick={onRetry} plugin>
84124
{i18n.t('Try again')}
85125
</UIButton>
86126
</div>

0 commit comments

Comments
 (0)