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

This commit is contained in:
Michael Shanks 2020-01-31 23:12:20 +00:00
commit e25cab15e2
22 changed files with 1276 additions and 99 deletions

File diff suppressed because one or more lines are too long

View File

@ -36,6 +36,7 @@
"dependencies": { "dependencies": {
"@budibase/client": "^0.0.16", "@budibase/client": "^0.0.16",
"@nx-js/compiler-util": "^2.0.0", "@nx-js/compiler-util": "^2.0.0",
"codemirror": "^5.51.0",
"date-fns": "^1.29.0", "date-fns": "^1.29.0",
"feather-icons": "^4.21.0", "feather-icons": "^4.21.0",
"flatpickr": "^4.5.7", "flatpickr": "^4.5.7",

View File

@ -0,0 +1,35 @@
const buildCodeForSingleScreen = (screen) => {
let code = "";
const walkProps = (props) => {
if(props._code && props._code.trim().length > 0) {
code += buildComponentCode(props)
}
if(!props._children) return;
for(let child of props._children) {
walkProps(child);
}
}
walkProps(screen.props);
return code;
}
export const buildCodeForScreens = screens => {
let allfunctions = "";
for(let screen of screens) {
allfunctions += buildCodeForSingleScreen(screen);
}
return (`return ({ ${allfunctions} });`);
}
const buildComponentCode = (componentProps) =>
`"${componentProps._id}" : (render, context) => {
${componentProps._code}
},
`;

View File

@ -22,6 +22,7 @@ import {
import { import {
loadLibs, loadLibUrls, loadGeneratorLibs loadLibs, loadLibUrls, loadGeneratorLibs
} from "./loadComponentLibraries"; } from "./loadComponentLibraries";
import { buildCodeForScreens } from "./buildCodeForScreens";
import { uuid } from './uuid'; import { uuid } from './uuid';
import { generate_screen_css } from './generate_css'; import { generate_screen_css } from './generate_css';
@ -97,6 +98,7 @@ export const getStore = () => {
store.selectComponent = selectComponent(store); store.selectComponent = selectComponent(store);
store.setComponentProp = setComponentProp(store); store.setComponentProp = setComponentProp(store);
store.setComponentStyle = setComponentStyle(store); store.setComponentStyle = setComponentStyle(store);
store.setComponentCode = setComponentCode(store);
return store; return store;
} }
@ -669,7 +671,8 @@ const savePackage = (store, s) => {
s.components, s.components,
s.screens, s.screens,
s.pages.unauthenticated.appBody) s.pages.unauthenticated.appBody)
} },
uiFunctions: buildCodeForScreens(s.screens)
}; };
const data = { const data = {
@ -687,6 +690,7 @@ const setCurrentScreen = store => screenName => {
s.currentFrontEndItem = screen; s.currentFrontEndItem = screen;
s.currentFrontEndType = "screen"; s.currentFrontEndType = "screen";
s.currentComponentInfo = getScreenInfo(s.components, screen); s.currentComponentInfo = getScreenInfo(s.components, screen);
setCurrentScreenFunctions(s);
return s; return s;
}) })
} }
@ -695,8 +699,9 @@ const setCurrentPage = store => pageName => {
store.update(s => { store.update(s => {
s.currentFrontEndType = "page"; s.currentFrontEndType = "page";
s.currentPageName = pageName; s.currentPageName = pageName;
setCurrentScreenFunctions(s);
return s; return s;
}) });
} }
const addChildComponent = store => component => { const addChildComponent = store => component => {
@ -770,3 +775,22 @@ const setComponentStyle = store => (type, name, value) => {
return s; return s;
}) })
} }
const setComponentCode = store => (code) => {
store.update(s => {
s.currentComponentInfo._code = code;
setCurrentScreenFunctions(s);
// save without messing with the store
_save(s.appname, s.currentFrontEndItem, store, s)
return s;
})
}
const setCurrentScreenFunctions = (s) => {
s.currentScreenFunctions =
s.currentFrontEndItem === "screen"
? buildCodeForScreens([s.currentFrontEndItem])
: "({});";
}

View File

