1
0
Fork 0
mirror of synced 2024-07-10 00:35:57 +12:00

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

This commit is contained in:
Michael Shanks 2020-01-28 11:00:56 +00:00
commit 511834492d
43 changed files with 1164 additions and 1019 deletions

2
.gitignore vendored
View file

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

View file

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

File diff suppressed because one or more lines are too long

View file

@ -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
}

View file

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

View file

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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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';

View file

@ -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>

View file

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

View file

@ -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>

View file

@ -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>

View file

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

View file

@ -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}

View file

@ -8,6 +8,7 @@ import {
groupBy, keys, find, sortBy groupBy, keys, find, sortBy
} from "lodash/fp"; } from "lodash/fp";
import { pipe } from "../common/core"; import { pipe } from "../common/core";
import { ImageIcon, InputIcon, LayoutIcon } from '../common/Icons/';
let componentLibraries=[]; let componentLibraries=[];
@ -26,12 +27,10 @@ const addRootComponent = (c, all) => {
} }
group.components.push(c) group.components.push(c)
}; };
const onComponentChosen = (component) => { const onComponentChosen = store.addChildComponent;
};
store.subscribe(s => { store.subscribe(s => {
@ -39,16 +38,14 @@ store.subscribe(s => {
for(let comp of sortBy(["name"])(s.components)) { for(let comp of sortBy(["name"])(s.components)) {
addRootComponent( addRootComponent(
comp, comp,
newComponentLibraries); newComponentLibraries);
} }
componentLibraries = newComponentLibraries; componentLibraries = newComponentLibraries;
}); });
let current_view = 'text';
</script> </script>
@ -59,22 +56,31 @@ store.subscribe(s => {
</div> </div>
<div class="library-container"> <div class="library-container">
<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="inner-header">
Components
</div>
{#each lib.components as component}
<div class="component" <div class="component"
on:click={() => onComponentChosen(component)}> on:click={() => onComponentChosen(component.name)}>
<div class="name"> <div class="name">
{splitName(component.name).componentName} {splitName(component.name).componentName}
</div> </div>
<div class="description">
{component.description}
</div>
</div> </div>
{/each} {/each}
@ -117,8 +123,16 @@ store.subscribe(s => {
} }
.component { .component {
padding: 2px 0px; padding: 0 15px;
cursor: pointer; 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 { .component:hover {
@ -126,8 +140,11 @@ store.subscribe(s => {
} }
.component > .name { .component > .name {
color: var(--secondary100); color: #163057;
display: inline-block; display: inline-block;
font-size: 12px;
font-weight: bold;
opacity: 0.6;
} }
.component > .description { .component > .description {
@ -137,6 +154,34 @@ store.subscribe(s => {
margin-left: 10px; 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> </style>

View file

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

View file

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

View file

@ -12,9 +12,10 @@ import {
} from "./pagesParsing/createProps"; } from "./pagesParsing/createProps";
import Button from "../common/Button.svelte"; import Button from "../common/Button.svelte";
import ButtonGroup from "../common/ButtonGroup.svelte"; import ButtonGroup from "../common/ButtonGroup.svelte";
import { LayoutIcon, PaintIcon, TerminalIcon } from '../common/Icons/';
import { import {
cloneDeep, cloneDeep,
join, join,
split, split,
map, map,
@ -50,43 +51,6 @@ store.subscribe(s => {
components = s.components; 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 updateComponent = doChange => {
const newComponent = cloneDeep(component); const newComponent = cloneDeep(component);
doChange(newComponent); doChange(newComponent);
@ -95,86 +59,27 @@ const updateComponent = doChange => {
} }
const onPropsChanged = newProps => { const onPropsChanged = newProps => {
updateComponent(newComponent => updateComponent(newComponent =>
assign(newComponent.props, newProps)); 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> </script>
<div class="root"> <div class="root">
<div class="title"> <ul>
<div>{shortName}</div> <li><button><PaintIcon /></button></li>
<div> <li><button><LayoutIcon /></button></li>
<IconButton icon="save" <li><button><TerminalIcon /></button></li>
on:click={save} </ul>
color="var(--secondary100)"
hoverColor="var(--primary100)"/>
<IconButton icon="trash"
on:click={deleteComponent}
color="var(--secondary100)"
hoverColor="var(--primary100)"/>
</div>
</div>
<div class="component-props-container"> <div class="component-props-container">
<PropsView onValidate={onPropsValidate} <PropsView
{componentInfo} {componentInfo}
{onPropsChanged} /> {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> </div>
@ -186,9 +91,7 @@ const showDialog = () => {
height: 100%; height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
border-style: solid;
border-width: 1px 0 0 0;
border-color: var(--slate);
} }
.title { .title {
@ -213,4 +116,32 @@ const showDialog = () => {
flex: 1 1 auto; flex: 1 1 auto;
overflow-y: 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>

View file

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

View file

@ -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>

View file

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

View file

@ -7,54 +7,70 @@ const getPage = (s, name) => {
return ({name, props}); return ({name, props});
} }
const pages = [{
title: 'Main',
id: 'main'
}, {
title: 'Login',
id: 'unauthenticated'
}]
store.setCurrentPage('main')
</script> </script>
<div class="root"> <div class="root">
<div class="hierarchy-item component" class:selected={$store.currentFrontEndType === "page" && $store.currentPageName === "main"} <select id="page" name="select" on:change={({target}) => store.setCurrentPage(target.value)}>
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"} {#each pages as {title, id}}
on:click|stopPropagation={() => store.setCurrentPage("unauthenticated")}> <option value="{id}">Page: {title}</option>
<span>{@html getIcon("circle", "7")}</span> {/each}
<span class="title">Login</span>
</div>
</select>
<span class="arrow">{@html getIcon("chevron-down","24")}</span>
</div> </div>
<style> <style>
.root { .root {
padding-bottom: 10px; padding-bottom: 10px;
padding-left: 10px;
font-size: .9rem; font-size: .9rem;
color: var(--secondary50); color: var(--secondary50);
font-weight: bold; font-weight: bold;
position: relative;
} }
.hierarchy-item {
cursor: pointer; select {
padding: 5px 0px; 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;
} }
.hierarchy-item:hover { .arrow {
color: var(--secondary100); position: absolute;
} right: 10px;
top: 0;
.component { bottom: 0;
margin-left: 5px; margin: auto;
} width: 30px;
height: 30px;
.selected { pointer-events: none;
color: var(--primary100); color: var(--primary100);
font-weight: bold;
} }
</style>
.title {
margin-left: 10px;
}
</style>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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;
}

View file

@ -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");
});
});

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

View file

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

View file

@ -3,28 +3,22 @@
"@budibase/client@file:../../../client": "@budibase/client@file:../../../client":
version "0.0.3" version "0.0.15"
dependencies: dependencies:
"@nx-js/compiler-util" "^2.0.0" "@nx-js/compiler-util" "^2.0.0"
date-fns "^1.29.0"
lodash "^4.17.15" lodash "^4.17.15"
lunr "^2.3.5" lunr "^2.3.5"
shortid "^2.2.8" shortid "^2.2.8"
svelte "^3.9.2" svelte "^3.9.2"
"@budibase/standard-components@file:../../../standard-components": "@budibase/standard-components@file:../../../standard-components":
version "0.0.5" version "0.0.15"
"@nx-js/compiler-util@^2.0.0": "@nx-js/compiler-util@^2.0.0":
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/@nx-js/compiler-util/-/compiler-util-2.0.0.tgz#c74c12165fa2f017a292bb79af007e8fce0af297" resolved "https://registry.yarnpkg.com/@nx-js/compiler-util/-/compiler-util-2.0.0.tgz#c74c12165fa2f017a292bb79af007e8fce0af297"
integrity sha512-AxSQbwj9zqt8DYPZ6LwZdytqnwfiOEdcFdq4l8sdjkZmU2clTht7RDLCI8xvkp7KqgcNaOGlTeCM55TULWruyQ== 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: lodash@^4.17.15:
version "4.17.15" version "4.17.15"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"

File diff suppressed because one or more lines are too long

View file

@ -8,7 +8,7 @@ export let _bb;
let rootDiv; let rootDiv;
$:{ $:{
if(_bb && rootDiv && _children && _children.length) 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 class="{className}" bind:this={rootDiv}>
</div> </div>

View file

@ -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.. ... keep the server running, and..
1. Open a new console 1. Open a new console
2. `cd packages/builder` 2. `yarn dev`
3. `yarn start` 3. Access the builder on http://localhost:3000
4. 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: Notice that when inside `packages/server`, you can use any Budibase CLI command via yarn: