Browse Source

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

pull/4023/head
Michael Shanks 7 years ago
parent
commit
511834492d
  1. 2
      .gitignore
  2. 3
      package.json
  3. 2
      packages/bootstrap-components/dist/generators.js
  4. 8
      packages/builder/.vscode/settings.json
  5. 52
      packages/builder/src/PackageRoot.svelte
  6. 298
      packages/builder/src/builderStore/store.js
  7. 4
      packages/builder/src/common/Icons/Image.svelte
  8. 4
      packages/builder/src/common/Icons/Input.svelte
  9. 3
      packages/builder/src/common/Icons/Layout.svelte
  10. 3
      packages/builder/src/common/Icons/Paint.svelte
  11. 4
      packages/builder/src/common/Icons/Terminal.svelte
  12. 5
      packages/builder/src/common/Icons/index.js
  13. 54
      packages/builder/src/common/Inputs/InputGroup.svelte
  14. 17
      packages/builder/src/global.css
  15. 119
      packages/builder/src/userInterface/CodeEditor.svelte
  16. 150
      packages/builder/src/userInterface/ComponentPanel.svelte
  17. 127
      packages/builder/src/userInterface/ComponentsHierarchy.svelte
  18. 24
      packages/builder/src/userInterface/ComponentsHierarchyChildren.svelte
  19. 87
      packages/builder/src/userInterface/ComponentsList.svelte
  20. 35
      packages/builder/src/userInterface/ComponentsPaneSwitcher.svelte
  21. 15
      packages/builder/src/userInterface/CurrentItemPreview.svelte
  22. 151
      packages/builder/src/userInterface/EditComponentProps.svelte
  23. 3
      packages/builder/src/userInterface/EventListSelector.svelte
  24. 116
      packages/builder/src/userInterface/LayoutEditor.svelte
  25. 13
      packages/builder/src/userInterface/NewComponent.svelte
  26. 76
      packages/builder/src/userInterface/PagesList.svelte
  27. 47
      packages/builder/src/userInterface/PropControl.svelte
  28. 86
      packages/builder/src/userInterface/PropsView.svelte
  29. 87
      packages/builder/src/userInterface/StateBindingControl.svelte
  30. 91
      packages/builder/src/userInterface/UserInterfaceRoot.svelte
  31. 68
      packages/builder/src/userInterface/pagesParsing/createProps.js
  32. 27
      packages/builder/src/userInterface/pagesParsing/validatePages.js
  33. 143
      packages/builder/src/userInterface/pagesParsing/validateProps.js
  34. 232
      packages/builder/tests/validateProps.spec.js
  35. 2
      packages/core/dist/budibase-core.cjs.js
  36. 2
      packages/core/dist/budibase-core.esm.mjs
  37. 2
      packages/core/dist/budibase-core.umd.js
  38. 5
      packages/server/.gitignore
  39. BIN
      packages/server/appPackages/testApp/dist/package.tar.gz
  40. 10
      packages/server/appPackages/testApp/yarn.lock
  41. 2
      packages/standard-components/dist/generators.js
  42. 3
      packages/standard-components/src/Div.svelte
  43. 9
      readme.md

2
.gitignore

@ -75,4 +75,4 @@ typings/
.vuepress/dist
# Serverless directories
.serverless
.serverless

3
package.json

@ -9,6 +9,7 @@
"bootstrap": "lerna bootstrap",
"build": "lerna run build",
"initialise": "lerna run initialise",
"clean": "lerna clean"
"clean": "lerna clean",
"dev": "lerna run --parallel --stream dev:builder"
}
}

2
packages/bootstrap-components/dist/generators.js

File diff suppressed because one or more lines are too long

8
packages/builder/.vscode/settings.json

@ -0,0 +1,8 @@
{
"javascript.format.enable": false,
"svelte.plugin.svelte.format.enable": false,
"html.format.enable": false,
"json.format.enable": false,
"editor.trimAutoWhitespace": false,
"sass.format.deleteWhitespace": false
}

52
packages/builder/src/PackageRoot.svelte