@ -0,0 +1,799 @@
import {
hierarchy as hierarchyFunctions,
} from "../../../core/src";
import {
filter, cloneDeep, sortBy,
map, last, keys, concat, keyBy,
find, isEmpty, values,
} from "lodash/fp";
import {
pipe, getNode, validate,
constructHierarchy, templateApi
} from "../common/core";
import { writable } from "svelte/store";
import { defaultPagesObject } from "../userInterface/pagesParsing/defaultPagesObject"
import { buildPropsHierarchy } from "../userInterface/pagesParsing/buildPropsHierarchy"
import api from "./api";
import { isRootComponent, getExactComponent } from "../userInterface/pagesParsing/searchComponents";
import { rename } from "../userInterface/pagesParsing/renameScreen";
import {
getNewComponentInfo, getScreenInfo,
} from "../userInterface/pagesParsing/createProps";
import {
loadLibs, loadLibUrls, loadGeneratorLibs
} from "./loadComponentLibraries";
<<<<<<< HEAD
import { buildCodeForScreens } from "./buildCodeForScreens";
=======
import { uuid } from './uuid';
import { generate_screen_css } from './generate_css';
>>>>>>> master
let appname = "";
export const getStore = () => {
const initial = {
apps: [],
appname: "",
hierarchy: {},
actions: [],
triggers: [],
pages: defaultPagesObject(),
mainUi: {},
unauthenticatedUi: {},
components: [],
currentFrontEndItem: null,
currentComponentInfo: null,
currentFrontEndType: "none",
currentPageName: "",
currentComponentProps: null,
currentNodeIsNew: false,
errors: [],
activeNav: "database",
isBackend: true,
hasAppPackage: false,
accessLevels: { version: 0, levels: [] },
currentNode: null,
libraries: null,
showSettings: false,
useAnalytics: true,
};
const store = writable(initial);
store.initialise = initialise(store, initial);
store.newChildRecord = newRecord(store, false);
store.newRootRecord = newRecord(store, true);
store.selectExistingNode = selectExistingNode(store);
store.newChildIndex = newIndex(store, false);
store.newRootIndex = newIndex(store, true);
store.saveCurrentNode = saveCurrentNode(store);
store.importAppDefinition = importAppDefinition(store);
store.deleteCurrentNode = deleteCurrentNode(store);
store.saveField = saveField(store);
store.deleteField = deleteField(store);
store.saveAction = saveAction(store);
store.deleteAction = deleteAction(store);
store.saveTrigger = saveTrigger(store);
store.deleteTrigger = deleteTrigger(store);
store.saveLevel = saveLevel(store);
store.deleteLevel = deleteLevel(store);
store.setActiveNav = setActiveNav(store);
store.saveScreen = saveScreen(store);
store.refreshComponents = refreshComponents(store);
store.addComponentLibrary = addComponentLibrary(store);
store.renameScreen = renameScreen(store);
store.deleteScreen = deleteScreen(store);
store.setCurrentScreen = setCurrentScreen(store);
store.setCurrentPage = setCurrentPage(store);
store.createScreen = createScreen(store);
store.removeComponentLibrary = removeComponentLibrary(store);
store.addStylesheet = addStylesheet(store);
store.removeStylesheet = removeStylesheet(store);
store.savePage = savePage(store);
store.showFrontend = showFrontend(store);
store.showBackend = showBackend(store);
store.showSettings = showSettings(store);
store.useAnalytics = useAnalytics(store);
store.createGeneratedComponents = createGeneratedComponents(store);
store.addChildComponent = addChildComponent(store);
store.selectComponent = selectComponent(store);
store.setComponentProp = setComponentProp(store);
store.setComponentStyle = setComponentStyle(store);
store.setComponentCode = setComponentCode(store);
return store;
}
export default getStore;
const initialise = (store, initial) => async () => {
appname = window.location.hash
? last(window.location.hash.substr(1).split("/"))
: "";
if (!appname) {
initial.apps = await api.get(`/_builder/api/apps`).then(r => r.json());
initial.hasAppPackage = false;
store.set(initial);
return initial;
}
const pkg = await api.get(`/_builder/api/${appname}/appPackage`)
.then(r => r.json());
initial.libraries = await loadLibs(appname, pkg);
initial.generatorLibraries = await loadGeneratorLibs(appname, pkg);
initial.loadLibraryUrls = () => loadLibUrls(appname, pkg);
initial.appname = appname;
initial.pages = pkg.pages;
initial.hasAppPackage = true;
initial.hierarchy = pkg.appDefinition.hierarchy;
initial.accessLevels = pkg.accessLevels;
initial.screens = values(pkg.screens);
initial.generators = generatorsArray(pkg.components.generators);
initial.components = values(pkg.components.components);
initial.actions = values(pkg.appDefinition.actions);
initial.triggers = pkg.appDefinition.triggers;
if (!!initial.hierarchy && !isEmpty(initial.hierarchy)) {
initial.hierarchy = constructHierarchy(initial.hierarchy);
const shadowHierarchy = createShadowHierarchy(initial.hierarchy);
if (initial.currentNode !== null)
initial.currentNode = getNode(
shadowHierarchy, initial.currentNode.nodeId
);
}
store.set(initial);
return initial;
}
const generatorsArray = generators =>
pipe(generators, [
keys,
filter(k => k !== "_lib"),
map(k => generators[k])
]);
const showSettings = store => show => {
store.update(s => {
s.showSettings = !s.showSettings;
return s;
});
}
const useAnalytics = store => useAnalytics => {
store.update(s => {
s.useAnalytics = !s.useAnalytics;
return s;
});
}
const showBackend = store => () => {
store.update(s => {
s.isBackend = true;
return s;
})
}
const showFrontend = store => () => {
store.update(s => {
s.isBackend = false;
return s;
})
}
const newRecord = (store, useRoot) => () => {
store.update(s => {
s.currentNodeIsNew = true;
const shadowHierarchy = createShadowHierarchy(s.hierarchy);
parent = useRoot ? shadowHierarchy
: getNode(
shadowHierarchy,
s.currentNode.nodeId);
s.errors = [];
s.currentNode = templateApi(shadowHierarchy)
.getNewRecordTemplate(parent, "", true);
return s;
});
}
const selectExistingNode = (store) => (nodeId) => {
store.update(s => {
const shadowHierarchy = createShadowHierarchy(s.hierarchy);
s.currentNode = getNode(
shadowHierarchy, nodeId
);
s.currentNodeIsNew = false;
s.errors = [];
s.activeNav = "database";
return s;
})
}
const newIndex = (store, useRoot) => () => {
store.update(s => {
s.currentNodeIsNew = true;
s.errors = [];
const shadowHierarchy = createShadowHierarchy(s.hierarchy);
parent = useRoot ? shadowHierarchy
: getNode(
shadowHierarchy,
s.currentNode.nodeId);
s.currentNode = templateApi(shadowHierarchy)
.getNewIndexTemplate(parent);
return s;
});
}
const saveCurrentNode = (store) => () => {
store.update(s => {
const errors = validate.node(s.currentNode);
s.errors = errors;
if (errors.length > 0) {
return s;
}
const parentNode = getNode(
s.hierarchy, s.currentNode.parent().nodeId);
const existingNode = getNode(
s.hierarchy, s.currentNode.nodeId);
let index = parentNode.children.length;
if (!!existingNode) {
// remove existing
index = existingNode.parent().children.indexOf(existingNode);
existingNode.parent().children = pipe(existingNode.parent().children, [
filter(c => c.nodeId !== existingNode.nodeId)
]);
}
// should add node into existing hierarchy
const cloned = cloneDeep(s.currentNode);
templateApi(s.hierarchy).constructNode(
parentNode,
cloned
);
const newIndexOfchild = child => {
if (child === cloned) return index;
const currentIndex = parentNode.children.indexOf(child);
return currentIndex >= index ? currentIndex + 1 : currentIndex;
}
parentNode.children = pipe(parentNode.children, [
sortBy(newIndexOfchild)
]);
if (!existingNode && s.currentNode.type === "record") {
const defaultIndex = templateApi(s.hierarchy)
.getNewIndexTemplate(cloned.parent());
defaultIndex.name = `all_${cloned.collectionName}`;
defaultIndex.allowedRecordNodeIds = [cloned.nodeId];
}
s.currentNodeIsNew = false;
savePackage(store, s);
return s;
});
}
const importAppDefinition = store => appDefinition => {
store.update(s => {
s.hierarchy = appDefinition.hierarchy;
s.currentNode = appDefinition.hierarchy.children.length > 0
? appDefinition.hierarchy.children[0]
: null;
s.actions = appDefinition.actions;
s.triggers = appDefinition.triggers;
s.currentNodeIsNew = false;
return s;
});
}
const deleteCurrentNode = store => () => {
store.update(s => {
const nodeToDelete = getNode(s.hierarchy, s.currentNode.nodeId);
s.currentNode = hierarchyFunctions.isRoot(nodeToDelete.parent())
? find(n => n != s.currentNode)
(s.hierarchy.children)
: nodeToDelete.parent();
if (hierarchyFunctions.isRecord(nodeToDelete)) {
nodeToDelete.parent().children = filter(c => c.nodeId !== nodeToDelete.nodeId)
(nodeToDelete.parent().children);
} else {
nodeToDelete.parent().indexes = filter(c => c.nodeId !== nodeToDelete.nodeId)
(nodeToDelete.parent().indexes);
}
s.errors = [];
savePackage(store, s);
return s;
});
}
const saveField = databaseStore => (field) => {
databaseStore.update(db => {
db.currentNode.fields = filter(f => f.name !== field.name)
(db.currentNode.fields);
templateApi(db.hierarchy).addField(db.currentNode, field);
return db;
});
}
const deleteField = databaseStore => field => {
databaseStore.update(db => {
db.currentNode.fields = filter(f => f.name !== field.name)
(db.currentNode.fields);
return db;
});
}
const saveAction = store => (newAction, isNew, oldAction = null) => {
store.update(s => {
const existingAction = isNew
? null
: find(a => a.name === oldAction.name)(s.actions);
if (existingAction) {
s.actions = pipe(s.actions, [
map(a => a === existingAction ? newAction : a)
]);
} else {
s.actions.push(newAction);
}
savePackage(store, s);
return s;
});
}
const deleteAction = store => action => {
store.update(s => {
s.actions = filter(a => a.name !== action.name)(s.actions);
savePackage(store, s);
return s;
});
}
const saveTrigger = store => (newTrigger, isNew, oldTrigger = null) => {
store.update(s => {
const existingTrigger = isNew
? null
: find(a => a.name === oldTrigger.name)(s.triggers);
if (existingTrigger) {
s.triggers = pipe(s.triggers, [
map(a => a === existingTrigger ? newTrigger : a)
]);
} else {
s.triggers.push(newTrigger);
}
savePackage(store, s);
return s;
});
}
const deleteTrigger = store => trigger => {
store.update(s => {
s.triggers = filter(t => t.name !== trigger.name)(s.triggers);
return s;
});
}
const incrementAccessLevelsVersion = (s) =>
s.accessLevels.version = (s.accessLevels.version || 0) + 1;
const saveLevel = store => (newLevel, isNew, oldLevel = null) => {
store.update(s => {
const levels = s.accessLevels.levels;
const existingLevel = isNew
? null
: find(a => a.name === oldLevel.name)(levels);
if (existingLevel) {
s.accessLevels.levels = pipe(levels, [
map(a => a === existingLevel ? newLevel : a)
]);
} else {
s.accessLevels.levels.push(newLevel);
}
incrementAccessLevelsVersion(s);
savePackage(store, s);
return s;
});
}
const deleteLevel = store => level => {
store.update(s => {
s.accessLevels.levels = filter(t => t.name !== level.name)(s.accessLevels.levels);
incrementAccessLevelsVersion(s);
savePackage(store, s);
return s;
});
}
const setActiveNav = store => navName => {
store.update(s => {
s.activeNav = navName;
return s;
});
}
const createShadowHierarchy = hierarchy =>
constructHierarchy(JSON.parse(JSON.stringify(hierarchy)));
const saveScreen = store => (screen) => {
store.update(s => {
return _saveScreen(store, s, screen);
})
};
const _saveScreen = (store, s, screen) => {
const screens = pipe(s.screens, [
filter(c => c.name !== screen.name),
concat([screen])
]);
s.screens = screens;
s.currentFrontEndItem = screen;
s.currentComponentInfo = getScreenInfo(
s.components, screen);
api.post(`/_builder/api/${s.appname}/screen`, screen)
.then(() => savePackage(store, s));
return s;
}
const _save = (appname, screen, store, s) =>
api.post(`/_builder/api/${appname}/screen`, screen)
.then(() => savePackage(store, s));
const createScreen = store => (screenName, layoutComponentName) => {
store.update(s => {
const newComponentInfo = getNewComponentInfo(
s.components, layoutComponentName, screenName);
s.currentFrontEndItem = newComponentInfo.component;
s.currentComponentInfo = newComponentInfo;
s.currentFrontEndType = "screen";
return _saveScreen(store, s, newComponentInfo.component);
});
};
const createGeneratedComponents = store => components => {
store.update(s => {
s.components = [...s.components, ...components];
s.screens = [...s.screens, ...components];
const doCreate = async () => {
for (let c of components) {
await api.post(`/_builder/api/${s.appname}/screen`, c);
}
await savePackage(store, s);
}
doCreate();
return s;
});
};
const deleteScreen = store => name => {
store.update(s => {
const components = pipe(s.components, [
filter(c => c.name !== name)
]);
const screens = pipe(s.screens, [
filter(c => c.name !== name)
]);
s.components = components;
s.screens = screens;
if (s.currentFrontEndItem.name === name) {
s.currentFrontEndItem = null;
s.currentFrontEndType = "";
}
api.delete(`/_builder/api/${s.appname}/screen/${name}`);
return s;
})
}
const renameScreen = store => (oldname, newname) => {
store.update(s => {
const {
screens, pages, error, changedScreens
} = rename(s.pages, s.screens, oldname, newname);
if (error) {
// should really do something with this
return s;
}
s.screens = screens;
s.pages = pages;
if (s.currentFrontEndItem.name === oldname)
s.currentFrontEndItem.name = newname;
const saveAllChanged = async () => {
for (let screenName of changedScreens) {
const changedScreen
= getExactComponent(screens, screenName);
await api.post(`/_builder/api/${s.appname}/screen`, changedScreen);
}
}
api.patch(`/_builder/api/${s.appname}/screen`, {
oldname, newname
})
.then(() => saveAllChanged())
.then(() => {
savePackage(store, s);
});
return s;
})
}
const savePage = store => async page => {
store.update(s => {
if (s.currentFrontEndType !== "page" || !s.currentPageName) {
return s;
}
s.pages[s.currentPageName] = page;
savePackage(store, s);
return s;
});
}
const addComponentLibrary = store => async lib => {
const response =
await api.get(`/_builder/api/${appname}/componentlibrary?lib=${encodeURI(lib)}`, undefined, false);
const success = response.status === 200;
const error = response.status === 404
? `Could not find library ${lib}`
: success
? ""
: response.statusText;
const components = success
? await response.json()
: [];
store.update(s => {
if (success) {
const componentsArray = [];
for (let c in components) {
componentsArray.push(components[c]);
}
s.components = pipe(s.components, [
filter(c => !c.name.startsWith(`${lib}/`)),
concat(componentsArray)
]);
s.pages.componentLibraries.push(lib);
savePackage(store, s);
}
return s;
})
}
const removeComponentLibrary = store => lib => {
store.update(s => {
s.pages.componentLibraries = filter(l => l !== lib)(
s.pages.componentLibraries);
savePackage(store, s);
return s;
})
}
const addStylesheet = store => stylesheet => {
store.update(s => {
s.pages.stylesheets.push(stylesheet);
savePackage(store, s);
return s;
})
}
const removeStylesheet = store => stylesheet => {
store.update(s => {
s.pages.stylesheets = filter(s => s !== stylesheet)(s.pages.stylesheets);
savePackage(store, s);
return s;
});
}
const refreshComponents = store => async () => {
const componentsAndGenerators =
await api.get(`/_builder/api/${db.appname}/components`).then(r => r.json());
const components = pipe(componentsAndGenerators.components, [
keys,
map(k => ({ ...componentsAndGenerators[k], name: k }))
]);
store.update(s => {
s.components = pipe(s.components, [
filter(c => !isRootComponent(c)),
concat(components)
]);
s.generators = componentsAndGenerators.generators;
return s;
});
};
const savePackage = (store, s) => {
const appDefinition = {
hierarchy: s.hierarchy,
triggers: s.triggers,
actions: keyBy("name")(s.actions),
props: {
main: buildPropsHierarchy(
s.components,
s.screens,
s.pages.main.appBody),
unauthenticated: buildPropsHierarchy(
s.components,
s.screens,
s.pages.unauthenticated.appBody)
},
uiFunctions: buildCodeForScreens(s.screens)
};
const data = {
appDefinition,
accessLevels: s.accessLevels,
pages: s.pages,
}
return api.post(`/_builder/api/${s.appname}/appPackage`, data);
}
const setCurrentScreen = store => screenName => {
store.update(s => {
const screen = getExactComponent(s.screens, screenName);
s.currentFrontEndItem = screen;
s.currentFrontEndType = "screen";
s.currentComponentInfo = getScreenInfo(s.components, screen);
setCurrentScreenFunctions(s);
return s;
})
}
const setCurrentPage = store => pageName => {
store.update(s => {
s.currentFrontEndType = "page";
s.currentPageName = pageName;
setCurrentScreenFunctions(s);
return s;
});
}
const addChildComponent = store => component => {
store.update(s => {
const newComponent = getNewComponentInfo(
s.components, component);
let children = s.currentComponentInfo.component ?
s.currentComponentInfo.component.props._children :
s.currentComponentInfo._children;
const component_definition = Object.assign(
cloneDeep(newComponent.fullProps), {
_component: component,
_styles: { position: {}, layout: {} },
_id: uuid()
})
if (children) {
if (s.currentComponentInfo.component) {
s.currentComponentInfo.component.props._children = children.concat(component_definition);
} else {
s.currentComponentInfo._children = children.concat(component_definition)
}
} else {
if (s.currentComponentInfo.component) {
s.currentComponentInfo.component.props._children = [component_definition];
} else {
s.currentComponentInfo._children = [component_definition]
}
}
_saveScreen(store, s, s.currentFrontEndItem);
_saveScreen(store, s, s.currentFrontEndItem);
return s;
})
}
const selectComponent = store => component => {
store.update(s => {
s.currentComponentInfo = component;
return s;
})
}
const setComponentProp = 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;
})
}
const setComponentStyle = store => (type, name, value) => {
store.update(s => {
if (!s.currentComponentInfo._styles) {
s.currentComponentInfo._styles = {};
}
s.currentComponentInfo._styles[type][name] = value;
s.currentFrontEndItem._css = generate_screen_css(s.currentFrontEndItem.props._children)
// save without messing with the store
_save(s.appname, s.currentFrontEndItem, store, s)
return s;
})
}
const setComponentCode = store => (code) => {
store.update(s => {
s.currentComponentInfo._code = code;
setCurrentScreenFunctions(s);
// save without messing with the store
_save(s.appname, s.currentFrontEndItem, store, s)
return s;
})
}
const setCurrentScreenFunctions = (s) => {
s.currentScreenFunctions =
s.currentFrontEndItem === "screen"
? buildCodeForScreens([s.currentFrontEndItem])
: "({});";
}

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 8 8" width="8" height="8">
<circle cx="4" cy="4" r="4" stroke-width="0" fill="currentColor" />
</svg>

