Merge branch 'master' of github.com:Budibase/budibase

This commit is contained in:
Michael Shanks 2020-01-31 22:18:08 +00:00
commit 634a7a8c3f
49 changed files with 140153 additions and 124104 deletions

23
.eslintrc.json Normal file
View File

@ -0,0 +1,23 @@
{
"env": {
"browser": true,
"es6": true,
"jest": true
},
"parserOptions": {
"ecmaVersion": 2019,
"sourceType": "module"
},
"ignorePatterns": ["node_modules", "dist", "public"],
"plugins": ["prettier", "svelte3"],
"extends": ["eslint:recommended"],
"overrides": [
{
"files": ["**/*.svelte"],
"processor": "svelte3/svelte3"
}
],
"rules": {
"prettier/prettier": "error"
}
}

35
.github/workflows/budibase_ci.yml vendored Normal file
View File

@ -0,0 +1,35 @@
name: Budibase CI
on:
# Trigger the workflow on push or pull request,
# but only for the master branch
push:
branches:
- master
pull_request:
branches:
- master
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [10.x]
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- run: yarn
- run: yarn lint
- run: yarn build
- run: yarn test
env:
CI: true
name: Budibase CI

7
.prettierrc Normal file
View File

@ -0,0 +1,7 @@
{
"tabWidth": 2,
"semi": false,
"singleQuote": false,
"trailingComma": "es5",
"plugins": ["prettier-plugin-svelte"]
}

View File

@ -2,7 +2,12 @@
"name": "root",
"private": true,
"devDependencies": {
"lerna": "^3.14.1"
"eslint": "^6.8.0",
"eslint-plugin-prettier": "^3.1.2",
"eslint-plugin-svelte3": "^2.7.3",
"lerna": "^3.14.1",
"prettier": "^1.19.1",
"prettier-plugin-svelte": "^0.7.0"
},
"dependencies": {},
"scripts": {
@ -10,6 +15,9 @@
"build": "lerna run build",
"initialise": "lerna run initialise",
"clean": "lerna clean",
"dev": "lerna run --parallel --stream dev:builder"
"dev": "lerna run --parallel --stream dev:builder",
"test": "lerna run test",
"lint": "eslint packages",
"format": "prettier --write \"{,!(node_modules)/**/}*.{js,jsx}\""
}
}

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

@ -0,0 +1,53 @@
<script>
export let disabled = false;
export let hidden = false;
export let primary = true;
export let alert = false;
export let warning = false;
</script>
<style>
.primary {
color: #0055ff;
background: rgb(54, 133, 249, 0.1);
}
.alert {
color: rgba(255, 0, 31, 1);
background: rgba(255, 0, 31, 0.1);;
}
.button {
font-size: 18px;
font-weight: bold;
border-radius: 5px;
border: none;
width: 167px;
height: 64px;
}
.button:hover {
cursor: pointer;
}
.button:disabled {
color: rgba(22, 48, 87, 0.2);
cursor: default;
background: transparent;
}
.hidden {
visibility: hidden;
}
</style>
<button
on:click
class="button"
class:hidden
class:primary
class:alert
class:warning
{disabled}>
<slot />
</button>

View File

@ -0,0 +1,9 @@
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="24">
<path fill="none" d="M0 0h24v24H0z" />
<path
d="M13 9h8L11 24v-9H4l9-15v9zm-2 2V7.22L7.532 13H13v4.394L17.263 11H11z" />
</svg>

After

Width:  |  Height:  |  Size: 228 B

View File

@ -0,0 +1,19 @@
<svg
on:click
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="24">
<path fill="none" d="M0 0h24v24H0z" />
<path
d="M15.728 9.686l-1.414-1.414L5
17.586V19h1.414l9.314-9.314zm1.414-1.414l1.414-1.414-1.414-1.414-1.414 1.414
1.414 1.414zM7.242 21H3v-4.243L16.435 3.322a1 1 0 0 1 1.414 0l2.829 2.829a1
1 0 0 1 0 1.414L7.243 21z" />
</svg>
<style>
svg:hover {
cursor: pointer;
}
</style>

After

Width:  |  Height:  |  Size: 446 B

View File

