mirror of
https://github.com/lowdefy/lowdefy.git
synced 2025-04-06 15:30:30 +08:00
Merge branch 'develop' into fix-date-time-selector
This commit is contained in:
commit
8cda2ab84a
@ -1,10 +1,12 @@
|
||||

|
||||
|
||||

|
||||
|
||||
[](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)
|
||||
[](https://twitter.com/intent/follow?screen_name=lowdefy)
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
[](https://codeclimate.com/github/lowdefy/lowdefy/maintainability)
|
||||
[](https://codeclimate.com/github/lowdefy/lowdefy/test_coverage)
|
||||
[](https://codecov.io/gh/lowdefy/lowdefy)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
},
|
||||
|
@ -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",
|
||||
|
@ -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,
|
||||
},
|
||||
|
@ -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",
|
||||
|
@ -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 {},
|
||||
],
|
||||
|
@ -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`;
|
||||
|
||||
|
@ -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 {},
|
||||
],
|
||||
|
@ -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`;
|
||||
|
||||
|
@ -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'),
|
||||
});
|
||||
}
|
||||
|
@ -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 });
|
||||
|
40
packages/build/src/build/validateApp.js
Normal file
40
packages/build/src/build/validateApp.js
Normal 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;
|
57
packages/build/src/build/validateApp.test.js
Normal file
57
packages/build/src/build/validateApp.test.js
Normal 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.'
|
||||
);
|
||||
});
|
@ -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 = {};
|
||||
|
@ -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 () => {
|
||||
|
24
packages/build/src/build/writeApp.js
Normal file
24
packages/build/src/build/writeApp.js
Normal 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;
|
77
packages/build/src/build/writeApp.test.js
Normal file
77
packages/build/src/build/writeApp.test.js
Normal 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: `{}`,
|
||||
},
|
||||
],
|
||||
]);
|
||||
});
|
@ -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": {
|
||||
|
@ -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',
|
||||
|
@ -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 };
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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'),
|
||||
|
234
packages/docs/actions/JsAction.yaml
Normal file
234
packages/docs/actions/JsAction.yaml
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
@ -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
|
||||
|
217
packages/docs/concepts/custom-code.yaml
Normal file
217
packages/docs/concepts/custom-code.yaml
Normal 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
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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
1
packages/docs/head.html
Normal file
@ -0,0 +1 @@
|
||||
<script type="module" src="/public/modules/index.js" ></script>
|
@ -24,6 +24,11 @@ global:
|
||||
sm:
|
||||
span: 23
|
||||
|
||||
app:
|
||||
html:
|
||||
appendHead:
|
||||
_ref: head.html
|
||||
|
||||
connections:
|
||||
- id: discord_channel
|
||||
type: AxiosHttp
|
||||
|
@ -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
|
||||
|
123
packages/docs/operators/_actions.yaml
Normal file
123
packages/docs/operators/_actions.yaml
Normal 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
|
||||
```
|
@ -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: |
|
||||
|
@ -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:
|
||||
|
@ -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: |
|
||||
```
|
||||
({
|
||||
|
@ -19,6 +19,7 @@ _ref:
|
||||
pageId: _list_contexts
|
||||
pageTitle: _list_contexts
|
||||
filePath: operators/_list_contexts.yaml
|
||||
env: Client Only
|
||||
types: |
|
||||
```
|
||||
(value: any): string[]
|
||||
|
@ -19,6 +19,7 @@ _ref:
|
||||
pageId: _media
|
||||
pageTitle: _media
|
||||
filePath: operators/_media.yaml
|
||||
env: Client Only
|
||||
types: |
|
||||
```
|
||||
(key: string): any
|
||||
|
@ -19,6 +19,7 @@ _ref:
|
||||
pageId: _menu
|
||||
pageTitle: _menu
|
||||
filePath: operators/_menu.yaml
|
||||
env: Client Only
|
||||
types: |
|
||||
```
|
||||
(menuId: string): object
|
||||
|
@ -19,6 +19,7 @@ _ref:
|
||||
pageId: _ref
|
||||
pageTitle: _ref
|
||||
filePath: operators/_ref.yaml
|
||||
env: Build Only
|
||||
types: |
|
||||
```
|
||||
(path: string): any
|
||||
|
@ -19,6 +19,7 @@ _ref:
|
||||
pageId: _request
|
||||
pageTitle: _request
|
||||
filePath: operators/_request.yaml
|
||||
env: Client Only
|
||||
types: |
|
||||
```
|
||||
(requestId: string): any
|
||||
|
@ -19,6 +19,7 @@ _ref:
|
||||
pageId: _secret
|
||||
pageTitle: _secret
|
||||
filePath: operators/_secret.yaml
|
||||
env: Server Only
|
||||
types: |
|
||||
```
|
||||
(key: string): any
|
||||
|
@ -19,6 +19,7 @@ _ref:
|
||||
pageId: _uuid
|
||||
pageTitle: _uuid
|
||||
filePath: operators/_uuid.yaml
|
||||
env: Server Only
|
||||
types: |
|
||||
```
|
||||
(void): string
|
||||
|
@ -19,6 +19,7 @@ _ref:
|
||||
pageId: _var
|
||||
pageTitle: _var
|
||||
filePath: operators/_var.yaml
|
||||
env: Build Only
|
||||
types: |
|
||||
```
|
||||
(name: string): any
|
||||
|
@ -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
|
||||
|
@ -45,4 +45,4 @@ function filterDefaultValue(value, defaultValue) {
|
||||
return filterObject({ obj: value, path: [] });
|
||||
}
|
||||
|
||||
module.exports = filterDefaultValue;
|
||||
export default filterDefaultValue;
|
3
packages/docs/public/modules/index.js
Normal file
3
packages/docs/public/modules/index.js
Normal file
@ -0,0 +1,3 @@
|
||||
import filterDefaultValue from './filterDefaultValue.js';
|
||||
|
||||
window.lowdefy.registerJsOperator('filterDefaultValue', filterDefaultValue);
|
@ -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 };
|
10
packages/docs/templates/actions.yaml.njk
vendored
10
packages/docs/templates/actions.yaml.njk
vendored
@ -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:
|
||||
|
92
packages/docs/templates/blocks/template.yaml.njk
vendored
92
packages/docs/templates/blocks/template.yaml.njk
vendored
@ -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
|
||||
|
25
packages/docs/templates/footer.yaml.njk
vendored
25
packages/docs/templates/footer.yaml.njk
vendored
@ -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:
|
||||
|
4
packages/docs/templates/header.yaml
vendored
4
packages/docs/templates/header.yaml
vendored
@ -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:
|
||||
|
22
packages/docs/templates/operators.yaml.njk
vendored
22
packages/docs/templates/operators.yaml.njk
vendored
@ -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:
|
||||
|
@ -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 }) {
|
||||
|
45
packages/engine/src/actions/JsAction.js
Normal file
45
packages/engine/src/actions/JsAction.js
Normal 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;
|
@ -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,
|
||||
|
@ -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 },
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -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']]);
|
||||
});
|
||||
|
447
packages/engine/test/Actions/JsAction.test.js
Normal file
447
packages/engine/test/Actions/JsAction.test.js
Normal 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 },
|
||||
});
|
||||
});
|
@ -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 },
|
||||
});
|
||||
});
|
||||
|
@ -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 },
|
||||
});
|
||||
});
|
||||
|
@ -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 },
|
||||
});
|
||||
});
|
||||
|
@ -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,
|
||||
},
|
||||
]
|
||||
`);
|
||||
|
@ -28,6 +28,10 @@ const testContext = async ({ lowdefy, rootBlock, initState = {} }) => {
|
||||
pageId: rootBlock.blockId,
|
||||
updateBlock: () => {},
|
||||
urlQuery: {},
|
||||
imports: {
|
||||
jsActions: {},
|
||||
jsOperators: {},
|
||||
},
|
||||
...lowdefy,
|
||||
};
|
||||
const ctx = {
|
||||
|
@ -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',
|
||||
|
@ -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',
|
||||
|
30
packages/operators/src/web/actions.js
Normal file
30
packages/operators/src/web/actions.js
Normal 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;
|
@ -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',
|
||||
|
@ -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;
|
@ -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,
|
||||
|
@ -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] });
|
||||
|
@ -7,6 +7,10 @@ const operators = Object.keys({
|
||||
});
|
||||
|
||||
const lowdefy = {
|
||||
imports: {
|
||||
jsOperators: {},
|
||||
jsActions: {},
|
||||
},
|
||||
inputs: {
|
||||
own: {
|
||||
string: 'input',
|
||||
|
54
packages/operators/test/web/actions.test.js
Normal file
54
packages/operators/test/web/actions.test.js
Normal 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',
|
||||
},
|
||||
],
|
||||
]);
|
||||
});
|
@ -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();
|
||||
// });
|
||||
// ! --------------
|
@ -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;
|
||||
}
|
||||
|
@ -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={''}
|
||||
|
@ -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`));
|
||||
|
@ -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>
|
||||
|
@ -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;
|
@ -0,0 +1,3 @@
|
||||
import filterDefaultValue from './filterDefaultValue.js';
|
||||
|
||||
window.lowdefy.registerJsOperator('filterDefaultValue', filterDefaultValue);
|
@ -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'),
|
||||
|
@ -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`));
|
||||
|
@ -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>
|
||||
|
@ -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'),
|
||||
|
@ -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>
|
||||
|
@ -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'),
|
||||
|
Loading…
x
Reference in New Issue
Block a user