mirror of
https://github.com/lowdefy/lowdefy.git
synced 2025-03-31 15:20:32 +08:00
commit
8d492a3999
@ -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"
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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')) {
|
||||
|
@ -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',
|
||||
|
@ -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,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
@ -44,6 +44,7 @@ const typeDefs = gql`
|
||||
type MenuResponse {
|
||||
menus: [Menu]
|
||||
homePageId: String
|
||||
initPageId: String
|
||||
}
|
||||
|
||||
type Menu {
|
||||
|
@ -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>
|
||||
|
@ -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) => (
|
||||
<>
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user