@ -11,9 +11,10 @@ import { fade } from "svelte/transition";
<div class="root">
<div class="top-nav">
<IconButton icon="home"
<button class="home-logo"><img src="/assets/budibase-logo-only.png"/></button>
<!-- <IconButton icon="home"
color="var(--slate)"
hoverColor="var(--secondary75)"/>
hoverColor="var(--secondary75)"/> -->
<span class:active={$store.isBackend}
class="topnavitem"
on:click={store.showBackend}>
@ -37,7 +38,7 @@ import { fade } from "svelte/transition";
</div>
{/if}
</div>
</div>
<style>
@ -47,14 +48,18 @@ import { fade } from "svelte/transition";
width:100%;
display: flex;
flex-direction: column;
}
.top-nav {
flex: 0 0 auto;
height: 25px;
height: 48px;
background: white;
padding: 5px;
padding: 0px 15px;
width: 100%;
display: flex;
align-items: center;
border-bottom: 1px solid #ddd;
}
.content {
@ -66,14 +71,20 @@ import { fade } from "svelte/transition";
.content > div {
height:100%;
width:100%;
}
.topnavitem {
cursor: pointer;
color: var(--secondary50);
padding: 0px 15px;
margin: 0px 15px;
padding-top: 4px;
font-weight: 600;
font-size: .9rem;
font-size: 1rem;
height: 100%;
display: flex;
align-items: center;
box-sizing: border-box;
}
.topnavitem:hover {
@ -84,8 +95,31 @@ import { fade } from "svelte/transition";
.active {
color: var(--primary100);
font-weight: 900;
font-weight: 600;
border-bottom: 2px solid var(--primary100);
border-top: 2px solid transparent;
}
.home-logo {
border-style: none;
background-color: rgba(0,0,0,0);
cursor: pointer;
outline: none;
height: 40px;
padding: 8px 10px;
}
.home-logo:hover {
color: var(--hovercolor);
}
.home-logo:active {
outline:none;
}
.home-logo img {
height: 100%;
}
</style>
</style>

298
packages/builder/src/builderStore/store.js

@ -1,8 +1,8 @@
import {
hierarchy as hierarchyFunctions,
hierarchy as hierarchyFunctions,
} from "../../../core/src";
import {
filter, cloneDeep, sortBy,
filter, cloneDeep, sortBy,
map, last, keys, concat, keyBy,
find, isEmpty, reduce, values
} from "lodash/fp";
@ -10,16 +10,16 @@ import {
pipe, getNode, validate,
constructHierarchy, templateApi
} from "../common/core";
import {writable} from "svelte/store";
import { writable } from "svelte/store";
import { defaultPagesObject } from "../userInterface/pagesParsing/defaultPagesObject"
import { buildPropsHierarchy } from "../userInterface/pagesParsing/buildPropsHierarchy"
import api from "./api";
import { isRootComponent, getExactComponent } from "../userInterface/pagesParsing/searchComponents";
import { rename } from "../userInterface/pagesParsing/renameScreen";
import {
getNewComponentInfo, getScreenInfo
import {
getNewComponentInfo, getScreenInfo, getComponentInfo
} from "../userInterface/pagesParsing/createProps";
import {
import {
loadLibs, loadLibUrls, loadGeneratorLibs
} from "./loadComponentLibraries";
@ -28,30 +28,30 @@ let appname = "";
export const getStore = () => {
const initial = {
apps:[],
appname:"",
apps: [],
appname: "",
hierarchy: {},
actions: [],
triggers: [],
pages:defaultPagesObject(),
mainUi:{},
unauthenticatedUi:{},
components:[],
currentFrontEndItem:null,
currentComponentInfo:null,
currentFrontEndType:"none",
pages: defaultPagesObject(),
mainUi: {},
unauthenticatedUi: {},
components: [],
currentFrontEndItem: null,
currentComponentInfo: null,
currentFrontEndType: "none",
currentPageName: "",
currentComponentProps:null,
currentComponentProps: null,
currentNodeIsNew: false,
errors: [],
activeNav: "database",
isBackend:true,
isBackend: true,
hasAppPackage: false,
accessLevels: {version:0, levels:[]},
accessLevels: { version: 0, levels: [] },
currentNode: null,
libraries:null,
showSettings:false,
useAnalytics:true,
libraries: null,
showSettings: false,
useAnalytics: true,
};
const store = writable(initial);
@ -82,7 +82,7 @@ export const getStore = () => {
store.setCurrentScreen = setCurrentScreen(store);
store.setCurrentPage = setCurrentPage(store);
store.createScreen = createScreen(store);
store.removeComponentLibrary =removeComponentLibrary(store);
store.removeComponentLibrary = removeComponentLibrary(store);
store.addStylesheet = addStylesheet(store);
store.removeStylesheet = removeStylesheet(store);
store.savePage = savePage(store);
@ -91,18 +91,22 @@ export const getStore = () => {
store.showSettings = showSettings(store);
store.useAnalytics = useAnalytics(store);
store.createGeneratedComponents = createGeneratedComponents(store);
store.addChildComponent = addChildComponent(store);
store.selectComponent = selectComponent(store);
store.updateComponentProp = updateComponentProp(store);
return store;
}
}
export default getStore;
const initialise = (store, initial) => async () => {
appname = window.location.hash
? last(window.location.hash.substr(1).split("/"))
: "";
appname = window.location.hash
? last(window.location.hash.substr(1).split("/"))
: "";
if(!appname) {
if (!appname) {
initial.apps = await api.get(`/_builder/api/apps`).then(r => r.json());
initial.hasAppPackage = false;
store.set(initial);
@ -110,7 +114,7 @@ const initialise = (store, initial) => async () => {
}
const pkg = await api.get(`/_builder/api/${appname}/appPackage`)
.then(r => r.json());
.then(r => r.json());
initial.libraries = await loadLibs(appname, pkg);
initial.generatorLibraries = await loadGeneratorLibs(appname, pkg);
@ -126,19 +130,20 @@ const initialise = (store, initial) => async () => {
initial.actions = values(pkg.appDefinition.actions);
initial.triggers = pkg.appDefinition.triggers;
if(!!initial.hierarchy && !isEmpty(initial.hierarchy)) {
if (!!initial.hierarchy && !isEmpty(initial.hierarchy)) {
initial.hierarchy = constructHierarchy(initial.hierarchy);
const shadowHierarchy = createShadowHierarchy(initial.hierarchy);
if(initial.currentNode !== null)
if (initial.currentNode !== null)
initial.currentNode = getNode(
shadowHierarchy, initial.currentNode.nodeId
);
}
store.set(initial);
return initial;
}
const generatorsArray = generators =>
const generatorsArray = generators =>
pipe(generators, [
keys,
filter(k => k !== "_lib"),
@ -179,12 +184,12 @@ const newRecord = (store, useRoot) => () => {
s.currentNodeIsNew = true;
const shadowHierarchy = createShadowHierarchy(s.hierarchy);
parent = useRoot ? shadowHierarchy
: getNode(
shadowHierarchy,
s.currentNode.nodeId);
: getNode(
shadowHierarchy,
s.currentNode.nodeId);
s.errors = [];
s.currentNode = templateApi(shadowHierarchy)
.getNewRecordTemplate(parent, "", true);
.getNewRecordTemplate(parent, "", true);
return s;
});
}
@ -209,12 +214,12 @@ const newIndex = (store, useRoot) => () => {
s.errors = [];
const shadowHierarchy = createShadowHierarchy(s.hierarchy);
parent = useRoot ? shadowHierarchy
: getNode(
shadowHierarchy,
s.currentNode.nodeId);
: getNode(
shadowHierarchy,
s.currentNode.nodeId);
s.currentNode = templateApi(shadowHierarchy)
.getNewIndexTemplate(parent);
.getNewIndexTemplate(parent);
return s;
});
}
@ -224,7 +229,7 @@ const saveCurrentNode = (store) => () => {
const errors = validate.node(s.currentNode);
s.errors = errors;
if(errors.length > 0) {
if (errors.length > 0) {
return s;
}
@ -235,23 +240,23 @@ const saveCurrentNode = (store) => () => {
s.hierarchy, s.currentNode.nodeId);
let index = parentNode.children.length;
if(!!existingNode) {
if (!!existingNode) {
// remove existing
index = existingNode.parent().children.indexOf(existingNode);
existingNode.parent().children = pipe(existingNode.parent().children, [
filter(c => c.nodeId !== existingNode.nodeId)
]);
}
}
// should add node into existing hierarchy
const cloned = cloneDeep(s.currentNode);
templateApi(s.hierarchy).constructNode(
parentNode,
parentNode,
cloned
);
const newIndexOfchild = child => {
if(child === cloned) return index;
if (child === cloned) return index;
const currentIndex = parentNode.children.indexOf(child);
return currentIndex >= index ? currentIndex + 1 : currentIndex;
}
@ -260,15 +265,15 @@ const saveCurrentNode = (store) => () => {
sortBy(newIndexOfchild)
]);
if(!existingNode && s.currentNode.type === "record") {
if (!existingNode && s.currentNode.type === "record") {
const defaultIndex = templateApi(s.hierarchy)
.getNewIndexTemplate(cloned.parent());
.getNewIndexTemplate(cloned.parent());
defaultIndex.name = `all_${cloned.collectionName}`;
defaultIndex.allowedRecordNodeIds = [cloned.nodeId];
}
s.currentNodeIsNew = false;
savePackage(store, s);
return s;
@ -279,28 +284,28 @@ const importAppDefinition = store => appDefinition => {
store.update(s => {
s.hierarchy = appDefinition.hierarchy;
s.currentNode = appDefinition.hierarchy.children.length > 0
? appDefinition.hierarchy.children[0]
: null;
? appDefinition.hierarchy.children[0]
: null;
s.actions = appDefinition.actions;
s.triggers = appDefinition.triggers;
s.currentNodeIsNew = false;
s.currentNodeIsNew = false;
return s;
});
}
}
const deleteCurrentNode = store => () => {
store.update(s => {
const nodeToDelete = getNode(s.hierarchy, s.currentNode.nodeId);
s.currentNode = hierarchyFunctions.isRoot(nodeToDelete.parent())
? find(n => n != s.currentNode)
(s.hierarchy.children)
: nodeToDelete.parent();
if(hierarchyFunctions.isRecord(nodeToDelete)) {
? find(n => n != s.currentNode)
(s.hierarchy.children)
: nodeToDelete.parent();
if (hierarchyFunctions.isRecord(nodeToDelete)) {
nodeToDelete.parent().children = filter(c => c.nodeId !== nodeToDelete.nodeId)
(nodeToDelete.parent().children);
(nodeToDelete.parent().children);
} else {
nodeToDelete.parent().indexes = filter(c => c.nodeId !== nodeToDelete.nodeId)
(nodeToDelete.parent().indexes);
(nodeToDelete.parent().indexes);
}
s.errors = [];
savePackage(store, s);
@ -311,8 +316,8 @@ const deleteCurrentNode = store => () => {
const saveField = databaseStore => (field) => {
databaseStore.update(db => {
db.currentNode.fields = filter(f => f.name !== field.name)
(db.currentNode.fields);
(db.currentNode.fields);
templateApi(db.hierarchy).addField(db.currentNode, field);
return db;
});
@ -322,21 +327,21 @@ const saveField = databaseStore => (field) => {
const deleteField = databaseStore => field => {
databaseStore.update(db => {
db.currentNode.fields = filter(f => f.name !== field.name)
(db.currentNode.fields);
(db.currentNode.fields);
return db;
});
}
const saveAction = store => (newAction, isNew, oldAction=null) => {
const saveAction = store => (newAction, isNew, oldAction = null) => {
store.update(s => {
const existingAction = isNew
? null
: find(a => a.name === oldAction.name)(s.actions);
if(existingAction) {
const existingAction = isNew
? null
: find(a => a.name === oldAction.name)(s.actions);
if (existingAction) {
s.actions = pipe(s.actions, [
map(a => a === existingAction ? newAction : a)
]);
@ -348,7 +353,7 @@ const saveAction = store => (newAction, isNew, oldAction=null) => {
});
}
const deleteAction = store => action => {
const deleteAction = store => action => {
store.update(s => {
s.actions = filter(a => a.name !== action.name)(s.actions);
savePackage(store, s);
@ -356,14 +361,14 @@ const deleteAction = store => action => {
});
}
const saveTrigger = store => (newTrigger, isNew, oldTrigger=null) => {
const saveTrigger = store => (newTrigger, isNew, oldTrigger = null) => {
store.update(s => {
const existingTrigger = isNew
? null
: find(a => a.name === oldTrigger.name)(s.triggers);
if(existingTrigger) {
const existingTrigger = isNew
? null
: find(a => a.name === oldTrigger.name)(s.triggers);
if (existingTrigger) {
s.triggers = pipe(s.triggers, [
map(a => a === existingTrigger ? newTrigger : a)
]);
@ -375,7 +380,7 @@ const saveTrigger = store => (newTrigger, isNew, oldTrigger=null) => {
});
}
const deleteTrigger = store => trigger => {
const deleteTrigger = store => trigger => {
store.update(s => {
s.triggers = filter(t => t.name !== trigger.name)(s.triggers);
return s;
@ -383,18 +388,18 @@ const deleteTrigger = store => trigger => {
}
const incrementAccessLevelsVersion = (s) =>
s.accessLevels.version = (s.accessLevels.version || 0) + 1;
s.accessLevels.version = (s.accessLevels.version || 0) + 1;
const saveLevel = store => (newLevel, isNew, oldLevel=null) => {
const saveLevel = store => (newLevel, isNew, oldLevel = null) => {
store.update(s => {
const levels = s.accessLevels.levels;
const existingLevel = isNew
? null
: find(a => a.name === oldLevel.name)(levels);
if(existingLevel) {
const existingLevel = isNew
? null
: find(a => a.name === oldLevel.name)(levels);
if (existingLevel) {
s.accessLevels.levels = pipe(levels, [
map(a => a === existingLevel ? newLevel : a)
]);
@ -425,7 +430,7 @@ const setActiveNav = store => navName => {
});
}
const createShadowHierarchy = hierarchy =>
const createShadowHierarchy = hierarchy =>
constructHierarchy(JSON.parse(JSON.stringify(hierarchy)));
const saveScreen = store => (screen) => {
@ -448,7 +453,7 @@ const _saveScreen = (store, s, screen) => {
api.post(`/_builder/api/${s.appname}/screen`, screen)
.then(() => savePackage(store, s));
return s;
return s;
}
const createScreen = store => (screenName, layoutComponentName) => {
@ -470,13 +475,13 @@ const createGeneratedComponents = store => components => {
s.screens = [...s.screens, ...components];
const doCreate = async () => {
for(let c of components) {
for (let c of components) {
await api.post(`/_builder/api/${s.appname}/screen`, c);
}
await savePackage(store, s);
}
doCreate();
return s;
@ -496,7 +501,7 @@ const deleteScreen = store => name => {
s.components = components;
s.screens = screens;
if(s.currentFrontEndItem.name === name) {
if (s.currentFrontEndItem.name === name) {
s.currentFrontEndItem = null;
s.currentFrontEndType = "";
}
@ -514,20 +519,20 @@ const renameScreen = store => (oldname, newname) => {
screens, pages, error, changedScreens
} = rename(s.pages, s.screens, oldname, newname);
if(error) {
if (error) {
// should really do something with this
return s;
}
s.screens = screens;
s.pages = pages;
if(s.currentFrontEndItem.name === oldname)
if (s.currentFrontEndItem.name === oldname)
s.currentFrontEndItem.name = newname;
const saveAllChanged = async () => {
for(let screenName of changedScreens) {
for (let screenName of changedScreens) {
const changedScreen
= getExactComponent(screens, screenName);
= getExactComponent(screens, screenName);
await api.post(`/_builder/api/${s.appname}/screen`, changedScreen);
}
}
@ -535,10 +540,10 @@ const renameScreen = store => (oldname, newname) => {
api.patch(`/_builder/api/${s.appname}/screen`, {
oldname, newname
})
.then(() => saveAllChanged())
.then(() => {
savePackage(store, s);
});
.then(() => saveAllChanged())
.then(() => {
savePackage(store, s);
});
return s;
})
@ -546,7 +551,7 @@ const renameScreen = store => (oldname, newname) => {
const savePage = store => async page => {
store.update(s => {
if(s.currentFrontEndType !== "page" || !s.currentPageName) {
if (s.currentFrontEndType !== "page" || !s.currentPageName) {
return s;
}
@ -558,26 +563,26 @@ const savePage = store => async page => {
const addComponentLibrary = store => async lib => {
const response =
await api.get(`/_builder/api/${appname}/componentlibrary?lib=${encodeURI(lib)}`,undefined, false);
const response =
await api.get(`/_builder/api/${appname}/componentlibrary?lib=${encodeURI(lib)}`, undefined, false);
const success = response.status === 200;
const error = response.status === 404
? `Could not find library ${lib}`
: success
? ""
: response.statusText;
const error = response.status === 404
? `Could not find library ${lib}`
: success
? ""
: response.statusText;
const components = success
? await response.json()
: [];
? await response.json()
: [];
store.update(s => {
if(success) {
if (success) {
const componentsArray = [];
for(let c in components) {
for (let c in components) {
componentsArray.push(components[c]);
}
@ -592,18 +597,18 @@ const addComponentLibrary = store => async lib => {
return s;
})
}
const removeComponentLibrary = store => lib => {
store.update(s => {
s.pages.componentLibraries = filter(l => l !== lib)(
s.pages.componentLibraries);
s.pages.componentLibraries);
savePackage(store, s);
return s;
})
@ -627,12 +632,12 @@ const removeStylesheet = store => stylesheet => {
const refreshComponents = store => async () => {
const componentsAndGenerators =
const componentsAndGenerators =
await api.get(`/_builder/api/${db.appname}/components`).then(r => r.json());
const components = pipe(componentsAndGenerators.components, [
keys,
map(k => ({...componentsAndGenerators[k], name:k}))
map(k => ({ ...componentsAndGenerators[k], name: k }))
]);
store.update(s => {
@ -648,25 +653,25 @@ const refreshComponents = store => async () => {
const savePackage = (store, s) => {
const appDefinition = {
hierarchy:s.hierarchy,
triggers:s.triggers,
hierarchy: s.hierarchy,
triggers: s.triggers,
actions: keyBy("name")(s.actions),
props: {
main: buildPropsHierarchy(
s.components,
s.screens,
s.pages.main.appBody),
unauthenticated: buildPropsHierarchy(
s.components,
s.screens,
s.pages.unauthenticated.appBody)
s.components,
s.screens,
s.pages.main.appBody),
unauthenticated: buildPropsHierarchy(
s.components,
s.screens,
s.pages.unauthenticated.appBody)
}
};
const data = {
appDefinition,
accessLevels:s.accessLevels,
pages:s.pages,
accessLevels: s.accessLevels,
pages: s.pages,
}
return api.post(`/_builder/api/${s.appname}/appPackage`, data);
@ -689,3 +694,44 @@ const setCurrentPage = store => pageName => {
return s;
})
}
const addChildComponent = store => component => {
store.update(s => {
const newComponent = getNewComponentInfo(
s.components, component);
const children = s.currentFrontEndItem.props._children;
const component_definition = Object.assign(
cloneDeep(newComponent.fullProps), {
_component: component,
})
s.currentFrontEndItem.props._children =
children ?
children.concat(component_definition) :
[component_definition];
return s;
})
}
const selectComponent = store => component => {
store.update(s => {
s.currentComponentInfo = component;
return s;
})
}
const updateComponentProp = store => (name, value) => {
store.update(s => {
const current_component = s.currentComponentInfo;
s.currentComponentInfo[name] = value;
_saveScreen(store, s, s.currentFrontEndItem);
s.currentComponentInfo = current_component;
return s;
})
}

4
packages/builder/src/common/Icons/Image.svelte

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<path fill="none" d="M0 0h24v24H0z"/>
<path fill="currentColor" d="M4.828 21l-.02.02-.021-.02H2.992A.993.993 0 0 1 2 20.007V3.993A1 1 0 0 1 2.992 3h18.016c.548 0 .992.445.992.993v16.014a1 1 0 0 1-.992.993H4.828zM20 15V5H4v14L14 9l6 6zm0 2.828l-6-6L6.828 19H20v-1.172zM8 11a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/>
</svg>

After

Width:  |  Height:  |  Size: 400 B

4
packages/builder/src/common/Icons/Input.svelte

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<path fill="none" d="M0 0h24v24H0z"/>
<path fill="currentColor" d="M5 5v14h14V5H5zM4 3h16a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1zm5.869 12l-.82 2H6.833L11 7h2l4.167 10H14.95l-.82-2H9.87zm.82-2h2.622L12 9.8 10.689 13z"/>
</svg>

After

Width:  |  Height:  |  Size: 339 B

3
packages/builder/src/common/Icons/Layout.svelte

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/>
<path fill="currentColor" d="M21 20a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h16a1 1 0 0 1 1 1v16zM11 5H5v14h6V5zm8 8h-6v6h6v-6zm0-8h-6v6h6V5z"/>
</svg>

After

Width:  |  Height:  |  Size: 280 B

3
packages/builder/src/common/Icons/Paint.svelte

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/>
<path fill="currentColor" d="M19.228 18.732l1.768-1.768 1.767 1.768a2.5 2.5 0 1 1-3.535 0zM8.878 1.08l11.314 11.313a1 1 0 0 1 0 1.415l-8.485 8.485a1 1 0 0 1-1.414 0l-8.485-8.485a1 1 0 0 1 0-1.415l7.778-7.778-2.122-2.121L8.88 1.08zM11 6.03L3.929 13.1 11 20.173l7.071-7.071L11 6.029z"/>
</svg>

After

Width:  |  Height:  |  Size: 415 B

4
packages/builder/src/common/Icons/Terminal.svelte

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<path fill="none" d="M0 0h24v24H0z"/>
<path fill="currentColor" d="M3 3h18a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1zm1 2v14h16V5H4zm8 10h6v2h-6v-2zm-3.333-3L5.838 9.172l1.415-1.415L11.495 12l-4.242 4.243-1.415-1.415L8.667 12z"/>
</svg>

After

Width:  |  Height:  |  Size: 346 B

5
packages/builder/src/common/Icons/index.js

@ -0,0 +1,5 @@
export { default as LayoutIcon } from './Layout.svelte';
export { default as PaintIcon } from './Paint.svelte';
export { default as TerminalIcon } from './Terminal.svelte';
export { default as InputIcon } from './Input.svelte';
export { default as ImageIcon } from './Image.svelte';

54
packages/builder/src/common/Inputs/InputGroup.svelte

@ -0,0 +1,54 @@
<script>
export let meta = [];
export let size = '';
export let values = [];
</script>
<div class="inputs {size}">
{#each meta as { placeholder }, i}
<input type="number" placeholder="{placeholder}" bind:value={values[i]}/>
{/each}
</div>
<style>
.inputs {
display: flex;
justify-content: space-between;
}
input {
width: 83px;
font-size: 12px;
font-weight: 700;
color: #163057;
opacity: 0.7;
padding: 5px 10px;
box-sizing: border-box;
border: 1px solid #DBDBDB;
border-radius: 2px;
outline: none;
}
input[type=number]::-webkit-inner-spin-button,
input[type=number]::-webkit-outer-spin-button {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
margin: 0;
}
.small > input {
width: 38px;
height: 38px;
display: flex;
justify-content: center;
align-items: center;
text-align: center;
padding: 0;
}
.small > input::placeholder {
text-align: center;
}
</style>

17
packages/builder/src/global.css

@ -1,29 +1,29 @@
:root {
--primary100: #454CA0FF;
--primary100: #173157FF;
--primary75: #454CA0BF;
--primary50: #454CA080;
--primary25: #454CA040;
--primary10: #454CA01A;
--primary5: #454ca00c;
--primarydark: #3F448A;
--secondary100: #162B4DFF;
--secondary100:#828fa5;
--secondary75: #162B4DBF;
--secondary50: #162B4D80;
--secondary25: #162B4D40;
--secondary10: #162B4D1A;
--secondary5: rgba(22, 43, 77, 0.068);
--secondary5:#fff;
--secondarydark: #3F448A;
--tertiary: #F2F5F7;
--success100: #49C39EFF;
--success75: #49C39EBF;
--success50: #49C39E80;
--success25: #49C39E40;
--success10: #49C39E1A;
--successdark: #44B492;
--deletion100: #F2545BFF;
--deletion75: #F2545BBF;
--deletion50: #F2545B80;
@ -52,6 +52,9 @@
--heavybodytext: var(--fontbold) "regular" var(--secondary100) 16pt;
--quotation: var(--fontnormal) "italics" var(--darkslate) 16pt;
--smallheavybodytext: var(--fontbold) "regular" var(--secondary100) 14pt;
--background-button: #e6eeff;
--button-text: #0055ff;
}
html, body {
@ -97,4 +100,4 @@ h5 {
font-family: var(--fontblack);
font-size: 12pt;
color: var(--darkslate);
}
}

119
packages/builder/src/userInterface/CodeEditor.svelte

@ -0,0 +1,119 @@
<script>
let snippets = [];
let current_snippet = 0;
let snippet_text = ''
let id = 0;
function save_snippet() {
if (!snippet_text) return;
const index = snippets.findIndex(({ id }) => current_snippet === id);
if (index > -1) {
snippets[index].snippet = snippet_text;
} else {
snippets = snippets.concat({ snippet: snippet_text , id: id });
}
snippet_text = '';
current_snippet = ++id;
}
function edit_snippet(id) {
const { snippet, id: _id } = snippets.find(({ id:_id }) => _id === id);
current_snippet = id
snippet_text = snippet;
}
</script>
<h3>Code</h3>
<p>Use the code box below to add snippets of javascript to enhance your webapp</p>
<div class="editor">
<textarea class="code" bind:value={snippet_text} />
<button on:click={save_snippet}>Save</button>
</div>
<div class="snippets">
<h3>Snippets added</h3>
{#each snippets as { id, snippet } }
<div class="snippet">
<pre class="code">{snippet}</pre>
<button on:click={() => edit_snippet(id)}>Edit</button>
</div>
{/each}
</div>
<style>
h3 {
text-transform: uppercase;
font-size: 12px;
font-weight: 700;
color: #8997ab;
margin-bottom: 10px;
}
p {
font-size: 12px;
color: #333;
margin-top: 0;
}
.editor {
position: relative;
}
.code {
width: 100%;
outline: none;
border: none;
background: #173157;
border-radius: 5px;
box-sizing: border-box;
white-space: pre;
color: #eee;
padding: 10px;
font-family: monospace;
overflow-y: scroll;
}
.editor textarea {
resize: none;
height: 150px;
}
button {
position: absolute;
box-shadow: 0 0 black;
color: #eee;
right: 5px;
bottom: 10px;
background: none;
border: none;
text-transform: uppercase;
font-size: 9px;
font-weight: 600;
outline: none;
cursor: pointer;
}
.snippets {
margin-top: 20px;
}
.snippet {
position: relative;
margin-top: 5px;
}
.snippet pre {
background: #f9f9f9;
color: #333;
max-height: 150px;
}
.snippet button {
color: #ccc;
}
</style>

150
packages/builder/src/userInterface/ComponentPanel.svelte

@ -0,0 +1,150 @@
<script>
import PropsView from "./PropsView.svelte";
import { store } from "../builderStore";
import { isRootComponent } from "./pagesParsing/searchComponents";
import IconButton from "../common/IconButton.svelte";
import Textbox from "../common/Textbox.svelte";
import { pipe } from "../common/core";
import {
getScreenInfo
} from "./pagesParsing/createProps";
import { LayoutIcon, PaintIcon, TerminalIcon } from '../common/Icons/';
import CodeEditor from './CodeEditor.svelte';
import LayoutEditor from './LayoutEditor.svelte';
import {
cloneDeep,
join,
split,
map,
keys,
isUndefined,
last
} from "lodash/fp";
import { assign } from "lodash";
let component;
let name = "";
let description = "";
let tagsString = "";
let nameInvalid = "";
let componentInfo = {};
let modalElement
let propsValidationErrors = [];
let originalName="";
let components;
let ignoreStore = false;
// $: shortName = last(name.split("/"));
store.subscribe(s => {
if(ignoreStore) return;
component = s.currentComponentInfo;
if(!component) return;
originalName = component.name;
name = component.name;
description = component.description;
tagsString = join(", ")(component.tags);
componentInfo = s.currentComponentInfo;
components = s.components;
});
const onPropsChanged = store.updateComponentProp;
let current_view = 'props';
</script>
<div class="root">
<ul>
<li>
<button class:selected={current_view === 'props'} on:click={() => current_view = 'props'}>
<PaintIcon />
</button>
</li>
<li>
<button class:selected={current_view === 'layout'} on:click={() => current_view = 'layout'}>
<LayoutIcon />
</button>
</li>
<li>
<button class:selected={current_view === 'code'} on:click={() => current_view = 'code'}>
<TerminalIcon />
</button>
</li>
</ul>
{#if !componentInfo.component}
<div class="component-props-container">
{#if current_view === 'props'}
<PropsView {componentInfo} {components} {onPropsChanged} />
{:else if current_view === 'layout'}
<LayoutEditor />
{:else}
<CodeEditor />
{/if}
</div>
{:else}
<h1> This is a screen, this will be dealt with later</h1>
{/if}
</div>
<style>
.root {
height: 100%;
display: flex;
flex-direction: column;
}
.title > div:nth-child(1) {
grid-column-start: name;
color: var(--secondary100);
}
.title > div:nth-child(2) {
grid-column-start: actions;
}
.component-props-container {
margin-top: 10px;
flex: 1 1 auto;
overflow-y: auto;
}
ul {
list-style: none;
display: flex;
padding: 0;
}
li {
margin-right: 20px;
background: none;
border-radius: 5px;
width: 48px;
height: 48px;
}
li button {
width: 100%;
height: 100%;
background: none;
border: none;
border-radius: 5px;
padding: 12px;
outline: none;
cursor: pointer;
}
.selected {
color: var(--button-text);
background: var(--background-button)!important;
}
</style>

127
packages/builder/src/userInterface/ComponentsHierarchy.svelte

@ -1,9 +1,10 @@
<script>
import ComponentsHierarchyChildren from './ComponentsHierarchyChildren.svelte';
import {
last,
sortBy,
filter,
import {
last,
sortBy,
filter,
map,
uniqWith,
isEqual,
@ -18,13 +19,6 @@ import getIcon from "../common/icon";
import { store } from "../builderStore";
export let components = []
export let thisLevel = "";
let pathPartsThisLevel;
let componentsThisLevel;
let subfolders;
let expandedFolders = [];
const joinPath = join("/");
@ -35,39 +29,8 @@ const normalizedName = name => pipe(name, [
trimChars(" ")
]);
const isOnThisLevel = (c) =>
normalizedName(c.name).split("/").length === pathPartsThisLevel
&&
(!thisLevel || normalizedName(c.name).startsWith(normalizedName(thisLevel)));
const notOnThisLevel = (c) => !isOnThisLevel(c);
const isInSubfolder = (subfolder, c) =>
normalizedName(c.name).startsWith(
trimCharsStart("/")(
joinPath([thisLevel, subfolder])));
const isOnNextLevel = (c) =>
normalizedName(c.name).split("/").length === pathPartsThisLevel + 1
const lastPartOfName = (c) =>
last(c.name.split("/"))
const subFolder = (c) => {
const cname = normalizedName(c.name);
const folderName = cname.substring(thisLevel.length, cname.length).split("/")[0];
return ({
name: folderName,
isExpanded: includes(folderName)(expandedFolders),
path: thisLevel + "/" + folderName
});
}
const subComponents = (subfolder) => pipe(components, [
filter(c => isInSubfolder(subfolder, c))
]);
const lastPartOfName = (c) =>
last(c.name ? c.name.split("/") : c._component.split("/"))
const expandFolder = folder => {
const expandedFolder = {...folder};
@ -84,62 +47,46 @@ const expandFolder = folder => {
1,
expandedFolder);
subfolders = newFolders;
}
const isComponentSelected = (type, current,c) =>
type==="screen"
&& current
&& current
&& current.name === c.name
const isFolderSelected = (current, folder) =>
const isFolderSelected = (current, folder) =>
isInSubfolder(current, folder)
$: {
pathPartsThisLevel = !thisLevel
? 1
: normalizedName(thisLevel).split("/").length + 1;
componentsThisLevel =
$: _components =
pipe(components, [
filter(isOnThisLevel),
map(c => ({component:c, title:lastPartOfName(c)})),
map(c => ({component: c, title:lastPartOfName(c)})),
sortBy("title")
]);
subfolders =
pipe(components, [
filter(notOnThisLevel),
sortBy("name"),
map(subFolder),
uniqWith((f1,f2) => f1.path === f2.path)
]);
function select_component(screen, component) {
store.setCurrentScreen(screen);
store.selectComponent(component);
}
</script>
<div class="root" style={`padding-left: calc(10px * ${pathPartsThisLevel})`}>
{#each subfolders as folder}
<div class="hierarchy-item folder"
on:click|stopPropagation={() => expandFolder(folder)}>
<span>{@html getIcon(folder.isExpanded ? "chevron-down" : "chevron-right", "16")}</span>
<span class="title" class:currentfolder={$store.currentFrontEndItem && isInSubfolder(folder.name, $store.currentFrontEndItem)}>{folder.name}</span>
{#if folder.isExpanded}
<svelte:self components={subComponents(folder.name)}
thisLevel={folder.path} />
{/if}
</div>
{/each}
<div class="root">
{#each componentsThisLevel as component}
<div class="hierarchy-item component" class:selected={isComponentSelected($store.currentFrontEndType, $store.currentFrontEndItem, component.component)}
{#each _components as component}
<div class="hierarchy-item component"
class:selected={isComponentSelected($store.currentFrontEndType, $store.currentFrontEndItem, component.component)}
on:click|stopPropagation={() => store.setCurrentScreen(component.component.name)}>
<span>{@html getIcon("circle", "7")}</span>
<span class="title">{component.title}</span>
</div>
{#if component.component.props && component.component.props._children}
<ComponentsHierarchyChildren components={component.component.props._children}
onSelect={child => select_component(component.component.name, child)} />
{/if}
{/each}
</div>
@ -147,31 +94,33 @@ $: {
<style>
.root {
color: var(--secondary50);
font-size: .9rem;
font-weight: bold;
font-weight: 500;
font-size: 0.9rem;
color: #828fa5;
}
.hierarchy-item {
cursor: pointer;
padding: 5px 0px;
padding: 11px 7px;
margin: 5px 0;
border-radius: 5px;
}
.hierarchy-item:hover {
color: var(--secondary);
/* color: var(--secondary); */
background: #fafafa;
}
.component {
margin-left: 5px;
}
.currentfolder {
color: var(--secondary100);
}
.selected {
color: var(--primary100);
font-weight: bold;
color: var(--button-text);
background: var(--background-button)!important;
}
.title {
@ -179,4 +128,4 @@ $: {
}
</style>
</style>

24
packages/builder/src/userInterface/ComponentsHierarchyChildren.svelte

@ -0,0 +1,24 @@
<script>
import { last } from "lodash/fp";
import { pipe } from "../common/core";
export let components = [];
export let onSelect = () => {};
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]);
</script>
{#each components as component}
<ul>
<li on:click|stopPropagation={() => onSelect(component)}>
{get_capitalised_name(component._component)}
{#if component._children}
<svelte:self components={component._children}/>
{/if}
</li>
</ul>
{/each}

87
packages/builder/src/userInterface/ComponentsList.svelte

@ -8,6 +8,7 @@ import {
groupBy, keys, find, sortBy
} from "lodash/fp";
import { pipe } from "../common/core";
import { ImageIcon, InputIcon, LayoutIcon } from '../common/Icons/';
let componentLibraries=[];
@ -26,29 +27,25 @@ const addRootComponent = (c, all) => {
}
group.components.push(c)
};
const onComponentChosen = (component) => {
};
const onComponentChosen = store.addChildComponent;
store.subscribe(s => {
const newComponentLibraries = [];
for(let comp of sortBy(["name"])(s.components)) {
addRootComponent(
comp,
comp,
newComponentLibraries);
}
componentLibraries = newComponentLibraries;
});
let current_view = 'text';
</script>
@ -59,22 +56,31 @@ store.subscribe(s => {
</div>
<div class="library-container">
<div class="inner-header">
Components
</div>
{#each lib.components as component}
<ul>
<li>
<button class:selected={current_view === 'text'} on:click={() => current_view = 'text'}>
<InputIcon />
</button>
</li>
<li>
<button class:selected={current_view === 'layout'} on:click={() => current_view = 'layout'}>
<LayoutIcon />
</button>
</li>
<li>
<button class:selected={current_view === 'media'} on:click={() => current_view = 'media'}>
<ImageIcon />
</button>
</li>
</ul>
{#each lib.components.filter(_ => true) as component}
<div class="component"
on:click={() => onComponentChosen(component)}>
on:click={() => onComponentChosen(component.name)}>
<div class="name">
{splitName(component.name).componentName}
</div>
<div class="description">
{component.description}
</div>
</div>
{/each}
@ -117,8 +123,16 @@ store.subscribe(s => {
}
.component {
padding: 2px 0px;
padding: 0 15px;
cursor: pointer;
border: 1px solid #ccc;
border-radius: 2px;
margin: 10px 0;
height: 40px;
box-sizing: border-box;
color: #163057;
display: flex;
align-items: center;
}
.component:hover {
@ -126,8 +140,11 @@ store.subscribe(s => {
}
.component > .name {
color: var(--secondary100);
color: #163057;
display: inline-block;
font-size: 12px;
font-weight: bold;
opacity: 0.6;
}
.component > .description {
@ -137,6 +154,34 @@ store.subscribe(s => {
margin-left: 10px;
}
ul {
list-style: none;
display: flex;
padding: 0;
}
li {
margin-right: 20px;
background: none;
border-radius: 5px;
width: 48px;
height: 48px;
}
li button {
width: 100%;
height: 100%;
background: none;
border: none;
border-radius: 5px;
padding: 12px;
outline: none;
cursor: pointer;
}
.selected {
color: var(--button-text);
background: var(--background-button)!important;
}
</style>

35
packages/builder/src/userInterface/ComponentsPaneSwitcher.svelte

@ -1,13 +1,13 @@
<script>
import EditComponentProps from "./EditComponentProps.svelte";
import ComponentPanel from "./ComponentPanel.svelte";
import ComponentsList from "./ComponentsList.svelte";
let selected="properties";
const isSelected = tab =>
const isSelected = tab =>
selected === tab;
const selectTab = tab =>
const selectTab = tab =>
selected = tab;
@ -17,13 +17,13 @@ const selectTab = tab =>
<div class="switcher">
<button
<button
class:selected={selected==="properties"}
on:click={() => selectTab("properties")}>
Properties
</button>
<button
<button
class:selected={selected==="components"}
on:click={() => selectTab("components")}>
Components
@ -33,11 +33,11 @@ const selectTab = tab =>
<div class="panel">
{#if selected==="properties"}
<EditComponentProps />
<ComponentPanel />
{/if}
{#if selected==="components"}
<ComponentsList />
<ComponentsList />
{/if}
</div>
@ -50,24 +50,29 @@ const selectTab = tab =>
height: 100%;
display: flex;
flex-direction: column;
padding: 2rem 1.5rem 2rem 1.5rem;
}
.switcher {
flex: 0 0 auto;
display: flex;
justify-content: space-between;
margin-bottom: 25px;
}
.switcher > button {
display: inline-block;
background-color: rgba(0,0,0,0);
border-style: solid;
border-color: var(--slate);
margin: 5px;
padding: 5px;
border: none;
margin: 0;
padding: 0;
cursor: pointer;
font-weight: 600;
font-size: 0.85rem;
text-transform: uppercase;
color: #999;
}
.switcher > .selected {
background-color: red;
color: #333;
}
.panel {
@ -76,4 +81,4 @@ const selectTab = tab =>
overflow-y: auto;
}
</style>
</style>

15
packages/builder/src/userInterface/CurrentItemPreview.svelte

@ -17,7 +17,7 @@ let appDefinition = {};
store.subscribe(s => {
hasComponent = !!s.currentFrontEndItem;
stylesheetLinks = pipe(s.pages.stylesheets, [
map(s => `<link rel="stylesheet" href="${s}"/>`),
join("\n")
@ -25,8 +25,8 @@ store.subscribe(s => {
appDefinition = {
componentLibraries: s.loadLibraryUrls(),
props: buildPropsHierarchy(
s.components,
s.screens,
s.components,
s.screens,
s.currentFrontEndItem),
hierarchy: s.hierarchy,
appRootPath: ""
@ -45,15 +45,16 @@ store.subscribe(s => {
title="componentPreview"
srcdoc={
`<html>
<head>
${stylesheetLinks}
<script>
window["##BUDIBASE_APPDEFINITION##"] = ${JSON.stringify(appDefinition)};
import('/_builder/budibase-client.esm.mjs')
.then(module => {
module.loadBudibase();
})
console.log(module, window);
module.loadBudibase({ window, localStorage });
})
</script>
<style>
@ -91,4 +92,4 @@ store.subscribe(s => {
width: 100%;
}
</style>
</style>

151
packages/builder/src/userInterface/EditComponentProps.svelte

@ -12,9 +12,10 @@ import {
} from "./pagesParsing/createProps";
import Button from "../common/Button.svelte";
import ButtonGroup from "../common/ButtonGroup.svelte";
import { LayoutIcon, PaintIcon, TerminalIcon } from '../common/Icons/';
import {
cloneDeep,
import {
cloneDeep,
join,
split,
map,
@ -50,43 +51,6 @@ store.subscribe(s => {
components = s.components;
});
const save = () => {
ignoreStore = true;
if(!validate()) {
ignoreStore = false;
return;
}
component.name = originalName || name;
component.description = description;
component.tags = pipe(tagsString, [
split(","),
map(s => s.trim())
]);
store.saveScreen(component);
ignoreStore = false;
// now do the rename
if(name !== originalName) {
store.renameScreen(originalName, name);
}
}
const deleteComponent = () => {
showDialog();
}
const confirmDeleteComponent = () => {
store.deleteScreen(component.name);
hideDialog();
}
const onPropsValidate = result => {
propsValidationErrors = result;
}
const updateComponent = doChange => {
const newComponent = cloneDeep(component);
doChange(newComponent);
@ -95,86 +59,27 @@ const updateComponent = doChange => {
}
const onPropsChanged = newProps => {
updateComponent(newComponent =>
updateComponent(newComponent =>
assign(newComponent.props, newProps));
}
const validate = () => {
const fieldInvalid = (field, err) =>
errors[field] = err;
const fieldValid = field =>
errors[field] && delete errors[field];
if(!name) nameInvalid = "component name i not supplied";
else nameInvalid = "";
return (!nameInvalid && propsValidationErrors.length === 0);
}
const hideDialog = () => {
UIkit.modal(modalElement).hide();
}
const showDialog = () => {
UIkit.modal(modalElement).show();
}
</script>
<div class="root">
<div class="title">
<div>{shortName}</div>
<div>
<IconButton icon="save"
on:click={save}
color="var(--secondary100)"
hoverColor="var(--primary100)"/>
<IconButton icon="trash"
on:click={deleteComponent}
color="var(--secondary100)"
hoverColor="var(--primary100)"/>
</div>
</div>
<ul>
<li><button><PaintIcon /></button></li>
<li><button><LayoutIcon /></button></li>
<li><button><TerminalIcon /></button></li>
</ul>
<div class="component-props-container">
<PropsView onValidate={onPropsValidate}
<PropsView
{componentInfo}
{onPropsChanged} />
</div>
</div>
<div bind:this={modalElement} uk-modal>
<div class="uk-modal-dialog">
<div class="uk-modal-header">
Delete {name} ?
</div>
<div class="uk-modal-body">
Are you sure you want to delete this component ?
</div>
<div class="uk-modal-footer">
<ButtonGroup>
<Button grouped
on:click={confirmDeleteComponent}>
OK
</Button>
<Button grouped
on:click={hideDialog}
color="secondary" >
Cancel
</Button>
</ButtonGroup>
</div>
</div>
@ -186,9 +91,7 @@ const showDialog = () => {
height: 100%;
display: flex;
flex-direction: column;
border-style: solid;
border-width: 1px 0 0 0;
border-color: var(--slate);
}
.title {
@ -213,4 +116,32 @@ const showDialog = () => {
flex: 1 1 auto;
overflow-y: auto;
}
</style>
ul {
list-style: none;
display: flex;
padding: 0;
}
li {
margin-right: 20px;
background: none;
border-radius: 5px;
width: 45px;
height: 45px;
}
li button {
width: 100%;
height: 100%;
background: none;
border: none;
border-radius: 5px;
padding: 12px;
}
.selected {
background: lightblue;
}
</style>

3
packages/builder/src/userInterface/EventListSelector.svelte

@ -5,6 +5,7 @@ import {
filter
} from "lodash/fp";
import {EVENT_TYPE_MEMBER_NAME} from "../common/eventHandlers";
export let parentProps;
export let propDef;
export let onValueChanged;
@ -58,7 +59,7 @@ const removeHandler = (index) => () => {
<div class="addelement-container"
on:click={addHandler}>
<IconButton icon="plus"
<IconButton icon="plus"
size="12"/>
</div>
</div>

116
packages/builder/src/userInterface/LayoutEditor.svelte

@ -0,0 +1,116 @@
<script>
import InputGroup from '../common/Inputs/InputGroup.svelte';
let grid_values = ['', '', '', ''];
let column_values = ['', ''];
let row_values = ['', ''];
let gap_values = [''];
let margin_values = ['', '', '', ''];
let padding_values = ['', '', '', ''];
let zindex_values = [''];
const tbrl = [
{ placeholder: 'T' },
{ placeholder: 'R' },
{ placeholder: 'B' },
{ placeholder: 'L' }
];
const se = [
{ placeholder: 'START' },
{ placeholder: 'END' },
]
const single = [{ placeholder: '' }];
</script>
<h3>Layout</h3>
<h4>Positioning</h4>
<div class="layout-pos">
<div class="grid">
<h5>Grid Area:</h5>
<InputGroup meta={tbrl} bind:values={grid_values} size="small"/>
</div>
<div class="grid">
<h5>Column:</h5>
<InputGroup meta={se} bind:values={column_values} />
</div>
<div class="grid">
<h5>Row:</h5>
<InputGroup meta={se} bind:values={row_values} />
</div>
<div class="grid">
<h5>Gap:</h5>
<InputGroup meta={single} bind:values={gap_values} />
</div>
</div>
<h4>Spacing</h4>
<div class="layout-spacing">
<div class="grid">
<h5>Margin:</h5>
<InputGroup meta={tbrl} bind:values={margin_values} size="small"/>
</div>
<div class="grid">
<h5>Padding:</h5>
<InputGroup meta={tbrl} bind:values={padding_values} size="small"/>
</div>
</div>
<h4>Z-Index</h4>
<div class="layout-layer">
<div class="grid">
<h5>Z-Index:</h5>
<InputGroup meta={single} bind:values={zindex_values}/>
</div>
</div>
<style>
h3 {
text-transform: uppercase;
font-size: 12px;
font-weight: 700;
color: #8997ab;
margin-bottom: 10px;
}
h4 {
text-transform: uppercase;
font-size: 10px;
font-weight: 700;
color: #163057;
opacity: 0.3;
margin-bottom: 15px;
}
h5 {
font-size: 12px;
font-weight: 700;
color: #163057;
opacity: 0.6;
padding-top: 12px;
margin-bottom: 0;
}
div > div {
display: grid;
grid-template-rows: 1fr;
grid-gap: 10px;
height: 40px;
margin-bottom: 15px;
}
.grid {
grid-template-columns: 70px 1fr;
}
</style>

13
packages/builder/src/userInterface/NewComponent.svelte

@ -11,7 +11,7 @@ import UIkit from "uikit";
import { isRootComponent } from "./pagesParsing/searchComponents";
import { splitName } from "./pagesParsing/splitRootComponentName.js"
import {
import {
find, filter, some, map, includes
} from "lodash/fp";
import { assign } from "lodash";
@ -29,13 +29,12 @@ let name="";
let saveAttempted=false;
store.subscribe(s => {
layoutComponents = pipe(s.components, [
filter(c => c.container),
map(c => ({name:c.name, ...splitName(c.name)}))
]);
layoutComponent = layoutComponent
layoutComponent = layoutComponent
? find(c => c.name === layoutComponent.name)(layoutComponents)
: layoutComponents[0];
@ -48,7 +47,7 @@ const save = () => {
const isValid = name.length > 0
&& !screenNameExists(name)
&& layoutComponent;
if(!isValid) return;
store.createScreen(name, layoutComponent.name);
@ -59,7 +58,7 @@ const cancel = () => {
UIkit.modal(componentSelectorModal).hide();
}
const screenNameExists = (name) =>
const screenNameExists = (name) =>
some(s => s.name.toLowerCase() === name.toLowerCase())(screens)
</script>
@ -84,7 +83,7 @@ const screenNameExists = (name) =>
<div class="uk-margin">
<label class="uk-form-label">Layout Component</label>
<div class="uk-form-controls">
<select class="uk-select uk-form-small"
<select class="uk-select uk-form-small"
bind:value={layoutComponent}
class:uk-form-danger={saveAttempted && !layoutComponent}>
{#each layoutComponents as comp}
@ -130,4 +129,4 @@ h1 {
font-size: 9pt;
}
</style>
</style>

76
packages/builder/src/userInterface/PagesList.svelte

@ -7,54 +7,70 @@ const getPage = (s, name) => {
return ({name, props});
}
const pages = [{
title: 'Main',
id: 'main'
}, {
title: 'Login',
id: 'unauthenticated'
}]
store.setCurrentPage('main')
</script>
<div class="root">
<div class="hierarchy-item component" class:selected={$store.currentFrontEndType === "page" && $store.currentPageName === "main"}
on:click|stopPropagation={() => store.setCurrentPage("main")}>
<span>{@html getIcon("circle", "7")}</span>
<span class="title">Main</span>
</div>
<div class="hierarchy-item component" class:selected={$store.currentFrontEndType === "page" && $store.currentPageName === "unauthenticated"}
on:click|stopPropagation={() => store.setCurrentPage("unauthenticated")}>
<span>{@html getIcon("circle", "7")}</span>
<span class="title">Login</span>
</div>
<select id="page" name="select" on:change={({target}) => store.setCurrentPage(target.value)}>
{#each pages as {title, id}}
<option value="{id}">Page: {title}</option>
{/each}
</select>
<span class="arrow">{@html getIcon("chevron-down","24")}</span>
</div>
<style>
.root {
padding-bottom: 10px;
padding-left: 10px;
font-size: .9rem;
color: var(--secondary50);
font-weight: bold;
position: relative;
}
.hierarchy-item {
cursor: pointer;
padding: 5px 0px;
}
.hierarchy-item:hover {
color: var(--secondary100);
}
.component {
margin-left: 5px;
select {
display: block;
font-size: 16px;
font-family: sans-serif;
font-weight: 700;
color: #444;
line-height: 1.3;
padding: 1em 2.6em 0.9em 1.4em;
width: 100%;
max-width: 100%;
box-sizing: border-box;
margin: 0;
border: none;
border-radius: .5em;
-moz-appearance: none;
-webkit-appearance: none;
appearance: none;
background-color: #fafafa;
}
.selected {
.arrow {
position: absolute;
right: 10px;
top: 0;
bottom: 0;
margin: auto;
width: 30px;
height: 30px;
pointer-events: none;
color: var(--primary100);
font-weight: bold;
}
.title {
margin-left: 10px;
}
</style>
</style>

47
packages/builder/src/userInterface/PropControl.svelte

@ -6,13 +6,12 @@ import Dropdown from "../common/Dropdown.svelte";
import EventListSelector from "./EventListSelector.svelte";
import StateBindingControl from "./StateBindingControl.svelte";
export let errors = [];
export let setProp = () => {};
export let fieldHasError =() => {};
export let propDef = {};
export let props = {};
export let disabled;
export let index;
export let prop_name;
export let prop_value;
export let prop_type = {};
$: isOdd = (index % 2 !== 0);
@ -25,35 +24,43 @@ const setComponentProp = (props) => {
<div class="root" >
{#if propDef.type === "event"}
{#if prop_type === "event"}
<div class="prop-label">{propDef.____name}</div>
<!-- <h5>{prop_name}</h5>
<EventListSelector parentProps={props}
{propDef}
onValueChanged={setComponentProp} />
onValueChanged={setComponentProp} /> -->
{:else }
<div class="prop-label">{propDef.____name}</div>
<StateBindingControl value={props[propDef.____name]}
type={propDef.type}
options={propDef.options}
onChanged={v => setProp(propDef.____name, v)}/>
<h5>{prop_name}</h5>
<StateBindingControl value={prop_value}
type={prop_type}
options={prop_type.options}
onChanged={v => setProp(prop_name, v)}/>
{/if}
{/if}
</div>
<style>
.root {
padding: 1rem 1rem 0rem 1rem;
height: 40px;
margin-bottom: 15px;
display: grid;
grid-template-rows: 1fr;
grid-template-columns: 70px 1fr;
grid-gap: 10px;
}
.prop-label {
font-size: 0.8rem;
color: var(--secondary100);
font-weight: bold;
}
h5 {
font-size: 12px;
font-weight: 700;
color: #163057;
opacity: 0.6;
padding-top: 12px;
margin-bottom: 0;
}
</style>
</style>

86
packages/builder/src/userInterface/PropsView.svelte

@ -10,70 +10,33 @@ import { getInstanceProps } from "./pagesParsing/createProps";
import Checkbox from "../common/Checkbox.svelte";
import Textbox from "../common/Textbox.svelte";
import Dropdown from "../common/Dropdown.svelte";
import { validateProps } from "./pagesParsing/validateProps";
import PropControl from "./PropControl.svelte";
import IconButton from "../common/IconButton.svelte";
export let shouldValidate = true;
export let onValidate = () => {};
export let componentInfo;
export let instanceProps = null;
export let onPropsChanged = () => {};
export let components;
let errors = [];
let props = {};
let propsDefinitions = [];
let isInstance = false;
const props_to_ignore = ['_component','_children', '_layout'];
$: {
if(componentInfo)
{
isInstance = !!instanceProps;
props = isInstance
? getInstanceProps(componentInfo, instanceProps)
: cloneDeep(componentInfo.fullProps);
propsDefinitions = pipe(componentInfo.propsDefinition, [
keys,
map(k => ({...componentInfo.propsDefinition[k], ____name:k})),
sortBy("____name")
]);
}
}
$: propDefs = componentInfo && Object.entries(componentInfo).filter(([name])=> !props_to_ignore.includes(name));
function find_type(prop_name) {
if(!componentInfo._component) return;
return components.find(({name}) => name === componentInfo._component).props[prop_name];
}
let setProp = (name, value) => {
const newProps = cloneDeep(props);
let finalProps = isInstance ? newProps : cloneDeep(componentInfo.component.props);
if(!isInstance) {
const nowSet = [];
for(let p of componentInfo.unsetProps) {
if(!isEqual(newProps[p])(componentInfo.rootDefaultProps[p])) {
finalProps[p] = newProps[p];
nowSet.push(p);
}
}
componentInfo.unsetProps = difference(nowSet)(componentInfo.unsetProps);
}
newProps[name] = value;
finalProps[name] = value;
props = newProps;
if(validate(finalProps))
onPropsChanged(finalProps);
}
const validate = (finalProps) => {
errors = validateProps(componentInfo.rootComponent, finalProps, [], false);
onValidate(errors);
return errors.length === 0;
onPropsChanged(name, value);
}
const fieldHasError = (propName) =>
const fieldHasError = (propName) =>
some(e => e.propName === propName)(errors);
</script>
@ -81,25 +44,22 @@ const fieldHasError = (propName) =>
<div class="root">
<form class="uk-form-stacked form-root">
{#each propsDefinitions as propDef, index}
<div class="prop-container">
<PropControl {setProp}
{fieldHasError}
{propDef}
{props}
{index}
disabled={false} />
</div>
{/each}
{#each propDefs as [prop_name, prop_value], index}
</form>
<div class="prop-container">
<PropControl {setProp}
{prop_name}
{prop_value}
prop_type={find_type(prop_name)}
{index}
disabled={false} />
</div>
{/each}
</form>
</div>
@ -121,4 +81,4 @@ const fieldHasError = (propName) =>
min-width: 250px;
}
</style>
</style>

87
packages/builder/src/userInterface/StateBindingControl.svelte

@ -66,10 +66,10 @@ const setBindingSource = ev => {
bind(bindingPath, bindingFallbackValue, ev.target.value);
}
const makeBinding = () => {
forceIsBound=true;
isExpanded=true;
}
// const makeBinding = () => {
// forceIsBound=true;
// isExpanded=true;
// }
</script>
@ -77,11 +77,11 @@ const makeBinding = () => {
<div>
<div class="bound-header">
<div>{isExpanded ? "" : bindingPath}</div>
<IconButton icon={isExpanded ? "chevron-up" : "chevron-down"}
<IconButton icon={isExpanded ? "chevron-up" : "chevron-down"}
size="12"
on:click={() => isExpanded=!isExpanded}/>
{#if !canOnlyBind}
<IconButton icon="trash"
<IconButton icon="trash"
size="12"
on:click={clearBinding}/>
{/if}
@ -97,8 +97,8 @@ const makeBinding = () => {
value={bindingFallbackValue}
on:change={setBindingFallback} >
<div class="binding-prop-label">Binding Source</div>
<select class="uk-select uk-form-small"
value={bindingSource}
<select class="uk-select uk-form-small"
value={bindingSource}
on:change={setBindingSource}>
<option>store</option>
@ -117,13 +117,13 @@ const makeBinding = () => {
<div>
<IconButton icon={value == true ? "check-square" : "square"}
size="19"
on:click={() => onChanged(!value)}/>
on:click={() => onChanged(!value)} />
</div>
{:else if type === "options"}
<select class="uk-select uk-form-small"
value={value}
<select class="uk-select uk-form-small"
value={value}
on:change={ev => onChanged(ev.target.value)}>
{#each options as option}
<option value={option}>{option}</option>
@ -132,49 +132,46 @@ const makeBinding = () => {
{:else}
<input class="uk-input uk-form-small"
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}
style="flex: 1 0 auto;" />
{/if}
<IconButton icon="link"
size="12"
on:click={makeBinding} />
</div>
{/if}
<style>
.unbound-container {
display:flex;
}
.unbound-container {
display:flex;
margin: .5rem 0rem .5rem 0rem;
}
.unbound-container > *:nth-child(1) {
width:auto;
flex: 1 0 auto;
font-size: 0.8rem;
color: var(--secondary100);
border-radius: .2rem;
}
.bound-header {
display: flex;
}
.bound-header > div:nth-child(1) {
flex: 1 0 auto;
width: 30px;
color: var(--secondary50);
padding-left: 5px;
}
.bound-header {
display: flex;
}
.binding-prop-label {
color: var(--secondary50);
}
.bound-header > div:nth-child(1) {
flex: 1 0 auto;
width: 30px;
color: var(--secondary50);
padding-left: 5px;
}
.binding-prop-label {
color: var(--secondary50);
}
</style>
input {
font-size: 12px;
font-weight: 700;
color: #163057;
opacity: 0.7;
padding: 5px 10px;
box-sizing: border-box;
border: 1px solid #DBDBDB;
border-radius: 2px;
outline: none;
}
</style>

91
packages/builder/src/userInterface/UserInterfaceRoot.svelte

@ -13,7 +13,7 @@ import SettingsView from "./SettingsView.svelte";
import PageView from "./PageView.svelte";
import ComponentsPaneSwitcher from "./ComponentsPaneSwitcher.svelte";
let newComponentPicker;
let newComponentPicker;
const newComponent = () => {
newComponentPicker.show();
}
@ -26,33 +26,34 @@ const settings = () => {
</script>
<div class="root">
<div class="ui-nav">
<div class="components-list-container">
<div class="pages-list-container">
<div class="nav-group-header">
<div>{@html getIcon("sidebar","18")}</div>
<span class="components-nav-header">Screens</span>
<div>
<IconButton icon="settings"
size="14px"
on:click={settings}/>
<IconButton icon="plus"
on:click={newComponent}/>
</div>
<span class="navigator-title">Navigator</span>
</div>
<div class="nav-items-container">
<ComponentsHierarchy components={$store.screens}/>
<PagesList />
</div>
</div>
<div class="pages-list-container">
<div class="components-list-container">
<div class="nav-group-header">
<div>{@html getIcon("grid","18")}</div>
<span>Pages</span>
<span class="components-nav-header">Screens</span>
<div>
<!-- <IconButton icon="settings"
size="14px"
on:click={settings}/> -->
<!-- <IconButton icon="plus"
on:click={newComponent}/> -->
<button on:click={newComponent}>+</button>
</div>
</div>
<div class="nav-items-container">
<PagesList />
<ComponentsHierarchy components={$store.screens}/>
</div>
</div>
@ -63,7 +64,7 @@ const settings = () => {
<CurrentItemPreview />
{:else if $store.currentFrontEndType === "page"}
<PageView />
{/if}
{/if}
</div>
{#if $store.currentFrontEndType === "screen"}
@ -80,22 +81,47 @@ const settings = () => {
<style>
button {
cursor: pointer;
outline: none;
border: none;
border-radius: 5px;
background: var(--background-button);
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: var(--button-text);
}
.root {
display: grid;
grid-template-columns: 250px 1fr 300px;
grid-template-columns: 290px 1fr 300px;
height: 100%;
width: 100%;
background: #fafafa;
}
.ui-nav {
grid-column: 1;
background-color: var(--secondary5);
height: 100%;
padding: 0 1.5rem 0rem 1.5rem
}
.preview-pane {
grid-column: 2;
margin: 80px 60px;
background: #fff;
border-radius: 5px;
box-shadow: 0 0px 6px rgba(0,0,0,0.05)
}
.components-pane {
@ -105,12 +131,10 @@ const settings = () => {
overflow-y: hidden;
}
.pages-list-container {
padding-top: 2rem;
}
.components-nav-header {
font-size: .9rem;
font-size: 0.75rem;
color: #999;
text-transform: uppercase;
}
.nav-group-header {
@ -119,15 +143,16 @@ const settings = () => {
}
.nav-items-container {
padding: 1rem 1rem 0rem 1rem;
padding: 1rem 0rem 0rem 0rem;
}
.nav-group-header {
display:grid;
grid-template-columns: [icon] auto [title] 1fr [button] auto;
padding: 2rem 1rem 0rem 1rem;
display: flex;
padding: 2rem 0 0 0;
font-size: .9rem;
font-weight: bold;
justify-content: space-between;
align-items: center;
}
.nav-group-header>div:nth-child(1) {
@ -152,7 +177,13 @@ const settings = () => {
}
.nav-group-header>div:nth-child(3):hover {
color: var(--primary75);
color: var(--primary75);
}
.navigator-title {
text-transform: uppercase;
font-weight: 400;
color: #999;
}
</style>
</style>

68
packages/builder/src/userInterface/pagesParsing/createProps.js

@ -1,6 +1,6 @@
import {
isString, isUndefined, find, keys, uniq,
some, filter, reduce, cloneDeep, includes,last
some, filter, reduce, cloneDeep, includes, last
} from "lodash/fp";
import { types, expandComponentDefinition } from "./types";
import { assign } from "lodash";
@ -11,7 +11,7 @@ import { ensureShardNameIsInShardMap } from "../../../../core/src/indexing/shard
export const getInstanceProps = (componentInfo, props) => {
const finalProps = cloneDeep(componentInfo.fullProps);
for(let p in props) {
for (let p in props) {
finalProps[p] = props[p];
}
@ -20,9 +20,9 @@ export const getInstanceProps = (componentInfo, props) => {
export const getNewComponentInfo = (components, rootComponent, name) => {
const component = {
name: name || "",
description:"",
props:{
name: name || "",
description: "",
props: {
_component: rootComponent
}
};
@ -34,22 +34,22 @@ export const getNewComponentInfo = (components, rootComponent, name) => {
export const getScreenInfo = (components, screen) => {
return getComponentInfo(
components,
components,
screen);
}
export const getComponentInfo = (components, comp) => {
const targetComponent = isString(comp)
? find(c => c.name === comp)(components)
: comp;
const targetComponent = isString(comp)
? find(c => c.name === comp)(components)
: comp;
let component;
let subComponent;
if(isRootComponent(targetComponent)) {
if (isRootComponent(targetComponent)) {
component = targetComponent;
} else {
subComponent = targetComponent;
component = find(c => c.name === subComponent.props._component)(
components);
component = find(c => c.name === (subComponent.props ? subComponent.props._component : subComponent._component))(
components);
}
const subComponentProps = subComponent ? subComponent.props : {};
@ -65,7 +65,7 @@ export const getComponentInfo = (components, comp) => {
fullProps._component = targetComponent.name;
return ({
propsDefinition:expandComponentDefinition(component),
propsDefinition: expandComponentDefinition(component),
rootDefaultProps: rootProps.props,
unsetProps,
fullProps: fullProps,
@ -78,7 +78,7 @@ export const getComponentInfo = (components, comp) => {
export const createProps = (componentDefinition, derivedFromProps) => {
const error = (propName, error) =>
errors.push({propName, error});
errors.push({ propName, error });
const props = {
_component: componentDefinition.name
@ -86,24 +86,24 @@ export const createProps = (componentDefinition, derivedFromProps) => {
const errors = [];
if(!componentDefinition.name)
if (!componentDefinition.name)
error("_component", "Component name not supplied");
const propsDef = componentDefinition.props;
for(let propDef in propsDef) {
for (let propDef in propsDef) {
const parsedPropDef = parsePropDef(propsDef[propDef]);
if(parsedPropDef.error)
error(propDef, parsedPropDef.error);
else
if (parsedPropDef.error)
error(propDef, parsedPropDef.error);
else
props[propDef] = parsedPropDef;
}
if(derivedFromProps) {
if (derivedFromProps) {
assign(props, derivedFromProps);
}
if(componentDefinition.children !== false
&& isUndefined(props._children)) {
if (componentDefinition.children !== false
&& isUndefined(props._children)) {
props._children = [];
}
@ -114,37 +114,37 @@ export const createProps = (componentDefinition, derivedFromProps) => {
const parsePropDef = propDef => {
const error = message => ({error:message, propDef});
const error = message => ({ error: message, propDef });
if(isString(propDef)) {
if(!types[propDef])
if (isString(propDef)) {
if (!types[propDef])
return error(`Do not recognise type ${propDef}`);
return types[propDef].default();
}
if(!propDef.type)
if (!propDef.type)
return error("Property Definition must declare a type");
const type = types[propDef.type];
if(!type)
if (!type)
return error(`Do not recognise type ${propDef.type}`);
if(isUndefined(propDef.default))
if (isUndefined(propDef.default))
return type.default(propDef);
if(!type.isOfType(propDef.default))
if (!type.isOfType(propDef.default))
return error(`${propDef.default} is not of type ${type}`);
return propDef.default;
}
export const arrayElementComponentName = (parentComponentName, arrayPropName) =>
export const arrayElementComponentName = (parentComponentName, arrayPropName) =>
`${parentComponentName}:${arrayPropName}`;
/*
Allowed propDefOptions
- type: string, bool, number, array
- default: default value, when undefined
- required: field is required
*/
- required: field is required
*/

27
packages/builder/src/userInterface/pagesParsing/validatePages.js

@ -1,7 +1,6 @@
import { recursivelyValidate } from "./validateProps";
import {
isString,
keys,
import {
isString,
keys,
flatten,
isArray,
map,
@ -15,12 +14,12 @@ export const validatePage = (page, getComponent) => {
const error = message => errors.push(message);
const noIndex = !page.index;
if(noIndex) {
if (noIndex) {
error("Page does not define an index member");
}
if(!page.appBody
|| !isString(page.appBody)
if (!page.appBody
|| !isString(page.appBody)
|| !page.appBody.endsWith(".json")) {
error("App body must be set toa valid JSON file");
}
@ -28,7 +27,7 @@ export const validatePage = (page, getComponent) => {
/* Commenting this for now
* index is a load of static members just now, but maybe useful
for pageLayout props (which is just a pipe dream at time of writing)
const indexHtmlErrors = noIndex
const indexHtmlErrors = noIndex
? []
: pipe(
recursivelyValidate(page.index, getComponent), [
@ -44,17 +43,17 @@ export const validatePages = (pages, getComponent) => {
let errors = [];
const error = message => errors.push(message);
if(!pages.main) {
if (!pages.main) {
error("must have a 'main' page");
}
if(!pages.unauthenticated) {
if (!pages.unauthenticated) {
error("must have a 'unauthenticated' (login) page");
}
if(!pages.componentLibraries
|| !isArray(pages.componentLibraries)
|| pages.componentLibraries.length === 0) {
if (!pages.componentLibraries
|| !isArray(pages.componentLibraries)
|| pages.componentLibraries.length === 0) {
error("componentLibraries must be set to a non-empty array of strings");
}
@ -67,4 +66,4 @@ export const validatePages = (pages, getComponent) => {
]);
return [...errors, ...pageErrors];
}
}

143
packages/builder/src/userInterface/pagesParsing/validateProps.js

@ -1,143 +0,0 @@
import { types } from "./types";
import {
createProps, arrayElementComponentName
} from "./createProps";
import { isString } from "util";
import {
includes, filter, map, keys,
flatten, flattenDeep, each,
indexOf, isUndefined
} from "lodash/fp";
import { common } from "../../../../core/src";
import {
isBinding
} from "../../common/binding";
const pipe = common.$;
const makeError = (errors, propName, stack) => (message) =>
errors.push({
stack,
propName,
error:message});
export const recursivelyValidate = (rootProps, getComponent, stack=[]) => {
if(!rootProps._component) {
const errs = [];
makeError(errs, "_component", stack)("Component is not set");
return errs;
// this would break everything else anyway
}
const componentDef = getComponent(
rootProps._component);
const errors = validateProps(
componentDef,
rootProps,
stack,
true);
const validateChildren = (_props, _stack) =>
!_props._children
? []
: pipe(_props._children, [
map(child => recursivelyValidate(
child,
getComponent,
[..._stack, _props._children.indexOf(child)]))
]);
const childErrors = validateChildren(
rootProps, stack);
return flattenDeep([errors, ...childErrors]);
}
const expandPropDef = propDef =>
isString(propDef)
? types[propDef].defaultDefinition()
: propDef;
export const validateProps = (componentDefinition, props, stack=[], isFinal=true) => {
const errors = [];
if(isFinal && !props._component) {
makeError(errors, "_component", stack)("Component is not set");
return errors;
// this would break everything else anyway
}
const propsDefinition = componentDefinition.props;
for(let propDefName in props) {
if(propDefName === "_component") continue;
if(propDefName === "_children") continue;
if(propDefName === "_layout") continue;
const propDef = expandPropDef(propsDefinition[propDefName]);
const type = types[propDef.type];
const error = makeError(errors, propDefName, stack);
const propValue = props[propDefName];
// component declarations dont need to define al props.
if(!isFinal && isUndefined(propValue)) continue;
if(isFinal && propDef.required && propValue) {
error(`Property ${propDefName} is required`);
continue;
}
if(isBinding(propValue)) {
if(propDef.type === "event") {
error(`Cannot apply binding to type ${propDef.type}`);
continue;
}
}
else if(!type.isOfType(propValue)) {
error(`Property ${propDefName} is not of type ${propDef.type}. Actual value ${propValue}`)
continue;
}
if(propDef.type === "options"
&& propValue
&& !isBinding(propValue)
&& !includes(propValue)(propDef.options)) {
error(`Property ${propDefName} is not one of allowed options. Acutal value is ${propValue}`);
}
}
return errors;
}
export const validateComponentDefinition = (componentDefinition) => {
const { errors } = createProps(componentDefinition);
const propDefinitions = expandPropDef(componentDefinition.props);
pipe(propDefinitions, [
keys,
map(k => ({
propDef:propDefinitions[k],
propName:k
})),
filter(d => d.propDef.type === "options"
&& (!d.propDef.options || d.propDef.options.length === 0)),
each(d => makeError(errors, d.propName)(`${d.propName} does not have any options`))
]);
return errors;
}

232
packages/builder/tests/validateProps.spec.js

@ -1,232 +0,0 @@
import {
validateComponentDefinition,
validateProps,
recursivelyValidate
} from "../src/userInterface/pagesParsing/validateProps";
import { createProps } from "../src/userInterface/pagesParsing/createProps";
import {
setBinding
} from "../src/common/binding";
// not that allot of this functionality is covered
// in createDefaultProps - as validate props uses that.
describe("validateComponentDefinition", () => {
it("should return error when no options for options field", () => {
const compDef = {
name:"some_component",
props: {
size: {
type: "options",
options: []
}
}
};
const errors = validateComponentDefinition(compDef);
expect(errors.length).toEqual(1);
expect(errors[0].propName).toBe("size");
});
it("should not return error when options field has options", () => {
const compDef = {
name: "some_component",
props: {
size: {
type: "options",
options: ["small", "medium", "large"]
}
}
};
const errors = validateComponentDefinition(compDef);
expect(errors).toEqual([]);
});
});
const validComponentDef = {
name: "some_component",
props: {
size: {
type: "options",
options: ["small", "medium", "large"],
default:"medium"
},
rowCount : "number"
}
};
const childComponentDef = {
name: "child_component",
props: {
width: "number",
units: {
type: "string",
default: "px"
}
}
};
const validProps = () => {
const { props } = createProps(validComponentDef);
props._children.push(
createProps(childComponentDef));
return props;
}
describe("validateProps", () => {
it("should have no errors with a big list of valid props", () => {
const errors = validateProps(validComponentDef, validProps(), [], true);
expect(errors).toEqual([]);
});
it("should return error with invalid value", () => {
const props = validProps();
props.rowCount = "1";
const errors = validateProps(validComponentDef, props, [], true);
expect(errors.length).toEqual(1);
expect(errors[0].propName).toBe("rowCount");
});
it("should return error with invalid option", () => {
const props = validProps();
props.size = "really_small";
const errors = validateProps(validComponentDef, props, [], true);
expect(errors.length).toEqual(1);
expect(errors[0].propName).toBe("size");
});
it("should not return error when has binding", () => {
const props = validProps();
props._children[0].width = setBinding({path:"some_path"});
props.size = setBinding({path:"other path", fallback:"small"});
const errors = validateProps(validComponentDef, props, [], true);
expect(errors.length).toEqual(0);
});
});
describe("recursivelyValidateProps", () => {
const rootComponent = {
name: "rootComponent",
children: true,
props: {
width: "number"
}
};
const todoListComponent = {
name: "todoListComponent",
props:{
showTitle: "bool"
}
};
const headerComponent = {
name: "headerComponent",
props: {
text: "string"
}
};
const iconComponent = {
name: "iconComponent",
props: {
iconName: "string"
}
};
const navItemComponent = {
name: "navItemComponent",
props: {
text: "string"
}
};
const getComponent = name => ({
rootComponent,
todoListComponent,
headerComponent,
iconComponent,
navItemComponent
})[name];
const rootProps = () => ({
_component: "rootComponent",
width: 100,
_children: [{
_component: "todoListComponent",
showTitle: true,
_children : [
{
_component: "navItemComponent",
text: "todos"
},
{
_component: "headerComponent",
text: "Your todo list"
},
{
_component: "iconComponent",
iconName: "fa fa-list"
},
{
_component: "iconComponent",
iconName:"fa fa-cog"
}
]
}]
});
it("should return no errors for valid structure", () => {
const result = recursivelyValidate(
rootProps(),
getComponent);
expect(result).toEqual([]);
});
it("should return error on root component", () => {
const root = rootProps();
root.width = "yeeeoooo";
const result = recursivelyValidate(root, getComponent);
expect(result.length).toBe(1);
expect(result[0].propName).toBe("width");
});
it("should return error on first nested child component", () => {
const root = rootProps();
root._children[0].showTitle = "yeeeoooo";
const result = recursivelyValidate(root, getComponent);
expect(result.length).toBe(1);
expect(result[0].stack).toEqual([0]);
expect(result[0].propName).toBe("showTitle");
});
it("should return error on second nested child component", () => {
const root = rootProps();
root._children[0]._children[0].text = false;
const result = recursivelyValidate(root, getComponent);
expect(result.length).toBe(1);
expect(result[0].stack).toEqual([0,0]);
expect(result[0].propName).toBe("text");
});
});

2
packages/core/dist/budibase-core.cjs.js

File diff suppressed because one or more lines are too long

2
packages/core/dist/budibase-core.esm.mjs

File diff suppressed because one or more lines are too long

2
packages/core/dist/budibase-core.umd.js

File diff suppressed because one or more lines are too long

5
packages/server/.gitignore

@ -1,3 +1,8 @@
myapps/
config.js
<<<<<<< HEAD
/builder/*
!/builder/assets/
=======
builder/
>>>>>>> ee5a4e8c962b29242152cbbd8065d8f3ccf65eaf

BIN
packages/server/appPackages/testApp/dist/package.tar.gz

Binary file not shown.

10
packages/server/appPackages/testApp/yarn.lock

@ -3,28 +3,22 @@
"@budibase/client@file:../../../client":
version "0.0.3"
version "0.0.15"
dependencies:
"@nx-js/compiler-util" "^2.0.0"
date-fns "^1.29.0"
lodash "^4.17.15"
lunr "^2.3.5"
shortid "^2.2.8"
svelte "^3.9.2"
"@budibase/standard-components@file:../../../standard-components":
version "0.0.5"
version "0.0.15"
"@nx-js/compiler-util@^2.0.0":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@nx-js/compiler-util/-/compiler-util-2.0.0.tgz#c74c12165fa2f017a292bb79af007e8fce0af297"
integrity sha512-AxSQbwj9zqt8DYPZ6LwZdytqnwfiOEdcFdq4l8sdjkZmU2clTht7RDLCI8xvkp7KqgcNaOGlTeCM55TULWruyQ==
date-fns@^1.29.0:
version "1.30.1"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.30.1.tgz#2e71bf0b119153dbb4cc4e88d9ea5acfb50dc05c"
integrity sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw==
lodash@^4.17.15:
version "4.17.15"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"

2
packages/standard-components/dist/generators.js

File diff suppressed because one or more lines are too long

3
packages/standard-components/src/Div.svelte

@ -8,7 +8,7 @@ export let _bb;
let rootDiv;
$:{
if(_bb && rootDiv && _children && _children.length)
_bb.hydrateChildren(_children, theButton);
_bb.hydrateChildren(_children, rootDiv);
}
@ -16,4 +16,3 @@ $:{
<div class="{className}" bind:this={rootDiv}>
</div>

9
readme.md

@ -73,9 +73,12 @@ if you then want to run the builder in dev mode (i.e. with hot reloading):
... keep the server running, and..
1. Open a new console
2. `cd packages/builder`
3. `yarn start`
4. Access the builder on http://localhost:3000
2. `yarn dev`
3. Access the builder on http://localhost:3000
This will enable watch mode for both the client AND the server.
### Running Commands from /server Directory
Notice that when inside `packages/server`, you can use any Budibase CLI command via yarn:

Loading…
Cancel
Save