Skip to content

Commit

Permalink
Merge branch 'master' into alpha
Browse files Browse the repository at this point in the history
  • Loading branch information
KaiVandivier committed Jul 8, 2024
2 parents 9cf797c + 82f05d5 commit c8331ac
Show file tree
Hide file tree
Showing 17 changed files with 1,577 additions and 1,454 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@

### Bug Fixes

* 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))
* 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))

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

Expand Down
13 changes: 8 additions & 5 deletions adapter/i18n/en.pot
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ msgstr ""
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
"POT-Creation-Date: 2024-04-24T13:58:13.591Z\n"
"PO-Revision-Date: 2024-04-24T13:58:13.591Z\n"
"POT-Creation-Date: 2024-06-21T08:27:55.991Z\n"
"PO-Revision-Date: 2024-06-21T08:27:55.991Z\n"

msgid "Save your data"
msgstr "Save your data"
Expand Down Expand Up @@ -39,6 +39,12 @@ msgstr "An error occurred in the DHIS2 application."
msgid "Technical details copied to clipboard"
msgstr "Technical details copied to clipboard"

msgid "There was a problem loading this plugin"
msgstr "There was a problem loading this plugin"

msgid "Copy debug info to clipboard"
msgstr "Copy debug info to clipboard"

msgid "Try again"
msgstr "Try again"

Expand All @@ -48,9 +54,6 @@ msgstr "Something went wrong"
msgid "Redirect to safe login mode"
msgstr "Redirect to safe login mode"

msgid "Redirect to safe login mode"
msgstr "Redirect to safe login mode"

msgid "Hide technical details"
msgstr "Hide technical details"