After

Width:  |  Height:  |  Size: 158 B

View File

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

View File

@ -0,0 +1,12 @@
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';
export { default as ArrowDownIcon } from './ArrowDown.svelte';
<<<<<<< HEAD
export { default as CircleIndicator } from './CircleIndicator.svelte';
=======
export { default as EventsIcon } from './Events.svelte';
export { default as PencilIcon } from './Pencil.svelte';
>>>>>>> master

View File

@ -8,6 +8,8 @@
<link rel='icon' type='image/png' href='/_builder/favicon.png'> <link rel='icon' type='image/png' href='/_builder/favicon.png'>
<link rel='stylesheet' href='/_builder/global.css'> <link rel='stylesheet' href='/_builder/global.css'>
<link rel='stylesheet' href='/_builder/codemirror.css'>
<link rel='stylesheet' href='/_builder/monokai.css'>
<link rel='stylesheet' href='/_builder/bundle.css'> <link rel='stylesheet' href='/_builder/bundle.css'>
<link rel='stylesheet' href='/_builder/fonts.css'> <link rel='stylesheet' href='/_builder/fonts.css'>
<link rel='stylesheet' href="/_builder/uikit.min.css"> <link rel='stylesheet' href="/_builder/uikit.min.css">

View File

@ -10,6 +10,8 @@ import "/assets/budibase-logo.png";
import "/assets/budibase-logo-only.png"; import "/assets/budibase-logo-only.png";
import "uikit/dist/css/uikit.min.css"; import "uikit/dist/css/uikit.min.css";
import "uikit/dist/js/uikit.min.js"; import "uikit/dist/js/uikit.min.js";
import "codemirror/lib/codemirror.css";
import 'codemirror/theme/monokai.css';
const app = new App({ const app = new App({
target: document.getElementById("app") target: document.getElementById("app")

View File

@ -1,47 +1,83 @@
<script> <script>
let snippets = []; import { store } from "../builderStore/store";
let current_snippet = 0; import UIkit from "uikit";
let snippet_text = '' import Button from "../common/Button.svelte";
let id = 0; import ButtonGroup from "../common/ButtonGroup.svelte";
import CodeMirror from "codemirror";
import "codemirror/mode/javascript/javascript.js";
function save_snippet() { export let onCodeChanged;
if (!snippet_text) return; export let code;
const index = snippets.findIndex(({ id }) => current_snippet === id); export const show = () => {
UIkit.modal(codeModal).show();
}
if (index > -1) { let codeModal;
snippets[index].snippet = snippet_text; let editor;
} else { let cmInstance;
snippets = snippets.concat({ snippet: snippet_text , id: id });
$: currentCode = code;
$: originalCode = code;
$: {
if(editor) {
if(!cmInstance) {
cmInstance = CodeMirror.fromTextArea(editor, {
mode: 'javascript',
lineNumbers: false,
lineWrapping: true,
smartIndent: true,
matchBrackets: true,
readOnly: false
});
cmInstance.on("change", () => currentCode = cmInstance.getValue());
} }
snippet_text = ''; cmInstance.focus();
current_snippet = ++id; cmInstance.setValue(code);
} }
}
const cancel = () => {
UIkit.modal(codeModal).hide();
currentCode = originalCode;
}
const save = () => {
originalCode = currentCode;
onCodeChanged(currentCode);
UIkit.modal(codeModal).hide();
}
function edit_snippet(id) {
const { snippet, id: _id } = snippets.find(({ id:_id }) => _id === id);
current_snippet = id
snippet_text = snippet;
}
</script> </script>
<h3>Code</h3>
<p>Use the code box below to add snippets of javascript to enhance your webapp</p> <div bind:this={codeModal} uk-modal>
<div class="uk-modal-dialog" uk-overflow-auto>
<div class="editor"> <div class="uk-modal-header">
<textarea class="code" bind:value={snippet_text} /> <h3>Code</h3>
<button on:click={save_snippet}>Save</button> </div>
</div>
<div class="snippets"> <div class="uk-modal-body uk-form-horizontal" >
<h3>Snippets added</h3>
{#each snippets as { id, snippet } } <p>Use the code box below to control how this component is displayed, with javascript.</p>
<div class="snippet">
<pre class="code">{snippet}</pre> <div>
<button on:click={() => edit_snippet(id)}>Edit</button> <div class="editor-code-surround">function(render, context, store) {"{"}</div>
</div> <div class="editor">
{/each} <textarea bind:this={editor}></textarea>
</div>
<div class="editor-code-surround">
{"}"}
</div>
</div>
</div>
<ButtonGroup style="float: right;">
<Button color="primary" grouped on:click={save}>Save</Button>
<Button color="tertiary" grouped on:click={cancel}>Close</Button>
</ButtonGroup>
</div>
</div> </div>
<style> <style>
@ -60,60 +96,14 @@
} }
.editor { .editor {
position: relative; border-style: dotted;
border-width: 1px;
border-color: gainsboro;
padding: 10px 30px;
} }
.code { .editor-code-surround {
width: 100%; font-family: "Courier New", Courier, monospace;
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> </style>

View File

@ -2,12 +2,13 @@
import PropsView from "./PropsView.svelte"; import PropsView from "./PropsView.svelte";
import { store } from "../builderStore"; import { store } from "../builderStore";
import IconButton from "../common/IconButton.svelte"; import IconButton from "../common/IconButton.svelte";
import { LayoutIcon, PaintIcon, TerminalIcon, EventsIcon } from '../common/Icons/'; import { LayoutIcon, PaintIcon, TerminalIcon, CircleIndicator, EventsIcon } from '../common/Icons/';
import CodeEditor from './CodeEditor.svelte'; import CodeEditor from './CodeEditor.svelte';
import LayoutEditor from './LayoutEditor.svelte'; import LayoutEditor from './LayoutEditor.svelte';
import EventsEditor from "./EventsEditor"; import EventsEditor from "./EventsEditor";
let current_view = 'props'; let current_view = 'props';
let codeEditor;
$: component = $store.currentComponentInfo; $: component = $store.currentComponentInfo;
$: originalName = component.name; $: originalName = component.name;
@ -34,7 +35,12 @@
</button> </button>
</li> </li>
<li> <li>
<button class:selected={current_view === 'code'} on:click={() => current_view = 'code'}> <button class:selected={current_view === 'code'} on:click={() => codeEditor && codeEditor.show()}>
{#if componentInfo._code && componentInfo._code.trim().length > 0}
<div class="button-indicator">
<CircleIndicator />
</div>
{/if}
<TerminalIcon /> <TerminalIcon />
</button> </button>
</li> </li>
@ -54,10 +60,13 @@
<LayoutEditor {onStyleChanged} {componentInfo}/> <LayoutEditor {onStyleChanged} {componentInfo}/>
{:else if current_view === 'events'} {:else if current_view === 'events'}
<EventsEditor {componentInfo} {components} {onPropChanged} /> <EventsEditor {componentInfo} {components} {onPropChanged} />
{:else}
<CodeEditor />
{/if} {/if}
<CodeEditor
bind:this={codeEditor}
code={$store.currentComponentInfo._code}
onCodeChanged={store.setComponentCode} />
</div> </div>
{:else} {:else}
<h1> This is a screen, this will be dealt with later</h1> <h1> This is a screen, this will be dealt with later</h1>
@ -113,6 +122,7 @@ li button {
padding: 12px; padding: 12px;
outline: none; outline: none;
cursor: pointer; cursor: pointer;
position: relative;
} }
.selected { .selected {
@ -120,4 +130,11 @@ li button {
background: var(--background-button)!important; background: var(--background-button)!important;
} }
.button-indicator {
position: absolute;
top: 8px;
right: 10px;
color: var(--button-text);
}
</style> </style>

View File

@ -0,0 +1,149 @@
<script>
import PropsView from "./PropsView.svelte";
import { store } from "../builderStore";
import IconButton from "../common/IconButton.svelte";
<<<<<<< HEAD
import { LayoutIcon, PaintIcon, TerminalIcon, CircleIndicator } from '../common/Icons/';
=======
import { LayoutIcon, PaintIcon, TerminalIcon, EventsIcon } from '../common/Icons/';
>>>>>>> master
import CodeEditor from './CodeEditor.svelte';
import LayoutEditor from './LayoutEditor.svelte';
import EventsEditor from "./EventsEditor";
let current_view = 'props';
let codeEditor;
$: component = $store.currentComponentInfo;
$: originalName = component.name;
$: name = component.name;
$: description = component.description;
$: componentInfo = $store.currentComponentInfo;
$: components = $store.components;
const onPropChanged = store.setComponentProp;
const onStyleChanged = store.setComponentStyle;
</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={() => codeEditor && codeEditor.show()}>
{#if componentInfo._code && componentInfo._code.trim().length > 0}
<div class="button-indicator">
<CircleIndicator />
</div>
{/if}
<TerminalIcon />
</button>
</li>
<li>
<button class:selected={current_view === 'events'} on:click={() => current_view = 'events'}>
<EventsIcon />
</button>
</li>
</ul>
{#if !componentInfo.component}
<div class="component-props-container">
{#if current_view === 'props'}
<PropsView {componentInfo} {components} {onPropChanged} />
{:else if current_view === 'layout'}
<LayoutEditor {onStyleChanged} {componentInfo}/>
<<<<<<< HEAD
=======
{:else if current_view === 'events'}
<EventsEditor {componentInfo} {components} {onPropChanged} />
{:else}
<CodeEditor />
>>>>>>> master
{/if}
<CodeEditor
bind:this={codeEditor}
code={$store.currentComponentInfo._code}
onCodeChanged={store.setComponentCode} />
</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;
position: relative;
}
.selected {
color: var(--button-text);
background: var(--background-button)!important;
}
.button-indicator {
position: absolute;
top: 6px;
right: 10px;
color: var(--button-text);
}
</style>

View File

@ -24,6 +24,7 @@
hierarchy: $store.hierarchy, hierarchy: $store.hierarchy,
appRootPath: "" appRootPath: ""
}; };
</script> </script>
@ -39,6 +40,8 @@
${stylesheetLinks} ${stylesheetLinks}
<script> <script>
window["##BUDIBASE_APPDEFINITION##"] = ${JSON.stringify(appDefinition)}; window["##BUDIBASE_APPDEFINITION##"] = ${JSON.stringify(appDefinition)};
window["##BUDIBASE_UIFUNCTIONS"] = ${$store.currentScreenFunctions};
import('/_builder/budibase-client.esm.mjs') import('/_builder/budibase-client.esm.mjs')
.then(module => { .then(module => {
module.loadBudibase({ window, localStorage }); module.loadBudibase({ window, localStorage });

View File

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

View File

@ -0,0 +1,74 @@
<script>
import { some, includes, filter } from "lodash/fp";
import Textbox from "../common/Textbox.svelte";
import Dropdown from "../common/Dropdown.svelte";
import PropControl from "./PropControl.svelte";
import IconButton from "../common/IconButton.svelte";
export let componentInfo;
export let onPropChanged = () => {};
export let components;
let errors = [];
let props = {};
<<<<<<< HEAD
const props_to_ignore = ['_component','_children', '_layout', '_code', '_id'];
=======
const props_to_ignore = ['_component','_children', '_styles', '_id'];
>>>>>>> master
$: propDefs = componentInfo && Object.entries(componentInfo).filter(([name])=> !props_to_ignore.includes(name));
function find_type(prop_name) {
if(!componentInfo._component) return;
return components.find(({name}) => name === componentInfo._component).props[prop_name];
}
let setProp = (name, value) => {
onPropChanged(name, value);
}
const fieldHasError = (propName) =>
some(e => e.propName === propName)(errors);
</script>
<div class="root">
<form class="uk-form-stacked form-root">
{#each propDefs as [prop_name, prop_value], index}
<div class="prop-container">
<PropControl {setProp}
{prop_name}
{prop_value}
prop_type={find_type(prop_name)}
{index}
disabled={false} />
</div>
{/each}
</form>
</div>
<style>
.root {
font-size:10pt;
width: 100%;
}
.form-root {
display: flex;
flex-wrap: wrap;
}
.prop-container {
flex: 1 1 auto;
min-width: 250px;
}
</style>

View File

@ -0,0 +1,64 @@
import { buildCodeForScreens } from "../src/builderStore/buildCodeForScreens";
describe("buildCodeForScreen",() => {
it("should package _code into runnable function, for simple screen props", () => {
const screen = {
props: {
_id: "1234",
_code: "render('render argument');"
}
}
let renderArg;
const render = (arg) => {
renderArg = arg;
}
const uiFunctions = getFunctions(screen);
const targetfunction = uiFunctions[screen.props._id];
expect(targetfunction).toBeDefined();
targetfunction(render);
expect(renderArg).toBe("render argument");
});
it("should package _code into runnable function, for _children ", () => {
const screen = {
props: {
_id: "parent",
_code: "render('parent argument');",
_children: [
{
_id: "child1",
_code: "render('child 1 argument');"
},
{
_id: "child2",
_code: "render('child 2 argument');"
}
]
}
}
let renderArg;
const render = (arg) => {
renderArg = arg;
}
const uiFunctions = getFunctions(screen);
const targetfunction = uiFunctions["child2"];
expect(targetfunction).toBeDefined();
targetfunction(render);
expect(renderArg).toBe("child 2 argument");
})
});
const getFunctions = (screen) =>
new Function(buildCodeForScreens([screen]))();

View File

@ -6,6 +6,8 @@ export const loadBudibase = async ({
window, localStorage, uiFunctions }) => { window, localStorage, uiFunctions }) => {
const appDefinition = window["##BUDIBASE_APPDEFINITION##"]; const appDefinition = window["##BUDIBASE_APPDEFINITION##"];
const uiFunctionsFromWindow = window["##BUDIBASE_APPDEFINITION##"];
uiFunctions = uiFunctionsFromWindow || uiFunctions;
const userFromStorage = localStorage.getItem("budibase:user") const userFromStorage = localStorage.getItem("budibase:user")

View File

@ -48,7 +48,7 @@ export const _initialiseChildren = (initialiseOpts) =>
parentNode: treeNode, parentNode: treeNode,
componentConstructor,uiFunctions, componentConstructor,uiFunctions,
htmlElement, anchor, initialProps, htmlElement, anchor, initialProps,
bb, document}); bb});
for(let comp of renderedComponentsThisIteration) { for(let comp of renderedComponentsThisIteration) {
comp.unsubscribe = bind(comp.component); comp.unsubscribe = bind(comp.component);

View File

@ -2,7 +2,7 @@
export const renderComponent = ({ export const renderComponent = ({
componentConstructor, uiFunctions, componentConstructor, uiFunctions,
htmlElement, anchor, props, htmlElement, anchor, props,
initialProps, bb, document, initialProps, bb,
parentNode}) => { parentNode}) => {
const func = initialProps._id const func = initialProps._id

View File

@ -84,11 +84,9 @@ const buildIndexHtml = async (config, appname, appPath, pages, pageName) => {
const buildClientAppDefinition = async (config, appname, appdefinition, appPath, pages, pageName) => { const buildClientAppDefinition = async (config, appname, appdefinition, appPath, pages, pageName) => {
const appPublicPath = publicPath(appPath, pageName); const appPublicPath = publicPath(appPath, pageName);
const appRootPath = rootPath(config, appname); const appRootPath = rootPath(config, appname);
const componentLibraries = []; const componentLibraries = [];
for(let lib of pages.componentLibraries) { for(let lib of pages.componentLibraries) {
@ -129,6 +127,7 @@ const buildClientAppDefinition = async (config, appname, appdefinition, appPath,
} }
await writeFile(filename, await writeFile(filename,
`window['##BUDIBASE_APPDEFINITION##'] = ${JSON.stringify(clientAppDefObj)}`); `window['##BUDIBASE_APPDEFINITION##'] = ${JSON.stringify(clientAppDefObj)};
window['##BUDIBASE_UIFUNCTIONS##'] = ${appdefinition.uiFunctions}`);
} }

File diff suppressed because one or more lines are too long