diff --git a/packages/build/src/lowdefySchema.json b/packages/build/src/lowdefySchema.json index e9f10e74c..cd698b7a7 100644 --- a/packages/build/src/lowdefySchema.json +++ b/packages/build/src/lowdefySchema.json @@ -591,6 +591,13 @@ "type": "App \"config.homePageId\" should be a string." } }, + "experimental_initPageId": { + "type": "string", + "description": "Id of the page to load when app is first run. After loading it, app will redirect to the requested page.", + "errorMessage": { + "type": "App \"config.experimental_initPageId\" should be a string." + } + }, "auth": { "$ref": "#/definitions/authConfig" } diff --git a/packages/docs/concepts/lowdefy-schema.yaml b/packages/docs/concepts/lowdefy-schema.yaml index 41f34e2af..cff79d3b9 100644 --- a/packages/docs/concepts/lowdefy-schema.yaml +++ b/packages/docs/concepts/lowdefy-schema.yaml @@ -43,7 +43,19 @@ _ref: The config object has the following properties: - `homePageId: string`: The pageId of the page that should be loaded when a user loads the app without a pageId in the url route. This is the page that is loaded when you navigate to `yourdomain.com`. + - `experimental_initPageId: string`: The pageId of the page that should be loaded when app is initialized. User is then redirected to requeted page. You can use onInit/onInitAsync/onEnter/onEnterAsync events to fetch and prepare global variables for other parts of the app. + - id: alert1 + type: Alert + properties: + type: warning + showIcon: false + message: Init page is an experimental feature, that may disappear in future releases as well as the flag itself can be changed. Use at your own risk. + + - id: md2 + type: MarkdownWithCode + properties: + content: | # Global Any data that you wish to use in your app can be stored in the __global__ object, and accessed using the [`_global`](/_global) operator. This is a good place to store data or configuration that is used throughout the app, for example the url of a logo or configuration of a page, since then these are only written once, and can be updated easily. diff --git a/packages/graphql/src/controllers/componentController.js b/packages/graphql/src/controllers/componentController.js index 24ce1548f..607c89532 100644 --- a/packages/graphql/src/controllers/componentController.js +++ b/packages/graphql/src/controllers/componentController.js @@ -14,7 +14,7 @@ limitations under the License. */ -import { get } from '@lowdefy/helpers'; +import { get, type } from '@lowdefy/helpers'; class ComponentController { constructor({ getController, getLoader }) { @@ -30,27 +30,32 @@ class ComponentController { async getMenus() { const loadedMenus = await this.componentLoader.load('menus'); - const menus = this.filterMenus({ menus: loadedMenus || [] }); + const initPageId = await this.getInitPageId(); + const menus = this.filterMenus({ menus: loadedMenus || [], initPageId }); const homePageId = await this.getHomePageId({ menus }); return { menus, homePageId, + initPageId, }; } - filterMenus({ menus }) { + filterMenus({ menus, initPageId }) { return menus.map((menu) => { return { ...menu, - links: this.filterMenuList({ menuList: get(menu, 'links', { default: [] }) }), + links: this.filterMenuList({ menuList: get(menu, 'links', { default: [] }), initPageId }), }; }); } - filterMenuList({ menuList }) { + filterMenuList({ menuList, initPageId }) { return menuList .map((item) => { if (item.type === 'MenuLink') { + if (!type.isNone(initPageId) && item.pageId === initPageId) { + return null; + } if (this.authorizationController.authorize(item)) { return item; } @@ -58,7 +63,8 @@ class ComponentController { } if (item.type === 'MenuGroup') { const filteredSubItems = this.filterMenuList({ - menuList: get(item, 'links', { default: [] }), + menuList: get(item, 'links', { default: [] },), + initPageId: initPageId, }); if (filteredSubItems.length > 0) { return { @@ -72,6 +78,15 @@ class ComponentController { .filter((item) => item !== null); } + async getInitPageId() { + const configData = await this.componentLoader.load('config'); + if (configData && get(configData, 'experimental_initPageId')) { + return get(configData, 'experimental_initPageId'); + } else { + return null; + } + } + async getHomePageId({ menus }) { const configData = await this.componentLoader.load('config'); if (configData && get(configData, 'homePageId')) { diff --git a/packages/graphql/src/controllers/componentController.test.js b/packages/graphql/src/controllers/componentController.test.js index 2c13a9984..ccf09470f 100644 --- a/packages/graphql/src/controllers/componentController.test.js +++ b/packages/graphql/src/controllers/componentController.test.js @@ -62,7 +62,7 @@ test('getMenus, menus not found', async () => { }); const controller = createComponentController(context); const res = await controller.getMenus(); - expect(res).toEqual({ menus: [], homePageId: null }); + expect(res).toEqual({ menus: [], homePageId: null, initPageId: null }); }); test('getMenus, menu with configured home page id', async () => { @@ -124,6 +124,7 @@ test('getMenus, menu with configured home page id', async () => { }, ], homePageId: 'homePageId', + initPageId: null, }); }); @@ -168,6 +169,7 @@ test('getMenus, get homePageId at first level', async () => { }, ], homePageId: 'page', + initPageId: null, }); }); @@ -228,6 +230,7 @@ test('getMenus, get homePageId at second level', async () => { }, ], homePageId: 'page', + initPageId: null, }); }); @@ -304,6 +307,7 @@ test('getMenus, get homePageId at third level', async () => { }, ], homePageId: 'page', + initPageId: null, }); }); @@ -348,6 +352,7 @@ test('getMenus, no default menu, no configured homepage', async () => { }, ], homePageId: 'page', + initPageId: null, }); }); @@ -416,6 +421,7 @@ test('getMenus, more than 1 menu, no configured homepage', async () => { }, ], homePageId: 'default-page', + initPageId: null, }); }); @@ -444,6 +450,7 @@ test('getMenus, default menu has no links', async () => { }, ], homePageId: null, + initPageId: null, }); }); @@ -527,6 +534,7 @@ describe('filter menus', () => { }, ], homePageId: null, + initPageId: null, }); }); @@ -655,6 +663,7 @@ describe('filter menus', () => { }, ], homePageId: 'page', + initPageId: null, }); }); @@ -737,6 +746,7 @@ describe('filter menus', () => { const res = await controller.getMenus(); expect(res).toEqual({ homePageId: 'page', + initPageId: null, menus: [ { links: [ @@ -776,6 +786,202 @@ describe('filter menus', () => { ], }); }); + + test('Init page defined', async () => { + mockLoadComponent.mockImplementation((id) => { + if (id === 'menus') { + return [ + { + menuId: 'default', + links: [ + { + id: 'menuitem:default:1', + menuItemId: '1', + type: 'MenuLink', + pageId: 'initPage', + auth: { public: true }, + }, + { + id: 'menuitem:default:2', + menuItemId: '2', + type: 'MenuGroup', + links: [ + { + id: 'menuitem:default:3', + menuItemId: '3', + type: 'MenuLink', + pageId: 'initPage', + auth: { public: true }, + }, + { + id: 'menuitem:default:4', + menuItemId: '4', + type: 'MenuGroup', + links: [ + { + id: 'menuitem:default:5', + menuItemId: '5', + type: 'MenuLink', + pageId: 'initPage', + auth: { public: true }, + }, + ], + }, + ], + }, + ], + }, + { + menuId: 'other', + links: [ + { + id: 'menuitem:other:1', + menuItemId: '1', + type: 'MenuLink', + pageId: 'initPage', + auth: { public: true }, + }, + ], + }, + ]; + } + if (id === 'config') { + return { + experimental_initPageId: 'initPage', + }; + } + return null; + }); + const controller = createComponentController(context); + const res = await controller.getMenus(); + expect(res).toEqual({ + menus: [ + { + menuId: 'default', + links: [], + }, + { + menuId: 'other', + links: [], + }, + ], + homePageId: null, + initPageId: 'initPage', + }); + }); + + test('Nested init page defined', async () => { + mockLoadComponent.mockImplementation((id) => { + if (id === 'menus') { + return [ + { + menuId: 'default', + links: [ + { + id: 'menuitem:default:1', + menuItemId: '1', + type: 'MenuLink', + pageId: 'page', + auth: { public: true }, + }, + { + id: 'menuitem:default:2', + menuItemId: '2', + type: 'MenuGroup', + links: [ + { + id: 'menuitem:default:3', + menuItemId: '3', + type: 'MenuLink', + pageId: 'page', + auth: { public: true }, + }, + { + id: 'menuitem:default:4', + menuItemId: '4', + type: 'MenuGroup', + links: [ + { + id: 'menuitem:default:5', + menuItemId: '5', + type: 'MenuLink', + pageId: 'initPage', + auth: { public: true }, + }, + ], + }, + ], + }, + ], + }, + { + menuId: 'other', + links: [ + { + id: 'menuitem:other:1', + menuItemId: '1', + type: 'MenuLink', + pageId: 'page', + auth: { public: true }, + }, + ], + }, + ]; + } + if (id === 'config') { + return { + experimental_initPageId: 'initPage', + }; + } + return null; + }); + const controller = createComponentController(context); + const res = await controller.getMenus(); + expect(res).toEqual({ + menus: [ + { + menuId: 'default', + links: [ + { + id: 'menuitem:default:1', + menuItemId: '1', + type: 'MenuLink', + pageId: 'page', + auth: { public: true }, + }, + { + id: 'menuitem:default:2', + menuItemId: '2', + type: 'MenuGroup', + links: [ + { + id: 'menuitem:default:3', + menuItemId: '3', + type: 'MenuLink', + pageId: 'page', + auth: { public: true }, + }, + ], + }, + ], + }, + { + menuId: 'other', + links: [ + { + id: 'menuitem:other:1', + menuItemId: '1', + type: 'MenuLink', + pageId: 'page', + auth: { public: true }, + }, + ], + }, + ], + homePageId: 'page', + initPageId: 'initPage', + }); + }); }); test('Filter invalid menu item types', async () => { @@ -805,6 +1011,7 @@ test('Filter invalid menu item types', async () => { const res = await controller.getMenus(); expect(res).toEqual({ homePageId: null, + initPageId: null, menus: [ { menuId: 'default', diff --git a/packages/graphql/src/resolvers/queries/menu/menu.test.js b/packages/graphql/src/resolvers/queries/menu/menu.test.js index e9c81d660..5c4aa5351 100644 --- a/packages/graphql/src/resolvers/queries/menu/menu.test.js +++ b/packages/graphql/src/resolvers/queries/menu/menu.test.js @@ -67,6 +67,7 @@ const mockGetMenus = jest.fn(() => { }, ], homePageId: 'page', + initPageId: 'initPage', }; }); @@ -115,6 +116,7 @@ const GET_MENUS = gql` } } homePageId + initPageId } } `; @@ -142,6 +144,7 @@ test('menu resolver', async () => { }, ], homePageId: 'page', + initPageId: 'initPage', }); }); @@ -177,6 +180,7 @@ test('menu graphql', async () => { }, ], homePageId: 'page', + initPageId: null, }, }); }); diff --git a/packages/graphql/src/schema.js b/packages/graphql/src/schema.js index 80ee73ad8..bad63ce9f 100644 --- a/packages/graphql/src/schema.js +++ b/packages/graphql/src/schema.js @@ -44,6 +44,7 @@ const typeDefs = gql` type MenuResponse { menus: [Menu] homePageId: String + initPageId: String } type Menu { diff --git a/packages/renderer/src/Renderer.js b/packages/renderer/src/Renderer.js index 7d0d00506..46f17fbd3 100644 --- a/packages/renderer/src/Renderer.js +++ b/packages/renderer/src/Renderer.js @@ -14,12 +14,12 @@ limitations under the License. */ -import React from 'react'; +import React, { useState } from 'react'; import { BrowserRouter, Route, Redirect, Switch, useLocation } from 'react-router-dom'; import { ApolloProvider, useQuery, gql } from '@apollo/client'; import { ErrorBoundary, Loading } from '@lowdefy/block-tools'; -import { get } from '@lowdefy/helpers'; +import { get, type } from '@lowdefy/helpers'; import useGqlClient from './utils/graphql/useGqlClient'; import createLogin from './utils/auth/createLogin'; @@ -89,6 +89,7 @@ const GET_ROOT = gql` } } homePageId + initPageId } } `; @@ -99,6 +100,7 @@ const RootQuery = ({ children, lowdefy }) => { if (error) return

