From 44fba68f28745e37712152781de0c0a5acf81002 Mon Sep 17 00:00:00 2001 From: Kai Vandivier Date: Wed, 5 Mar 2025 13:26:20 +0100 Subject: [PATCH 1/3] fix: css and i18n for update notification --- i18n/en.pot | 7 ++----- .../header-bar/profile-menu/update-notification.jsx | 7 ++++--- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index a472ad2..65f976d 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -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: 2025-03-05T12:12:10.076Z\n" -"PO-Revision-Date: 2025-03-05T12:12:10.076Z\n" +"POT-Creation-Date: 2025-03-05T12:25:47.036Z\n" +"PO-Revision-Date: 2025-03-05T12:25:47.036Z\n" msgid "Save your data" msgstr "Save your data" @@ -138,8 +138,5 @@ msgstr "Log out" msgid "App updates available — Click to reload" msgstr "App updates available — Click to reload" -msgid "New app version available — Reload to update" -msgstr "New app version available — Reload to update" - msgid "header bar profile" msgstr "header bar profile" diff --git a/src/components/header-bar/profile-menu/update-notification.jsx b/src/components/header-bar/profile-menu/update-notification.jsx index f505032..7a30955 100644 --- a/src/components/header-bar/profile-menu/update-notification.jsx +++ b/src/components/header-bar/profile-menu/update-notification.jsx @@ -53,7 +53,7 @@ export function UpdateNotification({ hideProfileMenu }) { return updateAvailable ? ( ) : null From ae681361ea1b35a2de3de35a411a57d309ac7e12 Mon Sep 17 00:00:00 2001 From: Kai Vandivier Date: Wed, 5 Mar 2025 13:57:47 +0100 Subject: [PATCH 2/3] feat: query apps-bundle.json --- package.json | 5 +- patches/@dhis2+app-service-data+3.14.0.patch | 136 +++++++++++++++++++ src/App.jsx | 6 +- 3 files changed, 144 insertions(+), 3 deletions(-) create mode 100644 patches/@dhis2+app-service-data+3.14.0.patch diff --git a/package.json b/package.json index da7b4e7..5049dd2 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "del-pwa": "rm -rf node_modules/@dhis2/pwa/build", "del-headerbar": "rm -rf node_modules/@dhis2-ui/header-bar/build", "del-offline": "rm -rf node_modules/@dhis2/app-service-offline/build", + "del-data": "rm -rf node_modules/@dhis2/app-service-data/build", "del-plugin": "rm -rf node_modules/@dhis2/app-service-plugin/build", "del-nm-cache": "rm -rf .d2/shell/node_modules/.cache", "cp-adapter": "cp -r ../app-platform/adapter/build node_modules/@dhis2/app-adapter/build", @@ -23,13 +24,15 @@ "cp-pwa": "cp -r ../app-platform/pwa/build node_modules/@dhis2/pwa/build", "cp-headerbar": "cp -r ../ui/components/header-bar/build node_modules/@dhis2-ui/header-bar/build", "cp-offline": "cp -r ../app-runtime/services/offline/build node_modules/@dhis2/app-service-offline/build", + "cp-data": "cp -r ../app-runtime/services/data/build node_modules/@dhis2/app-service-data/build", "cp-plugin": "cp -r ../app-runtime/services/plugin/build node_modules/@dhis2/app-service-plugin/build", "import-adapter": "yarn del-adapter && yarn del-nm-cache && yarn cp-adapter && yarn patch-package @dhis2/app-adapter", "import-cli": "yarn del-cli && yarn cp-cli && yarn patch-package @dhis2/cli-app-scripts", "import-pwa": "yarn del-pwa && yarn del-nm-cache && yarn cp-pwa && yarn patch-package @dhis2/pwa", "import-headerbar": "yarn del-headerbar && yarn del-nm-cache && yarn cp-headerbar && yarn patch-package @dhis2-ui/header-bar", "import-plugin": "yarn del-plugin && yarn del-nm-cache && yarn cp-plugin && yarn patch-package @dhis2/app-service-plugin", - "import-offline": "yarn del-offline && yarn del-nm-cache && yarn cp-offline" + "import-offline": "yarn del-offline && yarn del-nm-cache && yarn cp-offline", + "import-data": "yarn del-data && yarn del-nm-cache && yarn cp-data && yarn patch-package @dhis2/app-service-data" }, "devDependencies": { "@dhis2/cli-app-scripts": "^12.3.0", diff --git a/patches/@dhis2+app-service-data+3.14.0.patch b/patches/@dhis2+app-service-data+3.14.0.patch new file mode 100644 index 0000000..472c291 --- /dev/null +++ b/patches/@dhis2+app-service-data+3.14.0.patch @@ -0,0 +1,136 @@ +diff --git a/node_modules/@dhis2/app-service-data/build/cjs/links/RestAPILink/queryToResourcePath.js b/node_modules/@dhis2/app-service-data/build/cjs/links/RestAPILink/queryToResourcePath.js +index e74eb1b..5f3fdac 100644 +--- a/node_modules/@dhis2/app-service-data/build/cjs/links/RestAPILink/queryToResourcePath.js ++++ b/node_modules/@dhis2/app-service-data/build/cjs/links/RestAPILink/queryToResourcePath.js +@@ -51,6 +51,22 @@ const queryParametersToQueryString = params => { + const actionPrefix = 'action::'; + const isAction = resource => resource.startsWith(actionPrefix); + const makeActionPath = resource => (0, _path.joinPath)('dhis-web-commons', `${resource.substr(actionPrefix.length)}.action`); ++const legacyPrefix = 'legacy::'; ++const isLegacy = resource => resource.startsWith(legacyPrefix); ++const makeLegacyPath = resource => { ++ switch (resource) { ++ case 'legacy::bundledApps': ++ { ++ return 'dhis-web-apps/apps-bundle.json'; ++ } ++ // Not necessary here, but brainstorming: ++ // you can use whatever path you want 🤷 ++ default: ++ { ++ return resource.replace(legacyPrefix, ''); ++ } ++ } ++}; + const skipApiVersion = (resource, config) => { + if (resource === 'tracker' || resource.startsWith('tracker/')) { + var _config$serverVersion, _config$serverVersion2; +@@ -72,7 +88,7 @@ const queryToResourcePath = (link, query, type) => { + params = {} + } = query; + const apiBase = skipApiVersion(resource, link.config) ? link.unversionedApiPath : link.versionedApiPath; +- const base = isAction(resource) ? makeActionPath(resource) : (0, _path.joinPath)(apiBase, resource, id); ++ const base = isAction(resource) ? makeActionPath(resource) : isLegacy(resource) ? makeLegacyPath(resource) : (0, _path.joinPath)(apiBase, resource, id); + (0, _validateQuery.validateResourceQuery)(query, type); + if (Object.keys(params).length) { + return `${base}?${queryParametersToQueryString(params)}`; +diff --git a/node_modules/@dhis2/app-service-data/build/cjs/links/RestAPILink/queryToResourcePath.test.js b/node_modules/@dhis2/app-service-data/build/cjs/links/RestAPILink/queryToResourcePath.test.js +index 36485f5..2daa102 100644 +--- a/node_modules/@dhis2/app-service-data/build/cjs/links/RestAPILink/queryToResourcePath.test.js ++++ b/node_modules/@dhis2/app-service-data/build/cjs/links/RestAPILink/queryToResourcePath.test.js +@@ -34,6 +34,27 @@ describe('queryToResourcePath', () => { + expect((0, _queryToResourcePath.queryToResourcePath)(link, query, 'read')).toBe(`${actionPrefix}test${actionPostfix}?key=value`); + }); + }); ++ // todo: here ++ describe('legacy', () => { ++ it('should return the apps bundle url if using `legacy::bundledApps`', () => { ++ const query = { ++ resource: 'legacy::bundledApps' ++ }; ++ expect((0, _queryToResourcePath.queryToResourcePath)(link, query, 'read')).toBe('dhis-web-apps/apps-bundle.json'); ++ console.log({ ++ apiPath ++ }); ++ }); ++ it('should return the specified path if using `legacy::`', () => { ++ const query = { ++ resource: 'legacy::dhis-web-apps' ++ }; ++ expect((0, _queryToResourcePath.queryToResourcePath)(link, query, 'read')).toBe('dhis-web-apps'); ++ console.log({ ++ apiPath ++ }); ++ }); ++ }); + describe('resource with dot', () => { + it('should leave dots in resources', () => { + const query = { +diff --git a/node_modules/@dhis2/app-service-data/build/es/links/RestAPILink/queryToResourcePath.js b/node_modules/@dhis2/app-service-data/build/es/links/RestAPILink/queryToResourcePath.js +index eb37240..ec5af69 100644 +--- a/node_modules/@dhis2/app-service-data/build/es/links/RestAPILink/queryToResourcePath.js ++++ b/node_modules/@dhis2/app-service-data/build/es/links/RestAPILink/queryToResourcePath.js +@@ -45,6 +45,22 @@ const queryParametersToQueryString = params => { + const actionPrefix = 'action::'; + const isAction = resource => resource.startsWith(actionPrefix); + const makeActionPath = resource => joinPath('dhis-web-commons', `${resource.substr(actionPrefix.length)}.action`); ++const legacyPrefix = 'legacy::'; ++const isLegacy = resource => resource.startsWith(legacyPrefix); ++const makeLegacyPath = resource => { ++ switch (resource) { ++ case 'legacy::bundledApps': ++ { ++ return 'dhis-web-apps/apps-bundle.json'; ++ } ++ // Not necessary here, but brainstorming: ++ // you can use whatever path you want 🤷 ++ default: ++ { ++ return resource.replace(legacyPrefix, ''); ++ } ++ } ++}; + const skipApiVersion = (resource, config) => { + if (resource === 'tracker' || resource.startsWith('tracker/')) { + var _config$serverVersion, _config$serverVersion2; +@@ -66,7 +82,7 @@ export const queryToResourcePath = (link, query, type) => { + params = {} + } = query; + const apiBase = skipApiVersion(resource, link.config) ? link.unversionedApiPath : link.versionedApiPath; +- const base = isAction(resource) ? makeActionPath(resource) : joinPath(apiBase, resource, id); ++ const base = isAction(resource) ? makeActionPath(resource) : isLegacy(resource) ? makeLegacyPath(resource) : joinPath(apiBase, resource, id); + validateResourceQuery(query, type); + if (Object.keys(params).length) { + return `${base}?${queryParametersToQueryString(params)}`; +diff --git a/node_modules/@dhis2/app-service-data/build/es/links/RestAPILink/queryToResourcePath.test.js b/node_modules/@dhis2/app-service-data/build/es/links/RestAPILink/queryToResourcePath.test.js +index 9a44849..689cde1 100644 +--- a/node_modules/@dhis2/app-service-data/build/es/links/RestAPILink/queryToResourcePath.test.js ++++ b/node_modules/@dhis2/app-service-data/build/es/links/RestAPILink/queryToResourcePath.test.js +@@ -32,6 +32,27 @@ describe('queryToResourcePath', () => { + expect(queryToResourcePath(link, query, 'read')).toBe(`${actionPrefix}test${actionPostfix}?key=value`); + }); + }); ++ // todo: here ++ describe('legacy', () => { ++ it('should return the apps bundle url if using `legacy::bundledApps`', () => { ++ const query = { ++ resource: 'legacy::bundledApps' ++ }; ++ expect(queryToResourcePath(link, query, 'read')).toBe('dhis-web-apps/apps-bundle.json'); ++ console.log({ ++ apiPath ++ }); ++ }); ++ it('should return the specified path if using `legacy::`', () => { ++ const query = { ++ resource: 'legacy::dhis-web-apps' ++ }; ++ expect(queryToResourcePath(link, query, 'read')).toBe('dhis-web-apps'); ++ console.log({ ++ apiPath ++ }); ++ }); ++ }); + describe('resource with dot', () => { + it('should leave dots in resources', () => { + const query = { diff --git a/src/App.jsx b/src/App.jsx index 6b2942c..b844571 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -14,9 +14,11 @@ const APPS_INFO_QUERY = { apps: { resource: 'apps', }, - // todo: - // want to get versions of installed apps, i.e. /dhis-web-apps/apps-bundle.json + // Want to get versions of installed apps, i.e. /dhis-web-apps/apps-bundle.json // need to extend app-runtime to get that + bundledApps: { + resource: 'legacy::bundledApps', + }, } const Layout = ({ appsInfoQuery }) => { From bb14cd3219bd35601cc773d23e871bb66b796ff5 Mon Sep 17 00:00:00 2001 From: Kai Vandivier Date: Wed, 5 Mar 2025 13:58:01 +0100 Subject: [PATCH 3/3] feat: add app version and global shell version to debug info --- src/components/ConnectedHeaderbar.jsx | 18 ++++++++++---- .../header-bar/debug-info/use-debug-info.js | 11 ++++++--- .../header-bar/header-bar-context.jsx | 24 +++++++++++++++---- src/components/header-bar/header-bar.jsx | 4 ++++ 4 files changed, 45 insertions(+), 12 deletions(-) diff --git a/src/components/ConnectedHeaderbar.jsx b/src/components/ConnectedHeaderbar.jsx index dfa7f24..2765b36 100644 --- a/src/components/ConnectedHeaderbar.jsx +++ b/src/components/ConnectedHeaderbar.jsx @@ -13,10 +13,15 @@ const getAppDisplayName = (appName, modules) => { )?.displayName } -// currently, not all core apps are included in the api/apps response -const getAppVersion = (appName, apps) => { +// Currently, not all core apps are included in the api/apps response: +// need to check /api/apps and /dhis-web-apps/apps-bundle.json +const getAppVersion = (appName, apps, bundledApps) => { const parsedAppName = appName.replace('dhis-web-', '') - return apps.find((a) => a.short_name === parsedAppName)?.version + // First + return ( + apps.find((a) => a.short_name === parsedAppName)?.version || + bundledApps.find((ba) => ba.name === parsedAppName)?.version + ) } // todo: @@ -69,7 +74,11 @@ export function ConnectedHeaderBar({ appsInfoQuery }) { if (!params.appName || !appsInfoQuery.data) { return } - return getAppVersion(params.appName, appsInfoQuery.data.apps) + return getAppVersion( + params.appName, + appsInfoQuery.data.apps, + appsInfoQuery.data.bundledApps + ) }, [appsInfoQuery.data, params.appName]) // For now, the header bar can only show one "Update available" badge, so @@ -92,7 +101,6 @@ export function ConnectedHeaderBar({ appsInfoQuery }) { { - const { appName, appVersion, systemInfo } = useConfig() + const { appVersion: globalShellVersion, systemInfo } = useConfig() + const { clientAppName, clientAppVersion } = useHeaderBarContext() return { - app_name: appName || null, - app_version: appVersion?.full || null, + app_name: clientAppName || null, + // Unlike global shell version, which comes from config, app version + // comes from the API and is expected to be a string + app_version: clientAppVersion || null, + global_shell_version: globalShellVersion?.full || null, dhis2_version: systemInfo?.version || null, dhis2_revision: systemInfo?.revision || null, } diff --git a/src/components/header-bar/header-bar-context.jsx b/src/components/header-bar/header-bar-context.jsx index 5fcc06e..a8d2238 100644 --- a/src/components/header-bar/header-bar-context.jsx +++ b/src/components/header-bar/header-bar-context.jsx @@ -1,5 +1,5 @@ import PropTypes from 'prop-types' -import React, { createContext, useContext } from 'react' +import React, { createContext, useContext, useMemo } from 'react' const headerBarContext = createContext({ updateAvailable: false, @@ -9,18 +9,34 @@ const headerBarContext = createContext({ export const HeaderBarContextProvider = ({ updateAvailable, onApplyAvailableUpdate, + clientAppName, + clientAppVersion, children, }) => { + const contextValue = useMemo( + () => ({ + updateAvailable, + onApplyAvailableUpdate, + clientAppName, + clientAppVersion, + }), + [ + updateAvailable, + onApplyAvailableUpdate, + clientAppName, + clientAppVersion, + ] + ) return ( - + {children} ) } HeaderBarContextProvider.propTypes = { children: PropTypes.node, + clientAppName: PropTypes.string, + clientAppVersion: PropTypes.string, updateAvailable: PropTypes.bool, onApplyAvailableUpdate: PropTypes.func, } diff --git a/src/components/header-bar/header-bar.jsx b/src/components/header-bar/header-bar.jsx index 0e568b7..12768b0 100755 --- a/src/components/header-bar/header-bar.jsx +++ b/src/components/header-bar/header-bar.jsx @@ -37,6 +37,7 @@ const query = { export const HeaderBar = ({ appName, + appVersion, className, updateAvailable, onApplyAvailableUpdate, @@ -79,6 +80,8 @@ export const HeaderBar = ({
@@ -145,6 +148,7 @@ export const HeaderBar = ({ HeaderBar.propTypes = { appName: PropTypes.string, + appVersion: PropTypes.string, className: PropTypes.string, updateAvailable: PropTypes.bool, onApplyAvailableUpdate: PropTypes.func,