@ -4,3 +4,5 @@ export { default as TerminalIcon } from './Terminal.svelte';
export { default as InputIcon } from './Input.svelte';
export { default as ImageIcon } from './Image.svelte';
export { default as ArrowDownIcon } from './ArrowDown.svelte';
export { default as EventsIcon } from './Events.svelte';
export { default as PencilIcon } from './Pencil.svelte';

View File

@ -0,0 +1,27 @@
<script>
export let value = "";
</script>
<style>
input {
display: block;
font-size: 14px;
font-family: sans-serif;
font-weight: 500;
color: #163057;
line-height: 1.3;
padding: 1em 2.6em 0.9em 1.4em;
width: 100%;
max-width: 100%;
box-sizing: border-box;
margin: 0;
-moz-appearance: none;
-webkit-appearance: none;
appearance: none;
background: #fff;
border: 1px solid #ccc;
height: 50px;
}
</style>
<input type="text" on:change bind:value />

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,41 +1,44 @@
<script>
import UIkit from "uikit";
import UIkit from "uikit";
export let isOpen = false;
export let onClosed = () => {};
export let id = "";
export let isOpen = false;
export let onClosed = () => {};
export let id = "";
let ukModal;
let listenerAdded = false;
let ukModal;
let listenerAdded = false;
$: {
if(ukModal && !listenerAdded) {
listenerAdded = true;
ukModal.addEventListener("hidden", onClosed);
$: {
if (ukModal && !listenerAdded) {
listenerAdded = true;
ukModal.addEventListener("hidden", onClosed);
}
if(ukModal) {
if(isOpen) {
UIkit.modal(ukModal).show();
} else {
UIkit.modal(ukModal).hide();
}
if (ukModal) {
if (isOpen) {
UIkit.modal(ukModal).show();
} else {
UIkit.modal(ukModal).hide();
}
}
}
}
</script>
<div bind:this={ukModal} uk-modal {id}>
<div class="uk-modal-dialog uk-modal-body" uk-overflow-auto>
<slot />
</div>
</div>
<style>
.uk-modal-dialog {
border-radius: 0.3rem;
width: 60%;
height: 80vh;
display: flex;
flex-direction: column;
}
</style>
.uk-modal-dialog {
border-radius: .3rem;
}
</style>
<div bind:this={ukModal} uk-modal {id}>
<div class="uk-modal-dialog uk-modal-body" uk-overflow-auto>
{#if onClosed}
<button class="uk-modal-close-default" type="button" uk-close />
{/if}
<slot />
</div>
</div>

View File

@ -0,0 +1,23 @@
<style>
button {
cursor: pointer;
outline: none;
border: none;
border-radius: 5px;
background: rgba(249, 249, 249, 1);
width: 1.8rem;
height: 1.8rem;
padding-bottom: 10px;
display: flex;
justify-content: center;
align-items: center;
font-size: 1.2rem;
font-weight: 700;
color: rgba(22, 48, 87, 1);
}
</style>
<button on:click>+</button>

View File

@ -0,0 +1,55 @@
<script>
import getIcon from "./icon";
export let value;
</script>
<style>
.select-container {
padding-bottom: 10px;
font-size: 0.9rem;
color: var(--secondary50);
font-weight: bold;
position: relative;
}
select {
display: block;
font-size: 14px;
font-family: sans-serif;
font-weight: 500;
color: #163057;
line-height: 1.3;
padding: 1em 2.6em 0.9em 1.4em;
width: 100%;
max-width: 100%;
box-sizing: border-box;
margin: 0;
-moz-appearance: none;
-webkit-appearance: none;
appearance: none;
background: #fff;
border: 1px solid #ccc;
height: 50px;
}
.arrow {
position: absolute;
right: 10px;
top: 0;
bottom: 0;
margin: auto;
width: 30px;
height: 30px;
pointer-events: none;
color: var(--primary100);
}
</style>
<div class="select-container">
<select on:change {value}>
<slot />
</select>
<span class="arrow">
{@html getIcon('chevron-down', '24')}
</span>
</div>

View File

@ -2,9 +2,10 @@
import PropsView from "./PropsView.svelte";
import { store } from "../builderStore";
import IconButton from "../common/IconButton.svelte";
import { LayoutIcon, PaintIcon, TerminalIcon } from '../common/Icons/';
import { LayoutIcon, PaintIcon, TerminalIcon, EventsIcon } from '../common/Icons/';
import CodeEditor from './CodeEditor.svelte';
import LayoutEditor from './LayoutEditor.svelte';
import EventsEditor from "./EventsEditor";
let current_view = 'props';
@ -17,6 +18,7 @@
const onPropChanged = store.setComponentProp;
const onStyleChanged = store.setComponentStyle;
</script>
<div class="root">
@ -36,6 +38,11 @@
<TerminalIcon />
</button>
</li>
<li>
<button class:selected={current_view === 'events'} on:click={() => current_view = 'events'}>
<EventsIcon />
</button>
</li>
</ul>
{#if !componentInfo.component}
@ -45,6 +52,8 @@
<PropsView {componentInfo} {components} {onPropChanged} />
{:else if current_view === 'layout'}
<LayoutEditor {onStyleChanged} {componentInfo}/>
{:else if current_view === 'events'}
<EventsEditor {componentInfo} {components} {onPropChanged} />
{:else}
<CodeEditor />
{/if}

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

@ -1,88 +0,0 @@
<script>
import IconButton from "../common/IconButton.svelte";
import EventSelector from "./EventSelector.svelte";
import {
filter
} from "lodash/fp";
import {EVENT_TYPE_MEMBER_NAME} from "../common/eventHandlers";
export let parentProps;
export let propDef;
export let onValueChanged;
$: events = parentProps[propDef.____name];
const addHandler = () => {
const newHandler = {parameters:{}};
newHandler[EVENT_TYPE_MEMBER_NAME] = "";
events = [...events, newHandler];
onValueChanged(events);
}
const onEventHandlerChanged = (oldEvent) => (newEvent) => {
const indexOfOldEvent = events.indexOf(oldEvent);
const newEvents = [...events];
newEvents.splice(
events.indexOf(oldEvent),
1,
newEvent);
events = newEvents;
onValueChanged(events);
}
const removeHandler = (index) => () => {
events = filter(e => e !== events[index])(events);
onValueChanged(events);
}
</script>
<div class="root">
<div class="control-container">
{#each events as ev, index}
<div class="handler-container">
<EventSelector onChanged={onEventHandlerChanged(ev)}
onRemoved={removeHandler(index)}
event={ev} />
</div>
<div class="separator"></div>
{/each}
<div class="addelement-container"
on:click={addHandler}>
<IconButton icon="plus"
size="12"/>
</div>
</div>
</div>
<style>
.addelement-container {
cursor: pointer;
padding: 3px 0px;
text-align: center;
}
.addelement-container:hover {
background-color: var(--primary25);
margin-top: 5px;
}
.control-container {
padding-left: 3px;
background: var(--secondary10);
}
.separator {
width: 60%;
margin: 10px auto;
border-style:solid;
border-width: 1px 0 0 0;
border-color: var(--primary25);
}
</style>

View File

@ -1,105 +0,0 @@
<script>
import IconButton from "../common/IconButton.svelte";
import StateBindingControl from "./StateBindingControl.svelte";
import {
find, map, keys, reduce, keyBy
} from "lodash/fp";
import { pipe, userWithFullAccess } from "../common/core";
import { EVENT_TYPE_MEMBER_NAME, allHandlers } from "../common/eventHandlers";
import { store } from "../builderStore";
export let event;
export let onChanged;
export let onRemoved;
let eventType;
let parameters = [];
$: events = allHandlers(
{hierarchy: $store.hierarchy},
userWithFullAccess({
hierarchy: s.hierarchy,
actions: keyBy("name")($store.actions)
})
);
$: if(event) {
eventType = event[EVENT_TYPE_MEMBER_NAME];
parameters = pipe(event.parameters, [
keys,
map(k => ({name:k, value:event.parameters[k]}))
]);
} else {
eventType = "";
parameters = [];
}
const eventChanged = (type, parameters) => {
const paramsAsObject = reduce(
(obj, p) => {
obj[p.name] = p.value;
return obj;
}
, {}
)(parameters)
const ev = {};
ev[EVENT_TYPE_MEMBER_NAME]=type;
ev.parameters = paramsAsObject;
onChanged(ev);
}
const eventTypeChanged = (ev) => {
const eType = find(e => e.name === ev.target.value)(events);
const emptyParameters = map(p => ({name:p, value:""}))(eType.parameters);
eventChanged(eType.name, emptyParameters);
}
const onParameterChanged = index => val => {
const newparameters = [...parameters];
newparameters[index].value = val;
eventChanged(eventType, newparameters);
}
</script>
<div class="type-selector-container">
<select class="type-selector uk-select uk-form-small " value={eventType} on:change={eventTypeChanged}>
<option></option>
{#each events as ev}
<option value={ev.name}>{ev.name}</option>
{/each}
</select>
<IconButton icon="trash"
size="12"
on:click={onRemoved}/>
</div>
{#if parameters}
{#each parameters as p, index}
<div>
{p.name}
</div>
<StateBindingControl onChanged={onParameterChanged(index)}
value={p.value} />
{/each}
{/if}
<style>
.type-selector-container {
display: flex;
}
.type-selector {
border-color: var(--primary50);
border-radius: 2px;
width: 50px;
flex: 1 0 auto;
}
</style>

View File

@ -0,0 +1,164 @@
<script>
import Modal from "../../common/Modal.svelte";
import HandlerSelector from "./HandlerSelector.svelte";
import IconButton from "../../common/IconButton.svelte";
import ActionButton from "../../common/ActionButton.svelte";
import PlusButton from "../../common/PlusButton.svelte";
import Select from "../../common/Select.svelte";
import Input from "../../common/Input.svelte";
import getIcon from "../../common/icon";
import { EVENT_TYPE_MEMBER_NAME } from "../../common/eventHandlers";
export let event;
export let eventOptions;
export let open;
export let onClose;
export let onPropChanged;
let eventType = "onClick";
let draftEventHandler = { parameters: [] };
$: eventData = event || { handlers: [] };
const closeModal = () => {
onClose();
draftEventHandler = { parameters: [] };
eventData = { handlers: [] };
};
const updateEventHandler = (updatedHandler, index) => {
eventData.handlers[index] = updatedHandler;
};
const updateDraftEventHandler = updatedHandler => {
draftEventHandler = updatedHandler;
};
const deleteEventHandler = index => {
eventData.handlers.splice(index, 1);
eventData = eventData;
};
const createNewEventHandler = handler => {
const newHandler = handler || {
parameters: {},
[EVENT_TYPE_MEMBER_NAME]: ""
};
eventData.handlers.push(newHandler);
eventData = eventData;
};
const deleteEvent = () => {
onPropChanged(eventType, []);
closeModal();
};
const saveEventData = () => {
onPropChanged(eventType, eventData.handlers);
closeModal();
};
</script>
<style>
h2 {
color: var(--primary100);
font-size: 20px;
font-weight: bold;
margin-bottom: 0;
}
h5 {
color: rgba(22, 48, 87, 0.6);
font-size: 15px;
margin: 0;
}
.event-options {
display: grid;
grid-template-columns: 1fr 1fr;
grid-gap: 10px;
}
.actions,
header {
display: flex;
justify-content: space-between;
align-items: center;
}
.actions {
margin-top: auto;
}
header {
margin-top: 30px;
margin-bottom: 10px;
}
a {
color: rgba(22, 48, 87, 0.6);
font-size: 12px;
margin-top: 0;
}
</style>
<Modal bind:isOpen={open} onClosed={closeModal}>
<h2>
{eventData.name ? `${eventData.name} Event` : 'Create a New Component Event'}
</h2>
<a href="https://docs.budibase.com/" target="_blank">
Click here to learn more about component events
</a>
<div class="event-options">
<div>
<header>
<h5>Event Type</h5>
{@html getIcon('info', 20)}
</header>
<Select :value={eventType}>
{#each eventOptions as option}
<option value={option.name}>{option.name}</option>
{/each}
</Select>
</div>
</div>
<header>
<h5>Event Action(s)</h5>
{@html getIcon('info', 20)}
</header>
<HandlerSelector
newHandler
onChanged={updateDraftEventHandler}
onCreate={() => {
createNewEventHandler(draftEventHandler);
draftEventHandler = { parameters: [] };
}}
handler={draftEventHandler} />
{#if eventData}
{#each eventData.handlers as handler, index}
<HandlerSelector
{index}
onChanged={updateEventHandler}
onRemoved={() => deleteEventHandler(index)}
{handler} />
{/each}
{/if}
<div class="actions">
<ActionButton
alert
disabled={eventData.handlers.length === 0}
hidden={!eventData.name}
on:click={deleteEvent}>
Delete
</ActionButton>
<ActionButton
disabled={eventData.handlers.length === 0}
on:click={saveEventData}>
Save
</ActionButton>
</div>
</Modal>

View File

@ -0,0 +1,152 @@
<script>
import {
keys,
map,
some,
includes,
cloneDeep,
isEqual,
sortBy,
filter,
difference
} from "lodash/fp";
import { pipe } from "../../common/core";
import Checkbox from "../../common/Checkbox.svelte";
import Textbox from "../../common/Textbox.svelte";
import Dropdown from "../../common/Dropdown.svelte";
import PlusButton from "../../common/PlusButton.svelte";
import IconButton from "../../common/IconButton.svelte";
import Modal from "../../common/Modal.svelte";
import EventEditorModal from "./EventEditorModal.svelte";
import HandlerSelector from "./HandlerSelector.svelte";
import { PencilIcon } from "../../common/Icons";
import { EVENT_TYPE_MEMBER_NAME } from "../../common/eventHandlers";
export const EVENT_TYPE = "event";
export let componentInfo;
export let onPropChanged = () => {};
export let components;
let modalOpen = false;
let events = [];
let selectedEvent = null;
$: {
events = Object.keys(componentInfo)
.filter(key => findType(key) === EVENT_TYPE)
.map(key => ({ name: key, handlers: componentInfo[key] }));
}
function findType(propName) {
if (!componentInfo._component) return;
return components.find(({ name }) => name === componentInfo._component)
.props[propName];
}
const openModal = event => {
selectedEvent = event;
modalOpen = true;
};
const closeModal = () => {
selectedEvent = null;
modalOpen = false;
};
</script>
<style>
h3 {
text-transform: uppercase;
font-size: 12px;
font-weight: 700;
color: #8997ab;
margin-bottom: 10px;
}
.root {
font-size: 10pt;
width: 100%;
}
.form-root {
display: flex;
flex-wrap: wrap;
}
header {
display: flex;
align-items: center;
justify-content: space-between;
}
.handler-container {
display: grid;
grid-template-columns: repeat(2, 1fr);
border: 2px solid #f9f9f9;
height: 80px;
}
.hierarchy-item {
cursor: pointer;
padding: 11px 7px;
margin: 5px 0;
border-radius: 5px;
font-size: 1.5em;
width: 100%;
}
.hierarchy-item:hover {
background: #f9f9f9;
}
.event-name {
margin-top: 5px;
font-weight: bold;
font-size: 16px;
color: rgba(22, 48, 87, 0.6);
align-self: end;
}
.edit-text {
font-family: Arial, Helvetica, sans-serif;
font-weight: bold;
align-self: end;
justify-self: end;
font-size: 10px;
color: rgba(35, 65, 105, 0.4);
}
.selected {
color: var(--button-text);
background: var(--background-button) !important;
}
</style>
<header>
<h3>Events</h3>
<PlusButton on:click={() => openModal()} />
</header>
<div class="root">
<form class="uk-form-stacked form-root">
{#each events as event, index}
{#if event.handlers.length > 0}
<div
class="handler-container hierarchy-item {selectedEvent && selectedEvent.index === index ? 'selected' : ''}"
on:click={() => openModal({ ...event, index })}>
<span class="event-name">{event.name}</span>
<span class="edit-text">EDIT</span>
</div>
{/if}
{/each}
</form>
</div>
<EventEditorModal
{onPropChanged}
open={modalOpen}
onClose={closeModal}
eventOptions={events}
event={selectedEvent}
/>

View File

@ -0,0 +1,148 @@
<script>
import IconButton from "../../common/IconButton.svelte";
import PlusButton from "../../common/PlusButton.svelte";
import Select from "../../common/Select.svelte";
import StateBindingControl from "../StateBindingControl.svelte";
import { find, map, keys, reduce, keyBy } from "lodash/fp";
import { pipe, userWithFullAccess } from "../../common/core";
import {
EVENT_TYPE_MEMBER_NAME,
allHandlers
} from "../../common/eventHandlers";
import { store } from "../../builderStore";
export let handler;
export let onCreate;
export let onChanged;
export let onRemoved;
export let index;
export let newHandler;
let eventOptions;
let handlerType;
let parameters = [];
$: eventOptions = allHandlers(
{ hierarchy: $store.hierarchy },
userWithFullAccess({
hierarchy: $store.hierarchy,
actions: keyBy("name")($store.actions)
})
);
$: {
if (handler) {
handlerType = handler[EVENT_TYPE_MEMBER_NAME];
parameters = Object.entries(handler.parameters).map(([name, value]) => ({
name,
value
}));
} else {
// Empty Handler
handlerType = "";
parameters = [];
}
}
const handlerChanged = (type, params) => {
const handlerParams = {};
for (let param of params) {
handlerParams[param.name] = param.value;
}
const updatedHandler = {
[EVENT_TYPE_MEMBER_NAME]: type,
parameters: handlerParams
};
onChanged(updatedHandler, index);
};
const handlerTypeChanged = e => {
const handlerType = eventOptions.find(
handler => handler.name === e.target.value
);
const defaultParams = handlerType.parameters.map(param => ({
name: param,
value: ""
}));
handlerChanged(handlerType.name, defaultParams);
};
const onParameterChanged = index => value => {
const newParams = [...parameters];
newParams[index].value = value;
handlerChanged(handlerType, newParams);
};
</script>
<style>
.type-selector-container {
display: flex;
justify-content: space-between;
align-items: center;
background: rgba(223, 223, 223, 0.5);
border: 1px solid #dfdfdf;
margin-bottom: 18px;
}
.handler-option {
display: flex;
flex-direction: column;
}
.new-handler {
background: #fff;
}
.handler-controls {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-gap: 10px;
padding: 22px;
}
.event-action-button {
margin-right: 20px;
}
span {
font-size: 12px;
margin-bottom: 5px;
}
</style>
<div class="type-selector-container {newHandler && 'new-handler'}">
<div class="handler-controls">
<div class="handler-option">
<span>Action</span>
<Select value={handlerType} on:change={handlerTypeChanged}>
<option />
{#each eventOptions as option}
<option value={option.name}>{option.name}</option>
{/each}
</Select>
</div>
{#if parameters}
{#each parameters as param, idx}
<div class="handler-option">
<span>{param.name}</span>
<StateBindingControl
onChanged={onParameterChanged(idx)}
value={param.value} />
</div>
{/each}
{/if}
</div>
<div class="event-action-button">
{#if parameters.length > 0}
{#if newHandler}
<PlusButton on:click={onCreate} />
{:else}
<IconButton icon="x" on:click={onRemoved} />
{/if}
{/if}
</div>
</div>

View File

@ -0,0 +1 @@
export { default } from "./EventsEditor.svelte";

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

@ -1,5 +1,6 @@
<script>
import IconButton from "../common/IconButton.svelte";
import Input from "../common/Input.svelte";
import {
isBinding, getBinding, setBinding
} from "../common/binding";
@ -119,9 +120,9 @@
{/each}
</select>
{:else}
<input on:change={ev => onChanged(ev.target.value)}
bind:value={value}
style="flex: 1 0 auto;" />
<Input
on:change={ev => onChanged(ev.target.value)}
bind:value={value} />
{/if}
</div>

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: [],

View File

@ -1,12 +0,0 @@
{
"env": {
"browser": true,
"es6": true,
"jest": true
},
"extends": "eslint:recommended",
"parser": "babel-eslint",
"parserOptions": {
"sourceType": "module"
}
}

View File

@ -51,15 +51,9 @@
"@babel/plugin-transform-runtime": "^7.4.4",
"@babel/preset-env": "^7.4.5",
"@babel/runtime": "^7.4.5",
"babel-eslint": "^10.0.2",
"babel-jest": "^23.6.0",
"babel-plugin-transform-es2015-modules-commonjs": "^6.26.2",
"cross-env": "^5.1.4",
"eslint": "^5.3.0",
"eslint-config-airbnb": "^17.1.0",
"eslint-plugin-import": "^2.14.0",
"eslint-plugin-jsx-a11y": "^6.1.1",
"eslint-plugin-react": "^7.11.0",
"jest": "^24.8.0",
"readable-stream": "^3.1.1",
"regenerator-runtime": "^0.11.1",

View File

@ -14,7 +14,7 @@ module.exports = async (budibaseContext) => {
app.context.master,
config.latestPackagesFolder
);
app.use(koaBody({ multipart : true }));
app.use(koaBody({ multipart: true }));
app.use(router(config, app).routes());
return app.listen(config.port);
};

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

5343
yarn.lock Normal file

File diff suppressed because it is too large Load Diff