Merge pull request #931 from mat02/init-page

Add init page
This commit is contained in:
Sam 2021-11-09 18:23:33 +02:00 committed by GitHub
commit 8d492a3999
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 302 additions and 16 deletions

View File

@ -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"
}

View File

@ -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.

View File

@ -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')) {

View File

@ -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',

View File

@ -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,
},
});
});

View File

@ -44,6 +44,7 @@ const typeDefs = gql`
type MenuResponse {
menus: [Menu]
homePageId: String
initPageId: String
}
type Menu {

View File

@ -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 <h1>Error</h1>;
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 <Redirect to={`${lowdefy.basePath}/404`} />;
};
const PageLoader = ({ lowdefy }) => {
const { initPageId } = lowdefy;
const [initEventsTriggered, setInitEventsTriggered] = useState(false);
if (type.isNone(initPageId) || initEventsTriggered) {
return <Page lowdefy={lowdefy} initEventsTriggered={setInitEventsTriggered} />;
} else {
return (
<Page lowdefy={lowdefy} pageId={initPageId} initEventsTriggered={setInitEventsTriggered} />
);
}
};
const Root = ({ gqlUri }) => {
lowdefy.updateBlock = (blockId) => lowdefy.updaters[blockId] && lowdefy.updaters[blockId]();
lowdefy.client = useGqlClient({ gqlUri, lowdefy });
@ -151,7 +166,7 @@ const Root = ({ gqlUri }) => {
<OpenIdCallback lowdefy={lowdefy} />
</Route>
<Route exact path={`${lowdefy.basePath}/:pageId`}>
<Page lowdefy={lowdefy} />
<PageLoader lowdefy={lowdefy} />
</Route>
</Switch>
</RootQuery>

View File

@ -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) => (
<>

View File

@ -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 ? <LoadingBlock block={block} lowdefy={lowdefy} /> : children(context))}
</MountEvents>

View File

@ -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);
}