Merge pull request #1094 from lowdefy/link-component

Add Lowdefy Link component for blocks.
This commit is contained in:
Sam 2022-02-09 11:00:53 +02:00 committed by GitHub
commit d0897303db
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 611 additions and 254 deletions

View File

@ -32,6 +32,10 @@
"scripts": {
"build": "lerna run build",
"build:dev": "NODE_ENV=development lerna run build",
"build:plugins": "yarn build:blocks && yarn build:connections && yarn build:operators",
"build:blocks": "lerna run --scope '@lowdefy/blocks-*' build",
"build:connections": "lerna run --scope '@lowdefy/connection-*' build",
"build:operators": "lerna run --scope '@lowdefy/operators-*' build",
"clean": "lerna run clean",
"lerna:version": "lerna version --no-git-tag-version",
"lerna:publish": "lerna publish from-git",
@ -40,6 +44,7 @@
"start:server-dev": "yarn workspace @lowdefy/server-dev start --package-manager yarn --config-directory ../../app",
"start": "yarn workspace @lowdefy/server build:lowdefy --config-directory ../../app && yarn && yarn workspace @lowdefy/server build:next && yarn workspace @lowdefy/server start",
"start:dev": "yarn workspace @lowdefy/server build:lowdefy --config-directory ../../app && yarn && yarn workspace @lowdefy/server dev",
"start:dev-docs": "yarn workspace @lowdefy/server build:lowdefy --config-directory ../docs && yarn && yarn workspace @lowdefy/server dev",
"test": "lerna run test"
},
"devDependencies": {

View File

@ -39,6 +39,11 @@ async function validateConfig({ components }) {
if (type.isNone(components.config.theme)) {
components.config.theme = {};
}
if (type.isString(components.config.basePath)) {
if (components.config.basePath[0] !== '/') {
throw Error('Base path must start with "/".');
}
}
validate({
schema: lowdefySchema.definitions.authConfig,
data: components.config.auth,

View File

@ -155,3 +155,31 @@ test('validateConfig config error when protected or public are false.', async ()
'Public pages can not be set to false.'
);
});
test('validateConfig config error when basePath does not start with "/".', async () => {
let components = {
config: {
basePath: '/base',
},
};
const result = await validateConfig({ components, context });
expect(result).toEqual({
config: {
auth: {
pages: {
roles: {},
},
},
basePath: '/base',
theme: {},
},
});
components = {
config: {
basePath: 'base',
},
};
await expect(validateConfig({ components, context })).rejects.toThrow(
'Base path must start with "/".'
);
});

View File

@ -598,6 +598,13 @@ export default {
auth: {
$ref: '#/definitions/authConfig',
},
basePath: {
type: 'string',
description: 'App base path to apply to all routes. Base path must start with "/".',
errorMessage: {
type: 'App "config.basePath" should be a string.',
},
},
homePageId: {
type: 'string',
description:

View File

@ -123,6 +123,20 @@ _ref:
- `basePath: string`: The base path setting for the application. This variable is used to prefix route paths for example `${basePath}/public/logo-square-light-theme.png`. The default base path is ''.
- `blockId: string`: The block's id within the Lowdefy app, this is useful for setting a unique `id` on DOM elements.
- `components: object`: Helper React components that are exposed to blocks to use internally.
- `Icon`: component`: Lowdefy standard Icon React component to render build in icons.
- `Link`: component`: Lowdefy standard Link React component used as links to pages or external urls. The following props apply:
- `ariaLabel: string`: Arial-label to apply to link tag.
- `back: boolean`: When the link is clicked, trigger the browser back.
- `home: boolean`: When the link is clicked, route to the home page.
- `input: object`: When the link is clicked, pass data as the input object to the next Lowdefy page. Can only be used with pageId link and newTab false. See [Input]( TODO: Link to input page? ).
- `newTab: boolean`: When the link is clicked, open the page in a new browser tab.
- `pageId: string`: When the link is clicked, route to the provided Lowdefy page.
- `rel: string`: Overwrite `<a/>` tag property.
- `replace: boolean`: Prevent adding a new entry into browser history by replacing the url instead of pushing into history. Can only be used with pageId link and newTab false.
- `scroll: boolean`: Disable scrolling to the top of the page after page transition. Can only be used with pageId link and newTab false.
- `url: string`: When the link is clicked, route to an external url.
- `urlQuery: object`: When the link is clicked, pass data as a url query to the next Lowdefy page. See [url query]( TODO: Link to url query page? ).
- `content: object`: Passed to `container` and `context` block categories. The `content` object with methods to render sub blocks into content areas. The method name is the same as the area key, for example, 'content.content()` renders a blocks default `content` area.
- `events: object`: All events defined on the block in the Lowdefy app config.
- `[event_key]: object`: Event keys gives a handle name to the event details.

View File

@ -42,6 +42,7 @@ _ref:
The config object has the following properties:
- `basePath: string`: Set the base path to serve the Lowdefy application from. This will route all pages under `https://example.com/<base-path>/<page-id>` instead of the default `https://example.com/<page-id>`. The basePath value must start with "/".
- `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.

View File

@ -32,7 +32,7 @@ _ref:
The Lowdefy server can be configured using the following environment variables:
- `LOWDEFY_SERVER_BASE_PATH`: Set the base path to serve the Lowdefy application from. This will serve the application under `https://example.com/<base-path>`instead of `https://example.com`, and all pages under `https://example.com/<base-path>/<page-id>` instead of the default `https://example.com/<page-id>`.
- `LOWDEFY_BASE_PATH`: Set the base path to serve the Lowdefy application from. This will serve the application under `https://example.com/<base-path>`instead of `https://example.com`, and all pages under `https://example.com/<base-path>/<page-id>` instead of the default `https://example.com/<page-id>`.
- `LOWDEFY_SERVER_BUILD_DIRECTORY`: The directory of the built Lowdefy configuration (The output of `lowdefy build`, usually found at `./.lowdefy/build` in your project repository). The default is `./build` (or `/home/node/lowdefy/build`).
- `LOWDEFY_SERVER_PUBLIC_DIRECTORY`: The directory of the public assets to be served. The default is `./public` (or `/home/node/lowdefy/public`).
- `LOWDEFY_SERVER_PORT`: The port (inside the container) at which to run the server. The default is `3000`.

View File

@ -105,7 +105,7 @@ _ref:
The following environment variables can be specified:
- `LOWDEFY_SERVER_BASE_PATH`: Set the base path to serve the Lowdefy application from. This will serve the application under `https://example.com/<base-path>`instead of `https://example.com`, and all pages under `https://example.com/<base-path>/<page-id>` instead of the default `https://example.com/<page-id>`.
- `LOWDEFY_BASE_PATH`: Set the base path to serve the Lowdefy application from. This will serve the application under `https://example.com/<base-path>`instead of `https://example.com`, and all pages under `https://example.com/<base-path>/<page-id>` instead of the default `https://example.com/<page-id>`.
- `LOWDEFY_SERVER_BUILD_DIRECTORY`: The directory of the built Lowdefy configuration (The output of `lowdefy build`, usually found at `./.lowdefy/build` in your project repository). The default is `./.lowdefy/build`.
- `LOWDEFY_SERVER_PORT`: The port at which to run the server. The default is `3000`.
- `LOWDEFY_SERVER_PUBLIC_DIRECTORY`: The directory of the public assets to be served. The default is `./public`.

View File

@ -16,32 +16,42 @@
import { type, urlQuery as urlQueryFn } from '@lowdefy/helpers';
function createLink({ backLink, lowdefy, newOriginLink, sameOriginLink }) {
function link({ back, home, input, newTab, pageId, url, urlQuery }) {
let pathname = pageId;
if (back) {
return backLink();
function createLink({ backLink, disabledLink, lowdefy, newOriginLink, noLink, sameOriginLink }) {
function link(props) {
if (props.disabled === true) {
return disabledLink(props);
}
const lowdefyUrlQuery = type.isNone(urlQuery) ? '' : `?${urlQueryFn.stringify(urlQuery)}`;
if (home) {
if (lowdefy.home.configured) {
pathname = '';
pageId = lowdefy.home.pageId;
} else {
pathname = lowdefy.home.pageId;
pageId = lowdefy.home.pageId;
}
if ([!props.pageId, !props.back, !props.home, !props.url].filter((v) => !v).length > 1) {
throw Error(
`Invalid Link: To avoid ambiguity, only one of 'back', 'home', 'pageId' or 'url' can be defined.`
);
}
if (!type.isNone(pathname)) {
if (!type.isNone(input)) {
lowdefy.inputs[pageId] = input;
}
return sameOriginLink(`/${pathname}${lowdefyUrlQuery}`, newTab);
if (props.back === true) {
// Cannot set input or urlQuery on back
return backLink(props);
}
if (!type.isNone(url)) {
return newOriginLink(`${url}${lowdefyUrlQuery}`, newTab);
const lowdefyUrlQuery = type.isNone(props.urlQuery)
? ''
: `?${urlQueryFn.stringify(props.urlQuery)}`;
if (props.home === true) {
lowdefy.inputs[`page:${lowdefy.home.pageId}`] = props.input || {};
return sameOriginLink({
href: `/${lowdefy.home.configured ? '' : lowdefy.home.pageId}${lowdefyUrlQuery}`,
...props,
});
}
throw new Error(`Invalid Link.`);
if (type.isString(props.pageId)) {
lowdefy.inputs[`page:${props.pageId}`] = props.input || {};
return sameOriginLink({ href: `/${props.pageId}${lowdefyUrlQuery}`, ...props });
}
if (type.isString(props.url)) {
const protocol = props.url.includes(':') ? '' : 'https://';
return newOriginLink({
href: `${protocol}${props.url}${lowdefyUrlQuery}`,
...props,
});
}
return noLink(props);
}
return link;
}

View File

@ -1,12 +1,16 @@
import createLink from '../src/createLink.js';
const mockBackLink = jest.fn();
const mockDisabledLink = jest.fn();
const mockNewOriginLink = jest.fn();
const mockNoLink = jest.fn();
const mockSameOriginLink = jest.fn();
beforeEach(() => {
mockBackLink.mockReset();
mockDisabledLink.mockReset();
mockNewOriginLink.mockReset();
mockNoLink.mockReset();
mockSameOriginLink.mockReset();
});
@ -14,17 +18,34 @@ test('createLink, link with pageId', () => {
const lowdefy = { inputs: {} };
const link = createLink({
backLink: mockBackLink,
disabledLink: mockDisabledLink,
lowdefy,
newOriginLink: mockNewOriginLink,
noLink: mockNoLink,
sameOriginLink: mockSameOriginLink,
});
link({ pageId: 'page_1' });
link({ pageId: 'page_1', urlQuery: { p: 3 } });
expect(mockBackLink.mock.calls).toEqual([]);
expect(mockDisabledLink.mock.calls).toEqual([]);
expect(mockNewOriginLink.mock.calls).toEqual([]);
expect(mockNoLink.mock.calls).toEqual([]);
expect(mockSameOriginLink.mock.calls).toEqual([
['/page_1', undefined],
['/page_1?p=3', undefined],
[
{
href: '/page_1',
pageId: 'page_1',
},
],
[
{
href: '/page_1?p=3',
pageId: 'page_1',
urlQuery: {
p: 3,
},
},
],
]);
});
@ -32,17 +53,36 @@ test('createLink, link with pageId new tab', () => {
const lowdefy = { inputs: {} };
const link = createLink({
backLink: mockBackLink,
disabledLink: mockDisabledLink,
lowdefy,
newOriginLink: mockNewOriginLink,
noLink: mockNoLink,
sameOriginLink: mockSameOriginLink,
});
link({ pageId: 'page_1', newTab: true });
link({ pageId: 'page_1', newTab: true, urlQuery: { p: 3 } });
expect(mockBackLink.mock.calls).toEqual([]);
expect(mockDisabledLink.mock.calls).toEqual([]);
expect(mockNewOriginLink.mock.calls).toEqual([]);
expect(mockNoLink.mock.calls).toEqual([]);
expect(mockSameOriginLink.mock.calls).toEqual([
['/page_1', true],
['/page_1?p=3', true],
[
{
href: '/page_1',
pageId: 'page_1',
newTab: true,
},
],
[
{
href: '/page_1?p=3',
pageId: 'page_1',
newTab: true,
urlQuery: {
p: 3,
},
},
],
]);
});
@ -50,145 +90,366 @@ test('createLink, link with pageId with inputs', () => {
const lowdefy = { inputs: {} };
const link = createLink({
backLink: mockBackLink,
disabledLink: mockDisabledLink,
lowdefy,
newOriginLink: mockNewOriginLink,
noLink: mockNoLink,
sameOriginLink: mockSameOriginLink,
});
link({ pageId: 'page_1', input: { a: 1 } });
link({ pageId: 'page_2', input: { a: 2 }, urlQuery: { p: 3 } });
expect(mockBackLink.mock.calls).toEqual([]);
expect(mockDisabledLink.mock.calls).toEqual([]);
expect(mockNewOriginLink.mock.calls).toEqual([]);
expect(mockNoLink.mock.calls).toEqual([]);
expect(mockSameOriginLink.mock.calls).toEqual([
['/page_1', undefined],
['/page_2?p=3', undefined],
[
{
href: '/page_1',
input: {
a: 1,
},
pageId: 'page_1',
},
],
[
{
href: '/page_2?p=3',
input: {
a: 2,
},
pageId: 'page_2',
urlQuery: {
p: 3,
},
},
],
]);
expect(lowdefy.inputs).toEqual({
page_1: { a: 1 },
page_2: { a: 2 },
'page:page_1': { a: 1 },
'page:page_2': { a: 2 },
});
});
test('createLink, link with url', () => {
test('createLink, link with url and protocol', () => {
const lowdefy = { inputs: {} };
const link = createLink({
backLink: mockBackLink,
disabledLink: mockDisabledLink,
lowdefy,
newOriginLink: mockNewOriginLink,
noLink: mockNoLink,
sameOriginLink: mockSameOriginLink,
});
link({ url: 'http://localhost:8080/test' });
link({ url: 'http://localhost:8080/test', urlQuery: { p: 3 } });
expect(mockBackLink.mock.calls).toEqual([]);
expect(mockDisabledLink.mock.calls).toEqual([]);
expect(mockNoLink.mock.calls).toEqual([]);
expect(mockNewOriginLink.mock.calls).toEqual([
['http://localhost:8080/test', undefined],
['http://localhost:8080/test?p=3', undefined],
[
{
href: 'http://localhost:8080/test',
url: 'http://localhost:8080/test',
},
],
[
{
href: 'http://localhost:8080/test?p=3',
url: 'http://localhost:8080/test',
urlQuery: {
p: 3,
},
},
],
]);
expect(mockSameOriginLink.mock.calls).toEqual([]);
});
test('createLink, link with url new tab', () => {
test('createLink, link with url new tab and protocol', () => {
const lowdefy = { inputs: {} };
const link = createLink({
backLink: mockBackLink,
disabledLink: mockDisabledLink,
lowdefy,
newOriginLink: mockNewOriginLink,
noLink: mockNoLink,
sameOriginLink: mockSameOriginLink,
});
link({ url: 'http://localhost:8080/test', newTab: true });
link({ url: 'http://localhost:8080/test', newTab: true, urlQuery: { p: 3 } });
expect(mockBackLink.mock.calls).toEqual([]);
expect(mockDisabledLink.mock.calls).toEqual([]);
expect(mockNoLink.mock.calls).toEqual([]);
expect(mockNewOriginLink.mock.calls).toEqual([
['http://localhost:8080/test', true],
['http://localhost:8080/test?p=3', true],
[
{
href: 'http://localhost:8080/test',
url: 'http://localhost:8080/test',
newTab: true,
},
],
[
{
href: 'http://localhost:8080/test?p=3',
url: 'http://localhost:8080/test',
urlQuery: {
p: 3,
},
newTab: true,
},
],
]);
expect(mockSameOriginLink.mock.calls).toEqual([]);
});
test('createLink, link with home', () => {
const lowdefy = { inputs: {}, homePageId: 'home' };
test('createLink, link with url and no protocol', () => {
const lowdefy = { inputs: {} };
const link = createLink({
backLink: mockBackLink,
disabledLink: mockDisabledLink,
lowdefy,
newOriginLink: mockNewOriginLink,
noLink: mockNoLink,
sameOriginLink: mockSameOriginLink,
});
link({ url: 'external.com/test', newTab: true });
link({ url: 'external.com/test', newTab: true, urlQuery: { p: 3 } });
expect(mockBackLink.mock.calls).toEqual([]);
expect(mockDisabledLink.mock.calls).toEqual([]);
expect(mockNoLink.mock.calls).toEqual([]);
expect(mockNewOriginLink.mock.calls).toEqual([
[
{
href: 'https://external.com/test',
url: 'external.com/test',
newTab: true,
},
],
[
{
href: 'https://external.com/test?p=3',
url: 'external.com/test',
urlQuery: {
p: 3,
},
newTab: true,
},
],
]);
expect(mockSameOriginLink.mock.calls).toEqual([]);
});
test('createLink, link with home, not configured', () => {
const lowdefy = { inputs: {}, home: { pageId: 'home', configured: false } };
const link = createLink({
backLink: mockBackLink,
disabledLink: mockDisabledLink,
lowdefy,
newOriginLink: mockNewOriginLink,
noLink: mockNoLink,
sameOriginLink: mockSameOriginLink,
});
link({ home: true });
link({ home: true, urlQuery: { p: 3 } });
expect(mockBackLink.mock.calls).toEqual([]);
expect(mockDisabledLink.mock.calls).toEqual([]);
expect(mockNoLink.mock.calls).toEqual([]);
expect(mockNewOriginLink.mock.calls).toEqual([]);
expect(mockSameOriginLink.mock.calls).toEqual([
['/home', undefined],
['/home?p=3', undefined],
[
{
home: true,
href: '/home',
},
],
[
{
home: true,
href: '/home?p=3',
urlQuery: {
p: 3,
},
},
],
]);
});
test('createLink, link with home new tab', () => {
const lowdefy = { inputs: {}, homePageId: 'home' };
test('createLink, link with home, configured', () => {
const lowdefy = { inputs: {}, home: { pageId: 'home', configured: true } };
const link = createLink({
backLink: mockBackLink,
disabledLink: mockDisabledLink,
lowdefy,
newOriginLink: mockNewOriginLink,
noLink: mockNoLink,
sameOriginLink: mockSameOriginLink,
});
link({ home: true });
link({ home: true, urlQuery: { p: 3 } });
expect(mockBackLink.mock.calls).toEqual([]);
expect(mockDisabledLink.mock.calls).toEqual([]);
expect(mockNoLink.mock.calls).toEqual([]);
expect(mockNewOriginLink.mock.calls).toEqual([]);
expect(mockSameOriginLink.mock.calls).toEqual([
[
{
home: true,
href: '/',
},
],
[
{
home: true,
href: '/?p=3',
urlQuery: {
p: 3,
},
},
],
]);
});
test('createLink, link with home new tab, not configured', () => {
const lowdefy = { inputs: {}, home: { pageId: 'home' } };
const link = createLink({
backLink: mockBackLink,
disabledLink: mockDisabledLink,
lowdefy,
newOriginLink: mockNewOriginLink,
noLink: mockNoLink,
sameOriginLink: mockSameOriginLink,
});
link({ home: true, newTab: true });
link({ home: true, newTab: true, urlQuery: { p: 3 } });
expect(mockBackLink.mock.calls).toEqual([]);
expect(mockDisabledLink.mock.calls).toEqual([]);
expect(mockNoLink.mock.calls).toEqual([]);
expect(mockNewOriginLink.mock.calls).toEqual([]);
expect(mockSameOriginLink.mock.calls).toEqual([
['/home', true],
['/home?p=3', true],
[{ home: true, href: '/home', newTab: true }],
[
{
home: true,
href: '/home?p=3',
newTab: true,
urlQuery: {
p: 3,
},
},
],
]);
});
test('createLink, link with home with inputs', () => {
const lowdefy = { inputs: {}, homePageId: 'home' };
test('createLink, link with home with inputs, not configured', () => {
const lowdefy = { inputs: {}, home: { pageId: 'home' } };
const link = createLink({
backLink: mockBackLink,
disabledLink: mockDisabledLink,
lowdefy,
newOriginLink: mockNewOriginLink,
noLink: mockNoLink,
sameOriginLink: mockSameOriginLink,
});
link({ home: true, input: { a: 1 } });
expect(mockBackLink.mock.calls).toEqual([]);
expect(mockDisabledLink.mock.calls).toEqual([]);
expect(mockNoLink.mock.calls).toEqual([]);
expect(mockNewOriginLink.mock.calls).toEqual([]);
expect(mockSameOriginLink.mock.calls).toEqual([['/home', undefined]]);
expect(mockSameOriginLink.mock.calls).toEqual([
[
{
home: true,
href: '/home',
input: {
a: 1,
},
},
],
]);
expect(lowdefy.inputs).toEqual({
home: { a: 1 },
'page:home': { a: 1 },
});
});
test('createLink, link to throw if no params', () => {
const lowdefy = { inputs: {}, homePageId: 'home' };
test('createLink, no params calls noLink', () => {
const lowdefy = { inputs: {}, home: { pageId: 'home' } };
const link = createLink({
backLink: mockBackLink,
disabledLink: mockDisabledLink,
lowdefy,
newOriginLink: mockNewOriginLink,
noLink: mockNoLink,
sameOriginLink: mockSameOriginLink,
});
expect(() => link({})).toThrowErrorMatchingInlineSnapshot(`"Invalid Link."`);
link({});
expect(mockBackLink.mock.calls).toEqual([]);
expect(mockDisabledLink.mock.calls).toEqual([]);
expect(mockNoLink.mock.calls).toEqual([[{}]]);
expect(mockNewOriginLink.mock.calls).toEqual([]);
expect(mockSameOriginLink.mock.calls).toEqual([]);
});
test('createLink, disabled calls disabledLink', () => {
const lowdefy = { inputs: {}, home: { pageId: 'home' } };
const link = createLink({
backLink: mockBackLink,
disabledLink: mockDisabledLink,
lowdefy,
newOriginLink: mockNewOriginLink,
noLink: mockNoLink,
sameOriginLink: mockSameOriginLink,
});
link({ disabled: true, home: true });
expect(mockBackLink.mock.calls).toEqual([]);
expect(mockDisabledLink.mock.calls).toEqual([
[
{
disabled: true,
home: true,
},
],
]);
expect(mockNoLink.mock.calls).toEqual([]);
expect(mockNewOriginLink.mock.calls).toEqual([]);
expect(mockSameOriginLink.mock.calls).toEqual([]);
});
test('createLink, link with back', () => {
const lowdefy = { inputs: {} };
const link = createLink({
backLink: mockBackLink,
disabledLink: mockDisabledLink,
lowdefy,
newOriginLink: mockNewOriginLink,
noLink: mockNoLink,
sameOriginLink: mockSameOriginLink,
});
link({ back: true });
expect(mockBackLink.mock.calls).toEqual([[]]);
expect(mockBackLink.mock.calls).toEqual([
[
{
back: true,
},
],
]);
expect(mockDisabledLink.mock.calls).toEqual([]);
expect(mockNoLink.mock.calls).toEqual([]);
expect(mockNewOriginLink.mock.calls).toEqual([]);
expect(mockSameOriginLink.mock.calls).toEqual([]);
});
test('createLink, link with back equal to false is invalid', () => {
test('createLink, link with more than one parameter is invalid.', () => {
const lowdefy = { inputs: {} };
const link = createLink({
backLink: mockBackLink,
disabledLink: mockDisabledLink,
lowdefy,
newOriginLink: mockNewOriginLink,
noLink: mockNoLink,
sameOriginLink: mockSameOriginLink,
});
expect(() => link({ back: false })).toThrowErrorMatchingInlineSnapshot(`"Invalid Link."`);
expect(() => link({ back: true, home: true })).toThrowErrorMatchingInlineSnapshot(
`"Invalid Link: To avoid ambiguity, only one of 'back', 'home', 'pageId' or 'url' can be defined."`
);
});

View File

@ -19,26 +19,7 @@ import { type, get } from '@lowdefy/helpers';
import { Breadcrumb } from 'antd';
import { blockDefaultProps } from '@lowdefy/block-utils';
const ItemLink = ({ basePath, children, className, link, Link }) => {
if (type.isString(link.pageId)) {
return (
<Link href={`${basePath}/${link.pageId}`} className={className}>
{children}
</Link>
);
}
if (type.isString(link.url)) {
return (
<Link href={link.url} className={className}>
{children}
</Link>
);
}
return <span className={className}>{children}</span>;
};
const BreadcrumbBlock = ({
basePath,
blockId,
events,
components: { Icon, Link },
@ -61,30 +42,33 @@ const BreadcrumbBlock = ({
(() => methods.triggerEvent({ name: onClickActionName, event: { link, index } }))
}
>
<ItemLink
basePath={basePath}
<Link
id={`${blockId}_${index}`}
className={methods.makeCssClass([
{
cursor: events[onClickActionName] && 'pointer',
},
link.style,
])}
link={link}
Link={Link}
{...link}
>
{link.icon && (
<Icon
blockId={`${blockId}_${index}_icon`}
events={events}
properties={{
name: type.isString(link.icon) && link.icon,
...(type.isObject(link.icon) ? link.icon : {}),
style: { paddingRight: 8, ...(link.icon.style || {}) },
}}
/>
{(defaultTitle) => (
<>
{link.icon && (
<Icon
blockId={`${blockId}_${index}_icon`}
events={events}
properties={{
name: type.isString(link.icon) && link.icon,
...(type.isObject(link.icon) ? link.icon : {}),
style: { paddingRight: 8, ...(link.icon.style || {}) },
}}
/>
)}
{type.isString(link) ? link : link.label || defaultTitle}
</>
)}
{type.isString(link) ? link : link.label || link.pageId || link.url || `Link ${index}`}
</ItemLink>
</Link>
</Breadcrumb.Item>
))}
</Breadcrumb>

View File

@ -28,26 +28,8 @@ const getDefaultMenu = (menus, menuId = 'default', links) => {
return menu.links || [];
};
const getTitle = (id, properties, defaultTitle) =>
(properties && properties.title) || defaultTitle || id;
const MenuTitle = ({ basePath, id, Link, linkStyle, makeCssClass, pageId, properties, url }) => {
if (type.isString(pageId)) {
return (
<Link href={`${basePath}/${pageId}`} className={makeCssClass([linkStyle])}>
{getTitle(id, properties, pageId)}
</Link>
);
}
if (url) {
return (
<Link href={url} className={makeCssClass([linkStyle])}>
{getTitle(id, properties, url)}
</Link>
);
}
return <span className={makeCssClass([linkStyle])}>{getTitle(id, properties)}</span>;
};
const getTitle = ({ id, properties, pageId, url }) =>
(properties && properties.title) || pageId || url || id;
const getNestedColors = (menuColor, background) => {
const fontColor = color(menuColor, 6);
@ -69,7 +51,6 @@ const getNestedColors = (menuColor, background) => {
};
const MenuComp = ({
basePath,
blockId,
components: { Icon, Link },
events,
@ -218,14 +199,13 @@ const MenuComp = ({
])}
key={`${link.pageId || link.id}_${i}`}
title={
<MenuTitle
basePath={basePath}
Link={Link}
linkStyle={methods.makeCssClass(link.style, true)}
id={link.id}
makeCssClass={methods.makeCssClass}
properties={link.properties}
/>
<Link
id={`${link.pageId || link.id}_${i}`}
className={methods.makeCssClass(link.style, true)}
{...link}
>
{getTitle(link)}
</Link>
}
icon={
link.properties &&
@ -253,14 +233,13 @@ const MenuComp = ({
<Menu.ItemGroup
key={`${subLink.pageId || subLink.id}_${j}`}
title={
<MenuTitle
basePath={basePath}
Link={Link}
linkStyle={methods.makeCssClass(subLink.style, true)}
id={subLink.id}
makeCssClass={methods.makeCssClass}
properties={subLink.properties}
/>
<Link
id={`${subLink.pageId || subLink.id}_${j}`}
className={methods.makeCssClass(subLink.style, true)}
{...subLink}
>
{getTitle(subLink)}
</Link>
}
>
{subLink.links.map((subLinkGroup, k) => {
@ -288,16 +267,13 @@ const MenuComp = ({
)
}
>
<MenuTitle
basePath={basePath}
Link={Link}
linkStyle={methods.makeCssClass(subLinkGroup.style, true)}
id={subLinkGroup.id}
makeCssClass={methods.makeCssClass}
pageId={subLinkGroup.pageId}
properties={subLinkGroup.properties}
url={subLinkGroup.url}
/>
<Link
id={`${subLinkGroup.pageId || subLinkGroup.id}_${k}`}
className={methods.makeCssClass(subLinkGroup.style, true)}
{...subLinkGroup}
>
{getTitle(subLinkGroup)}
</Link>
</Menu.Item>
);
})}
@ -320,16 +296,13 @@ const MenuComp = ({
)
}
>
<MenuTitle
basePath={basePath}
Link={Link}
linkStyle={methods.makeCssClass(subLink.style, true)}
id={subLink.id}
makeCssClass={methods.makeCssClass}
pageId={subLink.pageId}
properties={subLink.properties}
url={subLink.url}
/>
<Link
id={`${subLink.pageId || subLink.id}_${j}`}
className={methods.makeCssClass(subLink.style, true)}
{...subLink}
>
{getTitle(subLink)}
</Link>
</Menu.Item>
);
}
@ -353,16 +326,13 @@ const MenuComp = ({
)
}
>
<MenuTitle
basePath={basePath}
Link={Link}
linkStyle={methods.makeCssClass(link.style, true)}
id={link.id}
makeCssClass={methods.makeCssClass}
pageId={link.pageId}
properties={link.properties}
url={link.url}
/>
<Link
id={`${link.pageId || link.id}_${i}`}
className={methods.makeCssClass(link.style, true)}
{...link}
>
{getTitle(link)}
</Link>
</Menu.Item>
);
}

View File

@ -32,7 +32,6 @@ const PageHeaderMenu = ({
components: { Icon, Link },
content,
events,
homePageId,
menus,
methods,
pageId,
@ -116,10 +115,9 @@ const PageHeaderMenu = ({
])}
content={{
// TODO: use next/image
// TODO: Link to home=true
content: () => (
<>
<Link href={`${homePageId}`}>
<Link home={true}>
<img
src={
(properties.logo && properties.logo.src) ||

View File

@ -35,7 +35,6 @@ const PageSiderMenu = ({
components: { Icon, Link },
events,
content,
homePageId,
menus,
methods,
pageId,
@ -117,7 +116,6 @@ const PageSiderMenu = ({
properties={{ style: mergeObjects([{ minHeight: '100vh' }, properties.style]) }}
content={{
// TODO: use next/image
// TODO: Link to home=true
content: () => (
<>
<Header
@ -167,7 +165,7 @@ const PageSiderMenu = ({
/>
</div>
</div>
<Link href={`${basePath}/${homePageId}`}>
<Link home={true}>
<img
src={
(properties.logo && properties.logo.src) ||

View File

@ -15,28 +15,9 @@
*/
import React from 'react';
import { get, type } from '@lowdefy/helpers';
import { get } from '@lowdefy/helpers';
import { blockDefaultProps } from '@lowdefy/block-utils';
const Strong = ({ children, strong }) => (strong ? <b>{children}</b> : <>{children}</>);
const Tag = ({ blockId, children, className, disabled, href, Link, newTab, onClick, rel }) =>
disabled ? (
<span id={blockId} className={className}>
{children}
</span>
) : (
<Link
id={blockId}
className={className}
href={href}
onClick={onClick}
rel={rel || 'noopener noreferrer'}
target={newTab ? '_blank' : '_self'}
>
{children}
</Link>
);
const AnchorBlock = ({
blockId,
events,
@ -45,38 +26,33 @@ const AnchorBlock = ({
methods,
properties,
}) => {
const title = type.isNone(properties.title)
? type.isNone(properties.href)
? properties.href
: blockId
: properties.title;
const showLoading = get(events, 'onClick.loading') || loading;
const disabled = properties.disabled || showLoading;
return (
<Tag
blockId={blockId}
<Link
id={blockId}
className={methods.makeCssClass([
properties.style,
disabled && { color: '#BEBEBE', cursor: 'not-allowed' },
])}
disabled={disabled}
href={properties.href}
Link={Link}
rel={properties.rel}
newTab={properties.newTab}
onClick={() => methods.triggerEvent({ name: 'onClick' })}
{...properties}
>
<Strong strong={properties.strong}>
{properties.icon && (
<Icon
blockId={`${blockId}_icon`}
events={events}
properties={showLoading ? { name: 'LoadingOutlined', spin: true } : properties.icon}
/>
)}
{` ${title}`}
</Strong>
</Tag>
{(defaultTitle) => (
<>
{properties.icon &&
(
<Icon
blockId={`${blockId}_icon`}
events={events}
properties={showLoading ? { name: 'LoadingOutlined', spin: true } : properties.icon}
/>
) + ` `}
{properties.title || defaultTitle}
</>
)}
</Link>
);
};

View File

@ -7,6 +7,7 @@ module.exports = withLess({
modifyVars: lowdefyConfig.theme.lessVariables,
},
},
basePath: process.env.LOWDEFY_BASE_PATH || lowdefyConfig.basePath,
// reactStrictMode: true,
webpack: (config, { isServer }) => {
if (!isServer) {

View File

@ -1,7 +1,9 @@
const withLess = require('next-with-less');
const lowdefyConfig = require('./build/config.json');
// TODO: Trance env and args from cli that is required on the server.
module.exports = withLess({
basePath: process.env.LOWDEFY_BASE_PATH || lowdefyConfig.basePath,
lessLoaderOptions: {
lessOptions: {
modifyVars: lowdefyConfig.theme.lessVariables,

View File

@ -19,14 +19,13 @@ import React from 'react';
import callRequest from '../utils/callRequest.js';
import blockComponents from '../../build/plugins/blocks.js';
import operators from '../../build/plugins/operatorsClient.js';
import components from './components.js';
const LowdefyContext = ({ children }) => {
const lowdefy = {
_internal: {
const LowdefyContext = ({ children, lowdefy }) => {
if (!lowdefy._internal) {
lowdefy._internal = {
blockComponents,
callRequest,
components,
components: {},
document,
operators,
updaters: {},
@ -36,14 +35,14 @@ const LowdefyContext = ({ children }) => {
return () => undefined;
},
link: () => undefined,
},
contexts: {},
inputs: {},
lowdefyGlobal: {},
};
};
lowdefy.contexts = {};
lowdefy.inputs = {};
lowdefy.lowdefyGlobal = {};
}
lowdefy._internal.updateBlock = (blockId) =>
lowdefy._internal.updaters[blockId] && lowdefy._internal.updaters[blockId]();
return <>{children(lowdefy)}</>;
return <>{children}</>;
};
export default LowdefyContext;

View File

@ -16,25 +16,29 @@
import React from 'react';
import { urlQuery } from '@lowdefy/helpers';
import { useRouter } from 'next/router';
import Context from './Context.js';
import Head from './Head.js';
import Block from './block/Block.js';
import setupLink from '../utils/setupLink.js';
import createComponents from './createComponents.js';
const LoadingBlock = () => <div>Loading...</div>;
const Page = ({ lowdefy, pageConfig, rootConfig }) => {
const router = useRouter();
lowdefy._internal.basePath = router.basePath;
lowdefy._internal.pathname = router.pathname;
lowdefy._internal.query = router.query;
lowdefy._internal.router = router;
lowdefy._internal.link = setupLink({ lowdefy });
lowdefy._internal.link = setupLink(lowdefy);
lowdefy._internal.components = createComponents(lowdefy);
lowdefy.basePath = lowdefy._internal.router.basePath;
lowdefy.home = rootConfig.home;
lowdefy.lowdefyGlobal = rootConfig.lowdefyGlobal;
lowdefy.menus = rootConfig.menus;
lowdefy.urlQuery = urlQuery.parse(window.location.search.slice(1));
return (
<Context config={pageConfig} lowdefy={lowdefy}>
{(context, loading) => {

View File

@ -64,12 +64,10 @@ const CategorySwitch = ({ block, Blocks, context, lowdefy }) => {
setValue: block.setValue,
triggerEvent: block.triggerEvent,
})}
// TODO: React throws a basePath warning
basePath={lowdefy._internal.basePath}
basePath={lowdefy.basePath}
blockId={block.blockId}
components={lowdefy._internal.components}
events={block.eval.events}
homePageId={lowdefy.home.pageId}
key={block.blockId}
loading={block.loading}
menus={lowdefy.menus}
@ -98,11 +96,10 @@ const CategorySwitch = ({ block, Blocks, context, lowdefy }) => {
registerMethod: block.registerMethod,
triggerEvent: block.triggerEvent,
})}
basePath={lowdefy._internal.basePath}
basePath={lowdefy.basePath}
blockId={block.blockId}
components={lowdefy._internal.components}
events={block.eval.events}
homePageId={lowdefy.home.pageId}
key={block.blockId}
loading={block.loading}
menus={lowdefy.menus}

View File

@ -65,12 +65,11 @@ const Container = ({ block, Blocks, Component, context, lowdefy }) => {
registerMethod: block.registerMethod,
triggerEvent: block.triggerEvent,
})}
basePath={lowdefy._internal.basePath}
basePath={lowdefy.basePath}
blockId={block.blockId}
components={lowdefy._internal.components}
content={content}
events={block.eval.events}
homePageId={lowdefy.home.pageId}
key={block.blockId}
loading={block.loading}
menus={lowdefy.menus}

View File

@ -72,11 +72,10 @@ const List = ({ block, Blocks, Component, context, lowdefy }) => {
triggerEvent: block.triggerEvent,
unshiftItem: block.unshiftItem,
})}
basePath={lowdefy._internal.basePath}
basePath={lowdefy.basePath}
blockId={block.blockId}
components={lowdefy._internal.components}
events={block.eval.events}
homePageId={lowdefy.home.pageId}
key={block.blockId}
list={contentList}
loading={block.loading}

View File

@ -14,12 +14,16 @@
limitations under the License.
*/
import Link from 'next/link';
import { createIcon } from '@lowdefy/block-utils';
import createLinkComponent from './createLinkComponent.js';
import icons from '../../build/plugins/icons.js';
export default {
Link,
Icon: createIcon(icons),
const createComponents = (lowdefy) => {
return {
Link: createLinkComponent(lowdefy),
Icon: createIcon(icons),
};
};
export default createComponents;

View File

@ -0,0 +1,92 @@
import React from 'react';
import NextLink from 'next/link';
import { createLink } from '@lowdefy/engine';
import { type } from '@lowdefy/helpers';
const createLinkComponent = (lowdefy) => {
const backLink = ({ ariaLabel, children, className, id, rel }) => (
<a
id={id}
onClick={() => lowdefy._internal.router.back()}
className={className}
rel={rel}
aria-label={ariaLabel || 'back'}
>
{type.isFunction(children) ? children(id) : children}
</a>
);
const newOriginLink = ({
ariaLabel,
children,
className,
href,
id,
newTab,
pageId,
rel,
url,
}) => {
return (
<a
id={id}
aria-label={ariaLabel}
className={className}
href={href}
rel={rel || (newTab && 'noopener noreferrer')}
target={newTab && '_blank'}
>
{type.isFunction(children) ? children(pageId || url || id) : children}
</a>
);
};
const sameOriginLink = ({
ariaLabel,
children,
className,
href,
id,
newTab,
pageId,
rel,
replace,
scroll,
url,
}) => {
if (newTab) {
return (
<a
id={id}
aria-label={ariaLabel}
className={className}
href={`${window.location.origin}${lowdefy.basePath}${href}`}
rel={rel || 'noopener noreferrer'}
target="_blank"
>
{type.isFunction(children) ? children(pageId || url || id) : children}
</a>
);
}
return (
<NextLink href={href} replace={replace} scroll={scroll}>
<a id={id} aria-label={ariaLabel} className={className} rel={rel}>
{type.isFunction(children) ? children(pageId || url || id) : children}
</a>
</NextLink>
);
};
const noLink = ({ className, children, id }) => (
<span id={id} className={className}>
{type.isFunction(children) ? children(id) : children}
</span>
);
return createLink({
backLink,
lowdefy,
newOriginLink,
sameOriginLink,
noLink,
disabledLink: noLink,
});
};
export default createLinkComponent;

View File

@ -22,12 +22,14 @@ import LowdefyContext from '../components/LowdefyContext.js';
import '../../build/plugins/styles.less';
const lowdefy = {};
function App({ Component, pageProps }) {
return (
<ErrorBoundary>
<Suspense>
<LowdefyContext>
{(lowdefy) => <Component lowdefy={lowdefy} {...pageProps} />}
<LowdefyContext lowdefy={lowdefy}>
<Component lowdefy={lowdefy} {...pageProps} />
</LowdefyContext>
</Suspense>
</ErrorBoundary>

View File

@ -19,6 +19,7 @@ import { createApiContext, getPageConfig, getRootConfig } from '@lowdefy/api';
import Page from '../components/Page.js';
export async function getServerSideProps() {
// TODO: is this build directory configurable from the cli?
const apiContext = await createApiContext({ buildDirectory: './build' });
const rootConfig = await getRootConfig(apiContext);
const { home } = rootConfig;

View File

@ -16,29 +16,30 @@
import { createLink } from '@lowdefy/engine';
function setupLink({ lowdefy }) {
function setupLink(lowdefy) {
const { router, window } = lowdefy._internal;
const sameOriginLink = (path, newTab) => {
const backLink = () => router.back();
const disabledLink = () => {};
const newOriginLink = ({ href, newTab }) => {
if (newTab) {
return window.open(`${window.location.origin}${lowdefy.basePath}${path}`, '_blank').focus();
return window.open(href, '_blank').focus();
} else {
return window.location.assign(href);
}
};
const sameOriginLink = ({ href, newTab }) => {
if (newTab) {
return window.open(`${window.location.origin}${lowdefy.basePath}${href}`, '_blank').focus();
} else {
// Next handles the basePath here.
return router.push({
pathname: path,
// TODO: Do we handle urlQuery as a param here?
// query: {},
pathname: href, // href includes the urlQuery as defined by engine
});
}
};
const newOriginLink = (path, newTab) => {
if (newTab) {
return window.open(path, '_blank').focus();
} else {
return (window.location.href = path);
}
const noLink = () => {
throw new Error(`Invalid Link.`);
};
const backLink = () => window.history.back();
return createLink({ backLink, lowdefy, newOriginLink, sameOriginLink });
return createLink({ backLink, disabledLink, lowdefy, newOriginLink, noLink, sameOriginLink });
}
export default setupLink;

View File

@ -30,7 +30,6 @@ const lowdefyProps = [
'components',
'content',
'eventLog',
'homePageId',
'list',
'loading',
'menus',

View File

@ -20,7 +20,7 @@ function getConfigFromEnv() {
logLevel: process.env.LOWDEFY_SERVER_LOG_LEVEL,
publicDirectory: process.env.LOWDEFY_SERVER_PUBLIC_DIRECTORY,
port: process.env.LOWDEFY_SERVER_PORT && parseInt(process.env.LOWDEFY_SERVER_PORT),
serverBasePath: process.env.LOWDEFY_SERVER_BASE_PATH,
basePath: process.env.LOWDEFY_BASE_PATH,
};
}

View File

@ -37,7 +37,7 @@ test('Get config from env', () => {
buildDirectory: 'build',
publicDirectory: 'public',
port: 8080,
serverBasePath: 'base',
basePath: 'base',
});
});