48 builder frontend 2 (#76)

* Implement collapsing component hierarchy.

* Save screen when adding new components.

* Allow creation of nested child components.

* Rename updateComponentProps to setComponentProps

* Compile layout and position properties to css strings.

* Correct ordering errors.

* Compile the css for an entire screen.

* Add unique id for each component.

* Ignore _id props.

* Update client to add correct class names to component elements.

* Add grid-template fields to layout styling panel.

* Inject css into iframe. Minor tweaks.

* Fix unset margins.

* Update failing tests.
This commit is contained in:
pngwn 2020-01-31 16:01:58 +00:00 committed by GitHub
parent 77fa60dae2
commit aa4c7fa1c1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 134034 additions and 123856 deletions

View File

@ -6,6 +6,7 @@
"build": "rollup -c",
"start": "rollup -c -w",
"test": "jest",
"test:watch": "jest --watchAll",
"dev:builder": "rollup -c -w"
},
"jest": {

View File

@ -11,34 +11,34 @@ import browsersync from "rollup-plugin-browsersync";
import proxy from "http-proxy-middleware";
const target = 'http://localhost:4001';
const _builderProxy = proxy('/_builder', {
target:"http://localhost:3000",
pathRewrite: {'^/_builder' : ''}
const _builderProxy = proxy('/_builder', {
target: "http://localhost:3000",
pathRewrite: { '^/_builder': '' }
});
const apiProxy = proxy(['/_builder/api/**', '/_builder/**/componentlibrary', '/_builder/**/componentlibraryGenerators'] , {
const apiProxy = proxy(['/_builder/api/**', '/_builder/**/componentlibrary', '/_builder/**/componentlibraryGenerators'], {
target,
logLevel: "debug",
changeOrigin: true,
cookieDomainRewrite: true,
onProxyReq(proxyReq) {
if (proxyReq.getHeader("origin")) {
proxyReq.setHeader("origin", target)
}
if (proxyReq.getHeader("origin")) {
proxyReq.setHeader("origin", target)
}
}
});
});
const production = !process.env.ROLLUP_WATCH;
const lodash_fp_exports = ["union", "reduce", "isUndefined", "cloneDeep", "split", "some", "map", "filter", "isEmpty", "countBy", "includes", "last", "find", "constant",
"take", "first", "intersection", "mapValues", "isNull", "has", "isInteger", "isNumber", "isString", "isBoolean", "isDate", "isArray", "isObject", "clone", "values", "keyBy", "isNaN",
"keys", "orderBy", "concat", "reverse", "difference", "merge", "flatten", "each", "pull", "join", "defaultCase", "uniqBy", "every", "uniqWith", "isFunction", "groupBy",
"differenceBy", "intersectionBy", "isEqual", "max", "sortBy", "assign", "uniq", "trimChars", "trimCharsStart", "isObjectLike", "flattenDeep", "indexOf", "isPlainObject",
"toNumber", "takeRight"];
const lodash_fp_exports = ["union", "reduce", "isUndefined", "cloneDeep", "split", "some", "map", "filter", "isEmpty", "countBy", "includes", "last", "find", "constant",
"take", "first", "intersection", "mapValues", "isNull", "has", "isInteger", "isNumber", "isString", "isBoolean", "isDate", "isArray", "isObject", "clone", "values", "keyBy", "isNaN",
"keys", "orderBy", "concat", "reverse", "difference", "merge", "flatten", "each", "pull", "join", "defaultCase", "uniqBy", "every", "uniqWith", "isFunction", "groupBy",
"differenceBy", "intersectionBy", "isEqual", "max", "sortBy", "assign", "uniq", "trimChars", "trimCharsStart", "isObjectLike", "flattenDeep", "indexOf", "isPlainObject",
"toNumber", "takeRight", "toPairs"];
const lodash_exports = ["flow", "join", "replace", "trim", "dropRight", "takeRight", "head", "reduce",
"tail", "startsWith", "findIndex", "merge",
"assign", "each", "find", "orderBy", "union"];
const lodash_exports = ["flow", "join", "replace", "trim", "dropRight", "takeRight", "head", "reduce",
"tail", "startsWith", "findIndex", "merge",
"assign", "each", "find", "orderBy", "union"];
const outputpath = "../server/builder";
@ -63,7 +63,7 @@ export default {
{ src: 'src/favicon.png', dest: outputpath },
{ src: 'src/assets', dest: outputpath },
{ src: 'node_modules/@budibase/client/dist/budibase-client.esm.mjs', dest: outputpath },
]
]
}),
svelte({
@ -80,28 +80,28 @@ export default {
resolve({
browser: true,
dedupe: importee => {
return importee === 'svelte'
|| importee.startsWith('svelte/')
|| coreExternal.includes(importee);
return importee === 'svelte'
|| importee.startsWith('svelte/')
|| coreExternal.includes(importee);
}
}),
commonjs({
namedExports: {
"lodash/fp": lodash_fp_exports,
"lodash":lodash_exports,
"lodash": lodash_exports,
"shortid": ["generate"]
}
}),
url({
limit: 0,
include: ["**/*.woff2", "**/*.png"],
limit: 0,
include: ["**/*.woff2", "**/*.png"],
fileName: "[dirname][name][extname]",
emitFiles: true
}),
url({
limit: 0,
include: ["**/*.css"],
limit: 0,
include: ["**/*.css"],
fileName: "[name][extname]",
emitFiles: true
}),
@ -113,7 +113,7 @@ export default {
!production && livereload(outputpath),
!production && browsersync({
server: outputpath,
middleware: [apiProxy,_builderProxy]
middleware: [apiProxy, _builderProxy]
}),
// If we're building for production (npm run build

View File

@ -0,0 +1,118 @@
import { filter, map, reduce, toPairs } from "lodash/fp";
import { pipe } from "../common/core";
const self = n => n;
const join_with = delimiter => a => a.join(delimiter);
const empty_string_to_unset = s => s.length ? s : "0";
const add_suffix = suffix => s => s + suffix;
export const make_margin = (values) => pipe(values, [
map(empty_string_to_unset),
map(add_suffix('px')),
join_with(' ')
]);
const tap = message => x => {
console.log(x);
return x;
}
const css_map = {
templaterows: {
name: 'grid-template-columns',
generate: self
},
templatecolumns: {
name: 'grid-template-rows',
generate: self
},
gridarea: {
name: 'grid-area',
generate: make_margin
},
gap: {
name: 'grid-gap',
generate: n => `${n}px`
},
columnstart: {
name: 'grid-column-start',
generate: self
},
columnend: {
name: 'grid-column-end',
generate: self
},
rowstart: {
name: 'grid-row-start',
generate: self
},
rowend: {
name: 'grid-row-end',
generate: self
},
padding: {
name: 'padding',
generate: make_margin
},
margin: {
name: 'margin',
generate: make_margin
},
zindex: {
name: 'z-index',
generate: self
}
}
export const generate_rule = ([name, values]) =>
`${css_map[name].name}: ${css_map[name].generate(values)};`
const handle_grid = (acc, [name, value]) => {
let tmp = [];
if (name === 'row' || name === 'column') {
if (value[0]) tmp.push([`${name}start`, value[0]]);
if (value[1]) tmp.push([`${name}end`, value[1]]);
return acc.concat(tmp)
}
return acc.concat([[name, value]]);
}
const object_to_css_string = [
toPairs,
reduce(handle_grid, []),
filter(v => Array.isArray(v[1]) ? v[1].some(s => s.length) : v[1].length),
map(generate_rule),
join_with('\n'),
];
export const generate_css = ({ layout, position }) => {
let _layout = pipe(layout, object_to_css_string);
_layout = _layout.length ? _layout + "\ndisplay: grid;" : _layout;
return {
layout: _layout,
position: pipe(position, object_to_css_string)
}
}
const apply_class = (id, name, styles) => `.${name}-${id} {\n${styles}\n}`;
export const generate_screen_css = (component_array) => {
let styles = "";
for (let i = 0; i < component_array.length; i += 1) {
const { _styles, _id, _children } = component_array[i];
const { layout, position } = generate_css(_styles);
styles += apply_class(_id, 'pos', position) + "\n";
styles += apply_class(_id, 'lay', layout) + "\n";
if (_children && _children.length) {
styles += generate_screen_css(_children) + "\n";
}
}
return styles.trim();
}

View File

@ -4,7 +4,7 @@ import {
import {
filter, cloneDeep, sortBy,
map, last, keys, concat, keyBy,
find, isEmpty, reduce, values, isEqual
find, isEmpty, values,
} from "lodash/fp";
import {
pipe, getNode, validate,
@ -17,11 +17,13 @@ import api from "./api";
import { isRootComponent, getExactComponent } from "../userInterface/pagesParsing/searchComponents";
import { rename } from "../userInterface/pagesParsing/renameScreen";
import {
getNewComponentInfo, getScreenInfo, getComponentInfo
getNewComponentInfo, getScreenInfo,
} from "../userInterface/pagesParsing/createProps";
import {
loadLibs, loadLibUrls, loadGeneratorLibs
} from "./loadComponentLibraries";
import { uuid } from './uuid';
import { generate_screen_css } from './generate_css';
let appname = "";
@ -710,7 +712,8 @@ const addChildComponent = store => component => {
const component_definition = Object.assign(
cloneDeep(newComponent.fullProps), {
_component: component,
_layout: {}
_styles: { position: {}, layout: {} },
_id: uuid()
})
if (children) {
@ -729,6 +732,8 @@ const addChildComponent = store => component => {
_saveScreen(store, s, s.currentFrontEndItem);
_saveScreen(store, s, s.currentFrontEndItem);
return s;
})
}
@ -751,12 +756,13 @@ const setComponentProp = store => (name, value) => {
})
}
const setComponentStyle = store => (name, value) => {
const setComponentStyle = store => (type, name, value) => {
store.update(s => {
if (!s.currentComponentInfo._layout) {
s.currentComponentInfo._layout = {};
if (!s.currentComponentInfo._styles) {
s.currentComponentInfo._styles = {};
}
s.currentComponentInfo._layout[name] = value;
s.currentComponentInfo._styles[type][name] = value;
s.currentFrontEndItem._css = generate_screen_css(s.currentFrontEndItem.props._children)
// save without messing with the store
_save(s.appname, s.currentFrontEndItem, store, s)

View File

@ -0,0 +1,7 @@
export function uuid() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
const r = (Math.random() * 16) | 0,
v = c == 'x' ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
}

View File

@ -2,6 +2,7 @@
export let meta = [];
export let size = '';
export let values = [];
export let type = "number";
export let onStyleChanged = () => {};
let _values = values.map(v => v);
@ -11,7 +12,7 @@
<div class="inputs {size}">
{#each meta as { placeholder }, i}
<input type="number"
<input {type}
placeholder="{placeholder}"
value={values[i]}
on:input={(e) => _values[i] = e.target.value} />

View File

@ -1,13 +1,10 @@
<script>
import { last } from "lodash/fp";
import { pipe } from "../common/core";
export let components = [];
export let currentComponent;
export let onSelect = () => {};
export let level = 0;
const capitalise = s => s.substring(0,1).toUpperCase() + s.substring(1);
const get_name = s => last(s.split('/'));
const get_capitalised_name = name => pipe(name, [get_name,capitalise]);
@ -38,18 +35,15 @@
padding-left: 0;
margin: 0;
}
.item {
display: block;
padding: 11px 67px;
border-radius: 3px;
}
.item:hover {
background: #fafafa;
cursor: pointer;
}
.selected {
color: var(--button-text);
background: var(--background-button)!important;

View File

@ -4,7 +4,11 @@
import { pipe } from "../common/core";
import { buildPropsHierarchy } from "./pagesParsing/buildPropsHierarchy";
let iframe;
$: iframe && console.log(iframe.contentDocument.head.insertAdjacentHTML('beforeend', '<style></style>'))
$: hasComponent = !!$store.currentFrontEndItem;
$: styles = hasComponent ? $store.currentFrontEndItem._css : '';
$: stylesheetLinks = pipe($store.pages.stylesheets, [
map(s => `<link rel="stylesheet" href="${s}"/>`),
@ -27,6 +31,7 @@
{#if hasComponent}
<iframe style="height: 100%; width: 100%"
title="componentPreview"
bind:this={iframe}
srcdoc={
`<html>
@ -45,6 +50,7 @@
box-sizing: border-box;
padding: 20px;
}
${styles}
</style>
</head>
<body>

View File

@ -19,13 +19,16 @@
const single = [{ placeholder: '' }];
$: layout = componentInfo._layout;
$: layout = { ...componentInfo._styles.position, ...componentInfo._styles.layout };
$: layouts = {
templaterows: ['Grid Rows', single],
templatecolumns: ['Grid Columns', single],
};
$: positions = {
gridarea: ['Grid Area', tbrl, 'small'],
column: ['Column', se],
row: ['Row', se],
gap: ['Gap', single],
};
$: spacing = {
@ -41,15 +44,28 @@
</script>
<h3>Layout</h3>
<h3>Styles</h3>
<h4>Positioning</h4>
<div class="layout-pos">
{#each Object.entries(layouts) as [key, [name, meta, size]]}
<div class="grid">
<h5>{name}:</h5>
<InputGroup onStyleChanged={_value => onStyleChanged('layout',key, _value)}
values={layout[key] || newValue(meta.length)}
{meta}
{size}
type="text"/>
</div>
{/each}
</div>
<h4>Positioning</h4>
<div class="layout-pos">
{#each Object.entries(positions) as [key, [name, meta, size]]}
<div class="grid">
<h5>Grid Area:</h5>
<InputGroup onStyleChanged={_value => onStyleChanged(key, _value)}
<h5>{name}:</h5>
<InputGroup onStyleChanged={_value => onStyleChanged('position',key, _value)}
values={layout[key] || newValue(meta.length)}
{meta}
{size} />
@ -61,8 +77,8 @@
<div class="layout-spacing">
{#each Object.entries(spacing) as [key, [name, meta, size]]}
<div class="grid">
<h5>Grid Area:</h5>
<InputGroup onStyleChanged={_value => onStyleChanged(key, _value)}
<h5>{name}:</h5>
<InputGroup onStyleChanged={_value => onStyleChanged('position', key, _value)}
values={layout[key] || newValue(meta.length)}
{meta}
{size} />
@ -74,8 +90,8 @@
<div class="layout-layer">
{#each Object.entries(zindex) as [key, [name, meta, size]]}
<div class="grid">
<h5>Grid Area:</h5>
<InputGroup onStyleChanged={_value => onStyleChanged(key, _value)}
<h5>{name}:</h5>
<InputGroup onStyleChanged={_value => onStyleChanged('position', key, _value)}
values={layout[key] || newValue(meta.length)}
{meta}
{size} />

View File

@ -12,7 +12,7 @@
let errors = [];
let props = {};
const props_to_ignore = ['_component','_children', '_layout'];
const props_to_ignore = ['_component','_children', '_styles', '_id'];
$: propDefs = componentInfo && Object.entries(componentInfo).filter(([name])=> !props_to_ignore.includes(name));

View File

@ -0,0 +1,337 @@
import { generate_css, make_margin, generate_screen_css } from '../src/builderStore/generate_css.js';
describe('make_margin', () => {
test('it should generate a valid rule', () => {
expect(make_margin(["1", "1", "1", "1"])).toEqual('1px 1px 1px 1px')
})
test('empty values should output 0', () => {
expect(make_margin(["1", "1", "", ""])).toEqual('1px 1px 0px 0px')
expect(make_margin(["1", "", "", "1"])).toEqual('1px 0px 0px 1px')
expect(make_margin(["", "", "", ""])).toEqual('0px 0px 0px 0px')
})
})
describe('generate_css', () => {
test('it should generate a valid css rule: grid-area', () => {
expect(
generate_css({ layout: { gridarea: ["", "", "", ""] } })
).toEqual({
layout: '',
position: ''
});
})
test('it should generate a valid css rule: grid-gap', () => {
expect(
generate_css({ layout: { gap: "10" } })
).toEqual({
layout: 'grid-gap: 10px;\ndisplay: grid;',
position: ''
});
})
test('it should generate a valid css rule: column 1', () => {
expect(
generate_css({ position: { column: ["", ""] } }
)).toEqual({ layout: '', position: '' });
})
test('it should generate a valid css rule: column 2', () => {
expect(
generate_css({ position: { column: ["1", ""] } })
).toEqual({
position: 'grid-column-start: 1;',
layout: ''
});
})
test('it should generate a valid css rule: column 3', () => {
expect(
generate_css({ position: { column: ["", "1"] } })
).toEqual({
position: 'grid-column-end: 1;',
layout: ''
});
})
test('it should generate a valid css rule: column 4', () => {
expect(
generate_css({ position: { column: ["1", "1"] } })
).toEqual({
position: 'grid-column-start: 1;\ngrid-column-end: 1;',
layout: ''
});
})
test('it should generate a valid css rule: row 1', () => {
expect(
generate_css({ position: { row: ["", ""] } })
).toEqual({ layout: '', position: '' });
})
test('it should generate a valid css rule: row 2', () => {
expect(
generate_css({ position: { row: ["1", ""] } })
).toEqual({
position: 'grid-row-start: 1;',
layout: ''
});
})
test('it should generate a valid css rule: row 3', () => {
expect(
generate_css({ position: { row: ["", "1"] } })
).toEqual({
position: 'grid-row-end: 1;',
layout: ''
});
})
test('it should generate a valid css rule: row 4', () => {
expect(
generate_css({ position: { row: ["1", "1"] } })
).toEqual({
position: 'grid-row-start: 1;\ngrid-row-end: 1;',
layout: ''
});
})
test('it should generate a valid css rule: padding 1', () => {
expect(
generate_css({ position: { padding: ["1", "1", "1", "1"] } })
).toEqual({
position: 'padding: 1px 1px 1px 1px;',
layout: ''
});
})
test('it should generate a valid css rule: padding 2', () => {
expect(
generate_css({ position: { padding: ["1", "", "", "1"] } })
).toEqual({
position: 'padding: 1px 0px 0px 1px;',
layout: ''
});
})
test('it should generate a valid css rule: margin 1', () => {
expect(
generate_css({ position: { margin: ["1", "1", "1", "1"] } })
).toEqual({
position: 'margin: 1px 1px 1px 1px;',
layout: ''
});
})
test('it should generate a valid css rule: margin 2', () => {
expect(
generate_css({ position: { margin: ["1", "", "", "1"] } })
).toEqual({
position: 'margin: 1px 0px 0px 1px;',
layout: ''
});
})
test('it should generate a valid css rule: z-index 1', () => {
expect(
generate_css({ position: { zindex: "" } })
).toEqual({
position: '',
layout: ''
});
})
test('it should generate a valid css rule: z-index 2', () => {
expect(
generate_css({ position: { zindex: "1" } })
).toEqual({
position: 'z-index: 1;',
layout: ''
});
})
})
describe('generate_screen_css', () => {
test('it should compile the css for a list of components', () => {
const components = [
{
_styles: {
layout: { gridarea: ["", "", "", ""] },
position: { margin: ["1", "1", "1", "1"] }
},
_id: 1
},
{
_styles: {
layout: { gridarea: ["", "", "", ""] },
position: { margin: ["1", "1", "1", "1"] }
},
_id: 2
}, {
_styles: {
layout: { gridarea: ["", "", "", ""] },
position: { margin: ["1", "1", "1", "1"] }
},
_id: 3
}
, {
_styles: {
layout: { gridarea: ["", "", "", ""] },
position: { margin: ["1", "1", "1", "1"] }
},
_id: 4
}
]
const compiled = `.pos-1 {
margin: 1px 1px 1px 1px;
}
.lay-1 {
}
.pos-2 {
margin: 1px 1px 1px 1px;
}
.lay-2 {
}
.pos-3 {
margin: 1px 1px 1px 1px;
}
.lay-3 {
}
.pos-4 {
margin: 1px 1px 1px 1px;
}
.lay-4 {
}`
expect(generate_screen_css(components)).toEqual(compiled)
})
test('it should compile the css for a list of components', () => {
const components = [
{
_styles: {
layout: { gridarea: ["", "", "", ""] },
position: { margin: ["1", "1", "1", "1"] }
},
_id: 1,
_children: [
{
_styles: {
layout: { gridarea: ["", "", "", ""] },
position: { margin: ["1", "1", "1", "1"] }
},
_id: 2,
_children: [
{
_styles: {
layout: { gridarea: ["", "", "", ""] },
position: { margin: ["1", "1", "1", "1"] }
},
_id: 3,
_children: [
{
_styles: {
layout: { gridarea: ["", "", "", ""] },
position: { margin: ["1", "1", "1", "1"] }
},
_id: 4,
_children: [
{
_styles: {
layout: { gridarea: ["", "", "", ""] },
position: { margin: ["1", "1", "1", "1"] }
},
_id: 5,
_children: [
]
},
]
},
]
},
]
},
]
},
{
_styles: {
layout: { gridarea: ["", "", "", ""] },
position: { margin: ["1", "1", "1", "1"] }
},
_id: 6
}, {
_styles: {
layout: { gridarea: ["", "", "", ""] },
position: { margin: ["1", "1", "1", "1"] }
},
_id: 7
}
, {
_styles: {
layout: { gridarea: ["", "", "", ""] },
position: { margin: ["1", "1", "1", "1"] }
},
_id: 8
}
]
const compiled = `.pos-1 {
margin: 1px 1px 1px 1px;
}
.lay-1 {
}
.pos-2 {
margin: 1px 1px 1px 1px;
}
.lay-2 {
}
.pos-3 {
margin: 1px 1px 1px 1px;
}
.lay-3 {
}
.pos-4 {
margin: 1px 1px 1px 1px;
}
.lay-4 {
}
.pos-5 {
margin: 1px 1px 1px 1px;
}
.lay-5 {
}
.pos-6 {
margin: 1px 1px 1px 1px;
}
.lay-6 {
}
.pos-7 {
margin: 1px 1px 1px 1px;
}
.lay-7 {
}
.pos-8 {
margin: 1px 1px 1px 1px;
}
.lay-8 {
}`
expect(generate_screen_css(components)).toEqual(compiled)
})
})

View File

@ -27,6 +27,8 @@ export const _initialiseChildren = (initialiseOpts) =>
htmlElement.removeChild(htmlElement.firstChild);
}
}
htmlElement.classList.add(`lay-${treeNode.props._id}`)
const renderedComponents = [];
for(let childProps of childrenProps) {

View File

@ -23,6 +23,7 @@ export const renderComponent = ({
const thisNode = createTreeNode();
thisNode.context = componentContext;
thisNode.parentNode = parentNode;
thisNode.props = props;
parentNode.children.push(thisNode);
renderedNodes.push(thisNode);
@ -38,6 +39,10 @@ export const renderComponent = ({
thisNode.rootElement = htmlElement.children[
htmlElement.children.length - 1];
if (initialProps._id) {
thisNode.rootElement.classList.add(`pos-${initialProps._id}`)
}
}
if(func) {
@ -51,6 +56,7 @@ export const renderComponent = ({
export const createTreeNode = () => ({
context: {},
props: {},
rootElement: null,
parentNode: null,
children: [],

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long