feat(renderer): page rendering, incomplete

This commit is contained in:
Sam Tolmay 2020-10-12 17:38:40 +02:00
parent 7e2e2048ef
commit a91fadf166
16 changed files with 722 additions and 130 deletions

View File

@ -1,31 +0,0 @@
/*
Copyright 2020 Lowdefy, Inc
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import Block from './Block';
const AutoBlock = ({ page }) => {
return (
<>
{page.blocks.map((block) => {
return <Block key={block.id} meta={block.meta} />;
})}
</>
);
};
export default AutoBlock;

View File

@ -1,53 +0,0 @@
/*
Copyright 2020 Lowdefy, Inc
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import { connectBlock } from '@lowdefy/block-tools';
import useDynamicScript from './utils/useDynamicScript';
import loadComponent from './utils/loadComponent';
const Comp = ({ bl }) => {
const CBlock = connectBlock(bl);
return <CBlock />;
};
function Block({ meta }) {
const { ready, failed } = useDynamicScript({
url: meta && meta.url,
});
if (!meta) {
return <h2>Not meta specified</h2>;
}
if (!ready) {
return <h2>Loading dynamic script: {meta.url}</h2>;
}
if (failed) {
return <h2>Failed to load dynamic script: {meta.url}</h2>;
}
const Component = React.lazy(loadComponent(meta.scope, meta.module));
return (
<React.Suspense fallback="Loading Block">
<Comp bl={Component} />
</React.Suspense>
);
}
export default Block;

View File

@ -1,43 +0,0 @@
/*
Copyright 2020 Lowdefy, Inc
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import { gql } from '@apollo/client';
import { useQuery } from '@apollo/client';
import AutoBlock from './AutoBlock';
const GET_PAGE = gql`
query page($pageId: ID!) {
page(pageId: $pageId)
}
`;
const Page = () => {
const { loading, error, data } = useQuery(GET_PAGE, {
variables: {
pageId: 'page1',
},
});
if (loading) return <h2>Loading</h2>;
if (error) {
console.log(error);
return <h2>Error</h2>;
}
console.log('data', data);
return <AutoBlock page={data.page} />;
};
export default Page;

View File

@ -22,7 +22,7 @@ import { ErrorBoundary } from '@lowdefy/block-tools';
import get from '@lowdefy/get';
import useGqlClient from './utils/graphql/useGqlClient';
import PageContext from './PageContext';
import PageContext from './page/PageContext';
// eslint-disable-next-line no-undef
const windowContext = window;

View File

@ -0,0 +1,133 @@
/*
Copyright 2020 Lowdefy, Inc
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import { BlockLayout } from '@lowdefy/layout';
import { makeContextId } from '@lowdefy/engine';
import { makeCssClass } from '@lowdefy/block-tools';
import Container from './Container';
import Context from './Context';
import List from './List';
const AutoBlock = ({ block, Blocks, Component, context, pageId, rootContext }) => {
switch (block.meta.category) {
case 'context':
return (
<Context
block={block}
Component={Component}
context={context}
contextId={makeContextId({
branch: rootContext.branch,
urlQuery: rootContext.urlQuery,
pageId,
blockId: block.blockId,
})}
pageId={pageId}
rootContext={rootContext}
/>
);
case 'list':
return (
<List
block={block}
Blocks={Blocks}
Component={Component}
context={context}
pageId={pageId}
rootContext={rootContext}
/>
);
case 'container':
return (
<Container
block={block}
Blocks={Blocks}
Component={Component}
context={context}
pageId={pageId}
rootContext={rootContext}
/>
);
case 'input':
return (
<BlockLayout
id={`bl-${block.blockId}`}
blockStyle={block.eval.style}
highlightBorders={rootContext.lowdefyGlobal.highlightBorders}
layout={block.eval.layout || {}}
makeCssClass={makeCssClass}
>
<Component
methods={{
callAction: block.callAction,
makeCssClass,
registerAction: block.registerAction,
registerMethod: block.registerMethod,
setValue: block.setValue,
}}
actions={block.eval.actions}
blockId={block.blockId}
Components={rootContext.Components}
homePageId={rootContext.homePageId}
key={block.blockId}
loading={block.loading}
menus={rootContext.menus}
pageId={pageId}
properties={block.eval.properties}
required={block.eval.required}
user={rootContext.user}
validate={block.eval.validate}
value={block.value}
/>
</BlockLayout>
);
default:
return (
<BlockLayout
id={`bl-${block.blockId}`}
blockStyle={block.eval.style}
highlightBorders={rootContext.lowdefyGlobal.highlightBorders}
layout={block.eval.layout || {}}
makeCssClass={makeCssClass}
>
<Component
methods={{
callAction: block.callAction,
makeCssClass,
registerAction: block.registerAction,
registerMethod: block.registerMethod,
}}
actions={block.eval.actions}
blockId={block.blockId}
Components={rootContext.Components}
homePageId={rootContext.homePageId}
key={block.blockId}
loading={block.loading}
menus={rootContext.menus}
pageId={pageId}
properties={block.eval.properties}
required={block.eval.required}
user={rootContext.user}
validate={block.eval.validate}
/>
</BlockLayout>
);
}
};
export default AutoBlock;

View File

@ -0,0 +1,85 @@
/*
Copyright 2020 Lowdefy, Inc
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { Suspense } from 'react';
import { useQuery, gql } from '@apollo/client';
import { Loading, makeCssClass, connectBlock, ErrorBoundary } from '@lowdefy/block-tools';
import AutoBlock from './AutoBlock';
import prepareBlock from './prepareBlock';
const getBlock = gql`
query getBlock($id: String!) {
block(id: $id) @client {
id
t
}
}
`;
const BindAutoBlock = ({ block, Blocks, context, pageId, rootContext }) => {
const { loading, error, data } = useQuery(getBlock, {
variables: {
id: `BlockClass:${block.id}`,
},
client: rootContext.client,
});
if (loading) return 'Loading render watcher';
if (error) throw error;
if (block.eval.visible === false)
return <div id={`vs-${block.blockId}`} style={{ display: 'none' }} />;
const Component = prepareBlock({
block,
Components: rootContext.Components,
});
if (data.block.loading) {
return (
<Loading
id={`lo-${block.blockId}-loading`}
meta={block.meta}
methods={{ makeCssClass }}
blockStyle={block.eval.style || {}}
/>
);
}
return (
<ErrorBoundary>
<Suspense
fallback={
<Loading
id={`sp-${block.blockId}-loading`}
meta={block.meta}
methods={{ makeCssClass }}
/>
}
>
<AutoBlock
block={block}
Blocks={Blocks}
Component={connectBlock(Component)}
context={context}
pageId={pageId}
rootContext={rootContext}
/>
</Suspense>
</ErrorBoundary>
);
};
export default BindAutoBlock;

View File

@ -0,0 +1,89 @@
/*
Copyright 2020 Lowdefy, Inc
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import { Area, BlockLayout, layoutParamsToArea } from '@lowdefy/layout';
import { connectBlock, makeCssClass } from '@lowdefy/block-tools';
import BindAutoBlock from './BindAutoBlock';
const ConnectedArea = connectBlock(Area);
const Container = ({ block, Blocks, Component, context, pageId, rootContext }) => {
const content = {};
// eslint-disable-next-line prefer-destructuring
const areas = Blocks.subBlocks[block.id][0].areas;
Object.keys(areas).forEach((areaKey) => {
content[areaKey] = (areaStyle) => (
<ConnectedArea
id={`ar-${block.blockId}-${areaKey}`}
key={`ar-${block.blockId}-${areaKey}`}
area={layoutParamsToArea({
area: block.eval.areas[areaKey] || {},
areaKey,
layout: block.eval.layout || {},
})}
areaStyle={[areaStyle, block.eval.areas[areaKey] && block.eval.areas[areaKey].style]}
highlightBorders={rootContext.lowdefyGlobal.highlightBorders}
makeCssClass={makeCssClass}
>
{areas[areaKey].blocks.map((bl) => (
<BindAutoBlock
key={`co-${bl.blockId}`}
Blocks={Blocks.subBlocks[block.id][0]}
block={bl}
context={context}
pageId={pageId}
rootContext={rootContext}
/>
))}
</ConnectedArea>
);
});
return (
<BlockLayout
id={`bl-${block.blockId}`}
blockStyle={block.eval.style}
highlightBorders={rootContext.lowdefyGlobal.highlightBorders}
layout={block.eval.layout || {}}
makeCssClass={makeCssClass}
>
<Component
methods={{
callAction: block.callAction,
makeCssClass,
registerAction: block.registerAction,
registerMethod: block.registerMethod,
}}
actions={block.eval.actions}
blockId={block.blockId}
Components={rootContext.Components}
content={content}
homePageId={rootContext.homePageId}
key={block.blockId}
loading={block.loading}
menus={rootContext.menus}
pageId={pageId}
properties={block.eval.properties}
required={block.eval.required}
user={rootContext.user}
validate={block.eval.validate}
/>
</BlockLayout>
);
};
export default Container;

View File

@ -0,0 +1,48 @@
/*
Copyright 2020 Lowdefy, Inc
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import { Loading, makeCssClass } from '@lowdefy/block-tools';
import useContext from './useContext';
import Container from './Container';
const Context = ({ block, Component, pageId, rootContext, contextId }) => {
const { context, loading, error } = useContext({ block, pageId, rootContext, contextId });
if (loading) {
return (
<Loading
meta={block.meta}
methods={{ makeCssClass }}
blockStyle={(block.eval && block.eval.style) || block.style}
/>
);
}
if (error) throw error;
return (
<Container
block={context.RootBlocks.areas.root.blocks[0]}
Blocks={context.RootBlocks}
Component={Component}
context={context}
pageId={pageId}
rootContext={rootContext}
/>
);
};
export default Context;

View File

@ -0,0 +1,37 @@
/*
Copyright 2020 Lowdefy, Inc
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import { Helmet } from 'react-helmet';
const BindHelmet = ({ pageProperties }) => {
return (
<Helmet>
<meta charSet="utf-8" />
<title>{pageProperties.title}</title>
<link
rel="shortcut icon"
href={
pageProperties.faviconPath
? `%PUBLIC_URL%/${pageProperties.faviconPath}`
: '%PUBLIC_URL%/favicon.ico'
}
/>
</Helmet>
);
};
export default BindHelmet;

View File

@ -0,0 +1,96 @@
/*
Copyright 2020 Lowdefy, Inc
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import { Area, BlockLayout, layoutParamsToArea } from '@lowdefy/layout';
import { connectBlock, makeCssClass } from '@lowdefy/block-tools';
import BindAutoBlock from './BindAutoBlock';
const ConnectedArea = connectBlock(Area);
const List = ({ block, Blocks, Component, context, pageId, rootContext }) => {
const content = {};
const contentList = [];
Blocks.subBlocks[block.id].forEach((SBlock) => {
Object.keys(SBlock.areas).forEach((areaKey) => {
content[areaKey] = (areaStyle) => (
<ConnectedArea
id={`ar-${block.blockId}-${SBlock.id}-${areaKey}`}
key={`ar-${block.blockId}-${SBlock.id}-${areaKey}`}
area={layoutParamsToArea({
area: block.eval.areas[areaKey] || {},
areaKey,
layout: block.eval.layout || {},
})}
areaStyle={[areaStyle, block.eval.areas[areaKey] && block.eval.areas[areaKey].style]}
highlightBorders={rootContext.lowdefyGlobal.highlightBorders}
makeCssClass={makeCssClass}
>
{SBlock.areas[areaKey].blocks.map((bl) => (
<BindAutoBlock
key={`ls-${bl.blockId}`}
Blocks={SBlock}
block={bl}
context={context}
pageId={pageId}
rootContext={rootContext}
/>
))}
</ConnectedArea>
);
});
contentList.push({ ...content });
});
return (
<BlockLayout
id={`bl-${block.blockId}`}
blockStyle={block.eval.style}
highlightBorders={rootContext.lowdefyGlobal.highlightBorders}
layout={block.eval.layout || {}}
makeCssClass={makeCssClass}
>
<Component
methods={{
callAction: block.callAction,
makeCssClass,
moveItemDown: block.moveItemDown,
moveItemUp: block.moveItemUp,
pushItem: block.pushItem,
registerAction: block.registerAction,
registerMethod: block.registerMethod,
removeItem: block.removeItem,
unshiftItem: block.unshiftItem,
}}
actions={block.eval.actions}
blockId={block.blockId}
Components={rootContext.Components}
content={contentList}
homePageId={rootContext.homePageId}
key={block.blockId}
loading={block.loading}
menus={rootContext.menus}
pageId={pageId}
properties={block.eval.properties}
required={block.eval.required}
user={rootContext.user}
validate={block.eval.validate}
/>
</BlockLayout>
);
};
export default List;

View File

@ -0,0 +1,84 @@
/* eslint-disable react/destructuring-assignment */
/*
Copyright 2020 Lowdefy, Inc
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { Suspense } from 'react';
import { useParams, useHistory, useLocation, Redirect } from 'react-router-dom';
import { useQuery, gql } from '@apollo/client';
import { Loading, connectBlock } from '@lowdefy/block-tools';
import get from '@lowdefy/get';
import { urlQuery } from '@lowdefy/helpers';
import AutoBlock from './AutoBlock';
import Helmet from './Helmet';
import prepareBlock from './prepareBlock';
const GET_PAGE = gql`
query getPage($id: ID!) {
page(pageId: $id)
}
`;
const PageContext = ({ rootContext }) => {
const { pageId } = useParams();
rootContext.routeHistory = useHistory();
const { search } = useLocation();
rootContext.urlQuery = urlQuery.parse(search || '');
const { loading, error, data } = useQuery(GET_PAGE, {
variables: { id: pageId, branch: rootContext.branch },
});
if (loading) {
console.log('loading');
return Loading;
}
// if (error) throw error;
if (error) {
console.log(error);
return <div>Error</div>;
}
console.log('finished loading');
// redirect 404
if (!data.page) return <Redirect to="/404" />;
console.log('data', data.page);
const Component = prepareBlock({
block: data.page,
Components: rootContext.Components,
});
return (
<>
<Helmet pageProperties={get(data.page, 'properties', { default: {} })} />
<div>Hello</div>
<div id={pageId}>
<Suspense fallback={<Loading />}>
<AutoBlock
block={data.page}
Blocks={null}
Component={connectBlock(Component)}
context={null}
pageId={pageId}
rootContext={rootContext}
/>
</Suspense>
</div>
</>
);
};
export default PageContext;

View File

@ -0,0 +1,38 @@
/*
Copyright 2020 Lowdefy, Inc
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { lazy } from 'react';
import useDynamicScript from '../utils/useDynamicScript';
import loadComponent from '../utils/loadComponent';
const prepareBlock = ({ Components, block }) => {
const componentId = `${block.meta.scope}:${block.meta.module}`;
const { ready, failed } = useDynamicScript({
url: block.meta.url,
});
if (!Components[componentId]) {
if (!ready) {
return <h2>Loading dynamic script: {block.meta.url}</h2>;
}
if (failed) {
return <h2>Failed to load dynamic script: {block.meta.url}</h2>;
}
Components[componentId] = lazy(loadComponent(block.meta.scope, block.meta.module));
}
return Components[componentId];
};
export default prepareBlock;

View File

@ -0,0 +1,90 @@
/*
Copyright 2020 Lowdefy, Inc
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { useEffect, useState } from 'react';
// import { message, notification } from 'antd';
import getContext from '@lowdefy/engine';
// message.config({
// duration: 4,
// maxCount: 12,
// });
// notification.config({
// placement: 'bottomRight',
// bottom: 50,
// duration: 5,
// });
const message = {
loading: (message) => console.log(message),
error: (message) => console.log(message),
success: (message) => console.log(message),
};
const notification = {
loading: (message) => console.log(message),
error: (message) => console.log(message),
success: (message) => console.log(message),
};
const onEnter = (context) => {
return context.RootBlocks.areas.root.blocks[0].callAction({
action: 'onEnter',
hideLoading: true,
});
};
const fetchAll = (context) => {
// fetch all new requests on page
return context.Requests.callRequests({
onlyNew: true,
});
};
const useContext = ({ block, pageId, rootContext, contextId }) => {
const [error, setError] = useState(null);
const [context, setContext] = useState(null);
useEffect(() => {
let mounted = true;
const mount = async () => {
try {
const ctx = await getContext({
block,
contextId,
pageId,
rootContext,
message,
notification,
});
if (mounted) await onEnter(ctx);
if (mounted) {
fetchAll(ctx);
setContext(ctx);
}
} catch (err) {
setError(err);
}
};
mount();
return () => {
mounted = false;
};
}, [block, pageId, rootContext, contextId]);
return { error, context, loading: !context && !error };
};
export default useContext;

View File

@ -29,7 +29,7 @@ const cache = new InMemoryCache({
});
const retryLink = new RetryLink();
const httpLink = new HttpLink({
uri: '/graphql',
uri: '/api/graphql',
});
// TODO: Handle errors

View File

@ -1,3 +1,5 @@
/* eslint-disable no-undef */
/*
Copyright 2020 Lowdefy, Inc
@ -18,7 +20,6 @@ function loadComponent(scope, module) {
return async () => {
// Initializes the share scope. This fills it with known provided modules from this build and all remotes
await __webpack_init_sharing__('default');
console.log(window);
const container = window[scope]; // or get the container somewhere else
// Initialize the container, it may provide shared modules
await container.init(__webpack_share_scopes__.default);

View File

@ -1,5 +1,6 @@
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { ModuleFederationPlugin } = require('webpack').container;
const webpack = require('webpack');
const path = require('path');
const deps = require('./package.json').dependencies;
@ -14,6 +15,13 @@ module.exports = {
output: {
path: path.resolve(__dirname, 'dist'),
},
// webpack 5 support polyfills
resolve: {
alias: {
buffer: require.resolve('buffer'),
},
fallback: { buffer: false },
},
module: {
rules: [
{
@ -23,6 +31,15 @@ module.exports = {
lazy: true,
},
},
// TODO: FIXME: do NOT webpack 5 support with this
// x-ref: https://github.com/webpack/webpack/issues/11467
// waiting for babel fix: https://github.com/vercel/next.js/pull/17095#issuecomment-692435147
{
test: /\.m?js/,
resolve: {
fullySpecified: false,
},
},
{
test: /\.jsx?$/,
loader: 'babel-loader',
@ -65,6 +82,7 @@ module.exports = {
},
},
}),
new webpack.ProvidePlugin({ Buffer: ['buffer', 'Buffer'] }),
new HtmlWebpackPlugin({
template: './public/index.html',
}),