Error

; lowdefy.homePageId = get(data, 'menu.homePageId'); + lowdefy.initPageId = get(data, 'menu.initPageId'); // Make a copy to avoid immutable error when calling setGlobal. lowdefy.lowdefyGlobal = JSON.parse(JSON.stringify(get(data, 'lowdefyGlobal', { default: {} }))); lowdefy.menus = get(data, 'menu.menus'); @@ -124,6 +126,19 @@ const Home = ({ lowdefy }) => { return ; }; +const PageLoader = ({ lowdefy }) => { + const { initPageId } = lowdefy; + const [initEventsTriggered, setInitEventsTriggered] = useState(false); + + if (type.isNone(initPageId) || initEventsTriggered) { + return ; + } else { + return ( + + ); + } +}; + const Root = ({ gqlUri }) => { lowdefy.updateBlock = (blockId) => lowdefy.updaters[blockId] && lowdefy.updaters[blockId](); lowdefy.client = useGqlClient({ gqlUri, lowdefy }); @@ -151,7 +166,7 @@ const Root = ({ gqlUri }) => { - + diff --git a/packages/renderer/src/page/Page.js b/packages/renderer/src/page/Page.js index 42251f2fb..2b64d65e4 100644 --- a/packages/renderer/src/page/Page.js +++ b/packages/renderer/src/page/Page.js @@ -21,7 +21,7 @@ import { useParams, useHistory, useLocation, Redirect } from 'react-router-dom'; import { useQuery, gql } from '@apollo/client'; import { Loading } from '@lowdefy/block-tools'; -import { get, urlQuery } from '@lowdefy/helpers'; +import { get, urlQuery, type } from '@lowdefy/helpers'; import { makeContextId } from '@lowdefy/engine'; import Block from './block/Block'; @@ -35,9 +35,21 @@ const GET_PAGE = gql` } `; -const PageContext = ({ lowdefy }) => { - const { pageId } = useParams(); - const { search } = useLocation(); +const PageContext = (pageArgs) => { + const { lowdefy } = pageArgs; + const { initEventsTriggered } = pageArgs; + const { pageId = useParams().pageId } = pageArgs; + const { search = useLocation().search } = pageArgs; + + if ( + type.isFunction(initEventsTriggered) && + !type.isNone(lowdefy.pageId) && + lowdefy.pageId !== pageId && + lowdefy.pageId !== lowdefy.initPageId + ) { + initEventsTriggered(false); + } + lowdefy.pageId = pageId; lowdefy.routeHistory = useHistory(); lowdefy.link = setupLink(lowdefy); @@ -74,6 +86,7 @@ const PageContext = ({ lowdefy }) => { urlQuery: lowdefy.urlQuery, })} lowdefy={lowdefy} + initEventsTriggered={initEventsTriggered} > {(context) => ( <> diff --git a/packages/renderer/src/page/block/Context.js b/packages/renderer/src/page/block/Context.js index 0e5aa07fe..62d1d0190 100644 --- a/packages/renderer/src/page/block/Context.js +++ b/packages/renderer/src/page/block/Context.js @@ -20,7 +20,7 @@ import getContext from '@lowdefy/engine'; import MountEvents from './MountEvents'; import LoadingBlock from './LoadingBlock'; -const Context = ({ block, children, contextId, lowdefy }) => { +const Context = ({ block, children, contextId, lowdefy, initEventsTriggered }) => { const [context, setContext] = useState({}); const [error, setError] = useState(null); @@ -58,6 +58,7 @@ const Context = ({ block, children, contextId, lowdefy }) => { triggerEvent={({ name, context }) => context.RootBlocks.areas.root.blocks[0].triggerEvent({ name }) } + initEventsTriggered={initEventsTriggered} > {(loaded) => (!loaded ? : children(context))} diff --git a/packages/renderer/src/page/block/MountEvents.js b/packages/renderer/src/page/block/MountEvents.js index 9f968b70d..f39fd0edd 100644 --- a/packages/renderer/src/page/block/MountEvents.js +++ b/packages/renderer/src/page/block/MountEvents.js @@ -15,8 +15,16 @@ */ import React, { useEffect, useState } from 'react'; +import { type } from '@lowdefy/helpers'; -const MountEvents = ({ asyncEventName, context, eventName, triggerEvent, children }) => { +const MountEvents = ({ + asyncEventName, + context, + eventName, + triggerEvent, + initEventsTriggered, + children, +}) => { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { @@ -28,6 +36,9 @@ const MountEvents = ({ asyncEventName, context, eventName, triggerEvent, childre triggerEvent({ name: asyncEventName, context }); setLoading(false); } + if (type.isFunction(initEventsTriggered)) { + initEventsTriggered(true); + } } catch (err) { setError(err); }