From 0cc2fbd5b633b9b8953ba29d8450f93b0037d458 Mon Sep 17 00:00:00 2001 From: Thomas Zemp Date: Tue, 5 Mar 2024 15:28:54 +0100 Subject: [PATCH] feat: updates for login_app type [LIBS-405] (#831) --- adapter/src/components/LoginAppWrapper.js | 32 +++++++++ .../src/components/ServerVersionProvider.js | 34 ++++++++- adapter/src/index.js | 72 +++++++++++++------ adapter/src/utils/useLocale.js | 30 +++++++- cli/src/commands/build.js | 9 +-- cli/src/commands/start.js | 3 +- cli/src/lib/compiler/compile.js | 11 +-- cli/src/lib/compiler/entrypoints.js | 3 +- cli/src/lib/parseConfig.js | 10 ++- cli/src/lib/pwa/getPWAEnvVars.js | 4 +- cli/src/lib/shell/index.js | 1 + docs/config/d2-config-js-reference.md | 2 +- docs/config/types.md | 2 + shell/src/App.js | 1 + 14 files changed, 174 insertions(+), 40 deletions(-) create mode 100644 adapter/src/components/LoginAppWrapper.js diff --git a/adapter/src/components/LoginAppWrapper.js b/adapter/src/components/LoginAppWrapper.js new file mode 100644 index 000000000..7ad42d5af --- /dev/null +++ b/adapter/src/components/LoginAppWrapper.js @@ -0,0 +1,32 @@ +import PropTypes from 'prop-types' +import React from 'react' +import { useSystemDefaultLocale } from '../utils/useLocale.js' +import { Alerts } from './Alerts.js' +import { ErrorBoundary } from './ErrorBoundary.js' +import { LoadingMask } from './LoadingMask.js' +import { styles } from './styles/AppWrapper.style.js' + +export const LoginAppWrapper = ({ children }) => { + const { loading: localeLoading } = useSystemDefaultLocale() + // cannot check current user for a loginApp (no api/me) + + if (localeLoading) { + return + } + + return ( +
+ +
+ window.location.reload()}> + {children} + +
+ +
+ ) +} + +LoginAppWrapper.propTypes = { + children: PropTypes.node, +} diff --git a/adapter/src/components/ServerVersionProvider.js b/adapter/src/components/ServerVersionProvider.js index 6dfc87cca..51430e9fa 100644 --- a/adapter/src/components/ServerVersionProvider.js +++ b/adapter/src/components/ServerVersionProvider.js @@ -17,6 +17,7 @@ export const ServerVersionProvider = ({ plugin, parentAlertsAdd, showAlertsInPlugin, + loginApp, children, }) => { const offlineInterface = useOfflineInterface() @@ -103,6 +104,27 @@ export const ServerVersionProvider = ({ ? state : { loading: true, error: undefined, systemInfo: undefined } ) + + // version is not currently available, minimum for login app + if (loginApp) { + const requestLogin = get(`${baseUrl}/api/loginConfig`) + requestLogin + .then((loginConfig) => { + setSystemInfoState({ + loading: false, + error: undefined, + systemInfo: { version: loginConfig.apiVersion }, + }) + }) + .catch((e) => { + setSystemInfoState({ + loading: false, + error: e, + systemInfo: undefined, + }) + }) + return + } const request = get(`${baseUrl}/api/system/info`) request .then((systemInfo) => { @@ -125,7 +147,7 @@ export const ServerVersionProvider = ({ return () => { request.abort() } - }, [appName, baseUrl]) + }, [appName, baseUrl, loginApp]) useEffect(() => { if (pwaEnabled) { @@ -137,7 +159,11 @@ export const ServerVersionProvider = ({ // This needs to come before 'loading' case to show modal at correct times if (systemInfoState.error || baseUrlState.error) { - return + return !loginApp ? ( + + ) : ( +

Specify DHIS2_BASE_URL environment variable

+ ) } if ( @@ -162,10 +188,11 @@ export const ServerVersionProvider = ({ systemInfo, pwaEnabled, }} - offlineInterface={offlineInterface} + offlineInterface={loginApp ? null : offlineInterface} plugin={plugin} parentAlertsAdd={parentAlertsAdd} showAlertsInPlugin={showAlertsInPlugin} + skipApiVersion={loginApp ? true : false} > {children} @@ -177,6 +204,7 @@ ServerVersionProvider.propTypes = { appVersion: PropTypes.string.isRequired, apiVersion: PropTypes.number, children: PropTypes.element, + loginApp: PropTypes.bool, parentAlertsAdd: PropTypes.func, plugin: PropTypes.bool, pwaEnabled: PropTypes.bool, diff --git a/adapter/src/index.js b/adapter/src/index.js index d47ff3b17..5caa9f30c 100644 --- a/adapter/src/index.js +++ b/adapter/src/index.js @@ -3,6 +3,7 @@ import PropTypes from 'prop-types' import React from 'react' import { AppWrapper } from './components/AppWrapper.js' import { ErrorBoundary } from './components/ErrorBoundary.js' +import { LoginAppWrapper } from './components/LoginAppWrapper.js' import { OfflineInterfaceProvider } from './components/OfflineInterfaceContext.js' import { PWALoadingBoundary } from './components/PWALoadingBoundary.js' import { ServerVersionProvider } from './components/ServerVersionProvider.js' @@ -19,38 +20,64 @@ const AppAdapter = ({ showAlertsInPlugin, onPluginError, clearPluginError, + loginApp, children, -}) => ( - - - +}) => { + if (loginApp) { + return ( + { + window.location.reload() + }} + plugin={false} + > - {children} + + + ) + } + return ( + + + + - {children} - - - - - -) + + {children} + + + + + + ) +} AppAdapter.propTypes = { appName: PropTypes.string.isRequired, @@ -59,6 +86,7 @@ AppAdapter.propTypes = { children: PropTypes.element, clearPluginError: PropTypes.func, direction: PropTypes.oneOf(['ltr', 'rtl', 'auto']), + loginApp: PropTypes.bool, parentAlertsAdd: PropTypes.func, plugin: PropTypes.bool, pwaEnabled: PropTypes.bool, diff --git a/adapter/src/utils/useLocale.js b/adapter/src/utils/useLocale.js index 0187622c1..b1ee33490 100644 --- a/adapter/src/utils/useLocale.js +++ b/adapter/src/utils/useLocale.js @@ -1,6 +1,6 @@ import { useDataQuery } from '@dhis2/app-runtime' import i18n from '@dhis2/d2-i18n' -import { useState, useEffect } from 'react' +import { useState, useEffect, useMemo } from 'react' import { setI18nLocale, parseLocale, @@ -56,3 +56,31 @@ export const useCurrentUserLocale = (configDirection) => { return { loading: loading || !locale, locale, direction } } + +const loginConfigQuery = { + loginConfig: { + resource: 'loginConfig', + }, +} + +export const useSystemDefaultLocale = () => { + // system language from loginConfiqQuery + const { loading, data, error } = useDataQuery(loginConfigQuery) + // set userSettings to use system locale by default + const localeInformation = useMemo( + () => ({ + userSettings: { + keyUiLocale: + data && + (data?.loginConfig?.uiLocale || window.navigator.language), + }, + configDirection: 'auto', + }), + [data] + ) + const locale = useLocale(localeInformation) + if (error) { + console.error(error) + } + return { loading: loading || !locale, locale } +} diff --git a/cli/src/commands/build.js b/cli/src/commands/build.js index c7c749cc4..20819901d 100644 --- a/cli/src/commands/build.js +++ b/cli/src/commands/build.js @@ -7,6 +7,7 @@ const generateManifests = require('../lib/generateManifests') const i18n = require('../lib/i18n') const loadEnvFiles = require('../lib/loadEnvFiles') const parseConfig = require('../lib/parseConfig') +const { isApp } = require('../lib/parseConfig') const makePaths = require('../lib/paths') const makePlugin = require('../lib/plugin') const { injectPrecacheManifest } = require('../lib/pwa') @@ -71,7 +72,7 @@ const handler = async ({ const shell = makeShell({ config, paths }) const plugin = makePlugin({ config, paths }) - if (config.type === 'app') { + if (isApp(config.type)) { setAppParameters(standalone, config) } @@ -106,7 +107,7 @@ const handler = async ({ paths, }) - if (config.type === 'app') { + if (isApp(config.type)) { reporter.info('Bootstrapping local appShell...') await shell.bootstrap({ shell: shellSource, force }) } @@ -115,7 +116,7 @@ const handler = async ({ `Building ${config.type} ${chalk.bold(config.name)}...` ) - if (config.type === 'app') { + if (isApp(config.type)) { await compile({ config, paths, @@ -168,7 +169,7 @@ const handler = async ({ } ) - if (config.type === 'app') { + if (isApp(config.type)) { if (!fs.pathExistsSync(paths.shellBuildOutput)) { reporter.error('No build output found') process.exit(1) diff --git a/cli/src/commands/start.js b/cli/src/commands/start.js index f2255b53d..660cea866 100644 --- a/cli/src/commands/start.js +++ b/cli/src/commands/start.js @@ -6,6 +6,7 @@ const generateManifests = require('../lib/generateManifests') const i18n = require('../lib/i18n') const loadEnvFiles = require('../lib/loadEnvFiles') const parseConfig = require('../lib/parseConfig') +const { isApp } = require('../lib/parseConfig') const makePaths = require('../lib/paths') const makePlugin = require('../lib/plugin') const createProxyServer = require('../lib/proxy') @@ -33,7 +34,7 @@ const handler = async ({ const shell = makeShell({ config, paths }) const plugin = makePlugin({ config, paths }) - if (config.type !== 'app') { + if (!isApp(config.type)) { reporter.error( `The command ${chalk.bold( 'd2-app-scripts start' diff --git a/cli/src/lib/compiler/compile.js b/cli/src/lib/compiler/compile.js index 775ef8836..94842eae1 100644 --- a/cli/src/lib/compiler/compile.js +++ b/cli/src/lib/compiler/compile.js @@ -4,6 +4,7 @@ const { reporter, prettyPrint } = require('@dhis2/cli-helpers-engine') const chokidar = require('chokidar') const fs = require('fs-extra') const makeBabelConfig = require('../../../config/makeBabelConfig.js') +const { isApp } = require('../parseConfig') const { verifyEntrypoints, createAppEntrypointWrapper, @@ -67,10 +68,10 @@ const compile = async ({ mode = 'development', watch = false, }) => { - const isApp = config.type === 'app' + const isAppType = isApp(config.type) verifyEntrypoints({ config, paths }) - if (isApp) { + if (isAppType) { await createAppEntrypointWrapper({ entrypoint: config.entryPoints.app, paths, @@ -83,13 +84,13 @@ const compile = async ({ } } - const outDir = isApp + const outDir = isAppType ? paths.shellApp : path.join(paths.buildOutput, moduleType) fs.removeSync(outDir) fs.ensureDirSync(outDir) - if (isApp) { + if (isAppType) { fs.removeSync(paths.shellPublic) fs.copySync(paths.shellSourcePublic, paths.shellPublic) } @@ -127,7 +128,7 @@ const compile = async ({ processFileCallback: compileFile, watch, }), - isApp && + isAppType && watchFiles({ inputDir: paths.public, outputDir: paths.shellPublic, diff --git a/cli/src/lib/compiler/entrypoints.js b/cli/src/lib/compiler/entrypoints.js index bfa801db1..943d548b0 100644 --- a/cli/src/lib/compiler/entrypoints.js +++ b/cli/src/lib/compiler/entrypoints.js @@ -1,6 +1,7 @@ const path = require('path') const { reporter, chalk } = require('@dhis2/cli-helpers-engine') const fs = require('fs-extra') +const { isApp } = require('../parseConfig') const { normalizeExtension } = require('./extensionHelpers.js') const verifyEntrypoint = ({ entrypoint, basePath, resolveModule }) => { @@ -26,7 +27,7 @@ exports.verifyEntrypoints = ({ paths, resolveModule = require.resolve, }) => { - if (config.type === 'app') { + if (isApp(config.type)) { if ( !config.entryPoints || (!config.entryPoints.app && !config.entryPoints.plugin) diff --git a/cli/src/lib/parseConfig.js b/cli/src/lib/parseConfig.js index 72cf0de6f..d1b5a40d0 100644 --- a/cli/src/lib/parseConfig.js +++ b/cli/src/lib/parseConfig.js @@ -5,9 +5,14 @@ const parseAuthorString = require('parse-author') const requiredConfigFields = { app: ['name', 'version', 'title', 'entryPoints.app'], + login_app: ['name', 'version', 'title', 'entryPoints.app'], lib: ['name', 'version', 'entryPoints.lib'], } +const appTypes = ['app', 'login_app'] + +const isApp = (type) => appTypes.includes(type) + const parseAuthor = (author) => { if (isPlainObject(author)) { return { @@ -58,7 +63,7 @@ const parseConfigObjects = ( config = defaultsDeep(config, defaults) // Add PWA defaults to apps - if (type === 'app') { + if (isApp(type)) { config = defaultsDeep(config, defaultsPWA) } @@ -112,3 +117,6 @@ const parseConfig = (paths) => { module.exports = parseConfig module.exports.parseConfigObjects = parseConfigObjects + +module.exports.parseConfigObjects = parseConfigObjects +module.exports.isApp = isApp diff --git a/cli/src/lib/pwa/getPWAEnvVars.js b/cli/src/lib/pwa/getPWAEnvVars.js index 6b88710bd..eb6e39a03 100644 --- a/cli/src/lib/pwa/getPWAEnvVars.js +++ b/cli/src/lib/pwa/getPWAEnvVars.js @@ -1,3 +1,5 @@ +const { isApp } = require('../parseConfig') + /** Preps string literals for regex conversion by escaping special chars */ function escapeForRegex(string) { return string.replace(/[-\\^$*+?.()|[\]{}]/g, '\\$&') @@ -31,7 +33,7 @@ function stringifyPatterns(patternsList) { * @param {Object} config */ function getPWAEnvVars(config) { - if (config.type !== 'app' || !config.pwa.enabled) { + if (!isApp(config.type) || !config.pwa.enabled) { return null } return { diff --git a/cli/src/lib/shell/index.js b/cli/src/lib/shell/index.js index c7edc4937..d2e75c946 100644 --- a/cli/src/lib/shell/index.js +++ b/cli/src/lib/shell/index.js @@ -7,6 +7,7 @@ module.exports = ({ config, paths }) => { const baseEnvVars = { name: config.title, version: config.version, + loginApp: config.type === 'login_app', } if (config.direction) { diff --git a/docs/config/d2-config-js-reference.md b/docs/config/d2-config-js-reference.md index eb3fae8ce..25da59c89 100644 --- a/docs/config/d2-config-js-reference.md +++ b/docs/config/d2-config-js-reference.md @@ -13,7 +13,7 @@ The following configuration properties are supported: | Property | Type | Default | Description | | :--------------------: | :---------------------------: | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **type** | _string_ | **app** | Either **app** or **lib** | +| **type** | _string_ | **app** | Either **app**, **login_app** or **lib** | | **name** | _string_ | `pkg.name` | A short, machine-readable unique name for this app | | **title** | _string_ | `config.name` | The human-readable application title, which will appear in the HeaderBar | | **direction** | `'ltr'`, `'rtl'`, or `'auto'` | `'ltr'` | Sets the `dir` HTML attribute on the `document` of the app. If set to `'auto'`, the direction will be inferred from the current user's UI locale setting. The header bar will always be considered 'auto' and is unaffected by this setting. | diff --git a/docs/config/types.md b/docs/config/types.md index c22ad696b..4f99d6c3e 100644 --- a/docs/config/types.md +++ b/docs/config/types.md @@ -2,4 +2,6 @@ The application platform works to compile, test, and bundle both **applications** and **front-end libraries** in a consistent way. This ensures that all DHIS2 application components are cross-compatible and build processes can be configured from a central location. +The default app type (**app**) is intended to be connected and used within a DHIS2 instance. App platform also supports a type **login_app** which does not require a user to be authenticated (intended for a login app). + Building a Library is nearly identical to building an Application - just set the `type: 'lib'` property in `d2.config.js`. You can then optionally specify an entrypoint source file in `entryPoints.lib` - the default is `src/index.js`. diff --git a/shell/src/App.js b/shell/src/App.js index 9e8106ba4..f7982602d 100644 --- a/shell/src/App.js +++ b/shell/src/App.js @@ -28,6 +28,7 @@ const appConfig = { apiVersion: parseInt(process.env.REACT_APP_DHIS2_API_VERSION), pwaEnabled: process.env.REACT_APP_DHIS2_APP_PWA_ENABLED === 'true', plugin: isPlugin, + loginApp: process.env.REACT_APP_DHIS2_APP_LOGINAPP === 'true', direction: process.env.REACT_APP_DHIS2_APP_DIRECTION, }