Merge pull request #649 from lowdefy/fixes

Various fixes
This commit is contained in:
Sam 2021-06-17 14:46:45 +02:00 committed by GitHub
commit d050e178e1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 396 additions and 44 deletions

View File

@ -17,9 +17,11 @@
import { type } from '@lowdefy/helpers';
import page404 from './404.json';
const defaultPages = [page404];
async function addDefaultPages({ components }) {
// If not copied, the same object is mutated by build every time
// build runs for dev server. See #647
const defaultPages = [JSON.parse(JSON.stringify(page404))];
if (type.isNone(components.pages)) {
components.pages = [];
}

View File

@ -214,7 +214,7 @@ test('addDefaultPages, pages with 404 page, should not overwrite', async () => {
});
});
test('addDefaultPages, pages not an ', async () => {
test('addDefaultPages, pages not an array', async () => {
const components = {
pages: { id: 'page1', type: 'Context' },
};
@ -222,3 +222,13 @@ test('addDefaultPages, pages not an ', async () => {
'lowdefy.pages is not an array.'
);
});
test('addDefaultPages, pages are copied', async () => {
const components1 = {};
const res1 = await addDefaultPages({ components: components1, context });
const page1 = res1.pages[0];
page1.id = 'page:404';
const components2 = {};
const res2 = await addDefaultPages({ components: components2, context });
expect(res2.pages[0].id).toEqual('404');
});

View File

@ -24,8 +24,15 @@ async function buildBlock(block, blockContext) {
`Expected block to be an object on ${blockContext.pageId}. Received ${JSON.stringify(block)}`
);
}
if (type.isUndefined(block.id)) {
throw new Error(`Block id missing at page ${blockContext.pageId}`);
if (!type.isString(block.id)) {
if (type.isUndefined(block.id)) {
throw new Error(`Block id missing at page "${blockContext.pageId}".`);
}
throw new Error(
`Block id is not a string at page "${blockContext.pageId}". Received ${JSON.stringify(
block.id
)}.`
);
}
block.blockId = block.id;
block.id = `block:${blockContext.pageId}:${block.id}`;

View File

@ -39,8 +39,13 @@ Blocks:
async function buildPages({ components, context }) {
const pages = type.isArray(components.pages) ? components.pages : [];
const pageBuildPromises = pages.map(async (page, i) => {
if (type.isUndefined(page.id)) {
throw new Error(`Page id missing at page ${i}`);
if (!type.isString(page.id)) {
if (type.isUndefined(page.id)) {
throw new Error(`Page id missing at page ${i}.`);
}
throw new Error(
`Page id is not a string at at page ${i}. Received ${JSON.stringify(page.id)}.`
);
}
page.pageId = page.id;
await checkPageIsContext(page, context.metaLoader);

View File

@ -212,7 +212,22 @@ test('page does not have an id', async () => {
},
],
};
await expect(buildPages({ components, context })).rejects.toThrow('Page id missing at page 0');
await expect(buildPages({ components, context })).rejects.toThrow('Page id missing at page 0.');
});
test('page id is not a string', async () => {
const components = {
pages: [
{
id: true,
type: 'Context',
auth,
},
],
};
await expect(buildPages({ components, context })).rejects.toThrow(
'Page id is not a string at at page 0. Received true.'
);
});
test('block does not have an id', async () => {
@ -231,7 +246,28 @@ test('block does not have an id', async () => {
],
};
await expect(buildPages({ components, context })).rejects.toThrow(
'Block id missing at page page1'
'Block id missing at page "page1".'
);
});
test('block id is not a string', async () => {
const components = {
pages: [
{
id: 'page1',
type: 'Context',
auth,
blocks: [
{
id: true,
type: 'Input',
},
],
},
],
};
await expect(buildPages({ components, context })).rejects.toThrow(
'Block id is not a string at page "page1". Received true.'
);
});
@ -1043,6 +1079,54 @@ describe('build requests', () => {
);
});
test('request id missing', async () => {
const components = {
pages: [
{
id: 'page_1',
auth,
type: 'Context',
requests: [{ type: 'Request' }],
},
],
};
await expect(buildPages({ components, context })).rejects.toThrow(
'Request id missing at page "page_1".'
);
});
test('request id not a string', async () => {
const components = {
pages: [
{
id: 'page_1',
auth,
type: 'Context',
requests: [{ id: true, type: 'Request' }],
},
],
};
await expect(buildPages({ components, context })).rejects.toThrow(
'Request id is not a string at page "page_1". Received true.'
);
});
test('request id contains a "."', async () => {
const components = {
pages: [
{
id: 'page_1',
auth,
type: 'Context',
requests: [{ id: 'my.request', type: 'Request' }],
},
],
};
await expect(buildPages({ components, context })).rejects.toThrow(
'Request id "my.request" should not include a period (".").'
);
});
test('give request an id', async () => {
const components = {
pages: [

View File

@ -16,6 +16,28 @@
import { type } from '@lowdefy/helpers';
function buildRequest({ request, blockContext }) {
if (!type.isString(request.id)) {
if (type.isUndefined(request.id)) {
throw new Error(`Request id missing at page "${blockContext.pageId}".`);
}
throw new Error(
`Request id is not a string at page "${blockContext.pageId}". Received ${JSON.stringify(
request.id
)}.`
);
}
if (request.id.includes('.')) {
throw new Error(`Request id "${request.id}" should not include a period (".").`);
}
request.auth = blockContext.auth;
request.requestId = request.id;
request.contextId = blockContext.contextId;
request.id = `request:${blockContext.pageId}:${blockContext.contextId}:${request.id}`;
blockContext.requests.push(request);
}
function buildRequests(block, blockContext) {
if (!type.isNone(block.requests)) {
if (!type.isArray(block.requests)) {
@ -26,11 +48,7 @@ function buildRequests(block, blockContext) {
);
}
block.requests.forEach((request) => {
request.auth = blockContext.auth;
request.requestId = request.id;
request.contextId = blockContext.contextId;
request.id = `request:${blockContext.pageId}:${blockContext.contextId}:${request.id}`;
blockContext.requests.push(request);
buildRequest({ request, blockContext });
});
delete block.requests;
}

View File

@ -0,0 +1,40 @@
# Copyright 2020-2021 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.
_ref:
path: templates/actions.yaml.njk
vars:
pageId: Wait
pageTitle: Wait
filePath: actions/Wait.yaml
types: |
```
(params: {
ms: integer,
}): void
```
description: |
The `Wait` waits for the set number of milliseconds before returning.
params: |
###### object
- `ms: integer`: __Required__ - The number of milliseconds to wait.
examples: |
###### Wait for 500 milliseconds:
```yaml
- id: wait
type: Wait
params:
ms: 500
```

View File

@ -1,20 +1 @@
<script src="https://cdn.jsdelivr.net/npm/docsearch.js@2.6.3/dist/cdn/docsearch.min.js"></script>
<script>
let time = 500;
function setDocsearh() {
try {
console.log('try', time);
docsearch({
apiKey: '4e88995ba28e39b8ed2bcfb6639379a1',
indexName: 'lowdefy',
inputSelector: '#docsearch_input',
debug: true,
});
} catch (err) {
console.log(err);
setTimeout(setDocsearh, time);
time = time * 2;
}
}
setDocsearh();
</script>

View File

@ -400,8 +400,8 @@ _ref:
- `ignoreUndefined: boolean`: Default: `false` - Specify if the BSON serializer should ignore undefined fields.
- `j: boolean`: Specify a journal write concern.
- `upsert: boolean`: Default: `false` - Insert document if no match is found.
- `w: number | string`: _Integer_ **|** _String_ - The write concern
- `wtimeout: number`: _Integer_ - The write concern timeout.
- `w: integer | string`: The write concern
- `wtimeout: integer`: The write concern timeout.
#### Examples

View File

@ -549,6 +549,9 @@
- id: Validate
type: MenuLink
pageId: Validate
- id: Wait
type: MenuLink
pageId: Wait
- id: operators
type: MenuGroup
properties:

View File

@ -25,7 +25,7 @@ _ref:
(requestId: string): any
```
description: |
The `_request` operator returns the response value of a request. If the request has not yet been call, or is still executing, the returned value is `null`.
The `_request` operator returns the response value of a request. If the request has not yet been call, or is still executing, the returned value is `null`. Dot notation and [block list indexes](/lists) are supported.
arguments: |
###### string
The id of the request.
@ -35,3 +35,16 @@ _ref:
_request: my_request
```
Returns: The response returned by the request.
###### Using dot notation to get the data object from the response:
```yaml
_request: my_request.data
```
###### Using dot notation to get the first element of an array response:
```yaml
_request: array_request.0
```
###### Using dot notation and block list indexes to get the name field from the element corresponding to the block index of an array response:
```yaml
_request: array_request.$.name
```

View File

@ -146,6 +146,7 @@
- _ref: actions/SetGlobal.yaml
- _ref: actions/SetState.yaml
- _ref: actions/Validate.yaml
- _ref: actions/Wait.yaml
- _ref: operators/_actions.yaml
- _ref: operators/_and.yaml

View File

@ -0,0 +1,33 @@
/*
Copyright 2020-2021 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.
*/
function connectDocsearch() {
let time = 100;
try {
// eslint-disable-next-line no-undef
docsearch({
apiKey: '4e88995ba28e39b8ed2bcfb6639379a1',
indexName: 'lowdefy',
inputSelector: '#docsearch_input',
debug: true,
});
} catch (err) {
setTimeout(connectDocsearch, time);
time = time * 2;
}
}
export default connectDocsearch;

View File

@ -1,3 +1,5 @@
import connectDocsearch from './connectDocsearch.js';
import filterDefaultValue from './filterDefaultValue.js';
window.lowdefy.registerJsAction('connectDocsearch', connectDocsearch);
window.lowdefy.registerJsOperator('filterDefaultValue', filterDefaultValue);

View File

@ -75,6 +75,10 @@ events:
_var: init_state_values
onEnterAsync:
- id: connect_docsearch
type: JsAction
params:
name: connectDocsearch
- id: post_telemetry
type: Request
messages:
@ -85,7 +89,7 @@ properties:
title: {{ block_type }}
header:
theme: light
menu:
menu:
forceSubMenuRender: true
sider:
width: 256px

View File

@ -44,6 +44,10 @@ events:
type: string
length: 16
onEnterAsync:
- id: connect_docsearch
type: JsAction
params:
name: connectDocsearch
- id: post_telemetry
type: Request
messages:
@ -54,7 +58,7 @@ properties:
title: {{ pageTitle }}
header:
theme: light
menu:
menu:
forceSubMenuRender: true
sider:
width: 256px

View File

@ -0,0 +1,26 @@
/*
Copyright 2020-2021 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 { type } from '@lowdefy/helpers';
async function Wait({ params }) {
if (!type.isInt(params.ms)) {
throw new Error(`Wait action "ms" param should be an integer.`);
}
return new Promise((resolve) => setTimeout(resolve, params.ms));
}
export default Wait;

View File

@ -26,6 +26,7 @@ import ScrollTo from './ScrollTo';
import SetGlobal from './SetGlobal';
import SetState from './SetState';
import Validate from './Validate';
import Wait from './Wait';
export default {
CallMethod,
@ -40,4 +41,5 @@ export default {
SetGlobal,
SetState,
Validate,
Wait,
};

View File

@ -0,0 +1,91 @@
/*
Copyright 2020-2021 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 testContext from '../testContext';
const pageId = 'one';
const lowdefy = {
auth: {
login: jest.fn(),
},
pageId,
};
const RealDate = Date;
const mockDate = jest.fn(() => ({ date: 0 }));
mockDate.now = jest.fn(() => 0);
beforeEach(() => {
global.Date = mockDate;
lowdefy.auth.login.mockReset();
});
afterAll(() => {
global.Date = RealDate;
});
const timeout = (ms) => {
return new Promise((resolve) => setTimeout(resolve, ms));
};
test('Wait', async () => {
const rootBlock = {
blockId: 'root',
meta: {
category: 'context',
},
areas: {
content: {
blocks: [
{
blockId: 'button',
type: 'Button',
meta: {
category: 'display',
valueType: 'string',
},
events: {
onClick: [
{
id: 'a',
type: 'Wait',
params: { ms: 500 },
},
],
},
},
],
},
},
};
const context = await testContext({
lowdefy,
rootBlock,
});
const { button } = context.RootBlocks.map;
let resolved = false;
button.triggerEvent({ name: 'onClick' }).then(() => {
resolved = true;
});
expect(resolved).toBe(false);
await timeout(100);
expect(resolved).toBe(false);
await timeout(300);
expect(resolved).toBe(false);
await timeout(150);
expect(resolved).toBe(true);
});

View File

@ -14,9 +14,9 @@
limitations under the License.
*/
import { type } from '@lowdefy/helpers';
import { applyArrayIndices, get, serializer, type } from '@lowdefy/helpers';
function _request({ params, requests, location }) {
function _request({ arrayIndices, params, requests, location }) {
if (!type.isString(params)) {
throw new Error(
`Operator Error: _request accepts a string value. Received: ${JSON.stringify(
@ -24,10 +24,18 @@ function _request({ params, requests, location }) {
)} at ${location}.`
);
}
if (params in requests && !requests[params].loading) {
return requests[params].response;
const splitKey = params.split('.');
const [requestId, ...keyParts] = splitKey;
if (requestId in requests && !requests[requestId].loading) {
if (splitKey.length === 1) {
return serializer.copy(requests[requestId].response);
}
const key = keyParts.reduce((acc, value) => (acc === '' ? value : acc.concat('.', value)), '');
return get(requests[requestId].response, applyArrayIndices(arrayIndices, key), {
copy: true,
});
}
return null; // return null for all requests which has not been filled on init
return null;
}
export default _request;

View File

@ -515,7 +515,7 @@ describe('parse operators', () => {
expect(errors).toEqual([]);
});
test.only('parse _js operator retuning a function', async () => {
test('parse _js operator retuning a function', async () => {
const test_fn = () => (a, b) => a + b;
const mockFn = jest.fn().mockImplementation(test_fn);
context.lowdefy.imports.jsOperators.test_fn = mockFn;

View File

@ -84,3 +84,21 @@ test('_request loading true', async () => {
expect(res.output).toBe(null);
expect(res.errors).toMatchInlineSnapshot(`Array []`);
});
test('_request dot notation', async () => {
const input = { _request: 'arr.0.a' };
const parser = new WebParser({ context, contexts });
await parser.init();
const res = parser.parse({ input, location: 'locationId', arrayIndices });
expect(res.output).toEqual('request a1');
expect(res.errors).toMatchInlineSnapshot(`Array []`);
});
test('_request dot notation with arrayindices', async () => {
const input = { _request: 'arr.$.a' };
const parser = new WebParser({ context, contexts });
await parser.init();
const res = parser.parse({ input, location: 'locationId', arrayIndices });
expect(res.output).toEqual('request a2');
expect(res.errors).toMatchInlineSnapshot(`Array []`);
});