Merge branch 'develop' into fix-date-time-selector

This commit is contained in:
Sam 2021-05-25 18:08:30 +02:00 committed by GitHub
commit 8cda2ab84a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
88 changed files with 2725 additions and 610 deletions

View File

@ -1,10 +1,12 @@
![Lowdefy](https://lowdefy.com/banner.png)
![Discord](https://img.shields.io/discord/729696747261263962?label=Discord%20Chat&logo=discord&logoColor=white)
[![Tweet](https://img.shields.io/twitter/url?logo=twitter&style=flat-square&url=https%3A%2F%2Flowdefy.com)](https://twitter.com/intent/tweet?text=Build%20web%20apps%2C%20admin%20panels%2C%20BI%20dashboards%2C%20and%20CRUD%20apps%20with%20ease%21%20Try%20&url=https://lowdefy.com&via=lowdefy&hashtags=lowcode,lowdefy,internaltools,developers,opensource)
[![Follow](https://img.shields.io/twitter/follow/lowdefy?logo=twitter&style=flat-square)](https://twitter.com/intent/follow?screen_name=lowdefy)
![Tests Main](https://github.com/lowdefy/lowdefy/workflows/Test%20Branches/badge.svg?branch=main)
![Tests Develop](https://github.com/lowdefy/lowdefy/workflows/Test%20Branches/badge.svg?branch=develop)
![Tests Main](https://github.com/lowdefy/lowdefy/workflows/Test%20Main/badge.svg?branch=main)
![Tests Develop](https://github.com/lowdefy/lowdefy/workflows/Test%20Develop/badge.svg?branch=develop)
[![Maintainability](https://api.codeclimate.com/v1/badges/6efe9bfa0648772cae00/maintainability)](https://codeclimate.com/github/lowdefy/lowdefy/maintainability)
[![Test Coverage](https://api.codeclimate.com/v1/badges/6efe9bfa0648772cae00/test_coverage)](https://codeclimate.com/github/lowdefy/lowdefy/test_coverage)
[![Codecov](https://codecov.io/gh/lowdefy/lowdefy/branch/main/graph/badge.svg?token=U2AEEH9K1W)](https://codecov.io/gh/lowdefy/lowdefy)

View File

@ -61,11 +61,11 @@
logo:
style:
border: 5px solid blue
- id: 'properties.logo.srcset'
- id: 'properties.logo.srcSet'
type: PageHeaderMenu
properties:
logo:
srcset: 'https://lowdefy.com/logos/name_250.png 250w, https://lowdefy.com/logos/name_450.png 450w'
srcSet: 'https://lowdefy.com/logos/name_250.png 250w, https://lowdefy.com/logos/name_450.png 450w'
menu:
links:
- id: Introduction
@ -84,7 +84,7 @@
type: PageHeaderMenu
properties:
logo:
srcset: '(max-width: 767px) 40px, 768px'
srcSet: '(max-width: 767px) 40px, 768px'
menu:
links:
- id: Introduction

View File

@ -61,11 +61,11 @@
logo:
style:
border: 5px solid blue
- id: 'properties.logo.srcset'
- id: 'properties.logo.srcSet'
type: PageHeaderMenu
properties:
logo:
srcset: 'https://lowdefy.com/logos/name_250.png 250w, https://lowdefy.com/logos/name_450.png 450w'
srcSet: 'https://lowdefy.com/logos/name_250.png 250w, https://lowdefy.com/logos/name_450.png 450w'
menu:
links:
- id: Introduction
@ -84,7 +84,7 @@
type: PageHeaderMenu
properties:
logo:
srcset: '(max-width: 767px) 40px, 768px'
srcSet: '(max-width: 767px) 40px, 768px'
menu:
links:
- id: Introduction

View File

@ -122,15 +122,15 @@ const PageHeaderMenu = ({
? '/public/logo-light-theme.png'
: '/public/logo-dark-theme.png')
}
srcset={
(properties.logo && (properties.logo.srcset || properties.logo.src)) ||
srcSet={
(properties.logo && (properties.logo.srcSet || properties.logo.src)) ||
(get(properties, 'header.theme') === 'light'
? '/public/logo-square-light-theme.png 40w, /public/logo-light-theme.png 768w'
: '/public/logo-square-dark-theme.png 40w, /public/logo-dark-theme.png 768w')
? '/public/logo-square-light-theme.png 40w, /public/logo-light-theme.png 577w'
: '/public/logo-square-dark-theme.png 40w, /public/logo-dark-theme.png 577w')
}
sizes={
(properties.logo && properties.logo.sizes) ||
'(max-width: 767px) 40px, 768px'
'(max-width: 576px) 40px, 577px'
}
alt={(properties.logo && properties.logo.alt) || 'Lowdefy'}
className={methods.makeCssClass([
@ -138,7 +138,7 @@ const PageHeaderMenu = ({
width: 130,
sm: {
width:
properties.logo && properties.logo.src && !properties.logo.srcset
properties.logo && properties.logo.src && !properties.logo.srcSet
? 130
: 40,
},

View File

@ -34,9 +34,9 @@
"type": "string",
"description": "Header logo source url."
},
"srcset": {
"srcSet": {
"type": "string",
"description": "Header logo srcset for logo img element."
"description": "Header logo srcSet for logo img element."
},
"size": {
"type": "string",

View File

@ -168,15 +168,15 @@ const PageSiderMenu = ({
? '/public/logo-light-theme.png'
: '/public/logo-dark-theme.png')
}
srcset={
(properties.logo && (properties.logo.srcset || properties.logo.src)) ||
srcSet={
(properties.logo && (properties.logo.srcSet || properties.logo.src)) ||
(get(properties, 'header.theme') === 'light'
? '/public/logo-square-light-theme.png 40w, /public/logo-light-theme.png 768w'
: '/public/logo-square-dark-theme.png 40w, /public/logo-dark-theme.png 768w')
? '/public/logo-square-light-theme.png 40w, /public/logo-light-theme.png 577w'
: '/public/logo-square-dark-theme.png 40w, /public/logo-dark-theme.png 577w')
}
sizes={
(properties.logo && properties.logo.sizes) ||
'(max-width: 767px) 40px, 768px'
'(max-width: 576px) 40px, 577px'
}
alt={(properties.logo && properties.logo.alt) || 'Lowdefy'}
className={methods.makeCssClass([
@ -184,7 +184,7 @@ const PageSiderMenu = ({
width: 130,
sm: {
width:
properties.logo && properties.logo.src && !properties.logo.srcset
properties.logo && properties.logo.src && !properties.logo.srcSet
? 130
: 40,
},

View File

@ -44,9 +44,9 @@
"type": "string",
"description": "Header logo source url."
},
"srcset": {
"srcSet": {
"type": "string",
"description": "Header logo srcset for logo img element."
"description": "Header logo srcSet for logo img element."
},
"size": {
"type": "string",

View File

@ -1079,13 +1079,13 @@ Array [
]
`;
exports[`Mock render - properties.logo.srcset - value[0] - default 1`] = `
exports[`Mock render - properties.logo.srcSet - value[0] - default 1`] = `
Array [
Array [
Object {
"children": <React.Fragment>
<HeaderBlock
blockId="properties.logo.srcset_header"
blockId="properties.logo.srcSet_header"
content={
Object {
"content": [Function],
@ -1131,7 +1131,7 @@ Array [
}
/>
<ContentBlock
blockId="properties.logo.srcset_content"
blockId="properties.logo.srcSet_content"
content={
Object {
"content": [Function],
@ -1176,7 +1176,7 @@ Array [
/>
</React.Fragment>,
"className": "css-vooagt",
"id": "properties.logo.srcset",
"id": "properties.logo.srcSet",
},
Object {},
],

View File

@ -40,9 +40,9 @@ exports[`Test Schema properties.logo.src 1`] = `true`;
exports[`Test Schema properties.logo.src 2`] = `null`;
exports[`Test Schema properties.logo.srcset 1`] = `true`;
exports[`Test Schema properties.logo.srcSet 1`] = `true`;
exports[`Test Schema properties.logo.srcset 2`] = `null`;
exports[`Test Schema properties.logo.srcSet 2`] = `null`;
exports[`Test Schema properties.logo.style 1`] = `true`;

View File

@ -903,13 +903,13 @@ Array [
]
`;
exports[`Mock render - properties.logo.srcset - value[0] - default 1`] = `
exports[`Mock render - properties.logo.srcSet - value[0] - default 1`] = `
Array [
Array [
Object {
"children": <React.Fragment>
<HeaderBlock
blockId="properties.logo.srcset_header"
blockId="properties.logo.srcSet_header"
content={
Object {
"content": [Function],
@ -956,7 +956,7 @@ Array [
}
/>
<LayoutBlock
blockId="properties.logo.srcset_layout"
blockId="properties.logo.srcSet_layout"
content={
Object {
"content": [Function],
@ -986,7 +986,7 @@ Array [
/>
</React.Fragment>,
"className": "css-vooagt",
"id": "properties.logo.srcset",
"id": "properties.logo.srcSet",
},
Object {},
],

View File

@ -56,9 +56,9 @@ exports[`Render default - value[0] 1`] = `
<img
alt="Lowdefy"
className="{\\"style\\":[{\\"width\\":130,\\"sm\\":{\\"width\\":40},\\"md\\":{\\"width\\":130}},{\\"margin\\":\\"0 30px 0 0\\",\\"flex\\":\\"0 1 auto\\",\\"sm\\":{\\"margin\\":\\"0 10px 0 0\\"},\\"md\\":{\\"margin\\":\\"0 15px 0 0\\"}},null]}"
sizes="(max-width: 767px) 40px, 768px"
sizes="(max-width: 576px) 40px, 577px"
src="/public/logo-dark-theme.png"
srcset="/public/logo-square-dark-theme.png 40w, /public/logo-dark-theme.png 768w"
srcSet="/public/logo-square-dark-theme.png 40w, /public/logo-dark-theme.png 577w"
/>
</a>
</header>
@ -231,9 +231,9 @@ exports[`Render properties.breadcrumb.list - value[0] 1`] = `
<img
alt="Lowdefy"
className="{\\"style\\":[{\\"width\\":130,\\"sm\\":{\\"width\\":40},\\"md\\":{\\"width\\":130}},{\\"margin\\":\\"0 30px 0 0\\",\\"flex\\":\\"0 1 auto\\",\\"sm\\":{\\"margin\\":\\"0 10px 0 0\\"},\\"md\\":{\\"margin\\":\\"0 15px 0 0\\"}},null]}"
sizes="(max-width: 767px) 40px, 768px"
sizes="(max-width: 576px) 40px, 577px"
src="/public/logo-dark-theme.png"
srcset="/public/logo-square-dark-theme.png 40w, /public/logo-dark-theme.png 768w"
srcSet="/public/logo-square-dark-theme.png 40w, /public/logo-dark-theme.png 577w"
/>
</a>
</header>
@ -440,9 +440,9 @@ exports[`Render properties.content.style - value[0] 1`] = `
<img
alt="Lowdefy"
className="{\\"style\\":[{\\"width\\":130,\\"sm\\":{\\"width\\":40},\\"md\\":{\\"width\\":130}},{\\"margin\\":\\"0 30px 0 0\\",\\"flex\\":\\"0 1 auto\\",\\"sm\\":{\\"margin\\":\\"0 10px 0 0\\"},\\"md\\":{\\"margin\\":\\"0 15px 0 0\\"}},null]}"
sizes="(max-width: 767px) 40px, 768px"
sizes="(max-width: 576px) 40px, 577px"
src="/public/logo-dark-theme.png"
srcset="/public/logo-square-dark-theme.png 40w, /public/logo-dark-theme.png 768w"
srcSet="/public/logo-square-dark-theme.png 40w, /public/logo-dark-theme.png 577w"
/>
</a>
</header>
@ -615,9 +615,9 @@ exports[`Render properties.footer.style - value[0] 1`] = `
<img
alt="Lowdefy"
className="{\\"style\\":[{\\"width\\":130,\\"sm\\":{\\"width\\":40},\\"md\\":{\\"width\\":130}},{\\"margin\\":\\"0 30px 0 0\\",\\"flex\\":\\"0 1 auto\\",\\"sm\\":{\\"margin\\":\\"0 10px 0 0\\"},\\"md\\":{\\"margin\\":\\"0 15px 0 0\\"}},null]}"
sizes="(max-width: 767px) 40px, 768px"
sizes="(max-width: 576px) 40px, 577px"
src="/public/logo-dark-theme.png"
srcset="/public/logo-square-dark-theme.png 40w, /public/logo-dark-theme.png 768w"
srcSet="/public/logo-square-dark-theme.png 40w, /public/logo-dark-theme.png 577w"
/>
</a>
</header>
@ -805,9 +805,9 @@ exports[`Render properties.header.color - value[0] 1`] = `
<img
alt="Lowdefy"
className="{\\"style\\":[{\\"width\\":130,\\"sm\\":{\\"width\\":40},\\"md\\":{\\"width\\":130}},{\\"margin\\":\\"0 30px 0 0\\",\\"flex\\":\\"0 1 auto\\",\\"sm\\":{\\"margin\\":\\"0 10px 0 0\\"},\\"md\\":{\\"margin\\":\\"0 15px 0 0\\"}},null]}"
sizes="(max-width: 767px) 40px, 768px"
sizes="(max-width: 576px) 40px, 577px"
src="/public/logo-dark-theme.png"
srcset="/public/logo-square-dark-theme.png 40w, /public/logo-dark-theme.png 768w"
srcSet="/public/logo-square-dark-theme.png 40w, /public/logo-dark-theme.png 577w"
/>
</a>
</header>
@ -1077,9 +1077,9 @@ exports[`Render properties.header.style - value[0] 1`] = `
<img
alt="Lowdefy"
className="{\\"style\\":[{\\"width\\":130,\\"sm\\":{\\"width\\":40},\\"md\\":{\\"width\\":130}},{\\"margin\\":\\"0 30px 0 0\\",\\"flex\\":\\"0 1 auto\\",\\"sm\\":{\\"margin\\":\\"0 10px 0 0\\"},\\"md\\":{\\"margin\\":\\"0 15px 0 0\\"}},null]}"
sizes="(max-width: 767px) 40px, 768px"
sizes="(max-width: 576px) 40px, 577px"
src="/public/logo-dark-theme.png"
srcset="/public/logo-square-dark-theme.png 40w, /public/logo-dark-theme.png 768w"
srcSet="/public/logo-square-dark-theme.png 40w, /public/logo-dark-theme.png 577w"
/>
</a>
</header>
@ -1252,9 +1252,9 @@ exports[`Render properties.header.theme: light - value[0] 1`] = `
<img
alt="Lowdefy"
className="{\\"style\\":[{\\"width\\":130,\\"sm\\":{\\"width\\":40},\\"md\\":{\\"width\\":130}},{\\"margin\\":\\"0 30px 0 0\\",\\"flex\\":\\"0 1 auto\\",\\"sm\\":{\\"margin\\":\\"0 10px 0 0\\"},\\"md\\":{\\"margin\\":\\"0 15px 0 0\\"}},null]}"
sizes="(max-width: 767px) 40px, 768px"
sizes="(max-width: 576px) 40px, 577px"
src="/public/logo-light-theme.png"
srcset="/public/logo-square-light-theme.png 40w, /public/logo-light-theme.png 768w"
srcSet="/public/logo-square-light-theme.png 40w, /public/logo-light-theme.png 577w"
/>
</a>
</header>
@ -1514,9 +1514,9 @@ exports[`Render properties.logo.alt - value[0] 1`] = `
<img
alt="Header logo alt text"
className="{\\"style\\":[{\\"width\\":130,\\"sm\\":{\\"width\\":40},\\"md\\":{\\"width\\":130}},{\\"margin\\":\\"0 30px 0 0\\",\\"flex\\":\\"0 1 auto\\",\\"sm\\":{\\"margin\\":\\"0 10px 0 0\\"},\\"md\\":{\\"margin\\":\\"0 15px 0 0\\"}},null]}"
sizes="(max-width: 767px) 40px, 768px"
sizes="(max-width: 576px) 40px, 577px"
src="/public/logo-dark-theme.png"
srcset="/public/logo-square-dark-theme.png 40w, /public/logo-dark-theme.png 768w"
srcSet="/public/logo-square-dark-theme.png 40w, /public/logo-dark-theme.png 577w"
/>
</a>
</header>
@ -1776,9 +1776,9 @@ exports[`Render properties.logo.size - value[0] 1`] = `
<img
alt="Lowdefy"
className="{\\"style\\":[{\\"width\\":130,\\"sm\\":{\\"width\\":40},\\"md\\":{\\"width\\":130}},{\\"margin\\":\\"0 30px 0 0\\",\\"flex\\":\\"0 1 auto\\",\\"sm\\":{\\"margin\\":\\"0 10px 0 0\\"},\\"md\\":{\\"margin\\":\\"0 15px 0 0\\"}},null]}"
sizes="(max-width: 767px) 40px, 768px"
sizes="(max-width: 576px) 40px, 577px"
src="/public/logo-dark-theme.png"
srcset="(max-width: 767px) 40px, 768px"
srcSet="(max-width: 767px) 40px, 768px"
/>
</a>
</header>
@ -2038,9 +2038,9 @@ exports[`Render properties.logo.src - value[0] 1`] = `
<img
alt="Lowdefy"
className="{\\"style\\":[{\\"width\\":130,\\"sm\\":{\\"width\\":130},\\"md\\":{\\"width\\":130}},{\\"margin\\":\\"0 30px 0 0\\",\\"flex\\":\\"0 1 auto\\",\\"sm\\":{\\"margin\\":\\"0 10px 0 0\\"},\\"md\\":{\\"margin\\":\\"0 15px 0 0\\"}},null]}"
sizes="(max-width: 767px) 40px, 768px"
sizes="(max-width: 576px) 40px, 577px"
src="https://lowdefy.com/logos/name_250.png"
srcset="https://lowdefy.com/logos/name_250.png"
srcSet="https://lowdefy.com/logos/name_250.png"
/>
</a>
</header>
@ -2244,14 +2244,14 @@ exports[`Render properties.logo.src - value[0] 1`] = `
</section>
`;
exports[`Render properties.logo.srcset - value[0] 1`] = `
exports[`Render properties.logo.srcSet - value[0] 1`] = `
<section
className="ant-layout {\\"style\\":{\\"minHeight\\":\\"100vh\\"}}"
id="properties.logo.srcset"
id="properties.logo.srcSet"
>
<header
className="ant-layout-header {\\"style\\":[{\\"backgroundColor\\":false},null,{\\"display\\":\\"flex\\",\\"alignItems\\":\\"center\\",\\"padding\\":\\"0 46px\\",\\"sm\\":{\\"padding\\":\\"0 15px\\"},\\"md\\":{\\"padding\\":\\"0 30px\\"},\\"lg\\":{\\"padding\\":\\"0 46px\\"},\\"flexDirection\\":\\"row-reverse\\"}]} hide-on-print"
id="properties.logo.srcset_header"
id="properties.logo.srcSet_header"
>
<div
className="{\\"style\\":{\\"alignItems\\":\\"center\\",\\"flex\\":\\"1 1 auto\\",\\"display\\":\\"flex\\",\\"justifyContent\\":\\"flex-end\\"}}"
@ -2260,18 +2260,18 @@ exports[`Render properties.logo.srcset - value[0] 1`] = `
className="{\\"style\\":[{\\"display\\":\\"block\\",\\"lg\\":{\\"display\\":\\"none\\"}},{\\"paddingLeft\\":\\"1rem\\"}]}"
>
<div
id="properties.logo.srcset_mobile_menu"
id="properties.logo.srcSet_mobile_menu"
>
<button
className="ant-btn {\\"style\\":[{},null]} ant-btn-primary ant-btn-icon-only"
id="properties.logo.srcset_mobile_menu_button"
id="properties.logo.srcSet_mobile_menu_button"
onClick={[Function]}
type="button"
>
<span
aria-label="loading-3-quarters"
className="anticon anticon-loading-3-quarters anticon-spin {\\"style\\":[{},null]}"
id="properties.logo.srcset_mobile_menu_button_icon"
id="properties.logo.srcSet_mobile_menu_button_icon"
required={false}
role="img"
>
@ -2300,19 +2300,19 @@ exports[`Render properties.logo.srcset - value[0] 1`] = `
<img
alt="Lowdefy"
className="{\\"style\\":[{\\"width\\":130,\\"sm\\":{\\"width\\":40},\\"md\\":{\\"width\\":130}},{\\"margin\\":\\"0 30px 0 0\\",\\"flex\\":\\"0 1 auto\\",\\"sm\\":{\\"margin\\":\\"0 10px 0 0\\"},\\"md\\":{\\"margin\\":\\"0 15px 0 0\\"}},null]}"
sizes="(max-width: 767px) 40px, 768px"
sizes="(max-width: 576px) 40px, 577px"
src="/public/logo-dark-theme.png"
srcset="https://lowdefy.com/logos/name_250.png 250w, https://lowdefy.com/logos/name_450.png 450w"
srcSet="https://lowdefy.com/logos/name_250.png 250w, https://lowdefy.com/logos/name_450.png 450w"
/>
</a>
</header>
<section
className="ant-layout ant-layout-has-sider {}"
id="properties.logo.srcset_layout"
id="properties.logo.srcSet_layout"
>
<aside
className="{\\"style\\":[{\\"overflow\\":\\"auto\\"},{\\"display\\":\\"none\\",\\"lg\\":{\\"display\\":\\"block\\"}}]} hide-on-print ant-layout-sider ant-layout-sider-light"
id="properties.logo.srcset_sider"
id="properties.logo.srcSet_sider"
style={
Object {
"flex": "0 0 200px",
@ -2336,7 +2336,7 @@ exports[`Render properties.logo.srcset - value[0] 1`] = `
>
<ul
className="ant-menu {\\"style\\":[{\\"lineHeight\\":\\"64px\\",\\"display\\":false},null,null,null,{\\"display\\":\\"none\\",\\"lg\\":{\\"display\\":\\"block\\"}}]} ant-menu-light ant-menu-root ant-menu-inline"
id="properties.logo.srcset_menu"
id="properties.logo.srcSet_menu"
onMouseEnter={[Function]}
onTransitionEnd={[Function]}
role="menu"
@ -2438,7 +2438,7 @@ exports[`Render properties.logo.srcset - value[0] 1`] = `
/>
<div
className="{}"
id="properties.logo.srcset_toggle_sider_affix"
id="properties.logo.srcSet_toggle_sider_affix"
>
<div
className=""
@ -2452,14 +2452,14 @@ exports[`Render properties.logo.srcset - value[0] 1`] = `
>
<button
className="ant-btn {\\"style\\":[{},null]} ant-btn-link ant-btn-icon-only ant-btn-block"
id="properties.logo.srcset_toggle_sider"
id="properties.logo.srcSet_toggle_sider"
onClick={[Function]}
type="button"
>
<span
aria-label="loading-3-quarters"
className="anticon anticon-loading-3-quarters anticon-spin {\\"style\\":[{},null]}"
id="properties.logo.srcset_toggle_sider_icon"
id="properties.logo.srcSet_toggle_sider_icon"
required={false}
role="img"
>
@ -2486,7 +2486,7 @@ exports[`Render properties.logo.srcset - value[0] 1`] = `
</aside>
<main
className="ant-layout-content {\\"style\\":{\\"padding\\":\\"0 40px 40px 40px\\",\\"sm\\":{\\"padding\\":\\"0 10px 10px 10px\\"},\\"md\\":{\\"padding\\":\\"0 20px 20px 20px\\"},\\"lg\\":{\\"padding\\":\\"0 40px 40px 40px\\"}}}"
id="properties.logo.srcset_content"
id="properties.logo.srcSet_content"
>
<div
className="{\\"style\\":{\\"padding\\":\\"20px 0\\",\\"sm\\":{\\"padding\\":\\"5px 0\\"},\\"md\\":{\\"padding\\":\\"10px 0\\"}}}"
@ -2562,9 +2562,9 @@ exports[`Render properties.logo.style - value[0] 1`] = `
<img
alt="Lowdefy"
className="{\\"style\\":[{\\"width\\":130,\\"sm\\":{\\"width\\":40},\\"md\\":{\\"width\\":130}},{\\"margin\\":\\"0 30px 0 0\\",\\"flex\\":\\"0 1 auto\\",\\"sm\\":{\\"margin\\":\\"0 10px 0 0\\"},\\"md\\":{\\"margin\\":\\"0 15px 0 0\\"}},{\\"border\\":\\"5px solid blue\\"}]}"
sizes="(max-width: 767px) 40px, 768px"
sizes="(max-width: 576px) 40px, 577px"
src="/public/logo-dark-theme.png"
srcset="/public/logo-square-dark-theme.png 40w, /public/logo-dark-theme.png 768w"
srcSet="/public/logo-square-dark-theme.png 40w, /public/logo-dark-theme.png 577w"
/>
</a>
</header>
@ -2737,9 +2737,9 @@ exports[`Render properties.menu - value[0] 1`] = `
<img
alt="Lowdefy"
className="{\\"style\\":[{\\"width\\":130,\\"sm\\":{\\"width\\":40},\\"md\\":{\\"width\\":130}},{\\"margin\\":\\"0 30px 0 0\\",\\"flex\\":\\"0 1 auto\\",\\"sm\\":{\\"margin\\":\\"0 10px 0 0\\"},\\"md\\":{\\"margin\\":\\"0 15px 0 0\\"}},null]}"
sizes="(max-width: 767px) 40px, 768px"
sizes="(max-width: 576px) 40px, 577px"
src="/public/logo-dark-theme.png"
srcset="/public/logo-square-dark-theme.png 40w, /public/logo-dark-theme.png 768w"
srcSet="/public/logo-square-dark-theme.png 40w, /public/logo-dark-theme.png 577w"
/>
</a>
</header>
@ -2999,9 +2999,9 @@ exports[`Render properties.menu.selectedColor - value[0] 1`] = `
<img
alt="Lowdefy"
className="{\\"style\\":[{\\"width\\":130,\\"sm\\":{\\"width\\":40},\\"md\\":{\\"width\\":130}},{\\"margin\\":\\"0 30px 0 0\\",\\"flex\\":\\"0 1 auto\\",\\"sm\\":{\\"margin\\":\\"0 10px 0 0\\"},\\"md\\":{\\"margin\\":\\"0 15px 0 0\\"}},null]}"
sizes="(max-width: 767px) 40px, 768px"
sizes="(max-width: 576px) 40px, 577px"
src="/public/logo-dark-theme.png"
srcset="/public/logo-square-dark-theme.png 40w, /public/logo-dark-theme.png 768w"
srcSet="/public/logo-square-dark-theme.png 40w, /public/logo-dark-theme.png 577w"
/>
</a>
</header>
@ -3261,9 +3261,9 @@ exports[`Render properties.sider.color - value[0] 1`] = `
<img
alt="Lowdefy"
className="{\\"style\\":[{\\"width\\":130,\\"sm\\":{\\"width\\":40},\\"md\\":{\\"width\\":130}},{\\"margin\\":\\"0 30px 0 0\\",\\"flex\\":\\"0 1 auto\\",\\"sm\\":{\\"margin\\":\\"0 10px 0 0\\"},\\"md\\":{\\"margin\\":\\"0 15px 0 0\\"}},null]}"
sizes="(max-width: 767px) 40px, 768px"
sizes="(max-width: 576px) 40px, 577px"
src="/public/logo-dark-theme.png"
srcset="/public/logo-square-dark-theme.png 40w, /public/logo-dark-theme.png 768w"
srcSet="/public/logo-square-dark-theme.png 40w, /public/logo-dark-theme.png 577w"
/>
</a>
</header>
@ -3523,9 +3523,9 @@ exports[`Render properties.sider.initialCollapsed - value[0] 1`] = `
<img
alt="Lowdefy"
className="{\\"style\\":[{\\"width\\":130,\\"sm\\":{\\"width\\":40},\\"md\\":{\\"width\\":130}},{\\"margin\\":\\"0 30px 0 0\\",\\"flex\\":\\"0 1 auto\\",\\"sm\\":{\\"margin\\":\\"0 10px 0 0\\"},\\"md\\":{\\"margin\\":\\"0 15px 0 0\\"}},null]}"
sizes="(max-width: 767px) 40px, 768px"
sizes="(max-width: 576px) 40px, 577px"
src="/public/logo-dark-theme.png"
srcset="/public/logo-square-dark-theme.png 40w, /public/logo-dark-theme.png 768w"
srcSet="/public/logo-square-dark-theme.png 40w, /public/logo-dark-theme.png 577w"
/>
</a>
</header>
@ -3709,9 +3709,9 @@ exports[`Render properties.sider.style - value[0] 1`] = `
<img
alt="Lowdefy"
className="{\\"style\\":[{\\"width\\":130,\\"sm\\":{\\"width\\":40},\\"md\\":{\\"width\\":130}},{\\"margin\\":\\"0 30px 0 0\\",\\"flex\\":\\"0 1 auto\\",\\"sm\\":{\\"margin\\":\\"0 10px 0 0\\"},\\"md\\":{\\"margin\\":\\"0 15px 0 0\\"}},null]}"
sizes="(max-width: 767px) 40px, 768px"
sizes="(max-width: 576px) 40px, 577px"
src="/public/logo-dark-theme.png"
srcset="/public/logo-square-dark-theme.png 40w, /public/logo-dark-theme.png 768w"
srcSet="/public/logo-square-dark-theme.png 40w, /public/logo-dark-theme.png 577w"
/>
</a>
</header>
@ -3895,9 +3895,9 @@ exports[`Render properties.sider.theme: dark - value[0] 1`] = `
<img
alt="Lowdefy"
className="{\\"style\\":[{\\"width\\":130,\\"sm\\":{\\"width\\":40},\\"md\\":{\\"width\\":130}},{\\"margin\\":\\"0 30px 0 0\\",\\"flex\\":\\"0 1 auto\\",\\"sm\\":{\\"margin\\":\\"0 10px 0 0\\"},\\"md\\":{\\"margin\\":\\"0 15px 0 0\\"}},null]}"
sizes="(max-width: 767px) 40px, 768px"
sizes="(max-width: 576px) 40px, 577px"
src="/public/logo-dark-theme.png"
srcset="/public/logo-square-dark-theme.png 40w, /public/logo-dark-theme.png 768w"
srcSet="/public/logo-square-dark-theme.png 40w, /public/logo-dark-theme.png 577w"
/>
</a>
</header>
@ -4070,9 +4070,9 @@ exports[`Render properties.style - value[0] 1`] = `
<img
alt="Lowdefy"
className="{\\"style\\":[{\\"width\\":130,\\"sm\\":{\\"width\\":40},\\"md\\":{\\"width\\":130}},{\\"margin\\":\\"0 30px 0 0\\",\\"flex\\":\\"0 1 auto\\",\\"sm\\":{\\"margin\\":\\"0 10px 0 0\\"},\\"md\\":{\\"margin\\":\\"0 15px 0 0\\"}},null]}"
sizes="(max-width: 767px) 40px, 768px"
sizes="(max-width: 576px) 40px, 577px"
src="/public/logo-dark-theme.png"
srcset="/public/logo-square-dark-theme.png 40w, /public/logo-dark-theme.png 768w"
srcSet="/public/logo-square-dark-theme.png 40w, /public/logo-dark-theme.png 577w"
/>
</a>
</header>
@ -4245,9 +4245,9 @@ exports[`Render properties.toggleSiderButton.hide - value[0] 1`] = `
<img
alt="Lowdefy"
className="{\\"style\\":[{\\"width\\":130,\\"sm\\":{\\"width\\":40},\\"md\\":{\\"width\\":130}},{\\"margin\\":\\"0 30px 0 0\\",\\"flex\\":\\"0 1 auto\\",\\"sm\\":{\\"margin\\":\\"0 10px 0 0\\"},\\"md\\":{\\"margin\\":\\"0 15px 0 0\\"}},null]}"
sizes="(max-width: 767px) 40px, 768px"
sizes="(max-width: 576px) 40px, 577px"
src="/public/logo-dark-theme.png"
srcset="/public/logo-square-dark-theme.png 40w, /public/logo-dark-theme.png 768w"
srcSet="/public/logo-square-dark-theme.png 40w, /public/logo-dark-theme.png 577w"
/>
</a>
</header>
@ -4431,9 +4431,9 @@ exports[`Render properties.toggleSiderButton.type - value[0] 1`] = `
<img
alt="Lowdefy"
className="{\\"style\\":[{\\"width\\":130,\\"sm\\":{\\"width\\":40},\\"md\\":{\\"width\\":130}},{\\"margin\\":\\"0 30px 0 0\\",\\"flex\\":\\"0 1 auto\\",\\"sm\\":{\\"margin\\":\\"0 10px 0 0\\"},\\"md\\":{\\"margin\\":\\"0 15px 0 0\\"}},null]}"
sizes="(max-width: 767px) 40px, 768px"
sizes="(max-width: 576px) 40px, 577px"
src="/public/logo-dark-theme.png"
srcset="/public/logo-square-dark-theme.png 40w, /public/logo-dark-theme.png 768w"
srcSet="/public/logo-square-dark-theme.png 40w, /public/logo-dark-theme.png 577w"
/>
</a>
</header>
@ -4601,9 +4601,9 @@ exports[`Test Schema properties.logo.src 1`] = `true`;
exports[`Test Schema properties.logo.src 2`] = `null`;
exports[`Test Schema properties.logo.srcset 1`] = `true`;
exports[`Test Schema properties.logo.srcSet 1`] = `true`;
exports[`Test Schema properties.logo.srcset 2`] = `null`;
exports[`Test Schema properties.logo.srcSet 2`] = `null`;
exports[`Test Schema properties.logo.style 1`] = `true`;

View File

@ -24,6 +24,7 @@ async function run() {
logger: console,
cacheDirectory: path.resolve(process.cwd(), '../servers/serverDev/.lowdefy/.cache'),
configDirectory: path.resolve(process.cwd(), '../docs'),
// configDirectory: path.resolve(process.cwd(), '../servers/serverDev'),
outputDirectory: path.resolve(process.cwd(), '../servers/serverDev/.lowdefy/build'),
});
}

View File

@ -27,7 +27,9 @@ import buildPages from './build/buildPages/buildPages';
import buildRefs from './build/buildRefs';
import cleanOutputDirectory from './build/cleanOutputDirectory';
import testSchema from './build/testSchema';
import validateApp from './build/validateApp';
import validateConfig from './build/validateConfig';
import writeApp from './build/writeApp';
import writeConfig from './build/writeConfig';
import writeConnections from './build/writeConnections';
import writeGlobal from './build/writeGlobal';
@ -54,12 +56,14 @@ async function build(options) {
let components = await buildRefs({ context });
await testSchema({ components, context });
context.metaLoader = createMetaLoader({ components, context });
await validateApp({ components, context });
await validateConfig({ components, context });
await buildAuth({ components, context });
await buildConnections({ components, context });
await buildPages({ components, context });
await buildMenu({ components, context });
await cleanOutputDirectory({ context });
await writeApp({ components, context });
await writeConnections({ components, context });
await writeRequests({ components, context });
await writePages({ components, context });

View File

@ -0,0 +1,40 @@
/* eslint-disable no-param-reassign */
/*
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 validateApp({ components }) {
if (type.isNone(components.app)) {
components.app = {};
}
if (!type.isObject(components.app)) {
throw new Error('lowdefy.app is not an object.');
}
if (type.isNone(components.app.html)) {
components.app.html = {};
}
if (type.isNone(components.app.html.appendBody)) {
components.app.html.appendBody = '';
}
if (type.isNone(components.app.html.appendHead)) {
components.app.html.appendHead = '';
}
return components;
}
export default validateApp;

View File

@ -0,0 +1,57 @@
/*
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 validateApp from './validateApp';
import testContext from '../test/testContext';
const context = testContext();
test('validateApp success', async () => {
let components = {
app: {
html: {
appendBody: 'abc',
appendHead: 'abc',
},
},
};
let result = await validateApp({ components, context });
expect(result).toEqual({ app: { html: { appendBody: 'abc', appendHead: 'abc' } } });
components = {};
result = await validateApp({ components, context });
expect(result).toEqual({ app: { html: { appendBody: '', appendHead: '' } } });
components = {
app: {},
};
result = await validateApp({ components, context });
expect(result).toEqual({ app: { html: { appendBody: '', appendHead: '' } } });
components = {
app: {
html: {},
},
};
result = await validateApp({ components, context });
expect(result).toEqual({ app: { html: { appendBody: '', appendHead: '' } } });
});
test('validateApp app not an object', async () => {
const components = {
app: 'app',
};
await expect(validateApp({ components, context })).rejects.toThrow(
'lowdefy.app is not an object.'
);
});

View File

@ -25,7 +25,7 @@ async function validateConfig({ components }) {
components.config = {};
}
if (!type.isObject(components.config)) {
throw new Error('Config is not an object.');
throw new Error('lowdefy.config is not an object.');
}
if (type.isNone(components.config.auth)) {
components.config.auth = {};

View File

@ -23,7 +23,9 @@ test('validateConfig config not an object', async () => {
const components = {
config: 'config',
};
await expect(validateConfig({ components, context })).rejects.toThrow('Config is not an object.');
await expect(validateConfig({ components, context })).rejects.toThrow(
'lowdefy.config is not an object.'
);
});
test('validateConfig config invalid auth config', async () => {

View File

@ -0,0 +1,24 @@
/*
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.
*/
async function writeApp({ components, context }) {
await context.artifactSetter.set({
filePath: 'app.json',
content: JSON.stringify(components.app || {}, null, 2),
});
}
export default writeApp;

View File

@ -0,0 +1,77 @@
/*
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 writeApp from './writeApp';
import testContext from '../test/testContext';
const mockSet = jest.fn();
const artifactSetter = {
set: mockSet,
};
const context = testContext({ artifactSetter });
beforeEach(() => {
mockSet.mockReset();
});
test('writeApp', async () => {
const components = {
app: {
key: 'value',
},
};
await writeApp({ components, context });
expect(mockSet.mock.calls).toEqual([
[
{
filePath: 'app.json',
content: `{
"key": "value"
}`,
},
],
]);
});
test('writeApp empty config', async () => {
const components = {
app: {},
};
await writeApp({ components, context });
expect(mockSet.mock.calls).toEqual([
[
{
filePath: 'app.json',
content: `{}`,
},
],
]);
});
test('writeApp config undefined', async () => {
const components = {};
await writeApp({ components, context });
expect(mockSet.mock.calls).toEqual([
[
{
filePath: 'app.json',
content: `{}`,
},
],
]);
});

View File

@ -33,6 +33,32 @@
}
}
},
"app": {
"type": "object",
"additionalProperties": false,
"properties": {
"html": {
"type": "object",
"errorMessage": {
"type": "App \"app.html\" should be an object."
},
"properties": {
"appendBody": {
"type": "string",
"errorMessage": {
"type": "App \"app.html.appendBody\" should be a string."
}
},
"appendHead": {
"type": "string",
"errorMessage": {
"type": "App \"app.html.appendHead\" should be a string."
}
}
}
}
}
},
"authConfig": {
"type": "object",
"additionalProperties": false,
@ -466,7 +492,6 @@
}
}
}
},
"additionalProperties": false,
"required": ["lowdefy"],
@ -489,6 +514,9 @@
"type": "App \"licence\" should be a string."
}
},
"app": {
"$ref": "#/definitions/app"
},
"cli": {
"type": "object",
"errorMessage": {

View File

@ -17,6 +17,7 @@
import path from 'path';
import { spawnSync } from 'child_process';
import fse from 'fs-extra';
import { readFile, writeFile } from '@lowdefy/node-utils';
import checkChildProcessError from '../../utils/checkChildProcessError';
import startUp from '../../utils/startUp';
@ -24,7 +25,7 @@ import getFederatedModule from '../../utils/getFederatedModule';
import fetchNpmTarball from '../../utils/fetchNpmTarball';
async function fetchNetlifyServer({ context, netlifyDir }) {
context.print.spin('Fetching Lowdefy Netlify server.');
context.print.log('Fetching Lowdefy Netlify server.');
await fetchNpmTarball({
packageName: '@lowdefy/server-netlify',
version: context.lowdefyVersion,
@ -39,7 +40,7 @@ async function npmInstall({ context, netlifyDir }) {
await fse.remove(path.resolve('./package-lock.json'));
await fse.emptyDir(path.resolve('./node_modules'));
context.print.spin('npm install production.');
context.print.log('npm install production.');
let processOutput = spawnSync('npm', ['install', '--production', '--legacy-peer-deps']);
checkChildProcessError({
context,
@ -51,7 +52,7 @@ async function npmInstall({ context, netlifyDir }) {
}
async function fetchBuildScript({ context }) {
context.print.spin('Fetching Lowdefy build script.');
context.print.log('Fetching Lowdefy build script.');
const { default: buildScript } = await getFederatedModule({
module: 'build',
packageName: '@lowdefy/build',
@ -63,7 +64,7 @@ async function fetchBuildScript({ context }) {
}
async function build({ context, buildScript, netlifyDir }) {
context.print.spin('Starting Lowdefy build.');
context.print.log('Starting Lowdefy build.');
const outputDirectory = path.resolve(netlifyDir, './package/dist/functions/graphql/build');
await buildScript({
logger: context.print,
@ -74,6 +75,21 @@ async function build({ context, buildScript, netlifyDir }) {
context.print.log(`Build artifacts saved at ${outputDirectory}.`);
}
async function buildIndexHtml({ context }) {
context.print.log('Starting Lowdefy index.html build.');
let appConfig = await readFile(path.resolve('./.lowdefy/functions/graphql/build/app.json'));
appConfig = JSON.parse(appConfig);
const indexHtmlPath = path.resolve('./.lowdefy/publish/index.html');
let indexHtml = await readFile(indexHtmlPath);
indexHtml = indexHtml.replace('<!-- __LOWDEFY_APP_HEAD_HTML__ -->', appConfig.html.appendHead);
indexHtml = indexHtml.replace('<!-- __LOWDEFY_APP_BODY_HTML__ -->', appConfig.html.appendBody);
await writeFile({
filePath: indexHtmlPath,
content: indexHtml,
});
context.print.log('Lowdefy index.html build complete.');
}
async function moveBuildArtifacts({ context, netlifyDir }) {
await fse.copy(
path.resolve(netlifyDir, 'package/dist/shell'),
@ -117,6 +133,9 @@ async function buildNetlify({ context, options }) {
await moveFunctions({ context, netlifyDir });
await movePublicAssets({ context });
context.print.log(`Build artifacts.`);
await buildIndexHtml({ context });
await context.sendTelemetry({
data: {
netlify: process.env.NETLIFY === 'true',

View File

@ -26,13 +26,20 @@ async function getExpress({ context, gqlServer, options }) {
const reloadPort = await findOpenPort();
const reloadReturned = await reload(app, { route: '/api/dev/reload.js', port: reloadPort });
app.use(express.static(path.join(__dirname, 'shell')));
app.use('/public', express.static(path.resolve(process.cwd(), 'public')));
app.use(express.static(path.resolve(__dirname, 'shell')));
app.use('/api/dev/version', (req, res) => {
res.json(context.lowdefyVersion);
});
app.use((req, res) => {
res.sendFile(path.resolve(__dirname, 'shell/index.html'));
app.use(async (req, res) => {
let indexHtml = await readFile(path.resolve(__dirname, 'shell/index.html'));
let appConfig = await readFile(path.resolve(context.outputDirectory, 'app.json'));
appConfig = JSON.parse(appConfig);
indexHtml = indexHtml.replace('<!-- __LOWDEFY_APP_HEAD_HTML__ -->', appConfig.html.appendHead);
indexHtml = indexHtml.replace('<!-- __LOWDEFY_APP_BODY_HTML__ -->', appConfig.html.appendBody);
res.send(indexHtml);
});
return { expressApp: app, reloadFn: reloadReturned.reload };
}

View File

@ -13,22 +13,46 @@
See the License for the specific language governing permissions and
limitations under the License. -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Lowdefy App</title>
<link rel="manifest" href="/public/manifest.webmanifest">
<link rel="icon" type="image/svg+xml" href="/public/icon.svg">
<link rel="icon" type="image/png" href="/public/icon-32.png">
<link rel="apple-touch-icon" href="/public/apple-touch-icon.png">
<script src="/api/dev/reload.js"></script>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="emotion"></div>
<div id="root"></div>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Lowdefy App</title>
<link rel="manifest" href="/public/manifest.webmanifest">
<link rel="icon" type="image/svg+xml" href="/public/icon.svg">
<link rel="icon" type="image/png" href="/public/icon-32.png">
<link rel="apple-touch-icon" href="/public/apple-touch-icon.png">
<script type="text/javascript">
const jsActions = {}
const jsOperators = {}
const getMethodLoader = (scope, reference) =>
(name, method) => {
if (typeof name !== 'string') {
throw new Error(`${scope} requires a string for the first argument.`)
}
if (typeof method !== 'function') {
throw new Error(`${scope} requires a function for the second argument.`)
}
reference[name] = method;
}
window.lowdefy = {
imports: {
jsActions,
jsOperators,
},
registerJsAction: getMethodLoader('registerJsAction', jsActions),
registerJsOperator: getMethodLoader('registerJsOperator', jsOperators)
}
</script>
<!-- __LOWDEFY_APP_HEAD_HTML__ -->
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="emotion"></div>
<div id="root"></div>
<!-- __LOWDEFY_APP_BODY_HTML__ -->
</body>
</html>

View File

@ -113,8 +113,9 @@ module.exports = [
},
}),
new HtmlWebpackPlugin({
template: './src/commands/dev/shell/index.html',
minify: false,
publicPath: '/',
template: './src/commands/dev/shell/index.html',
}),
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify('production'),

View File

@ -0,0 +1,234 @@
# 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: JsAction
pageTitle: JsAction
filePath: actions/JsAction.yaml
warning: |
SECURITY WARNING: The JsAction executes JavaScript inside your Lowdefy app. Insecure code can expose your app or data. Since Lowdefy doesn't validate your JavaScript, make sure that you only load trusted code.
types: |
```
(params: {
name: string,
args?: any[]
}): void
```
description: |
The `JsAction` action is used to call a custom JavaScript function which was loaded onto the page using the `window.lowdefy.registerJsAction()` method. This JavaScript function can be asynchronous. See [Custom Code](/custom-code) for more details on how to register a new JavaScript action.
The returned result of the JavaScript function is accessible through the [`_actions`](/_actions) operator for subsequent action in the event action list.
A `JsAction` is called with a context object which includes all [`context` data objects](/context-and-state) as well as the list of `args` passed to the action.
```text
(context: {
user: object,
global: object,
state: object,
urlQuery: object,
input: object,
},
...args?: any[]): any
```
params: |
###### object
- `name: string`: __Required__ - The registered name of the JavaScript function to call when the action is triggered.
- `args: any[]`: The array of positional arguments with which the JavaScript function should be called.
examples: |
##### Set a [Intercom](https://www.intercom.com/) user when a page is initialized:
```yaml
# lowdefy.yaml
name: intercom-example
lowdefy: '3.15.0'
app:
html:
appendBody: |
<script>
function setIntercomUser(context) {
window.intercomSettings = {
app_id: "{{ your_intercom_app_id }}",
name: context.user.name,
email: context.user.email,
};
}
window.lowdefy.registerJsAction('setIntercomUser', setIntercomUser);
</script>
<script>
(function(){var w=window;var ic=w.Intercom;if(typeof ic==="function"){ic('reattach_activator');
ic('update',w.intercomSettings);}else{var d=document;var i=function(){i.c(arguments);};i.q=[];
i.c=function(args){i.q.push(args);};w.Intercom=i;var l=function(){var s=d.createElement('script');
s.type='text/javascript';s.async=true;s.src='https://widget.intercom.io/widget/{{ your_intercom_app_id }}';
var x=d.getElementsByTagName('script')[0];x.parentNode.insertBefore(s,x);};
if(w.attachEvent){w.attachEvent('onload',l);}else{w.addEventListener('load',l,false);}}})();
</script>
pages:
- id: home
type: PageHeaderMenu
events:
onInitAsync:
- id: set_intercom_user
type: JsAction
params:
name: setIntercomUser
blocks:
# ...
```
##### Highlight search term returned by [MongoDB Search Highlight](https://docs.atlas.mongodb.com/reference/atlas-search/highlighting/):
Add a JavaScript file to highlight the search text by wapping the highlighted text with `<span style="background: yellow;">{{ value }}</span>`:
```js
// file: /public/highlightText.js
function highlightText(context, data) {
return data.map((item) => {
item.highlights.forEach((light) => {
const paths = light.path.split('.');
const key = paths[paths.length - 1];
paths.pop();
let res = item;
paths.forEach((key) => {
res = res[key];
});
res[key] = light.texts.reduce((acc, obj) => {
if (obj.type === 'hit') {
return acc.concat('<span style="background: yellow;">', obj.value, '</span>');
}
return acc.concat(obj.value);
}, '');
});
return item;
});
}
export default highlightText;
```
Import custom JavaScript modules:
```js
// file: /public/modules.js
import highlightText from './highlightText.js';
window.lowdefy.registerJsAction('highlightText', highlightText);
````
Lowdefy setup:
```yaml
# file: lowdefy.yaml
name: text-highlight-example
lowdefy: '3.15.0'
app:
html:
# Load the custom modules into the index.html head tag.
appendHead: |
<script type="module" src="/public/modules.js"></script>
connections:
- id: products
type: MongoDBCollection
properties:
collection: products
databaseUri:
_secret: MDB_URI
pages:
- id: home
type: PageHeaderMenu
requests:
- id: search_products
type: MongoDBAggregation
connectionId: products
properties:
pipeline:
_array.concat:
- - $search:
compound:
should:
- text:
query:
_string.concat:
- '*'
- _state: search.input
- '*'
path:
- title
- description
- wildcard:
query:
_string.concat:
- '*'
- _state: search.input
- '*'
path:
- title
- description
allowAnalyzedField: true
highlight:
path:
- title
- description
- $addFields:
score:
$meta: searchScore
highlights:
$meta: searchHighlights
blocks:
- id: search.input
type: TextInput
properties:
title: Type to search products
prefix: SearchOutlined
events:
onChange:
- id: get_search # get search_products query for search.input
type: Request
params: search_products
- id: apply_highlight # apply the highlight transformation to the request data.
type: JsAction
params:
name: highlightText
args:
- _request: search_products
- id: set_state # set the response of the apply_highlight action to state
type: SetState
params:
found_products:
_actions: apply_highlight.response
- id: product_results
type: Html
properties:
html:
_nunjucks:
template: |
<ul>
{% for item in found_products %}
<li>{{ item.title | safe }} - {{ item.description | safe}}</li>
{% endfor %}
</ul>
on:
found_products:
_state: found_products
```
NOTE: For this example to work, you will need a `products` collection in your MongoDB database, populated with `{title: '...', description: '...'}` data objects including the following search index on the `products` collection:
```json
{
"mappings": {
"dynamic": true,
"fields": {
"title": {
"type": "string"
},
"description": {
"type": "string"
}
}
}
}
```

View File

@ -20,20 +20,24 @@ _ref:
section: Concepts
filePath: concepts/custom-blocks.yaml
content:
- id: warning
type: Alert
properties:
message: |
SECURITY WARNING: Blocks execute JavaScript inside your Lowdefy app. Insecure code can expose your app or data. Make sure that you only load blocks from a trusted source.
type: warning
- id: md1
type: MarkdownWithCode
properties:
content: |
> __NOTE__: Blocks run javascript on your site - this can lead to potential security vulnerabilities. __Make sure you trust the block publisher or even better, host your own blocks.__
Blocks in Lowdefy are simple, most often state-less, [React components](https://reactjs.org/docs/components-and-props.html). Lowdefy uses [webpack module federation](https://webpack.js.org/concepts/module-federation/) to implement a micro front-end strategy. This means blocks are imported at load time, and not part of the Lowdefy app build.
The decoupling of blocks provides the considerable advantages:
- Block developers can extend the UI capabilities of Lowdefy by building blocks for the community to utilize.
- Lowdefy app developers can use community blocks to experiment and extend their apps.
- Lowdefy blocks are simple, most often stateless React components, thus blocks can be developed fast and can be used inside Lowdefy apps with ease.
- Lowdefy blocks are simple, most often stateless React components, thus blocks can be developed quickly and can be used inside Lowdefy apps with ease.
- The build process is simple and fast since you only build the code for your block, and not the entire application.
- The Lowdefy engine takes care off the application state, the the block only has to concern itself which a easy application interface.
- The Lowdefy engine takes care off the application state, the the block only has to concern itself with a easy application interface.
## Using Custom Blocks
@ -41,7 +45,7 @@ _ref:
```yaml
name: dashboard-app
lowdefy: 3.10.0
lowdefy: 3.15.0
types:
AmChartsXY:
url: https://blocks-cdn.lowdefy.com/v3.10.1/blocks-amcharts/meta/AmChartsXY.json
@ -89,9 +93,9 @@ _ref:
In your `lowdefy.yaml` file, add your custom block type to the `types` object with the local path.
```yaml
```yaml
name: dashboard-app
lowdefy: 3.10.0
lowdefy: 3.15.0
types:
MyCustomBlock:
url: http://localhost:3002/meta/MyCustomBlock.json
@ -125,16 +129,18 @@ _ref:
- `actions: actionObjects[]`: The list of [Lowdefy action objects](https://docs.lowdefy.com/events-and-actions) which will be evaluated by the Lowdefy engine.
- `history: object[]`: A list of objects logging the event calls and responses.
- `blockId: string`: The block id from which the event was called.
- `endTimestamp: datetime`: Timestamp for when the event was completed.
- `event: object`: The event object passed to the event.
- `eventName: string`: The event name which which triggerEvent was called.
- `success: boolean`: True if all actions for the event executed without throwing any errors.
- `timestamp: datetime`: Timestamp for when the event was completed.
- `responses: object[]`: The list of action responses.
- `actionId: string`: The id of the triggered action.
- `actionType: string`: The type of action called.
- `error: Error: If the action throw an error.
- `response: any`: The returned result of the action.
- `skipped: boolean`: True if the action was skipped.
- `startTimestamp: datetime`: Timestamp for when the event was started.
- `responses: object`: The list of action responses, where the object key is equal to the action id.
- `{{ key }}: string`:
- `type: string`: The type of action called.
- `error: Error`: If the action throw an error.
- `index: number`: Index of the action in the event array.
- `response: any`: The returned result of the action.
- `skipped: boolean`: True if the action was skipped.
- `methods: object`: All application methods built into Lowdefy, available for the block.
- `makeCssClass(cssObject | cssObject[]): string`: This methods creates a css class for the block to apply to DOM elements. Css classes are created using [Emotion](https://emotion.sh/docs/introduction). If a list of cssObject are given the cssObjects are shallow merged with the preceding objects properties being overwritten by the latter. Any valid css style object can be passed, including media queries. Default media queries are built in:
- `xs?: object`: Css object applied for screen media with max width of 576px.
@ -155,7 +161,7 @@ _ref:
## Deploying Custom Blocks
Both the block metadata and block React component need to be built by webpack and hosted on a publicly accessible static file server. Any Lowdefy app can then load and use the block. You also need to set the `remoteEntryUrl` in `webpack.prod.js` in order to build the correct block meta data, make sure URL is pointing to where your block is hosted.
Both the block metadata and block React component need to be built by webpack and hosted on a publicly accessible static file server. Any Lowdefy app can then load and use the block. You also need to set the `remoteEntryUrl` in `webpack.prod.js` in order to build the correct block meta data, make sure the URL is pointing to where your block is hosted.
The easiest way to host your custom block is the deploy the custom block to [npm](https://www.npmjs.com/) and [Unpkg](https://unpkg.com/) will automatically host your block for you on their CDN. Although this option is easy, the cache settings for Unpkg can result in longer load times in some cases which can result in a unreliable user experience. It is thus best to deploy you blocks to your own static file servers.
@ -163,7 +169,7 @@ _ref:
- _ref:
path: templates/navigation_buttons.yaml
vars:
previous_page_title: Lists
previous_page_id: lists
previous_page_title: Custom Code
previous_page_id: custom-code
next_page_title: User authentication
next_page_id: users-introduction

View File

@ -0,0 +1,217 @@
# 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/general.yaml.njk
vars:
pageId: custom-code
pageTitle: Custom Code
section: Concepts
filePath: concepts/custom-code.yaml
content:
- id: warning
type: Alert
properties:
message: |
SECURITY WARNING: Custom code executes JavaScript inside your Lowdefy app. Insecure code can expose your app or data. Since Lowdefy doesn't validate your custom code, make sure that you only load trusted code.
type: warning
- id: md1
type: MarkdownWithCode
properties:
content: |
Lowdefy runs as a single page web app (SPA). It is possible to extend the functionality of a Lowdefy app by loading custom code (HTML, CSS and JavaScript) into the HTML `head` and `body` to of the default `index.html` page.
The content loaded into the `head` and `body` tag can be any valid HTML, most often `script` tags are loaded to register a new [`JsAction`](/JsAction) or [`_js`](/_js) operator. However, third party code can also be imported, for example Google Analytics, Intercom, etc. Be sure to only load trusted code into your app, as this code will be able to execute JavaScript on all pages of your Lowdefy app, which could expose you app or data to security vulnerabilities.
> __Warning__: Lowdefy implements the [Ant design](https://ant.design/) UI component framework for app layout and most blocks, thus the default Ant Design CSS is loaded for all Lowdefy apps. Take caution not to unintentionally overwrite existing style settings and classes which can result in a degraded user experience.
## Schema to load custom code
- `app.html.appendHead: string`: Any valid HTML content can be loaded just before the `</head>` tag of the Lowdefy app `index.html` file.
- `app.html.appendBody: string`: Any valid HTML content can be loaded just before the `</body>` tag of the Lowdefy app `index.html` file.
Most often it is convenient to abstract this HTML out to a separate file using the [`_ref`](/_ref) operator.
> __Warning__: Code imported using `appendHead` or `appendBody` will be loaded, and can execute JavaScript on every page of your Lowdefy app.
###### Loading third party code snippet like Google Analytics:
To add [Google Analytics](/https://developers.google.com/analytics/devguides/collection/analyticsjs) to a Lowdefy app, the `lowdefy.yaml` can be setup with:
```yaml
name: google-analytics-example
lowdefy: 3.15.0
# ...
app:
html:
appendHead: |
<!-- Google Analytics -->
<script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-XXXXX-Y', 'auto');
ga('send', 'pageview');
</script>
<!-- End Google Analytics -->
# ...
```
## Hosting static files
A Lowdefy app provides a convenient method to host __public__ files under the `/public/*` app route. Add content to be hosted publicly by creating a folder named `public` in the root of a Lowdefy project folder, next to the `lowdefy.yaml` file. Place the public content in this folder to host this content with your app.
All content in this folder will be publicly accessible at `{{ APP_URL }}/public/{{ FILE_PATH_NAME }}`. For example, the logo at the top of this page is hosted at [`https://docs.lowdefy.com/public/logo-light-theme.png`](http://localhost:3000/public/logo-light-theme.png). Sub-folders inside the public folder are supported.
By default, the `public` folder of a Lowdefy app will serve some files which most apps need:
- `apple-touch-icon.png`: A 180x180px png image file to be used as the apple PWA icon.
- `icon-32.png`: A 32x32px png image file to be used as fallback favicon for some browsers.
- `icon-512.png`: A 512x512px png image icon.
- `icon.svg`: A svg image file which will be used as favicon if supported by browser.
- `logo-dark-theme.png`: A ~250x72px png image used as the header image for [`PageHeaderMenu`](/PageHeaderMenu) and [`PageSiderMenu`](/PageSiderMenu) blocks on desktop when the block theme is set to `dark`.
- `logo-light-theme.png`: A ~250x72px png image used as the header image for `PageHeaderMenu` and `PageSiderMenu` blocks on desktop when the block theme is set to `light`.
- `logo-square-dark-theme.png`: A ~125x125px png image used as the header image for `PageHeaderMenu` and `PageSiderMenu` blocks on mobile when the block theme is set to `dark`.
- `logo-square-light-theme.png`: A ~125x125px png image used as the header image for `PageHeaderMenu` and `PageSiderMenu` blocks on mobile when the block theme is set to `light`.
- `manifest.webmanifest`: The app [web manifest](https://developer.mozilla.org/en-US/docs/Web/Manifest).
Any of these files can be overwritten by replacing the file with a modified version. For example, to replace the logo inside the header of `PageSiderMenu` on all pages, add a ~250x72px logo inside the project folder at `/public/logo-light-theme.png`.
## Loading and registering a [`JsAction`](/JsAction)
In order for the Lowdefy app engine to execute a custom JavaScript [action](/events-and-actions), the JavaScript code for the action must be loaded onto the page and registered using the `registerJsAction` method available on the browser [`window`](https://developer.mozilla.org/en-US/docs/Web/API/window) object by calling `window.lowdefy.registerJsAction(name: string, action: function)`.
The `JsAction` allows the use of async functions.
###### Load, register and trigger a custom `JsAction` from code in the app `public` folder:
This example fetches a list of Todos from [{JSON}placeholder](https://jsonplaceholder.typicode.com/), and updates [state](/context-and-state).
1) First, add the JavaScript code to the `public` folder and resister the `JsAction`:
```js
// /public/fetchTodos.js
function async fetchTodos(context, numItems, skip) {
const data = await fetch('https://jsonplaceholder.typicode.com/todos');
const todos = await data.json();
return todos.slice(skip, skip + numItems);
}
// Register the JsAction for the Lowdefy app to use.
window.lowdefy.registerJsAction('fetchTodos', fetchTodos);
```
2) Import the JavaScript as a module into the page:
```html
<!-- /header_modules.html -->
<script type="module" src="/public/fetchTodos.js"></script>
```
3) Set load the custom code into the app header and trigger the action on page load:
```yaml
# /lowdefy.yaml
name: json-todos
lowdefy: 3.15.0
app:
html:
appendHead:
_ref: header_modules.html # Load the custom HTML into the header.
pages:
- id: todos
type: PageHeaderMenu
events:
onEnter:
- id: get_todos
type: JsAction
params:
name: fetchTodos # Trigger the custom JavaScript action.
args:
- 10 # numItems
- 0 # skip
- id: set_todos
type: SetState
params:
todos:
# Set the response of the get_todos action to state.
_actions: get_todos.response
# ...
```
## Loading and registering a [`_js`](/_js) operator
Similar to the loading custom JavaScript actions, custom JavaScript operators can also be loaded. In order for the Lowdefy app engine to execute a custom JavaScript [operator](/operators), the JavaScript code for the operator must be loaded onto the page and registered using the `registerJsOperator` method available on the browser [`window`](https://developer.mozilla.org/en-US/docs/Web/API/window) object by calling `window.lowdefy.registerJsOperator(name: string, action: function)`.
All `_js` functions must be synchronous.
###### Load, register and use a custom `_js` operator from code in the app `public` folder:
This example uses a `_js` operator to remove all duplicates from a list of names.
1) First, add the JavaScript code to the `public` folder and resister the `_js` operator:
```js
// /public/foo_operators.js
function removeDuplicates(items) {
return [ ...new Set(items) ];
}
// Register the removeDuplicates function as a _js.deduplicate operator.
window.lowdefy.registerJsOperator('deduplicate', removeDuplicates);
```
2) Import the JavaScript as a module into the page:
```html
<!-- /header.html -->
<script type="module" src="/public/foo_operators.js"></script>
```
3) Set load the custom code into the app header and use the new operator on the page:
```yaml
# /lowdefy.yaml
name: operator-example
lowdefy: 3.15.0
app:
html:
appendHead:
_ref: header.html # Load the custom HTML into the header.
pages:
- id: some_names
type: PageHeaderMenu
blocks:
- id: names
type: ButtonSelector
properties:
title: Select your new friend
options:
# use the removeDuplicates function and pass a list of names as a function argument
_js.deduplicate:
- - Anne
- Sam
- Joe
- Micheal
- Sam
- Steven
- Anne
- Pepper
# ...
```
------
Custom code provides an easy way to extent the logic functionality of Lowdefy apps. However, to extend the UI capabilities beyond the existing features provided by the default Lowdefy blocks, custom blocks can be loaded onto apps.
- _ref:
path: templates/navigation_buttons.yaml
vars:
previous_page_title: Lists
previous_page_id: lists
next_page_title: Custom Blocks
next_page_id: custom-blocks

View File

@ -21,7 +21,7 @@ _ref:
filePath: concepts/events-and-actions.yaml
content:
- id: md1
type: Markdown
type: MarkdownWithCode
properties:
content: |
### TLDR
@ -83,6 +83,9 @@ _ref:
params:
# ...
```
# The actions object
When an event is triggered each completed action writes its response to the actions object under the action id object key. Thus all following actions in a event action list has access to the response of all preceding actions in the same event list through the [`_actions`](/_actions) operator.
# The event object
@ -111,6 +114,7 @@ _ref:
The following actions can be used:
- [`CallMethod`](/CallMethod) - Call a method defined by another block.
- [`JsAction`](/JsAction) - Call a custom JavaScript function as an action.
- [`Link`](/Link) - Link to another page.
- [`Message`](/Message) - Show a message to the user.
- [`Notification`](/Notification) - Show a notification to the user.

View File

@ -152,11 +152,11 @@ _ref:
### Lists and state
List blocks create a content area for each item in `state`. If `state` is updated, the list block will also update, creating or destroying content areas as necessary. Thus list blocks can also be manipulated by setting `state` directly using the [`SetState`](/SetState) operator.
- _ref:
path: templates/navigation_buttons.yaml
vars:
previous_page_title: Deployment
previous_page_id: deployment
next_page_title: Custom Blocks
next_page_id: custom-blocks
next_page_title: Custom Code
next_page_id: custom-code

View File

@ -30,7 +30,31 @@ _ref:
If an operator errors while evaluating, it returns a `null` value, and logs the error to the console.
# Build time operators
## Client or server operators
Some operators are only available on either the client or the server. For example, the [`_menu`](/_menu) operator is only useful on the client and is thus not included in server requests. Likewise, the [`_secret`](/_secret) operator is only available on the server for security reasons.
If a operator has special environment considerations, it is indicated on the individual operator documentation page. If no indication is made, the operator can be used under both environments.
##### Client only operators:
- [_actions](/_actions)
- [_format](/_format)
- [_js](/_js)
- [_list_contexts](/_list_contexts)
- [_media](/_media)
- [_menu](/_menu)
- [_request](/_request)
##### Server only operators:
- [_diff](/_diff)
- [_secret](/_secret)
- [_uuid](/_uuid)
Operators that are client side only cannot be used in `Requests` and `Connections`, and operators which are server side only cannot be used in `Blocks` and `Actions`.
## Build time operators
Besides the client and server environment, app build time is considered a third environment where special operator logic applies.
The `_ref` and `_var` operators do not work like other operators. They are evaluated while an app is being built, and can thus be used anywhere in the app configuration. They are used to split a app into multiple files, and to reuse configuration. See [`_ref`](/_ref) for more details.
- _ref:

1
packages/docs/head.html Normal file
View File

@ -0,0 +1 @@
<script type="module" src="/public/modules/index.js" ></script>

View File

@ -24,6 +24,11 @@ global:
sm:
span: 23
app:
html:
appendHead:
_ref: head.html
connections:
- id: discord_channel
type: AxiosHttp

View File

@ -113,6 +113,11 @@
pageId: lists
properties:
title: Lists
- id: custom-code
type: MenuLink
pageId: custom-code
properties:
title: Custom Code
- id: custom-blocks
type: MenuLink
pageId: custom-blocks
@ -481,6 +486,9 @@
- id: CallMethod
type: MenuLink
pageId: CallMethod
- id: JsAction
type: MenuLink
pageId: JsAction
- id: Link
type: MenuLink
pageId: Link
@ -524,6 +532,9 @@
title: Operators
icon: ToolOutlined
links:
- id: _actions
type: MenuLink
pageId: _actions
- id: _and
type: MenuLink
pageId: _and

View File

@ -0,0 +1,123 @@
# 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/operators.yaml.njk
transformer: templates/operatorsMethodTransformer.js
vars:
pageId: _actions
pageTitle: _actions
filePath: operators/_actions.yaml
env: Client Only
types: |
```
(key: string): any
(all: boolean): any
(arguments: {
all?: boolean,
key?: string,
default?: any,
}): any
```
description: |
The `_actions` operator returns the response value for a preceding action in the same event list.
The action response object has the following structure:
```yaml
error: Error,
index: number,
response: any,
skipped: boolean,
type: string,
```
arguments: |
###### string
If the `_actions` operator is called with a string equal to a preceding action id in the same event list, the action response object returned. If a string is passed which does not match preceding action id in the same event list, `null` is returned. Dot notation is supported.
###### boolean
If the `_actions` operator is called with boolean argument `true`, an object with all the preceding action id responses in the same event list is returned.
examples: |
##### Using a action response:
```yaml
_actions: my_action.response
```
Returns: The response returned by the action.
##### Setting a action response to `state`:
```yaml
id: refresh
type: Button
events:
onClick:
- id: get_fresh_data
type: Request
skip:
_state: should_not_fetch
params: get_data
- id: set_data
type: SetState
params:
did_not_fetch_data:
_actions: get_fresh_data.skipped
```
##### Setting a returned [`JsAction`](/JsAction) response to `state`:
First register a custom JavaScript action: `getNormalizedEigenvector`
```yaml
# file: lowdefy.yaml
lowdefy: '3.15.0'
app:
html:
appendHead: |
<script type="text/javascript">
const getNormalizedEigenvector = (context, ...args) => {
const vectorLength = Math.sqrt(args.reduce((acc, curVal) => Math.pow(curVal, 2) + acc), 0);
return args.map(val => val / vectorLength );
}
window.lowdefy.registerJsAction('getNormalizedEigenvector', getNormalizedEigenvector);
</script>
```
Then, calculate the vector and update state.
```yaml
# onClick event on a block.
id: calculate
type: Button
events:
onClick:
- id: get_normalized_eigenvector
type: JsAction
params:
name: getNormalizedEigenvector
args:
- 1
- 5
- -12
- 7
- 4
- id: update_state
type: SetState
params:
normal_eigenvector:
_actions: get_normalized_eigenvector.response
```
In this example, state will now be equal to:
```yaml
normal_eigenvector:
- 0.06523280730534423
- 0.3261640365267211
- -0.7827936876641306
- 0.45662965113740955
- 0.2609312292213769
```

View File

@ -19,8 +19,7 @@ _ref:
pageId: _diff
pageTitle: _diff
filePath: operators/_diff.yaml
description: |
> The `_diff` operator is only useable on the Lowdefy server, and not on the web client. Therefore it can only be used in requests and connections.
env: Server Only
methods:
- name: deep
types: |

View File

@ -18,6 +18,7 @@ _ref:
vars:
pageId: _format
pageTitle: _format
env: Client Only
description: |
The `_format` operator converts objects to strings, using a specified format. It can only be used on the web-client (not in requests or connections).
methods:

View File

@ -19,12 +19,28 @@ _ref:
pageId: _js
pageTitle: _js
filePath: operators/_js.yaml
env: Client Only
warning: |
SECURITY WARNING: The _js operator executes JavaScript inside your Lowdefy app. Insecure code can expose your app or data. Since Lowdefy doesn't validate your JavaScript, make sure that you only load trusted code.
types: |
```
(args: any[]): any
```
description: |
The `_js` operator can evaluate or create a JavaScript function. The JavaScript function can return any primitives or JSON data. Since operators are synchronous functions, the JavaScript function should be synchronous.
The `_js` operator executes a JavaScript function when the operator is evaluated. If used in a block, this function is called every time the Lowdefy engine checks if any block on the page should be updated. This function must be synchronous, and it is highly recommended that it is a pure function (it always returns the same result for the same input, and does not have side effects). Since it is called often it should also execute quickly, a slow operator will slow down the entire app. The [`JsAction`](/JsAction) action can be used to execute asynchronous code when an event is triggered.
To make this as secure as possible, the JavaScript is evaluated inside a JavaScript WebAssembly VM. The JavaScript engine is [QuickJS](https://bellard.org/quickjs/), a small and fast JavaScript implementation.
To use the `_js` operator the following is needed:
As a result, the following applies to the JavaScript function definitions:
- Load the custom JavaScript into the app `head` tag. See the [Custom Code](/custom-code) section for more details.
- In the custom JavaScript, pass the JavaScript operator function to the app using the `window.lowdefy.registerJsOperator(name: string, operatorFunction: function)` method.
- A list of arguments can be passed to the JavaScript function which will be spread as function parameters.
- The returned value of the custom JavaScript function will be the operator result.
-----------
> DEPRECATION WARNING: The QuickJS JavaScript operators are depreciated and will be removed in the next version. Instead native browser functions can now be loaded at build time by registering the operator function with `window.lowdefy.registerJsOperator(name: string, operatorFunction: function)`. See the [Custom Code](/custom-code) section for details.
For depreciated `evaluate`, and `function`, the following applies to the JavaScript function definitions:
- The `code` operator argument requires a function definition.
- Function arguments can be used inside the function, and are passed via the `args` operator argument as a array.
- A primitive or JSON result will be returned, so the function result must be JSON serializable.
@ -35,7 +51,158 @@ _ref:
> The JavaScript function can be passed to the `code` argument during build using the `_ref.eval` method. See the examples below and the [`_ref`](/_ref) operator for more details.
methods:
- name: evaluate
- name: '{{ method_name }}'
types: |
```
(args?: any[]): any
```
description: |
The `_js.{{ method_name }}` method evaluates the JavaScript function as registered using `window.lowdefy.registerJsOperator(method_name: string, method: function)`. When passing a list of arguments to the JavaScript function, then list of arguments will be spread as function parameters.
The loaded JavaScript function must be a synchronous pure function. See the [Custom JavaScript](/custom-javascript) section for more detail on how to load custom JavaScript operators.
examples: |
##### A custom JavaScript operator to calculate primes:
```yaml
# lowdefy.yaml
name: make-me-primes
lowdefy: '3.15.0'
app:
html:
# This HTML will be appended to the head HTML tag in the Lowdefy app
appendHead: |
<script type="text/javascript">
// Define the JavaScript function.
function makePrimes(to) {
return [...Array((to || 1)-1).keys()].map(i=>i+2).filter(n =>
[...Array(n-2).keys()].map(i=>i+2).reduce((acc,x)=> acc && n % x !== 0, true));
}
// Register the JavaScript operator function.
window.lowdefy.registerJsOperator('makePrimes', makePrimes);
</script>
pages:
- id: home
type: PageHeaderMenu
blocks:
- id: num_of_primes
type: NumberInput
properties:
title: Give me all the primes below?
- id: primes
type: Markdown
properties:
content:
_nunjucks:
template: |
All primes below {{ num_of_primes }} is: {{ primes }}
on:
num_of_primes:
_state: num_of_primes
primes:
# Use the custom JavaScript operator.
_js.makePrimes:
- _state: num_of_primes
````
##### _js.function to create a label.formatter function for an EChart block:
Create a JavaScript file which are hosted publicly. The `public` folder in your Lowdefy app makes this easy. Also check to register the custom operator using `window.lowdefy.registerJsOperator`.
```js
// file: /public/getLabel.js
const getLabel = (levelTwo, levelThree, levelFour) => (param) => {
var depth = param.treePathInfo.length;
if (depth === 2) {
return levelTwo;
}
else if (depth === 3) {
return levelThree;
}
else if (depth === 4) {
return levelFour;
}
}
window.lowdefy.registerJsOperator('getLabel', getLabel);
```
Load the JavaScript by referencing it into the Lowdefy application HTML `head` tag.
```html
// file: imports.html
<script type="text/javascript" src="/public/getLabel.js"></script>
```
Use the `_js.getLabel` operator
```yaml
# file: lowdefy.yaml
name: my-chart
lowdefy: '3.15.0'
app:
html:
appendHead:
_ref: imports.html # Loading html content into the application head tag.
pages:
- id: home
type: PageHeaderMenu
blocks:
- id: chart
type: EChart
properties:
height: 600
option:
series:
- radius:
- '15%'
- '80%'
type: 'sunburst'
sort: null
emphasis:
focus: 'ancestor'
data:
- value: 8,
children:
- value: 4,
children:
- value: 2
- value: 1
- value: 1
- value: 0.5
- value: 2
- value: 4
children:
- children:
- value: 2
- value: 4
children:
- children:
- value: 2
- value: 3
children:
- children:
- value: 1
label:
color: '#000'
textBorderColor: '#fff'
textBorderWidth: 2
formatter:
_js.getLabel: # Use the getLabel function
- 'radial'
- 'tangential'
- 'other'
levels:
- {}
- itemStyle:
color: '#CD4949'
label:
rotate: 'radial'
- itemStyle:
color: '#F47251'
label:
rotate: 'tangential'
- itemStyle:
color: '#FFC75F'
label:
rotate: 0
````
- name: evaluate [DEPRECATED]
types: |
```
({
@ -94,7 +261,7 @@ _ref:
```
[2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47]
```
- name: function
- name: function [DEPRECATED]
types: |
```
({

View File

@ -19,6 +19,7 @@ _ref:
pageId: _list_contexts
pageTitle: _list_contexts
filePath: operators/_list_contexts.yaml
env: Client Only
types: |
```
(value: any): string[]

View File

@ -19,6 +19,7 @@ _ref:
pageId: _media
pageTitle: _media
filePath: operators/_media.yaml
env: Client Only
types: |
```
(key: string): any

View File

@ -19,6 +19,7 @@ _ref:
pageId: _menu
pageTitle: _menu
filePath: operators/_menu.yaml
env: Client Only
types: |
```
(menuId: string): object

View File

@ -19,6 +19,7 @@ _ref:
pageId: _ref
pageTitle: _ref
filePath: operators/_ref.yaml
env: Build Only
types: |
```
(path: string): any

View File

@ -19,6 +19,7 @@ _ref:
pageId: _request
pageTitle: _request
filePath: operators/_request.yaml
env: Client Only
types: |
```
(requestId: string): any

View File

@ -19,6 +19,7 @@ _ref:
pageId: _secret
pageTitle: _secret
filePath: operators/_secret.yaml
env: Server Only
types: |
```
(key: string): any

View File

@ -19,6 +19,7 @@ _ref:
pageId: _uuid
pageTitle: _uuid
filePath: operators/_uuid.yaml
env: Server Only
types: |
```
(void): string

View File

@ -19,6 +19,7 @@ _ref:
pageId: _var
pageTitle: _var
filePath: operators/_var.yaml
env: Build Only
types: |
```
(name: string): any

View File

@ -23,6 +23,7 @@
- _ref: concepts/secrets.yaml
- _ref: concepts/deployment.yaml
- _ref: concepts/lists.yaml
- _ref: concepts/custom-code.yaml
- _ref: concepts/custom-blocks.yaml
- _ref: users/users-introduction.yaml
@ -127,6 +128,7 @@
- _ref: connections/SQLite.yaml
- _ref: actions/CallMethod.yaml
- _ref: actions/JsAction.yaml
- _ref: actions/Link.yaml
- _ref: actions/Login.yaml
- _ref: actions/Logout.yaml
@ -139,6 +141,7 @@
- _ref: actions/SetState.yaml
- _ref: actions/Validate.yaml
- _ref: operators/_actions.yaml
- _ref: operators/_and.yaml
- _ref: operators/_args.yaml
- _ref: operators/_array.yaml

View File

@ -45,4 +45,4 @@ function filterDefaultValue(value, defaultValue) {
return filterObject({ obj: value, path: [] });
}
module.exports = filterDefaultValue;
export default filterDefaultValue;

View File

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

View File

@ -14,7 +14,7 @@
limitations under the License.
*/
import filterDefaultValue from '../blocks/filterDefaultValue';
import filterDefaultValue from '../filterDefaultValue';
test('no default value', () => {
const value = { a: 1 };

View File

@ -21,6 +21,16 @@ _ref:
_var: pageTitle
section: Actions
content:
- id: warning
type: Alert
visible:
_not:
_not:
_var: warning
properties:
message:
_var: warning
type: warning
- id: types
type: Markdown
style:

View File

@ -181,21 +181,17 @@ areas:
required:
_state: block.required
properties:
_js.evaluate:
code:
_ref:
eval: templates/blocks/filterDefaultValue.js
args:
- _ref:
path:
_var: schema
vars:
block_type: {{ block_type }}
transformer: templates/blocks/propertiesGetterTransformer.js
- _ref:
path:
_var: schema
transformer: templates/blocks/defaultValueTransformer.js
_js.filterDefaultValue:
- _ref:
path:
_var: schema
vars:
block_type: {{ block_type }}
transformer: templates/blocks/propertiesGetterTransformer.js
- _ref:
path:
_var: schema
transformer: templates/blocks/defaultValueTransformer.js
{% if areas %}
areas:
@ -302,41 +298,37 @@ areas:
options:
sortKeys: false
on:
_js.evaluate:
code:
_ref:
eval: templates/blocks/filterDefaultValue.js
args:
- id: block_id
type: {{ block_type }}
required:
_state: block.required
visible:
_state: block.visible
layout:
_state: block.layout
style:
_yaml.parse:
_if_none:
- _state: style_block_input
- ''
properties:
_ref:
path:
_var: schema
vars:
block_type: {{ block_type }}
transformer: templates/blocks/propertiesGetterTransformer.js
- required: false
visible: true
layout:
align: top
span: 24
properties:
_ref:
path:
_var: schema
transformer: templates/blocks/defaultValueTransformer.js
_js.filterDefaultValue:
- id: block_id
type: {{ block_type }}
required:
_state: block.required
visible:
_state: block.visible
layout:
_state: block.layout
style:
_yaml.parse:
_if_none:
- _state: style_block_input
- ''
properties:
_ref:
path:
_var: schema
vars:
block_type: {{ block_type }}
transformer: templates/blocks/propertiesGetterTransformer.js
- required: false
visible: true
layout:
align: top
span: 24
properties:
_ref:
path:
_var: schema
transformer: templates/blocks/defaultValueTransformer.js
- id: right_column
type: Box

View File

@ -316,6 +316,31 @@
type: Link
params:
url: https://github.com/lowdefy/lowdefy/discussions
- id: lowdefy_discord
type: Box
style:
width: 24
height: 24
layout:
size: auto
blocks:
- id: discord_icon
type: Html
properties:
html: |
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="28px" height="28px" viewBox="0 0 48 48" version="1.1">
<g id="surface113672">
<path style=" stroke:none;fill-rule:nonzero;fill:rgba(0, 0, 0, 0.65);fill-opacity:1;" d="M 42 45 L 33 38 L 34 41 L 10 41 C 7.238281 41 5 38.761719 5 36 L 5 10 C 5 7.238281 7.238281 5 10 5 L 37 5 C 39.761719 5 42 7.238281 42 10 Z M 42 45 "/>
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(100%,100%,100%);fill-opacity:1;" d="M 32.589844 16.238281 C 32.589844 16.238281 29.988281 14.230469 26.910156 14 L 26.640625 14.550781 C 29.421875 15.21875 30.691406 16.191406 32.019531 17.378906 C 29.730469 16.210938 27.460938 15 23.5 15 C 19.539062 15 17.269531 16.210938 14.980469 17.378906 C 16.308594 16.191406 17.828125 15.109375 20.359375 14.550781 L 20.089844 14 C 16.859375 14.308594 14.410156 16.238281 14.410156 16.238281 C 14.410156 16.238281 11.5 20.429688 11 28.621094 C 13.941406 31.980469 18.390625 32 18.390625 32 L 19.308594 30.769531 C 17.738281 30.230469 15.949219 29.261719 14.410156 27.5 C 16.25 28.878906 19.019531 30 23.5 30 C 27.980469 30 30.75 28.878906 32.589844 27.5 C 31.050781 29.261719 29.261719 30.230469 27.691406 30.769531 L 28.609375 32 C 28.609375 32 33.058594 31.980469 36 28.621094 C 35.5 20.429688 32.589844 16.238281 32.589844 16.238281 Z M 20 27 C 18.898438 27 18 25.878906 18 24.5 C 18 23.121094 18.898438 22 20 22 C 21.101562 22 22 23.121094 22 24.5 C 22 25.878906 21.101562 27 20 27 Z M 27 27 C 25.898438 27 25 25.878906 25 24.5 C 25 23.121094 25.898438 22 27 22 C 28.101562 22 29 23.121094 29 24.5 C 29 25.878906 28.101562 27 27 27 Z M 27 27 "/>
</g>
</svg>
events:
onClick:
- id: link_discord
type: Link
params:
url: https://discord.gg/WmcJgXt
- id: lowdefy_github
type: Icon
layout:

View File

@ -75,7 +75,7 @@
layout:
flex: 1 0 auto
properties:
title: Join the Community
title: Join our Discord
type: text
size: large
events:
@ -83,7 +83,7 @@
- id: website_link
type: Link
params:
url: https://github.com/lowdefy/lowdefy/discussions
url: https://discord.gg/WmcJgXt
- id: github_button
type: Button
layout:

View File

@ -23,6 +23,28 @@ page:
_var: pageTitle
section: Operators
content:
- id: warning
type: Alert
visible:
_not:
_not:
_var: warning
properties:
message:
_var: warning
type: warning
- id: environment
type: Alert
visible:
_not:
_not:
_var: env
properties:
message:
_string.concat:
- 'Environment: '
- _var: env
type: info
- id: types
type: Markdown
style:

View File

@ -27,51 +27,72 @@ class Actions {
}
async callActions({ actions, arrayIndices, block, event, eventName }) {
const responses = [];
let success = true;
const startTimestamp = new Date();
const responses = {};
try {
for (const action of actions) {
const response = await this.callAction({ action, arrayIndices, block, event });
responses.push(response);
for (const [index, action] of actions.entries()) {
try {
const response = await this.callAction({
action,
arrayIndices,
block,
event,
index,
responses,
});
responses[action.id] = response;
} catch (error) {
throw {
error,
action,
};
}
}
} catch (error) {
responses.push(error);
console.log(error);
success = false;
responses[error.action.id] = error.error;
console.error(error);
return {
blockId: block.blockId,
error,
event,
eventName,
responses,
endTimestamp: new Date(),
startTimestamp,
success: false,
};
}
return {
blockId: block.blockId,
event,
eventName,
responses,
timestamp: new Date(),
success,
endTimestamp: new Date(),
startTimestamp,
success: true,
};
}
async callAction({ action, arrayIndices, block, event }) {
async callAction({ action, arrayIndices, block, event, index, responses }) {
if (!actions[action.type]) {
throw {
actionId: action.id,
actionType: action.type,
error: new Error(`Invalid action type "${action.type}" at "${block.blockId}".`),
type: action.type,
index,
};
}
const { output: parsedAction, errors: parserErrors } = this.context.parser.parse({
actions: responses,
event,
arrayIndices,
input: action,
location: block.blockId,
});
if (parserErrors.length > 0) {
throw {
actionId: action.id,
actionType: action.type,
error: parserErrors[0],
};
throw { error: parserErrors[0], type: action.type, index };
}
if (parsedAction.skip === true) {
return { actionId: action.id, actionType: action.type, skipped: true };
return { type: action.type, skipped: true, index };
}
const messages = parsedAction.messages || {};
let response;
@ -98,9 +119,9 @@ class Actions {
status: 'error',
});
throw {
actionId: action.id,
actionType: action.type,
type: action.type,
error,
index,
};
}
closeLoading();
@ -109,7 +130,7 @@ class Actions {
message: messages.success,
status: 'success',
});
return { actionId: action.id, actionType: action.type, response };
return { type: action.type, response, index };
}
displayMessage({ defaultMessage, duration, hideExplicitly, message, status }) {

View File

@ -0,0 +1,45 @@
/*
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, serializer } from '@lowdefy/helpers';
async function JsAction({ context, params }) {
if (!type.isString(params.name)) {
throw new Error(`JsAction requires a string for 'params.name'.`);
}
if (!type.isNone(params.args) && !type.isArray(params.args)) {
throw new Error(`JsAction requires a array for 'params.args'.`);
}
if (!type.isFunction(context.lowdefy.imports.jsActions[params.name])) {
throw new Error(`JsAction ${params.name} is not a function.`);
}
return context.lowdefy.imports.jsActions[params.name](
{
...serializer.copy({
user: context.lowdefy.user,
global: context.lowdefy.lowdefyGlobal,
state: context.state,
urlQuery: context.lowdefy.urlQuery,
input: context.lowdefy.inputs[context.id],
}),
contextId: context.id,
pageId: context.pageId,
requests: { ...context.requests },
},
...(params.args || [])
);
}
export default JsAction;

View File

@ -15,6 +15,7 @@
*/
import CallMethod from './CallMethod';
import JsAction from './JsAction';
import Link from './Link';
import Login from './Login';
import Logout from './Logout';
@ -28,12 +29,13 @@ import Validate from './Validate';
export default {
CallMethod,
JsAction,
Link,
Login,
Logout,
Message,
Reset,
Request,
Reset,
ScrollTo,
SetGlobal,
SetState,

View File

@ -79,17 +79,16 @@ test('call a synchronous action', async () => {
blockId: 'blockId',
event: {},
eventName: 'eventName',
responses: [
{
actionId: 'test',
actionType: 'ActionSync',
responses: {
test: {
type: 'ActionSync',
index: 0,
response: 'params',
},
],
success: true,
timestamp: {
date: 0,
},
success: true,
startTimestamp: { date: 0 },
endTimestamp: { date: 0 },
});
expect(actions.ActionSync.mock.calls.length).toBe(1);
});
@ -117,17 +116,16 @@ test('call a asynchronous action', async () => {
blockId: 'blockId',
event: {},
eventName: 'eventName',
responses: [
{
actionId: 'test',
actionType: 'ActionAsync',
responses: {
test: {
type: 'ActionAsync',
index: 0,
response: 'params',
},
],
success: true,
timestamp: {
date: 0,
},
success: true,
startTimestamp: { date: 0 },
endTimestamp: { date: 0 },
});
expect(actions.ActionAsync.mock.calls.length).toBe(1);
});
@ -158,22 +156,21 @@ test('call 2 actions', async () => {
blockId: 'blockId',
event: {},
eventName: 'eventName',
responses: [
{
actionId: 'test1',
actionType: 'ActionSync',
responses: {
test1: {
type: 'ActionSync',
index: 0,
response: 'params1',
},
{
actionId: 'test2',
actionType: 'ActionAsync',
test2: {
type: 'ActionAsync',
index: 1,
response: 'params2',
},
],
success: true,
timestamp: {
date: 0,
},
success: true,
startTimestamp: { date: 0 },
endTimestamp: { date: 0 },
});
});
@ -275,17 +272,16 @@ test('skip a action', async () => {
blockId: 'blockId',
event: {},
eventName: 'eventName',
responses: [
{
actionId: 'test',
actionType: 'ActionSync',
responses: {
test: {
type: 'ActionSync',
index: 0,
skipped: true,
},
],
success: true,
timestamp: {
date: 0,
},
success: true,
startTimestamp: { date: 0 },
endTimestamp: { date: 0 },
});
expect(actions.ActionSync.mock.calls.length).toBe(0);
});
@ -313,17 +309,28 @@ test('action throws a error', async () => {
blockId: 'blockId',
event: {},
eventName: 'eventName',
responses: [
{
actionId: 'test',
actionType: 'ActionError',
error: {
action: {
id: 'test',
params: 'params',
type: 'ActionError',
},
error: {
error: new Error('Test error'),
index: 0,
type: 'ActionError',
},
},
responses: {
test: {
type: 'ActionError',
index: 0,
error: new Error('Test error'),
},
],
success: false,
timestamp: {
date: 0,
},
success: false,
startTimestamp: { date: 0 },
endTimestamp: { date: 0 },
});
expect(actions.ActionError.mock.calls.length).toBe(1);
});
@ -354,17 +361,28 @@ test('actions after a error are not called throws a error', async () => {
blockId: 'blockId',
event: {},
eventName: 'eventName',
responses: [
{
actionId: 'test',
actionType: 'ActionError',
error: {
action: {
id: 'test',
params: 'params',
type: 'ActionError',
},
error: {
error: new Error('Test error'),
index: 0,
type: 'ActionError',
},
},
responses: {
test: {
type: 'ActionError',
index: 0,
error: new Error('Test error'),
},
],
success: false,
timestamp: {
date: 0,
},
success: false,
startTimestamp: { date: 0 },
endTimestamp: { date: 0 },
});
expect(actions.ActionError.mock.calls.length).toBe(1);
expect(actions.ActionSync.mock.calls.length).toBe(0);
@ -393,17 +411,28 @@ test('Invalid action type', async () => {
blockId: 'blockId',
event: {},
eventName: 'eventName',
responses: [
{
actionId: 'test',
actionType: 'Invalid',
error: {
action: {
id: 'test',
params: 'params',
type: 'Invalid',
},
error: {
error: new Error('Invalid action type "Invalid" at "blockId".'),
index: 0,
type: 'Invalid',
},
},
responses: {
test: {
type: 'Invalid',
index: 0,
error: new Error('Invalid action type "Invalid" at "blockId".'),
},
],
success: false,
timestamp: {
date: 0,
},
success: false,
startTimestamp: { date: 0 },
endTimestamp: { date: 0 },
});
});
@ -430,19 +459,34 @@ test('Parser error in action', async () => {
blockId: 'blockId',
event: {},
eventName: 'eventName',
responses: [
{
actionId: 'test',
actionType: 'ActionSync',
error: {
action: {
id: 'test',
params: {
_state: [],
},
type: 'ActionSync',
},
error: {
error: new Error(
'Operator Error: _state params must be of type string, integer, boolean or object. Received: [] at blockId.'
),
index: 0,
type: 'ActionSync',
},
},
responses: {
test: {
type: 'ActionSync',
index: 0,
error: new Error(
'Operator Error: _state params must be of type string, integer, boolean or object. Received: [] at blockId.'
),
},
],
success: false,
timestamp: {
date: 0,
},
success: false,
startTimestamp: { date: 0 },
endTimestamp: { date: 0 },
});
});

View File

@ -86,17 +86,18 @@ test('CallMethod with no args, synchronous method', async () => {
blockId: 'button',
event: undefined,
eventName: 'onClick',
responses: [
{
actionId: 'a',
actionType: 'CallMethod',
responses: {
a: {
type: 'CallMethod',
index: 0,
response: {
args: [],
},
},
],
},
success: true,
timestamp: new Date(),
startTimestamp: { date: 0 },
endTimestamp: { date: 0 },
});
expect(blockMethod.mock.calls).toEqual([[]]);
});
@ -161,19 +162,18 @@ test('CallMethod method return a promise', async () => {
blockId: 'button',
event: undefined,
eventName: 'onClick',
responses: [
{
actionId: 'a',
actionType: 'CallMethod',
responses: {
a: {
type: 'CallMethod',
index: 0,
response: {
args: ['arg'],
},
},
],
success: true,
timestamp: {
date: 0,
},
success: true,
startTimestamp: { date: 0 },
endTimestamp: { date: 0 },
});
expect(calls).toEqual([['arg']]);
});
@ -230,19 +230,36 @@ test('CallMethod with args not an array', async () => {
blockId: 'button',
event: undefined,
eventName: 'onClick',
responses: [
{
actionId: 'a',
actionType: 'CallMethod',
error: {
action: {
id: 'a',
params: {
args: 'arg',
blockId: 'textInput',
method: 'blockMethod',
},
type: 'CallMethod',
},
error: {
error: new Error(
'Failed to call method "blockMethod" on block "textInput": "args" should be an array.'
),
index: 0,
type: 'CallMethod',
},
},
responses: {
a: {
type: 'CallMethod',
index: 0,
error: new Error(
'Failed to call method "blockMethod" on block "textInput": "args" should be an array.'
),
},
],
success: false,
timestamp: {
date: 0,
},
success: false,
startTimestamp: { date: 0 },
endTimestamp: { date: 0 },
});
expect(blockMethod.mock.calls).toEqual([]);
});
@ -299,19 +316,18 @@ test('CallMethod with multiple positional args, synchronous method', async () =>
blockId: 'button',
event: undefined,
eventName: 'onClick',
responses: [
{
actionId: 'a',
actionType: 'CallMethod',
responses: {
a: {
type: 'CallMethod',
index: 0,
response: {
args: ['arg1', 'arg2'],
},
},
],
success: true,
timestamp: {
date: 0,
},
success: true,
startTimestamp: { date: 0 },
endTimestamp: { date: 0 },
});
expect(blockMethod.mock.calls).toEqual([['arg1', 'arg2']]);
});

View File

@ -0,0 +1,447 @@
/*
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 = { pageId };
const RealDate = Date;
const mockDate = jest.fn(() => ({ date: 0 }));
mockDate.now = jest.fn(() => 0);
// Comment out to use console.log
console.log = () => {};
beforeAll(() => {
global.Date = mockDate;
});
afterAll(() => {
global.Date = RealDate;
});
test('JsAction with no args, synchronous fn', 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: 'JsAction',
params: {
name: 'test_fn',
},
},
],
},
},
],
},
},
};
const context = await testContext({
lowdefy,
rootBlock,
});
const mockFn = jest.fn(() => 'js_fn');
context.lowdefy.imports.jsActions.test_fn = mockFn;
const { button } = context.RootBlocks.map;
const res = await button.triggerEvent({ name: 'onClick' });
expect(res).toEqual({
blockId: 'button',
event: undefined,
eventName: 'onClick',
responses: {
a: {
type: 'JsAction',
index: 0,
response: 'js_fn',
},
},
success: true,
startTimestamp: { date: 0 },
endTimestamp: { date: 0 },
});
expect(mockFn).toHaveBeenCalledTimes(1);
});
test('JsAction with no args, async fn', 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: 'JsAction',
params: {
name: 'test_fn',
},
},
],
},
},
],
},
},
};
const context = await testContext({
lowdefy,
rootBlock,
});
const timeout = (ms) => {
return new Promise((resolve) => setTimeout(resolve, ms));
};
const fn = async () => {
await timeout(300);
return 'js_fn';
};
const mockFn = jest.fn().mockImplementation(fn);
context.lowdefy.imports.jsActions.test_fn = mockFn;
const { button } = context.RootBlocks.map;
const res = await button.triggerEvent({ name: 'onClick' });
expect(res).toEqual({
blockId: 'button',
event: undefined,
eventName: 'onClick',
responses: {
a: {
type: 'JsAction',
index: 0,
response: 'js_fn',
},
},
success: true,
startTimestamp: { date: 0 },
endTimestamp: { date: 0 },
});
expect(mockFn).toHaveBeenCalledTimes(1);
});
test('JsAction with args, synchronous fn', 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: 'JsAction',
params: {
name: 'test_fn',
args: [1, '2', new Date()],
},
},
],
},
},
],
},
},
};
const context = await testContext({
lowdefy,
rootBlock,
});
const mockFn = jest.fn((...args) => args);
context.lowdefy.imports.jsActions.test_fn = mockFn;
const { button } = context.RootBlocks.map;
const res = await button.triggerEvent({ name: 'onClick' });
expect(res).toEqual({
blockId: 'button',
event: undefined,
eventName: 'onClick',
responses: {
a: {
type: 'JsAction',
index: 0,
response: [
{
contextId: 'test',
input: {},
pageId: 'root',
requests: {},
state: {},
urlQuery: {},
},
1,
'2',
{
date: 0,
},
],
},
},
success: true,
startTimestamp: { date: 0 },
endTimestamp: { date: 0 },
});
expect(mockFn).toHaveBeenCalledTimes(1);
});
test('JsAction name not a string', 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: 'JsAction',
params: {
name: 1,
},
},
],
},
},
],
},
},
};
const context = await testContext({
lowdefy,
rootBlock,
});
const { button } = context.RootBlocks.map;
const res = await button.triggerEvent({ name: 'onClick' });
expect(res).toEqual({
blockId: 'button',
event: undefined,
eventName: 'onClick',
error: {
action: {
id: 'a',
params: {
name: 1,
},
type: 'JsAction',
},
error: {
error: new Error(`JsAction requires a string for 'params.name'.`),
index: 0,
type: 'JsAction',
},
},
responses: {
a: {
type: 'JsAction',
index: 0,
error: new Error(`JsAction requires a string for 'params.name'.`),
},
},
success: false,
startTimestamp: { date: 0 },
endTimestamp: { date: 0 },
});
});
test('JsAction args not an array', 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: 'JsAction',
params: {
name: 'js_fn',
args: { a: 1 },
},
},
],
},
},
],
},
},
};
const context = await testContext({
lowdefy,
rootBlock,
});
const mockFn = jest.fn(() => 'js_fn');
const { button } = context.RootBlocks.map;
const res = await button.triggerEvent({ name: 'onClick' });
expect(res).toEqual({
blockId: 'button',
event: undefined,
eventName: 'onClick',
error: {
action: {
id: 'a',
params: {
args: {
a: 1,
},
name: 'js_fn',
},
type: 'JsAction',
},
error: {
error: new Error(`JsAction requires a array for 'params.args'.`),
index: 0,
type: 'JsAction',
},
},
responses: {
a: {
type: 'JsAction',
index: 0,
error: new Error(`JsAction requires a array for 'params.args'.`),
},
},
success: false,
startTimestamp: { date: 0 },
endTimestamp: { date: 0 },
});
});
test('JsAction args not a function', 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: 'JsAction',
params: {
name: 'js_not_fn',
},
},
],
},
},
],
},
},
};
const context = await testContext({
lowdefy,
rootBlock,
});
const mockFn = jest.fn(() => 'js_fn');
const { button } = context.RootBlocks.map;
const res = await button.triggerEvent({ name: 'onClick' });
expect(res).toEqual({
blockId: 'button',
event: undefined,
eventName: 'onClick',
error: {
action: {
id: 'a',
params: {
name: 'js_not_fn',
},
type: 'JsAction',
},
error: {
error: new Error(`JsAction js_not_fn is not a function.`),
index: 0,
type: 'JsAction',
},
},
responses: {
a: {
type: 'JsAction',
index: 0,
error: new Error(`JsAction js_not_fn is not a function.`),
},
},
success: false,
startTimestamp: { date: 0 },
endTimestamp: { date: 0 },
});
});

View File

@ -164,14 +164,29 @@ test('Link error', async () => {
blockId: 'button',
event: undefined,
eventName: 'onClick',
responses: [
{
actionId: 'a',
actionType: 'Link',
error: new Error('Invalid Link, check action params. Received "{"invalid":true}".'),
error: {
action: {
id: 'a',
params: {
invalid: true,
},
type: 'Link',
},
],
error: {
error: new Error('Invalid Link, check action params. Received "{"invalid":true}".'),
index: 0,
type: 'Link',
},
},
responses: {
a: {
type: 'Link',
error: new Error('Invalid Link, check action params. Received "{"invalid":true}".'),
index: 0,
},
},
success: false,
timestamp: { date: 0 },
startTimestamp: { date: 0 },
endTimestamp: { date: 0 },
});
});

View File

@ -142,17 +142,16 @@ test('Request call one request', async () => {
blockId: 'button',
event: undefined,
eventName: 'onClick',
responses: [
{
actionId: 'a',
actionType: 'Request',
responses: {
a: {
type: 'Request',
index: 0,
response: [1],
},
],
success: true,
timestamp: {
date: 0,
},
success: true,
startTimestamp: { date: 0 },
endTimestamp: { date: 0 },
});
});
@ -223,17 +222,16 @@ test('Request call all requests', async () => {
blockId: 'button',
event: undefined,
eventName: 'onClick',
responses: [
{
actionId: 'a',
actionType: 'Request',
responses: {
a: {
type: 'Request',
index: 0,
response: [1, 2],
},
],
success: true,
timestamp: {
date: 0,
},
success: true,
startTimestamp: { date: 0 },
endTimestamp: { date: 0 },
});
});
@ -304,17 +302,16 @@ test('Request call array of requests', async () => {
blockId: 'button',
event: undefined,
eventName: 'onClick',
responses: [
{
actionId: 'a',
actionType: 'Request',
responses: {
a: {
type: 'Request',
index: 0,
response: [1, 2],
},
],
success: true,
timestamp: {
date: 0,
},
success: true,
startTimestamp: { date: 0 },
endTimestamp: { date: 0 },
});
});
@ -404,15 +401,28 @@ test('Request call request error', async () => {
blockId: 'button',
event: undefined,
eventName: 'onClick',
responses: [
{
actionId: 'a',
actionType: 'Request',
error: {
action: {
id: 'a',
params: 'req_error',
type: 'Request',
},
error: {
error: new Error('Request error'),
index: 0,
type: 'Request',
},
},
responses: {
a: {
type: 'Request',
index: 0,
error: new Error('Request error'),
},
],
},
success: false,
timestamp: { date: 0 },
startTimestamp: { date: 0 },
endTimestamp: { date: 0 },
});
});
@ -460,14 +470,27 @@ test('Request call request graphql error', async () => {
blockId: 'button',
event: undefined,
eventName: 'onClick',
responses: [
{
actionId: 'a',
actionType: 'Request',
error: {
action: {
id: 'a',
params: 'req_gql_error',
type: 'Request',
},
error: {
error: new Error('displayTitle: displayMessage'),
index: 0,
type: 'Request',
},
},
responses: {
a: {
type: 'Request',
index: 0,
error: new Error('displayTitle: displayMessage'),
},
],
},
success: false,
timestamp: { date: 0 },
startTimestamp: { date: 0 },
endTimestamp: { date: 0 },
});
});

View File

@ -98,17 +98,27 @@ test('Validate required field', async () => {
blockId: 'button',
event: undefined,
eventName: 'onClick',
responses: [
{
actionId: 'validate',
actionType: 'Validate',
error: {
action: {
id: 'validate',
type: 'Validate',
},
error: {
error: new Error('Your input has 1 validation error.'),
index: 0,
type: 'Validate',
},
},
responses: {
validate: {
type: 'Validate',
index: 0,
error: new Error('Your input has 1 validation error.'),
},
],
success: false,
timestamp: {
date: 0,
},
success: false,
startTimestamp: { date: 0 },
endTimestamp: { date: 0 },
});
expect(text1.validationEval.output).toEqual({
errors: ['This field is required'],
@ -134,17 +144,16 @@ test('Validate required field', async () => {
blockId: 'button',
event: undefined,
eventName: 'onClick',
responses: [
{
actionId: 'validate',
actionType: 'Validate',
responses: {
validate: {
type: 'Validate',
index: 0,
response: undefined,
},
],
success: true,
timestamp: {
date: 0,
},
success: true,
startTimestamp: { date: 0 },
endTimestamp: { date: 0 },
});
expect(text1.validationEval.output).toEqual({
errors: [],
@ -227,17 +236,27 @@ test('Validate all fields', async () => {
blockId: 'button',
event: undefined,
eventName: 'onClick',
responses: [
{
actionId: 'validate',
actionType: 'Validate',
error: {
action: {
id: 'validate',
type: 'Validate',
},
error: {
error: new Error('Your input has 2 validation errors.'),
index: 0,
type: 'Validate',
},
},
responses: {
validate: {
type: 'Validate',
index: 0,
error: new Error('Your input has 2 validation errors.'),
},
],
success: false,
timestamp: {
date: 0,
},
success: false,
startTimestamp: { date: 0 },
endTimestamp: { date: 0 },
});
expect(text1.validationEval.output).toEqual({
errors: ['text1 does not match pattern "text1"'],
@ -268,17 +287,27 @@ test('Validate all fields', async () => {
blockId: 'button',
event: undefined,
eventName: 'onClick',
responses: [
{
actionId: 'validate',
actionType: 'Validate',
error: {
action: {
id: 'validate',
type: 'Validate',
},
error: {
error: new Error('Your input has 1 validation error.'),
index: 0,
type: 'Validate',
},
},
responses: {
validate: {
type: 'Validate',
index: 0,
error: new Error('Your input has 1 validation error.'),
},
],
success: false,
timestamp: {
date: 0,
},
success: false,
startTimestamp: { date: 0 },
endTimestamp: { date: 0 },
});
expect(text1.validationEval.output).toEqual({
errors: [],
@ -309,17 +338,16 @@ test('Validate all fields', async () => {
blockId: 'button',
event: undefined,
eventName: 'onClick',
responses: [
{
actionId: 'validate',
actionType: 'Validate',
responses: {
validate: {
type: 'Validate',
index: 0,
response: undefined,
},
],
success: true,
timestamp: {
date: 0,
},
success: true,
startTimestamp: { date: 0 },
endTimestamp: { date: 0 },
});
expect(text1.validationEval.output).toEqual({
errors: [],
@ -406,17 +434,28 @@ test('Validate only one field', async () => {
blockId: 'button',
event: undefined,
eventName: 'onClick',
responses: [
{
actionId: 'validate',
actionType: 'Validate',
error: {
action: {
id: 'validate',
params: 'text1',
type: 'Validate',
},
error: {
error: new Error('Your input has 1 validation error.'),
index: 0,
type: 'Validate',
},
},
responses: {
validate: {
type: 'Validate',
index: 0,
error: new Error('Your input has 1 validation error.'),
},
],
success: false,
timestamp: {
date: 0,
},
success: false,
startTimestamp: { date: 0 },
endTimestamp: { date: 0 },
});
expect(text1.validationEval.output).toEqual({
errors: ['text1 does not match pattern "text1"'],
@ -447,17 +486,16 @@ test('Validate only one field', async () => {
blockId: 'button',
event: undefined,
eventName: 'onClick',
responses: [
{
actionId: 'validate',
actionType: 'Validate',
responses: {
validate: {
type: 'Validate',
index: 0,
response: undefined,
},
],
success: true,
timestamp: {
date: 0,
},
success: true,
startTimestamp: { date: 0 },
endTimestamp: { date: 0 },
});
expect(text1.validationEval.output).toEqual({
errors: [],
@ -569,17 +607,28 @@ test('Validate list of fields', async () => {
blockId: 'button',
event: undefined,
eventName: 'onClick',
responses: [
{
actionId: 'validate',
actionType: 'Validate',
error: {
action: {
id: 'validate',
params: ['text1', 'text2'],
type: 'Validate',
},
error: {
error: new Error('Your input has 1 validation error.'),
index: 0,
type: 'Validate',
},
},
responses: {
validate: {
type: 'Validate',
index: 0,
error: new Error('Your input has 1 validation error.'),
},
],
success: false,
timestamp: {
date: 0,
},
success: false,
startTimestamp: { date: 0 },
endTimestamp: { date: 0 },
});
expect(displayMessage.mock.calls).toMatchInlineSnapshot(`
Array [
@ -615,17 +664,16 @@ test('Validate list of fields', async () => {
blockId: 'button',
event: undefined,
eventName: 'onClick',
responses: [
{
actionId: 'validate',
actionType: 'Validate',
responses: {
validate: {
type: 'Validate',
index: 0,
response: undefined,
},
],
success: true,
timestamp: {
date: 0,
},
success: true,
startTimestamp: { date: 0 },
endTimestamp: { date: 0 },
});
expect(text3.validationEval.output).toEqual({
errors: ['text3 does not match pattern "text3"'],
@ -674,17 +722,30 @@ test('Invalid Validate params', async () => {
blockId: 'button',
event: undefined,
eventName: 'onClick',
responses: [
{
actionId: 'validate',
actionType: 'Validate',
error: {
action: {
id: 'validate',
params: {
invalid: true,
},
type: 'Validate',
},
error: {
error: new Error('Invalid validate params.'),
index: 0,
type: 'Validate',
},
},
responses: {
validate: {
type: 'Validate',
index: 0,
error: new Error('Invalid validate params.'),
},
],
success: false,
timestamp: {
date: 0,
},
success: false,
startTimestamp: { date: 0 },
endTimestamp: { date: 0 },
});
expect(displayMessage.mock.calls).toMatchInlineSnapshot(`
Array [
@ -757,15 +818,14 @@ test('Validate does not fail on warnings', async () => {
blockId: 'button',
event: undefined,
eventName: 'onClick',
responses: [
{
actionId: 'validate',
actionType: 'Validate',
responses: {
validate: {
type: 'Validate',
index: 0,
},
],
success: true,
timestamp: {
date: 0,
},
success: true,
startTimestamp: { date: 0 },
endTimestamp: { date: 0 },
});
});

View File

@ -187,17 +187,16 @@ test('triggerEvent x1', async () => {
x: 1,
},
eventName: 'onClick',
responses: [
{
actionId: 'a',
actionType: 'SetState',
responses: {
a: {
type: 'SetState',
index: 0,
response: undefined,
},
],
success: true,
timestamp: {
date: 0,
},
success: true,
startTimestamp: { date: 0 },
endTimestamp: { date: 0 },
});
expect(button.Events.events.onClick.loading).toEqual(false);
@ -238,7 +237,7 @@ test('triggerEvent, 2 actions', async () => {
const { button } = context.RootBlocks.map;
await button.triggerEvent({ name: 'onClick', event: { x: 1 } });
expect(button.Events.events.onClick.history[0].event).toEqual({ x: 1 });
expect(button.Events.events.onClick.history[0].responses.length).toEqual(2);
expect(Object.keys(button.Events.events.onClick.history[0].responses).length).toEqual(2);
expect(button.Events.events.onClick.loading).toEqual(false);
});
@ -284,17 +283,30 @@ test('triggerEvent error', async () => {
x: 1,
},
eventName: 'onClick',
responses: [
{
actionId: 'e',
actionType: 'Error',
error: {
action: {
id: 'e',
params: {
a: 'a',
},
type: 'Error',
},
error: {
error: new Error('Invalid action type "Error" at "button".'),
index: 0,
type: 'Error',
},
},
responses: {
e: {
type: 'Error',
index: 0,
error: new Error('Invalid action type "Error" at "button".'),
},
],
success: false,
timestamp: {
date: 0,
},
success: false,
startTimestamp: { date: 0 },
endTimestamp: { date: 0 },
});
});
@ -342,17 +354,16 @@ test('registerEvent then triggerEvent x1', async () => {
x: 1,
},
eventName: 'onClick',
responses: [
{
actionId: 'a',
actionType: 'SetState',
responses: {
a: {
type: 'SetState',
index: 0,
response: undefined,
},
],
success: true,
timestamp: {
date: 0,
},
success: true,
startTimestamp: { date: 0 },
endTimestamp: { date: 0 },
});
});
@ -403,21 +414,24 @@ test('triggerEvent skip', async () => {
"history": Array [
Object {
"blockId": "button",
"endTimestamp": Object {
"date": 0,
},
"event": Object {
"x": 1,
},
"eventName": "onClick",
"responses": Array [
Object {
"actionId": "a",
"actionType": "SetState",
"responses": Object {
"a": Object {
"index": 0,
"skipped": true,
"type": "SetState",
},
],
"success": true,
"timestamp": Object {
},
"startTimestamp": Object {
"date": 0,
},
"success": true,
},
],
"loading": false,
@ -428,21 +442,24 @@ test('triggerEvent skip', async () => {
Array [
Object {
"blockId": "button",
"endTimestamp": Object {
"date": 0,
},
"event": Object {
"x": 1,
},
"eventName": "onClick",
"responses": Array [
Object {
"actionId": "a",
"actionType": "SetState",
"responses": Object {
"a": Object {
"index": 0,
"skipped": true,
"type": "SetState",
},
],
"success": true,
"timestamp": Object {
},
"startTimestamp": Object {
"date": 0,
},
"success": true,
},
]
`);
@ -495,21 +512,24 @@ test('triggerEvent skip tests === true', async () => {
"history": Array [
Object {
"blockId": "button",
"endTimestamp": Object {
"date": 0,
},
"event": Object {
"x": 1,
},
"eventName": "onClick",
"responses": Array [
Object {
"actionId": "a",
"actionType": "SetState",
"responses": Object {
"a": Object {
"index": 0,
"response": undefined,
"type": "SetState",
},
],
"success": true,
"timestamp": Object {
},
"startTimestamp": Object {
"date": 0,
},
"success": true,
},
],
"loading": false,
@ -520,21 +540,24 @@ test('triggerEvent skip tests === true', async () => {
Array [
Object {
"blockId": "button",
"endTimestamp": Object {
"date": 0,
},
"event": Object {
"x": 1,
},
"eventName": "onClick",
"responses": Array [
Object {
"actionId": "a",
"actionType": "SetState",
"responses": Object {
"a": Object {
"index": 0,
"response": undefined,
"type": "SetState",
},
],
"success": true,
"timestamp": Object {
},
"startTimestamp": Object {
"date": 0,
},
"success": true,
},
]
`);

View File

@ -28,6 +28,10 @@ const testContext = async ({ lowdefy, rootBlock, initState = {} }) => {
pageId: rootBlock.blockId,
updateBlock: () => {},
urlQuery: {},
imports: {
jsActions: {},
jsOperators: {},
},
...lowdefy,
};
const ctx = {

View File

@ -31,7 +31,6 @@ export default {
_if: 'common/if',
_index: 'common/_index',
_input: 'common/input',
_js: 'common/js',
_json: 'common/json',
_log: 'common/log',
_lt: 'common/lt',

View File

@ -48,7 +48,7 @@ class NodeParser {
);
}
parse({ args, event, input, location }) {
parse({ actions, args, event, input, location }) {
if (type.isUndefined(input)) {
return { output: input, errors: [] };
}
@ -69,6 +69,7 @@ class NodeParser {
try {
if (!type.isUndefined(this.operations[op])) {
const res = this.operations[op]({
actions,
args,
arrayIndices: this.arrayIndices,
env: 'node',

View File

@ -0,0 +1,30 @@
/*
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 getFromObject from '../getFromObject';
function _actions({ actions, arrayIndices, env, location, params }) {
return getFromObject({
arrayIndices,
env,
location,
object: actions,
operator: '_actions',
params,
});
}
export default _actions;

View File

@ -15,10 +15,12 @@
*/
export default {
_actions: 'web/actions',
_event_log: 'web/event_log',
_base64: 'web/base64',
_format: 'web/format',
_list_contexts: 'web/list_contexts',
_js: 'web/js',
_media: 'web/media',
_menu: 'web/menu',
_request_details: 'web/request_details',

View File

@ -14,9 +14,13 @@
limitations under the License.
*/
import { getQuickJS, shouldInterruptAfterDeadline } from 'quickjs-emscripten';
import { type, serializer } from '@lowdefy/helpers';
import { type } from '@lowdefy/helpers';
// ! ---------------
// ! DEPRECATED
// ! ---------------
import { serializer } from '@lowdefy/helpers';
import { getQuickJS, shouldInterruptAfterDeadline } from 'quickjs-emscripten';
let QuickJsVm;
function createFunction({ params, location, methodName }) {
@ -90,20 +94,12 @@ function evaluate({ params, location, methodName }) {
const methods = { evaluate, function: createFunction };
function _js({ params, location, methodName }) {
function _DEPRECATED_js({ params, location, methodName }) {
if (!QuickJsVm) {
throw new Error(
`Operator Error: _js is not initialized. Received: ${JSON.stringify(params)} at ${location}.`
);
}
if (!type.isObject(params)) {
throw new Error(`Operator Error: _js.${methodName} takes an object as input at ${location}.`);
}
if (!methods[methodName]) {
throw new Error(
`Operator Error: _js.${methodName} is not supported at ${location}. Use one of the following: evaluate, function.`
);
}
return methods[methodName]({ params, location, methodName });
}
@ -118,5 +114,28 @@ async function clear() {
_js.init = init;
_js.clear = clear;
// ! ---------------
function _js({ context, params, location, methodName }) {
// ! DEPRECATED methods
if (!type.isFunction(context.lowdefy.imports.jsOperators[methodName]) && !methods[methodName]) {
throw new Error(`Operator Error: _js.${methodName} is not a function.`);
}
if (context.lowdefy.imports.jsOperators[methodName]) {
if (!type.isNone(params) && !type.isArray(params)) {
throw new Error(`Operator Error: _js.${methodName} takes an array as input at ${location}.`);
}
return context.lowdefy.imports.jsOperators[methodName](...(params || []));
}
// ! DEPRECATED ---------------
console.warn(
'WARNING: _js.evaluate and _js.function will has been deprecated and will be removed in the next version. Please see: https://docs.lowdefy.com/_js for more details.'
);
if (!type.isObject(params)) {
throw new Error(`Operator Error: _js.${methodName} takes an object as input at ${location}.`);
}
return _DEPRECATED_js({ params, location, methodName });
// ! ---------------
}
export default _js;

View File

@ -52,7 +52,7 @@ class WebParser {
);
}
parse({ args, arrayIndices, event, input, location }) {
parse({ actions, args, arrayIndices, event, input, location }) {
if (type.isUndefined(input)) {
return { output: input, errors: [] };
}
@ -75,6 +75,7 @@ class WebParser {
if (!type.isUndefined(this.operations[op])) {
const res = this.operations[op]({
eventLog: this.context.eventLog,
actions,
args,
arrayIndices,
context: this.context,

View File

@ -479,22 +479,6 @@ describe('parse operators', () => {
expect(errors).toEqual([]);
});
test('parse _js operator', async () => {
const input = {
'_js.function': {
code: `function (a,b){
return a+b;
}`,
},
};
const parser = new NodeParser({});
await parser.init();
const { output, errors } = parser.parse({ input, location: 'locationId' });
expect(output).toBeInstanceOf(Function);
expect(output(1, 2)).toEqual(3);
expect(errors).toEqual([]);
});
test('parse _index operator', async () => {
const input = { _index: 0 };
const parser = new NodeParser({ input: { key: 'value' }, arrayIndices: [3, 2] });

View File

@ -7,6 +7,10 @@ const operators = Object.keys({
});
const lowdefy = {
imports: {
jsOperators: {},
jsActions: {},
},
inputs: {
own: {
string: 'input',

View File

@ -0,0 +1,54 @@
/*
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 actions from '../../src/web/actions';
import getFromObject from '../../src/getFromObject';
jest.mock('../../src/getFromObject');
const input = {
actions: {
action_id: {
response: 'returned from action',
},
},
arrayIndices: [0],
context: { context: true },
contexts: { contexts: true },
env: 'env',
location: 'location',
params: 'params',
};
test('actions calls getFromObject', () => {
actions(input);
expect(getFromObject.mock.calls).toEqual([
[
{
arrayIndices: [0],
env: 'env',
location: 'location',
object: {
action_id: {
response: 'returned from action',
},
},
operator: '_actions',
params: 'params',
},
],
]);
});

View File

@ -14,13 +14,54 @@
limitations under the License.
*/
import _js from '../../src/common/js';
import _js from '../../src/web/js';
import { context } from '../testContext';
const location = 'location';
beforeAll(async () => {
await _js.init();
});
test('_js.test_fn and params to return a value', () => {
const params = [12, 14];
const test_fn = (a, b) => a + b;
const mockFn = jest.fn().mockImplementation(test_fn);
context.lowdefy.imports.jsOperators.test_fn = mockFn;
expect(_js({ context, location, params, methodName: 'test_fn' })).toEqual(26);
});
test('_js.test_fn no params to return a value', () => {
const test_fn = () => 'some value';
const mockFn = jest.fn().mockImplementation(test_fn);
context.lowdefy.imports.jsOperators.test_fn = mockFn;
expect(_js({ context, location, params: undefined, methodName: 'test_fn' })).toEqual(
'some value'
);
});
test('_js.test_fn and params to return a function', () => {
const params = [12, 14];
const test_fn = (a, b) => (c) => a + b + c;
const mockFn = jest.fn().mockImplementation(test_fn);
context.lowdefy.imports.jsOperators.test_fn = mockFn;
const fn = _js({ context, location, params, methodName: 'test_fn' });
expect(fn).toBeInstanceOf(Function);
expect(fn(4)).toEqual(30);
});
test('_js.test_fn params not an array', () => {
const params = 10;
const test_fn = (a, b) => a + b;
const mockFn = jest.fn().mockImplementation(test_fn);
context.lowdefy.imports.jsOperators.test_fn = mockFn;
expect(() => _js({ context, location, params, methodName: 'test_fn' })).toThrow(
new Error('Operator Error: _js.test_fn takes an array as input at location.')
);
});
// ! --------------
// ! DEPRECATED
// ! --------------
test('_js with code and args specified', () => {
const params = {
code: `function (one, two) {
@ -28,11 +69,13 @@ test('_js with code and args specified', () => {
}`,
args: [12, 14],
};
const fn = _js({ location, params, methodName: 'function' });
const fn = _js({ context, location, params, methodName: 'function' });
expect(fn).toBeInstanceOf(Function);
expect(fn(1, 2)).toEqual(3);
expect(_js({ location, params, methodName: 'evaluate' })).toEqual(26);
expect(_js({ location, params: { code: params.code }, methodName: 'evaluate' })).toEqual(null);
expect(_js({ context, location, params, methodName: 'evaluate' })).toEqual(26);
expect(_js({ context, location, params: { code: params.code }, methodName: 'evaluate' })).toEqual(
null
);
});
test('_js with code and args specified to return json object', () => {
@ -42,17 +85,19 @@ test('_js with code and args specified to return json object', () => {
}`,
args: [12, 14],
};
const fn = _js({ location, params, methodName: 'function' });
const fn = _js({ context, location, params, methodName: 'function' });
expect(fn).toBeInstanceOf(Function);
expect(fn(1, 2)).toEqual({ a: 1, b: 2, c: [1, 2, 3, 1, 2, 'three'] });
expect(_js({ location, params, methodName: 'evaluate' })).toEqual({
expect(_js({ context, location, params, methodName: 'evaluate' })).toEqual({
a: 12,
b: 14,
c: [1, 2, 3, 12, 14, 'three'],
});
expect(_js({ location, params: { code: params.code }, methodName: 'evaluate' })).toEqual({
c: [1, 2, 3, null, null, 'three'],
});
expect(_js({ context, location, params: { code: params.code }, methodName: 'evaluate' })).toEqual(
{
c: [1, 2, 3, null, null, 'three'],
}
);
});
test('_js with code and args specified to return json array', () => {
@ -62,18 +107,20 @@ test('_js with code and args specified to return json array', () => {
}`,
args: [12, 14],
};
const fn = _js({ location, params, methodName: 'function' });
const fn = _js({ context, location, params, methodName: 'function' });
expect(fn).toBeInstanceOf(Function);
expect(fn(1, 2)).toEqual([1, 2, 3, 1, 2, 'three']);
expect(_js({ location, params, methodName: 'evaluate' })).toEqual([1, 2, 3, 12, 14, 'three']);
expect(_js({ location, params: { code: params.code }, methodName: 'evaluate' })).toEqual([
expect(_js({ context, location, params, methodName: 'evaluate' })).toEqual([
1,
2,
3,
null,
null,
12,
14,
'three',
]);
expect(
_js({ context, location, params: { code: params.code }, methodName: 'evaluate' })
).toEqual([1, 2, 3, null, null, 'three']);
});
test('_js with open "\'" in result', () => {
@ -84,10 +131,10 @@ test('_js with open "\'" in result', () => {
}`,
args: [str],
};
const fn = _js({ location, params, methodName: 'function' });
const fn = _js({ context, location, params, methodName: 'function' });
expect(fn).toBeInstanceOf(Function);
expect(fn(str)).toEqual([{ x: str, b: 1 }]);
expect(_js({ location, params, methodName: 'evaluate' })).toEqual([{ x: str, b: 1 }]);
expect(_js({ context, location, params, methodName: 'evaluate' })).toEqual([{ x: str, b: 1 }]);
});
test('_js with date in input and result', () => {
@ -103,10 +150,10 @@ test('_js with date in input and result', () => {
}`,
args: [new Date(0), new Date(10)],
};
const fn = _js({ location, params, methodName: 'function' });
const fn = _js({ context, location, params, methodName: 'function' });
expect(fn).toBeInstanceOf(Function);
expect(_js({ location, params, methodName: 'evaluate' })).toEqual({
expect(_js({ context, location, params, methodName: 'evaluate' })).toEqual({
from: new Date(0),
to: new Date(10),
duration: 10,
@ -129,11 +176,13 @@ test('_js with undefined result returns null', () => {
}`,
args: [12, 14],
};
const fn = _js({ location, params, methodName: 'function' });
const fn = _js({ context, location, params, methodName: 'function' });
expect(fn).toBeInstanceOf(Function);
expect(fn()).toEqual(null);
expect(_js({ location, params, methodName: 'evaluate' })).toEqual(null);
expect(_js({ location, params: { code: params.code }, methodName: 'evaluate' })).toEqual(null);
expect(_js({ context, location, params, methodName: 'evaluate' })).toEqual(null);
expect(_js({ context, location, params: { code: params.code }, methodName: 'evaluate' })).toEqual(
null
);
});
test('_js with console.log', () => {
@ -148,9 +197,9 @@ test('_js with console.log', () => {
}`,
args: [12, new Date(1)],
};
const fn = _js({ location, params, methodName: 'function' });
const fn = _js({ context, location, params, methodName: 'function' });
expect(fn).toBeInstanceOf(Function);
_js({ location, params, methodName: 'evaluate' });
_js({ context, location, params, methodName: 'evaluate' });
expect(console.log.mock.calls).toEqual([
[12],
[new Date(1)],
@ -170,10 +219,10 @@ test('_js with code with args that needs escaped characters', () => {
},
],
};
expect(_js({ location, params, methodName: 'evaluate' })).toEqual(
expect(_js({ context, location, params, methodName: 'evaluate' })).toEqual(
'<div><a href="https://lowdefy.com">Lowdefy Website</a></html>'
);
const fn = _js({ location, params, methodName: 'function' });
const fn = _js({ context, location, params, methodName: 'function' });
expect(fn).toBeInstanceOf(Function);
expect(
fn({
@ -191,10 +240,10 @@ test('_js with code and no "function" specified to throw', () => {
};
expect(() =>
_js({ location, params, methodName: 'evaluate' })
_js({ context, location, params, methodName: 'evaluate' })
).toThrowErrorMatchingInlineSnapshot(`"unexpected token in expression: 'var'"`);
expect(() => {
const fn = _js({ location, params, methodName: 'function' });
const fn = _js({ context, location, params, methodName: 'function' });
fn();
}).toThrowErrorMatchingInlineSnapshot(`"unexpected token in expression: 'var'"`);
});
@ -204,9 +253,9 @@ test('_js invalid js code', () => {
code: 'Hello',
};
expect(() =>
_js({ location, params, methodName: 'evaluate' })
_js({ context, location, params, methodName: 'evaluate' })
).toThrowErrorMatchingInlineSnapshot(`"'Hello' is not defined"`);
const fn = _js({ location, params, methodName: 'function' });
const fn = _js({ context, location, params, methodName: 'function' });
expect(() => fn()).toThrowErrorMatchingInlineSnapshot(`"'Hello' is not defined"`);
});
@ -215,21 +264,21 @@ test('_js not a function', () => {
code: '"Hello"',
};
expect(() =>
_js({ location, params, methodName: 'evaluate' })
_js({ context, location, params, methodName: 'evaluate' })
).toThrowErrorMatchingInlineSnapshot(`"not a function"`);
const fn = _js({ location, params, methodName: 'function' });
const fn = _js({ context, location, params, methodName: 'function' });
expect(() => fn()).toThrowErrorMatchingInlineSnapshot(`"not a function"`);
});
test('_js params not a object', () => {
const params = [];
expect(() =>
_js({ location, params, methodName: 'function' })
_js({ context, location, params, methodName: 'function' })
).toThrowErrorMatchingInlineSnapshot(
`"Operator Error: _js.function takes an object as input at location."`
);
expect(() =>
_js({ location, params, methodName: 'evaluate' })
_js({ context, location, params, methodName: 'evaluate' })
).toThrowErrorMatchingInlineSnapshot(
`"Operator Error: _js.evaluate takes an object as input at location."`
);
@ -241,9 +290,9 @@ test('_js.invalid methodName', () => {
return args[0] + args[1]
}`,
};
expect(() => _js({ location, params, methodName: 'invalid' })).toThrowErrorMatchingInlineSnapshot(
`"Operator Error: _js.invalid is not supported at location. Use one of the following: evaluate, function."`
);
expect(() =>
_js({ context, location, params, methodName: 'invalid' })
).toThrowErrorMatchingInlineSnapshot(`"Operator Error: _js.invalid is not a function."`);
});
test('_js invalid js code', () => {
@ -251,12 +300,12 @@ test('_js invalid js code', () => {
code: 1,
};
expect(() =>
_js({ location, params, methodName: 'function' })
_js({ context, location, params, methodName: 'function' })
).toThrowErrorMatchingInlineSnapshot(
`"Operator Error: _js.function \\"code\\" argument should be a string at location."`
);
expect(() =>
_js({ location, params, methodName: 'evaluate' })
_js({ context, location, params, methodName: 'evaluate' })
).toThrowErrorMatchingInlineSnapshot(
`"Operator Error: _js.evaluate \\"code\\" argument should be a string at location."`
);
@ -265,12 +314,12 @@ test('_js invalid js code', () => {
test('_js no body or file', () => {
const params = {};
expect(() =>
_js({ location, params, methodName: 'function' })
_js({ context, location, params, methodName: 'function' })
).toThrowErrorMatchingInlineSnapshot(
`"Operator Error: _js.function \\"code\\" argument should be a string at location."`
);
expect(() =>
_js({ location, params, methodName: 'evaluate' })
_js({ context, location, params, methodName: 'evaluate' })
).toThrowErrorMatchingInlineSnapshot(
`"Operator Error: _js.evaluate \\"code\\" argument should be a string at location."`
);
@ -284,7 +333,7 @@ test('_js.evaluate, args not an array', () => {
}`,
};
expect(() =>
_js({ location, params, methodName: 'evaluate' })
_js({ context, location, params, methodName: 'evaluate' })
).toThrowErrorMatchingInlineSnapshot(
`"Operator Error: _js.evaluate \\"args\\" argument should be an array, null or undefined at location."`
);
@ -298,25 +347,14 @@ test('_js with undefined vm', () => {
};
_js.clear();
expect(() =>
_js({ location, params, methodName: 'function' })
_js({ context, location, params, methodName: 'function' })
).toThrowErrorMatchingInlineSnapshot(
`"Operator Error: _js is not initialized. Received: {\\"code\\":\\"{\\\\n return args[0] + args[1]\\\\n }\\"} at location."`
);
expect(() =>
_js({ location, params, methodName: 'evaluate' })
_js({ context, location, params, methodName: 'evaluate' })
).toThrowErrorMatchingInlineSnapshot(
`"Operator Error: _js is not initialized. Received: {\\"code\\":\\"{\\\\n return args[0] + args[1]\\\\n }\\"} at location."`
);
});
// TODO: interrupt handler does not seem to work.
// test('_js interrupts infinite loop execution', () => {
// const params = {
// body: `{
// i = 0; while (1) { i++ }
// }`,
// };
// expect(() =>
// _js({ location, instances, params, methodName: 'evaluate' })
// ).toThrowErrorMatchingInlineSnapshot();
// });
// ! --------------

View File

@ -33,13 +33,20 @@ const lowdefy = {
contexts: {},
displayMessage: () => () => undefined,
document,
imports: {
jsActions: window.lowdefy.imports.jsActions,
jsOperators: window.lowdefy.imports.jsOperators,
},
inputs: {},
link: () => {},
localStorage,
registerJsAction: window.lowdefy.registerJsAction,
registerJsOperator: window.lowdefy.registerJsOperator,
updaters: {},
window,
};
delete window.lowdefy.imports;
if (window.location.origin.includes('http://localhost')) {
window.lowdefy = lowdefy;
}

View File

@ -15,7 +15,7 @@
*/
import React, { Suspense } from 'react';
import { version } from '../../package.json';
import packageJson from '../../package.json';
import { ErrorBoundary, makeCssClass } from '@lowdefy/block-tools';
@ -30,8 +30,8 @@ const Block = ({ methods }) => {
moduleFederation: {
module: 'Message',
scope: '_at_lowdefy_slash_blocks_dash_antd',
version,
remoteEntryUrl: `https://blocks-cdn.lowdefy.com/v${version}/blocks-antd/remoteEntry.js`,
version: packageJson.version,
remoteEntryUrl: `https://blocks-cdn.lowdefy.com/v${packageJson.version}/blocks-antd/remoteEntry.js`,
},
}}
Loading={''}

View File

@ -19,7 +19,7 @@ import path from 'path';
import express from 'express';
import { ApolloServer } from 'apollo-server-express';
import { typeDefs, resolvers, createContext } from '@lowdefy/graphql';
import { createGetSecretsFromEnv } from '@lowdefy/node-utils';
import { createGetSecretsFromEnv, readFile } from '@lowdefy/node-utils';
dotenv.config({ silent: true });
const config = {
@ -44,8 +44,13 @@ app.use(express.static('dist/shell'));
// Redirect all 404 to index.html with status 200
// This should always be the last route
app.use((req, res) => {
res.sendFile(path.resolve(process.cwd(), 'dist/shell/index.html'));
app.use(async (req, res) => {
let indexHtml = await readFile(path.resolve(process.cwd(), 'dist/shell/index.html'));
let appConfig = await readFile(path.resolve(config.CONFIGURATION_BASE_PATH, 'app.json'));
appConfig = JSON.parse(appConfig);
indexHtml = indexHtml.replace('<!-- __LOWDEFY_APP_HEAD_HTML__ -->', appConfig.html.appendHead);
indexHtml = indexHtml.replace('<!-- __LOWDEFY_APP_BODY_HTML__ -->', appConfig.html.appendBody);
res.send(indexHtml);
});
app.listen({ port: 3000 }, () => console.log(`🚀 Server ready at http://localhost:3000`));

View File

@ -24,11 +24,34 @@
<link rel="icon" type="image/svg+xml" href="/public/icon.svg">
<link rel="icon" type="image/png" href="/public/icon-32.png">
<link rel="apple-touch-icon" href="/public/apple-touch-icon.png">
<script type="text/javascript">
const jsActions = {}
const jsOperators = {}
const getMethodLoader = (scope, reference) =>
(name, method) => {
if (typeof name !== 'string') {
throw new Error(`${scope} requires a string for the first argument.`)
}
if (typeof method !== 'function') {
throw new Error(`${scope} requires a function for the second argument.`)
}
reference[name] = method;
}
window.lowdefy = {
imports: {
jsActions,
jsOperators,
},
registerJsAction: getMethodLoader('registerJsAction', jsActions),
registerJsOperator: getMethodLoader('registerJsOperator', jsOperators)
}
</script>
<!-- __LOWDEFY_APP_HEAD_HTML__ -->
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="emotion"></div>
<div id="root"></div>
<!-- __LOWDEFY_APP_BODY_HTML__ -->
</body>
</html>

View File

@ -0,0 +1,48 @@
/*
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 filterDefaultValue(value, defaultValue) {
const isObject = (obj) => typeof obj === 'object' && obj !== null && !Array.isArray(obj);
const isEmptyObject = (obj) => isObject(obj) && Object.keys(obj).length === 0;
const getNestedValue = (obj, path) => {
const keys = [...path];
const key = keys.shift();
const value = obj[key];
if (keys.length > 0 && isObject(value)) return getNestedValue(value, keys);
return value;
};
const filterObject = ({ obj, path }) => {
Object.keys(obj).forEach((key) => {
const propPath = path.concat([key]);
if (isObject(obj[key])) {
filterObject({ obj: obj[key], path: propPath });
}
const dv = getNestedValue(defaultValue, propPath);
if (obj[key] === dv) {
delete obj[key];
}
if (obj[key] === null || isEmptyObject(obj[key])) {
delete obj[key];
}
});
return obj;
};
return filterObject({ obj: value, path: [] });
}
export default filterDefaultValue;

View File

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

View File

@ -40,8 +40,9 @@ module.exports = {
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
template: './src/shell/index.html',
minify: false,
publicPath: '/',
template: './src/shell/index.html',
}),
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify('development'),

View File

@ -18,7 +18,7 @@ import path from 'path';
import express from 'express';
import { ApolloServer } from 'apollo-server-express';
import { typeDefs, resolvers, createContext } from '@lowdefy/graphql';
import { createGetSecretsFromEnv } from '@lowdefy/node-utils';
import { createGetSecretsFromEnv, readFile } from '@lowdefy/node-utils';
const config = {
CONFIGURATION_BASE_PATH: path.resolve(process.cwd(), './build'),
@ -41,8 +41,13 @@ app.use(express.static('dist/shell'));
// Redirect all 404 to index.html with status 200
// This should always be the last route
app.use((req, res) => {
res.sendFile(path.resolve(process.cwd(), 'dist/shell/index.html'));
app.use(async (req, res) => {
let indexHtml = await readFile(path.resolve(process.cwd(), 'dist/shell/index.html'));
let appConfig = await readFile(path.resolve(config.CONFIGURATION_BASE_PATH, 'app.json'));
appConfig = JSON.parse(appConfig);
indexHtml = indexHtml.replace('<!-- __LOWDEFY_APP_HEAD_HTML__ -->', appConfig.html.appendHead);
indexHtml = indexHtml.replace('<!-- __LOWDEFY_APP_BODY_HTML__ -->', appConfig.html.appendBody);
res.send(indexHtml);
});
app.listen({ port: 3000 }, () => console.log(`Server started at port 3000`));

View File

@ -20,15 +20,39 @@
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Lowdefy App</title>
<link rel="manifest" href="/public/manifest.webmanifest">
<link rel="icon" type="image/svg+xml" href="/public/icon.svg">
<link rel="icon" type="image/png" href="/public/icon-32.png">
<link rel="apple-touch-icon" href="/public/apple-touch-icon.png">
<script type="text/javascript">
const jsActions = {}
const jsOperators = {}
const getMethodLoader = (scope, reference) =>
(name, method) => {
if (typeof name !== 'string') {
throw new Error(`${scope} requires a string for the first argument.`)
}
if (typeof method !== 'function') {
throw new Error(`${scope} requires a function for the second argument.`)
}
reference[name] = method;
}
window.lowdefy = {
imports: {
jsActions,
jsOperators,
},
registerJsAction: getMethodLoader('registerJsAction', jsActions),
registerJsOperator: getMethodLoader('registerJsOperator', jsOperators)
}
</script>
<!-- __LOWDEFY_APP_HEAD_HTML__ -->
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="emotion"></div>
<div id="root"></div>
<!-- __LOWDEFY_APP_BODY_HTML__ -->
</body>
</html>

View File

@ -44,8 +44,9 @@ module.exports = {
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
template: './src/shell/index.html',
minify: false,
publicPath: '/',
template: './src/shell/index.html',
}),
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify('production'),

View File

@ -20,15 +20,39 @@
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Lowdefy App</title>
<link rel="manifest" href="/public/manifest.webmanifest">
<link rel="icon" type="image/svg+xml" href="/public/icon.svg">
<link rel="icon" type="image/png" href="/public/icon-32.png">
<link rel="apple-touch-icon" href="/public/apple-touch-icon.png">
<script type="text/javascript">
const jsActions = {}
const jsOperators = {}
const getMethodLoader = (scope, reference) =>
(name, method) => {
if (typeof name !== 'string') {
throw new Error(`${scope} requires a string for the first argument.`)
}
if (typeof method !== 'function') {
throw new Error(`${scope} requires a function for the second argument.`)
}
reference[name] = method;
}
window.lowdefy = {
imports: {
jsActions,
jsOperators,
},
registerJsAction: getMethodLoader('registerJsAction', jsActions),
registerJsOperator: getMethodLoader('registerJsOperator', jsOperators)
}
</script>
<!-- __LOWDEFY_APP_HEAD_HTML__ -->
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="emotion"></div>
<div id="root"></div>
<!-- __LOWDEFY_APP_BODY_HTML__ -->
</body>
</html>

View File

@ -44,8 +44,9 @@ module.exports = {
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
template: './src/shell/index.html',
minify: false,
publicPath: '/',
template: './src/shell/index.html',
}),
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify('production'),