Expand Down
2 changes: 1 addition & 1 deletion adapter/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
"peerDependencies": {
"@dhis2/app-runtime": "^3.10.4",
"@dhis2/d2-i18n": "^1",
"@dhis2/ui": ">=9.4.4",
"@dhis2/ui": ">=9.8.9",
"classnames": "^2",
"moment": "^2",
"prop-types": "^15",
Expand Down
140 changes: 73 additions & 67 deletions adapter/src/components/Alerts.js
Original file line number Diff line number Diff line change
@@ -1,53 +1,89 @@
import { useAlerts } from '@dhis2/app-runtime'
import { AlertBar, AlertStack } from '@dhis2/ui'
import React, { useState, useEffect } from 'react'
import { AlertStack, AlertBar } from '@dhis2/ui'
import React, { useCallback, useState } from 'react'

/*
* The alert-manager which populates the `useAlerts` hook from `@dhis2/app-service-alerts`
* hook with alerts only supports simply adding and removing alerts. However, the
* `AlertBar` from `@dhis2/ui` should leave the screen with a hide-animation, so this
* requires an additional state. The `alertStackAlerts` state in the Alerts component
* provides this addional state:
* - It contains all alerts from the alert-manager, with `options.hidden` set to `false`
* - And also alerts which have been removed from the alert-manager, but still have their
* leave animation in progress, whtih `options.hidden` set to `true`)
* Alerts are removed from the `alertStackAlerts` state once the `onHidden` callback fires
*/
/* The alerts-manager which populates the `useAlerts` hook from
* `@dhis2/app-service-alerts` hook with alerts only supports
* simply adding and removing alerts. However, the `AlertBar`
* from `@dhis2/ui` should leave the screen with a hide-animation.
* This works well, for alerts that hide "naturally" (after the
* timeout expires or when the close icon is clicked). In these
* cases the component will request to be removed from the alerts-
* manager after the animation completes. However, when
* programatically hiding an alert this is the other way around:
* the alert is removed from the alerts-manager straight away and
* if we were to render the alerts from the `useAlerts` hook, these
* alerts would be removed from the DOM abruptly without an animation.
* To prevent this from happening, we have implemented the
* `useAlertsWithHideCache` hook:
* - It contains all alerts from the alert-manager, with
* `options.hidden` set to `false`
* - And also alerts which have been removed from the alert-manager,
* but still have their leave animation in progress, with
* `options.hidden` set to `true`
* - Alerts are removed once the `onHidden` callback fires */

const Alerts = () => {
const useAlertsWithHideCache = () => {
const [alertsMap] = useState(new Map())
/* We don't use this state value, it is used to trigger
* a rerender to remove the hidden alert from the DOM */
const [, setLastRemovedId] = useState(null)
const alertManagerAlerts = useAlerts()
const [alertStackAlerts, setAlertStackAlerts] = useState(alertManagerAlerts)
const removeAlertStackAlert = (id) =>
setAlertStackAlerts(
alertStackAlerts.filter(
(alertStackAlert) => alertStackAlert.id !== id
)
)
const updateAlertsFromManager = useCallback(
(newAlerts = []) => {
const newAlertsIdLookup = new Set()
newAlerts.forEach((alert) => {
newAlertsIdLookup.add(alert.id)
if (!alertsMap.has(alert.id)) {
// new alerts, these are not hiding
alertsMap.set(alert.id, {
...alert,
options: {
...alert.options,
hidden: alert.options.hidden || false,
},
})
}
})
// alerts in alertsMap but not in newAlerts are hiding
alertsMap.forEach((alert) => {
if (!newAlertsIdLookup.has(alert.id)) {
alert.options.hidden = true
}
})
},
[alertsMap]
)
const removeAlert = useCallback(
(id) => {
alertsMap.delete(id)
setLastRemovedId(id)
},
[alertsMap]
)

useEffect(() => {
if (alertManagerAlerts.length > 0) {
setAlertStackAlerts((currentAlertStackAlerts) =>
mergeAlertStackAlerts(
currentAlertStackAlerts,
alertManagerAlerts
)
)
}
}, [alertManagerAlerts])
updateAlertsFromManager(alertManagerAlerts)

return {
alerts: Array.from(alertsMap.values()).sort((a, b) => a.id - b.id),
removeAlert,
}
}

const Alerts = () => {
const { alerts, removeAlert } = useAlertsWithHideCache()

return (
<AlertStack>
{alertStackAlerts.map(
{alerts.map(
({ message, remove, id, options: { onHidden, ...props } }) => (
<AlertBar
{...props}
key={id}
onHidden={() => {
onHidden && onHidden()
removeAlertStackAlert(id)
if (alertManagerAlerts.some((a) => a.id === id)) {
remove()
}
removeAlert(id)
remove()
}}
>
{message}
Expand All @@ -58,34 +94,4 @@ const Alerts = () => {
)
}

function mergeAlertStackAlerts(alertStackAlerts, alertManagerAlerts) {
return Object.values({
/*
* Assume that all alerts in the alertStackAlerts array are hiding.
* After the object merge only the alerts not in the alertManagerAlerts
* array will have `options.hidden === true`.
*/
...toIdBasedObjectWithHiddenOption(alertStackAlerts, true),
/*
* All alertManagerAlerts should be showing. This object merge will
* overwrite any alertStackAlert by the alertManagerAlert with
* the same `id`, thus ensuring the alert is visible.
*/
...toIdBasedObjectWithHiddenOption(alertManagerAlerts, false),
})
}

function toIdBasedObjectWithHiddenOption(arr, hidden) {
return arr.reduce((obj, item) => {
obj[item.id] = {
...item,
options: {
...item.options,
hidden,
},
}
return obj
}, {})
}

export { Alerts, mergeAlertStackAlerts }
export { Alerts }
50 changes: 45 additions & 5 deletions adapter/src/components/ErrorBoundary.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,35 @@ import styles from './styles/ErrorBoundary.style.js'
// In order to avoid using @dhis2/ui components in the error boundary - as anything
// that breaks within it will not be caught properly - we define a component
// with the same styles as Button
const UIButton = ({ children, onClick }) => (
const UIButton = ({ children, onClick, plugin }) => (
<>
<style jsx>{buttonStyles}</style>
<button onClick={onClick}>{children}</button>
<button className={plugin ? 'pluginButton' : null} onClick={onClick}>
{children}
</button>
</>
)

UIButton.propTypes = {
children: PropTypes.node.isRequired,
onClick: PropTypes.func.isRequired,
plugin: PropTypes.bool,
}

const InfoIcon24 = () => (
<svg
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
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"
fill="#A0ADBA"
></path>
</svg>
)

const translatedErrorHeading = i18n.t(
'An error occurred in the DHIS2 application.'
)
Expand Down Expand Up @@ -61,6 +78,13 @@ export class ErrorBoundary extends Component {
})
}

handleCopyErrorDetailsPlugin = ({ error, errorInfo }) => {
const errorDetails = `${error}\n${error?.stack}\n${errorInfo?.componentStack}`
navigator.clipboard.writeText(errorDetails).then(() => {
alert(i18n.t('Technical details copied to clipboard'))
})
}

handleSafeLoginRedirect = () => {
window.location.href =
this.props.baseURL +
Expand All @@ -77,10 +101,26 @@ export class ErrorBoundary extends Component {
<>
<style jsx>{styles}</style>
<div className="pluginBoundary">
<span>I am the default plugin boundary</span>
<InfoIcon24 />
<div className="pluginErrorMessage">
{i18n.t(
'There was a problem loading this plugin'
)}
</div>
<div
className="pluginErrorCopy"
onClick={() => {
this.handleCopyErrorDetailsPlugin({
error: this.state.error,
errorInfo: this.state.errorInfo,
})
}}
>
{i18n.t('Copy debug info to clipboard')}
</div>
{onRetry && (
<div className="retry">
<UIButton onClick={onRetry}>
<div className="pluginRetry">
<UIButton onClick={onRetry} plugin>
{i18n.t('Try again')}
</UIButton>
</div>
Expand Down
Loading

0 comments on commit c8331ac

Please sign in to comment.