Browse Source

Merge branch 'master' of https://github.com/Budibase/budibase into property-panel/components-from-design

pull/286/head
Conor_Mack 6 years ago
parent
commit
2c407d4da7
  1. 2
      packages/builder/package.json
  2. 14
      packages/builder/src/budibase.css
  3. 77
      packages/builder/src/builderStore/generate_css.js
  4. 951
      packages/builder/src/builderStore/store/index.js
  5. 59
      packages/builder/src/builderStore/storeUtils.js
  6. 29
      packages/builder/src/components/common/AppNotification.svelte
  7. 12
      packages/builder/src/components/common/Icons/More.svelte
  8. 1
      packages/builder/src/components/common/Icons/index.js
  9. 5
      packages/builder/src/components/common/Input.svelte
  10. 2
      packages/builder/src/components/common/Inputs/InputGroup.svelte
  11. 77
      packages/builder/src/components/common/Switcher.svelte
  12. 64
      packages/builder/src/components/database/ModelDataTable/modals/CreateEditModel/FieldView.svelte
  13. 21
      packages/builder/src/components/database/ModelDataTable/modals/CreateEditRecord.svelte
  14. 36
      packages/builder/src/components/database/ModelDataTable/modals/RecordFieldControl.svelte
  15. 1
      packages/builder/src/components/nav/BackendNav.svelte
  16. 276
      packages/builder/src/components/userInterface/ComponentDropdownMenu.svelte
  17. 8
      packages/builder/src/components/userInterface/ComponentPropertiesPanel.svelte
  18. 10
      packages/builder/src/components/userInterface/ComponentSelectionList.svelte
  19. 31
      packages/builder/src/components/userInterface/ComponentsHierarchy.svelte
  20. 89
      packages/builder/src/components/userInterface/ComponentsHierarchyChildren.svelte
  21. 57
      packages/builder/src/components/userInterface/FrontendNavigatePane.svelte
  22. 2
      packages/builder/src/components/userInterface/ItemTab/Item.svelte
  23. 1
      packages/builder/src/components/userInterface/ItemTab/Tab.svelte
  24. 85
      packages/builder/src/components/userInterface/PageLayout.svelte
  25. 73
      packages/builder/src/components/userInterface/PagesList.svelte
  26. 36
      packages/builder/src/components/userInterface/UserInterfaceRoot.svelte
  27. 758
      packages/builder/src/components/userInterface/propertyCategories.js
  28. 2
      packages/builder/src/global.css
  29. 2
      packages/builder/src/pages/[application]/_reset.svelte
  30. 11
      packages/builder/src/pages/[application]/backend/_layout.svelte
  31. 122
      packages/builder/src/pages/[application]/frontend/_layout.svelte
  32. 14
      packages/builder/src/pages/_layout.svelte
  33. 4
      packages/builder/tests/generate_css.spec.js
  34. 5
      packages/server/.env.template
  35. 2
      packages/server/package.json
  36. 1
      packages/server/scripts/jestSetup.js
  37. 42
      packages/server/src/api/controllers/record.js
  38. 5
      packages/server/src/api/routes/model.js
  39. 7
      packages/server/src/api/routes/tests/couchTestUtils.js
  40. 47
      packages/server/src/api/routes/tests/record.spec.js
  41. 2
      packages/server/src/app.js
  42. 197
      packages/server/yarn.lock

2
packages/builder/package.json

@ -51,7 +51,7 @@
"safe-buffer": "^5.1.2", "safe-buffer": "^5.1.2",
"shortid": "^2.2.8", "shortid": "^2.2.8",
"string_decoder": "^1.2.0", "string_decoder": "^1.2.0",
"svelte-simple-modal": "^0.3.0", "svelte-simple-modal": "^0.4.2",
"uikit": "^3.1.7" "uikit": "^3.1.7"
}, },
"devDependencies": { "devDependencies": {

14
packages/builder/src/budibase.css

@ -57,23 +57,23 @@
.budibase__nav-item { .budibase__nav-item {
cursor: pointer; cursor: pointer;
padding: 0 7px 0 3px; padding: 0 4px 0 2px;
height: 35px; height: 35px;
margin: 5px 20px 5px 0px; margin: 5px 0px 4px 0px;
border-radius: 0 5px 5px 0; border-radius: 0 5px 5px 0;
display: flex; display: flex;
align-items: center; align-items: center;
font-weight: 500; font-size: 14px;
font-size: 13px; transition: 0.2s;
} }
.budibase__nav-item.selected { .budibase__nav-item.selected {
color: var(--button-text); color: var(--ink);
background: #f1f4fc; background: var(--blue-light);
} }
.budibase__nav-item:hover { .budibase__nav-item:hover {
background: #fafafa; background: var(--grey-light);
} }
.budibase__input { .budibase__input {

77
packages/builder/src/builderStore/generate_css.js

@ -1,42 +1,41 @@
export const generate_screen_css = (component_arr) => { export const generate_screen_css = component_arr => {
let styles = ''; let styles = ""
for (const { _styles, _id, _children, _component } of component_arr) { for (const { _styles, _id, _children, _component } of component_arr) {
let [ componentName ] = _component.match(/[a-z]*$/); let [componentName] = _component.match(/[a-z]*$/)
Object.keys(_styles).forEach((selector) => { Object.keys(_styles).forEach(selector => {
const cssString = generate_css(_styles[selector]); const cssString = generate_css(_styles[selector])
if (cssString) { if (cssString) {
styles += apply_class(_id, componentName, cssString, selector); styles += apply_class(_id, componentName, cssString, selector)
} }
}); })
if (_children && _children.length) { if (_children && _children.length) {
styles += generate_screen_css(_children) + '\n'; styles += generate_screen_css(_children) + "\n"
} }
} }
return styles.trim(); return styles.trim()
}; }
export const generate_css = (style) => { export const generate_css = style => {
let cssString = Object.entries(style).reduce((str, [ key, value ]) => { let cssString = Object.entries(style).reduce((str, [key, value]) => {
//TODO Handle arrays and objects here also if (typeof value === "string") {
if (typeof value === 'string') { if (value) {
if (value) { return (str += `${key}: ${value};\n`)
return (str += `${key}: ${value};\n`); }
} } else if (Array.isArray(value)) {
} else if (Array.isArray(value)) { if (value.length > 0 && !value.every(v => v === "")) {
if (value.length > 0 && !value.every((v) => v === '')) { return (str += `${key}: ${value.join(" ")};\n`)
return (str += `${key}: ${value.join(' ')};\n`); }
} }
} }, "")
}, '');
return (cssString || '').trim(); return (cssString || "").trim()
}; }
export const apply_class = (id, name = 'element', styles, selector) => { export const apply_class = (id, name = "element", styles, selector) => {
if (selector === 'normal') { if (selector === "normal") {
return `.${name}-${id} {\n${styles}\n}`; return `.${name}-${id} {\n${styles}\n}`
} else { } else {
let sel = selector === 'selected' ? '::selection' : `:${selector}`; let sel = selector === "selected" ? "::selection" : `:${selector}`
return `.${name}-${id}${sel} {\n${styles}\n}`; return `.${name}-${id}${sel} {\n${styles}\n}`
} }
}; }

951
packages/builder/src/builderStore/store/index.js

@ -1,107 +1,123 @@
import { cloneDeep, values } from 'lodash/fp';
import { backendUiStore } from 'builderStore';
import * as backendStoreActions from './backend';
import { writable, get } from 'svelte/store';
import api from '../api';
import { DEFAULT_PAGES_OBJECT } from '../../constants';
import { getExactComponent } from 'components/userInterface/pagesParsing/searchComponents';
import { rename } from 'components/userInterface/pagesParsing/renameScreen';
import { createProps, makePropsSafe, getBuiltin } from 'components/userInterface/pagesParsing/createProps';
import { fetchComponentLibDefinitions } from '../loadComponentLibraries';
import { buildCodeForScreens } from '../buildCodeForScreens';
import { generate_screen_css } from '../generate_css';
import { insertCodeMetadata } from '../insertCodeMetadata';
import { uuid } from '../uuid';
export const getStore = () => { import { values } from "lodash/fp"
const initial = { import { backendUiStore } from "builderStore"
apps: [], import * as backendStoreActions from "./backend"
appname: '', import { writable, get } from "svelte/store"
pages: DEFAULT_PAGES_OBJECT, import api from "../api"
mainUi: {}, import { DEFAULT_PAGES_OBJECT } from "../../constants"
unauthenticatedUi: {}, import { getExactComponent } from "components/userInterface/pagesParsing/searchComponents"
components: [], import { rename } from "components/userInterface/pagesParsing/renameScreen"
currentPreviewItem: null, import {
currentComponentInfo: null, createProps,
currentFrontEndType: 'none', makePropsSafe,
currentPageName: '', getBuiltin,
currentComponentProps: null, } from "components/userInterface/pagesParsing/createProps"
errors: [], import { fetchComponentLibDefinitions } from "../loadComponentLibraries"
hasAppPackage: false, import { buildCodeForScreens } from "../buildCodeForScreens"
libraries: null, import { generate_screen_css } from "../generate_css"
appId: '' import { insertCodeMetadata } from "../insertCodeMetadata"
}; import { uuid } from "../uuid"
import {
const store = writable(initial); selectComponent as _selectComponent,
getParent,
store.setPackage = setPackage(store, initial); walkProps,
savePage as _savePage,
store.createDatabaseForApp = backendStoreActions.createDatabaseForApp(store); saveCurrentPreviewItem as _saveCurrentPreviewItem,
saveScreenApi as _saveScreenApi,
store.saveScreen = saveScreen(store); } from "../storeUtils"
store.renameScreen = renameScreen(store);
store.deleteScreen = deleteScreen(store);
store.setCurrentScreen = setCurrentScreen(store);
store.setCurrentPage = setCurrentPage(store);
store.createScreen = createScreen(store);
store.addStylesheet = addStylesheet(store);
store.removeStylesheet = removeStylesheet(store);
store.savePage = savePage(store);
store.addChildComponent = addChildComponent(store);
store.selectComponent = selectComponent(store);
store.setComponentProp = setComponentProp(store);
store.setComponentStyle = setComponentStyle(store);
store.setComponentCode = setComponentCode(store);
store.setScreenType = setScreenType(store);
store.deleteComponent = deleteComponent(store);
store.moveUpComponent = moveUpComponent(store);
store.moveDownComponent = moveDownComponent(store);
store.copyComponent = copyComponent(store);
store.getPathToComponent = getPathToComponent(store);
store.addTemplatedComponent = addTemplatedComponent(store);
store.setMetadataProp = setMetadataProp(store);
return store;
};
export default getStore;
const setPackage = (store, initial) => async (pkg) => {
const [ main_screens, unauth_screens ] = await Promise.all([
api.get(`/_builder/api/${pkg.application._id}/pages/main/screens`).then((r) => r.json()),
api.get(`/_builder/api/${pkg.application._id}/pages/unauthenticated/screens`).then((r) => r.json())
]);
pkg.pages = {
main: {
...pkg.pages.main,
_screens: Object.values(main_screens)
},
unauthenticated: {
...pkg.pages.unauthenticated,
_screens: Object.values(unauth_screens)
}
};
initial.libraries = pkg.application.componentLibraries;
initial.components = await fetchComponentLibDefinitions(pkg.application._id);
initial.appname = pkg.application.name;
initial.appId = pkg.application._id;
initial.pages = pkg.pages;
initial.hasAppPackage = true;
initial.screens = values(pkg.screens);
initial.builtins = [ getBuiltin('##builtin/screenslot') ];
initial.appInstances = pkg.application.instances;
initial.appId = pkg.application._id;
store.set(initial);
return initial;
};
const saveScreen = (store) => (screen) => { export const getStore = () => {
store.update((state) => { const initial = {
return _saveScreen(store, state, screen); apps: [],
}); appname: "",
}; pages: DEFAULT_PAGES_OBJECT,
mainUi: {},
unauthenticatedUi: {},
components: [],
currentPreviewItem: null,
currentComponentInfo: null,
currentFrontEndType: "none",
currentPageName: "",
currentComponentProps: null,
errors: [],
hasAppPackage: false,
libraries: null,
appId: "",
}
const store = writable(initial)
store.setPackage = setPackage(store, initial)
store.createDatabaseForApp = backendStoreActions.createDatabaseForApp(store)
store.saveScreen = saveScreen(store)
store.renameScreen = renameScreen(store)
store.deleteScreen = deleteScreen(store)
store.setCurrentScreen = setCurrentScreen(store)
store.setCurrentPage = setCurrentPage(store)
store.createScreen = createScreen(store)
store.addStylesheet = addStylesheet(store)
store.removeStylesheet = removeStylesheet(store)
store.savePage = savePage(store)
store.addChildComponent = addChildComponent(store)
store.selectComponent = selectComponent(store)
store.setComponentProp = setComponentProp(store)
store.setComponentStyle = setComponentStyle(store)
store.setComponentCode = setComponentCode(store)
store.setScreenType = setScreenType(store)
store.getPathToComponent = getPathToComponent(store)
store.addTemplatedComponent = addTemplatedComponent(store)
store.setMetadataProp = setMetadataProp(store)
return store
}
export default getStore
export const getComponentDefinition = (state, name) =>
name.startsWith("##") ? getBuiltin(name) : state.components[name]
const setPackage = (store, initial) => async pkg => {
const [main_screens, unauth_screens] = await Promise.all([
api
.get(`/_builder/api/${pkg.application._id}/pages/main/screens`)
.then(r => r.json()),
api
.get(`/_builder/api/${pkg.application._id}/pages/unauthenticated/screens`)
.then(r => r.json()),
])
pkg.pages = {
main: {
...pkg.pages.main,
_screens: Object.values(main_screens),
},
unauthenticated: {
...pkg.pages.unauthenticated,
_screens: Object.values(unauth_screens),
},
}
initial.libraries = pkg.application.componentLibraries
initial.components = await fetchComponentLibDefinitions(pkg.application._id)
initial.appname = pkg.application.name
initial.appId = pkg.application._id
initial.pages = pkg.pages
initial.hasAppPackage = true
initial.screens = values(pkg.screens)
initial.builtins = [getBuiltin("##builtin/screenslot")]
initial.appInstances = pkg.application.instances
initial.appId = pkg.application._id
store.set(initial)
return initial
}
const saveScreen = store => screen => {
store.update(state => {
return _saveScreen(store, state, screen)
})
}
const _saveScreen = async (store, s, screen) => { const _saveScreen = async (store, s, screen) => {
const currentPageScreens = s.pages[s.currentPageName]._screens; const currentPageScreens = s.pages[s.currentPageName]._screens;
@ -127,430 +143,349 @@ const _saveScreen = async (store, s, screen) => {
return s; return s;
}; };
const _saveScreenApi = (screen, s) => { const createScreen = store => (screenName, route, layoutComponentName) => {
api.post(`/_builder/api/${s.appId}/pages/${s.currentPageName}/screen`, screen).then(() => _savePage(s)); store.update(state => {
}; const rootComponent = state.components[layoutComponentName]
const createScreen = (store) => (screenName, route, layoutComponentName) => { const newScreen = {
store.update((state) => { name: screenName || "",
const rootComponent = state.components[layoutComponentName]; description: "",
url: "",
const newScreen = { _css: "",
name: screenName || '', uiFunctions: "",
description: '', props: createProps(rootComponent).props,
url: '', }
_css: '',
uiFunctions: '', newScreen.route = route
props: createProps(rootComponent).props state.currentPreviewItem = newScreen
}; state.currentComponentInfo = newScreen.props
state.currentFrontEndType = "screen"
newScreen.route = route;
state.currentPreviewItem = newScreen; _saveScreen(store, state, newScreen)
state.currentComponentInfo = newScreen.props;
state.currentFrontEndType = 'screen'; return state
})
_saveScreen(store, state, newScreen); }
return state; const setCurrentScreen = store => screenName => {
}); store.update(s => {
}; const screen = getExactComponent(s.screens, screenName)
screen._css = generate_screen_css([screen.props])
const setCurrentScreen = (store) => (screenName) => { s.currentPreviewItem = screen
store.update((s) => { s.currentFrontEndType = "screen"
const screen = getExactComponent(s.screens, screenName); s.currentView = "detail"
screen._css = generate_screen_css([ screen.props ]);
s.currentPreviewItem = screen; const safeProps = makePropsSafe(
s.currentFrontEndType = 'screen'; s.components[screen.props._component],
s.currentView = 'detail'; screen.props
)
const safeProps = makePropsSafe(s.components[screen.props._component], screen.props); screen.props = safeProps
screen.props = safeProps; s.currentComponentInfo = safeProps
s.currentComponentInfo = safeProps; setCurrentPageFunctions(s)
setCurrentPageFunctions(s); return s
return s; })
}); }
};
const deleteScreen = store => name => {
const deleteScreen = (store) => (name) => { store.update(s => {
store.update((s) => { const components = s.components.filter(c => c.name !== name)
const components = s.components.filter((c) => c.name !== name); const screens = s.screens.filter(c => c.name !== name)
const screens = s.screens.filter((c) => c.name !== name);
s.components = components
s.components = components; s.screens = screens
s.screens = screens; if (s.currentPreviewItem.name === name) {
if (s.currentPreviewItem.name === name) { s.currentPreviewItem = null
s.currentPreviewItem = null; s.currentFrontEndType = ""
s.currentFrontEndType = ''; }
}
api.delete(`/_builder/api/${s.appId}/screen/${name}`)
api.delete(`/_builder/api/${s.appId}/screen/${name}`);
return s
return s; })
}); }
};
const renameScreen = store => (oldname, newname) => {
const renameScreen = (store) => (oldname, newname) => { store.update(s => {
store.update((s) => { const { screens, pages, error, changedScreens } = rename(
const { screens, pages, error, changedScreens } = rename(s.pages, s.screens, oldname, newname); s.pages,
s.screens,
if (error) { oldname,
// should really do something with this newname
return s; )
}
if (error) {
s.screens = screens; // should really do something with this
s.pages = pages; return s
if (s.currentPreviewItem.name === oldname) s.currentPreviewItem.name = newname; }
const saveAllChanged = async () => { s.screens = screens
for (let screenName of changedScreens) { s.pages = pages
const changedScreen = getExactComponent(screens, screenName); if (s.currentPreviewItem.name === oldname)
await api.post(`/_builder/api/${s.appId}/screen`, changedScreen); s.currentPreviewItem.name = newname
}
}; const saveAllChanged = async () => {
for (let screenName of changedScreens) {
api const changedScreen = getExactComponent(screens, screenName)
.patch(`/_builder/api/${s.appId}/screen`, { await api.post(`/_builder/api/${s.appId}/screen`, changedScreen)
oldname, }
newname }
})
.then(() => saveAllChanged()) api
.then(() => { .patch(`/_builder/api/${s.appId}/screen`, {
_savePage(s); oldname,
}); newname,
})
return s; .then(() => saveAllChanged())
}); .then(() => {
}; _savePage(s)
})
const savePage = (store) => async (page) => {
store.update((state) => { return s
if (state.currentFrontEndType !== 'page' || !state.currentPageName) { })
return state; }
}
const savePage = store => async page => {
state.pages[state.currentPageName] = page; store.update(state => {
_savePage(state); if (state.currentFrontEndType !== "page" || !state.currentPageName) {
return state; return state
}); }
};
state.pages[state.currentPageName] = page
const addStylesheet = (store) => (stylesheet) => { _savePage(state)
store.update((s) => { return state
s.pages.stylesheets.push(stylesheet); })
_savePage(s); }
return s;
}); const addStylesheet = store => stylesheet => {
}; store.update(s => {
s.pages.stylesheets.push(stylesheet)
const removeStylesheet = (store) => (stylesheet) => { _savePage(s)
store.update((state) => { return s
state.pages.stylesheets = state.pages.stylesheets.filter((s) => s !== stylesheet); })
_savePage(state); }
return state;
}); const removeStylesheet = store => stylesheet => {
}; store.update(state => {
state.pages.stylesheets = state.pages.stylesheets.filter(
const _savePage = async (s) => { s => s !== stylesheet
const page = s.pages[s.currentPageName]; )
await api.post(`/_builder/api/${s.appId}/pages/${s.currentPageName}`, { _savePage(state)
page: { componentLibraries: s.pages.componentLibraries, ...page }, return state
uiFunctions: s.currentPageFunctions, })
screens: page._screens }
});
}; const setCurrentPage = store => pageName => {
store.update(state => {
const setCurrentPage = (store) => (pageName) => { const current_screens = state.pages[pageName]._screens
store.update((state) => {
const current_screens = state.pages[pageName]._screens; const currentPage = state.pages[pageName]
const currentPage = state.pages[pageName]; state.currentFrontEndType = "page"
state.currentPageName = pageName
state.currentFrontEndType = 'page'; state.screens = Array.isArray(current_screens)
state.currentPageName = pageName; ? current_screens
state.screens = Array.isArray(current_screens) ? current_screens : Object.values(current_screens); : Object.values(current_screens)
const safeProps = makePropsSafe(state.components[currentPage.props._component], currentPage.props); const safeProps = makePropsSafe(
state.currentComponentInfo = safeProps; state.components[currentPage.props._component],
currentPage.props = safeProps; currentPage.props
state.currentPreviewItem = state.pages[pageName]; )
state.currentPreviewItem._css = generate_screen_css([ state.currentPreviewItem.props ]); state.currentComponentInfo = safeProps
currentPage.props = safeProps
for (let screen of state.screens) { state.currentPreviewItem = state.pages[pageName]
screen._css = generate_screen_css([ screen.props ]); state.currentPreviewItem._css = generate_screen_css([
} state.currentPreviewItem.props,
])
setCurrentPageFunctions(state);
return state; for (let screen of state.screens) {
}); screen._css = generate_screen_css([screen.props])
}; }
// const getComponentDefinition = (components, name) => components.find(c => c.name === name) setCurrentPageFunctions(state)
return state
})
}
/** /**
* @param {string} componentToAdd - name of the component to add to the application * @param {string} componentToAdd - name of the component to add to the application
* @param {string} presetName - name of the component preset if defined * @param {string} presetName - name of the component preset if defined
*/ */
const addChildComponent = (store) => (componentToAdd, presetName) => { const addChildComponent = store => (componentToAdd, presetName) => {
store.update((state) => { store.update(state => {
function findSlot(component_array) { function findSlot(component_array) {
for (let i = 0; i < component_array.length; i += 1) { for (let i = 0; i < component_array.length; i += 1) {
if (component_array[i]._component === '##builtin/screenslot') { if (component_array[i]._component === "##builtin/screenslot") {
return true; return true
} }
if (component_array[i]._children) findSlot(component_array[i]); if (component_array[i]._children) findSlot(component_array[i])
} }
return false; return false
} }
if (componentToAdd.startsWith('##') && findSlot(state.pages[state.currentPageName].props._children)) { if (
return state; componentToAdd.startsWith("##") &&
} findSlot(state.pages[state.currentPageName].props._children)
) {
return state
}
const component = componentToAdd.startsWith('##') const component = getComponentDefinition(state, componentToAdd)
? getBuiltin(componentToAdd)
: state.components[componentToAdd];
const presetProps = presetName ? component.presets[presetName] : {}; const presetProps = presetName ? component.presets[presetName] : {}
const instanceId = get(backendUiStore).selectedDatabase._id; const instanceId = get(backendUiStore).selectedDatabase._id
const newComponent = createProps( const newComponent = createProps(
component, component,
{ {
...presetProps, ...presetProps,
_instanceId: instanceId _instanceId: instanceId,
}, },
state state
); )
state.currentComponentInfo._children = state.currentComponentInfo._children.concat(newComponent.props); state.currentComponentInfo._children = state.currentComponentInfo._children.concat(
newComponent.props
)
state.currentFrontEndType === 'page' ? _savePage(state) : _saveScreenApi(state.currentPreviewItem, state); state.currentFrontEndType === "page"
? _savePage(state)
: _saveScreenApi(state.currentPreviewItem, state)
state.currentView = 'component'; state.currentView = "component"
state.currentComponentInfo = newComponent.props; state.currentComponentInfo = newComponent.props
return state; return state
}); })
}; }
/** /**
* @param {string} props - props to add, as child of current component * @param {string} props - props to add, as child of current component
*/ */
const addTemplatedComponent = (store) => (props) => {
store.update((state) => {
walkProps(props, (p) => {
p._id = uuid();
});
state.currentComponentInfo._children = state.currentComponentInfo._children.concat(props);
state.currentPreviewItem._css = generate_screen_css([ state.currentPreviewItem.props ]);
setCurrentPageFunctions(state);
_saveCurrentPreviewItem(state);
return state;
});
};
const selectComponent = (store) => (component) => {
store.update((state) => {
const componentDef = component._component.startsWith('##') ? component : state.components[component._component];
state.currentComponentInfo = makePropsSafe(componentDef, component);
state.currentView = 'component';
return state;
});
};
const setComponentProp = (store) => (name, value) => {
store.update((state) => {
const current_component = state.currentComponentInfo;
state.currentComponentInfo[name] = value;
_saveCurrentPreviewItem(state);
state.currentComponentInfo = current_component;
return state;
});
};
const setComponentStyle = (store) => (type, name, value) => {
store.update((state) => {
if (!state.currentComponentInfo._styles) {
state.currentComponentInfo._styles = {};
}
state.currentComponentInfo._styles[type][name] = value;
state.currentPreviewItem._css = generate_screen_css([ state.currentPreviewItem.props ]);
// save without messing with the store
_saveCurrentPreviewItem(state);
return state;
});
};
const setComponentCode = (store) => (code) => {
store.update((state) => {
state.currentComponentInfo._code = code;
setCurrentPageFunctions(state);
// save without messing with the store
_saveScreenApi(state.currentPreviewItem, state);
return state;
});
};
const setCurrentPageFunctions = (s) => {
s.currentPageFunctions = buildPageCode(s.screens, s.pages[s.currentPageName]);
insertCodeMetadata(s.currentPreviewItem.props);
};
const buildPageCode = (screens, page) => buildCodeForScreens([ page, ...screens ]);
const setScreenType = (store) => (type) => {
store.update((state) => {
state.currentFrontEndType = type;
const pageOrScreen =
type === 'page' ? state.pages[state.currentPageName] : state.pages[state.currentPageName]._screens[0];
state.currentComponentInfo = pageOrScreen ? pageOrScreen.props : null;
state.currentPreviewItem = pageOrScreen;
return state;
});
};
const deleteComponent = (store) => (componentName) => {
store.update((state) => {
const parent = getParent(state.currentPreviewItem.props, componentName);
if (parent) {
parent._children = parent._children.filter((component) => component !== componentName);
}
_saveCurrentPreviewItem(state);
return state;
});
};
const moveUpComponent = (store) => (component) => {
store.update((s) => {
const parent = getParent(s.currentPreviewItem.props, component);
if (parent) {
const currentIndex = parent._children.indexOf(component);
if (currentIndex === 0) return s;
const newChildren = parent._children.filter((c) => c !== component);
newChildren.splice(currentIndex - 1, 0, component);
parent._children = newChildren;
}
s.currentComponentInfo = component;
_saveCurrentPreviewItem(s);
return s;
});
};
const moveDownComponent = (store) => (component) => {
store.update((s) => {
const parent = getParent(s.currentPreviewItem.props, component);
if (parent) {
const currentIndex = parent._children.indexOf(component);
if (currentIndex === parent._children.length - 1) return s;
const newChildren = parent._children.filter((c) => c !== component);
newChildren.splice(currentIndex + 1, 0, component);
parent._children = newChildren;
}
s.currentComponentInfo = component;
_saveCurrentPreviewItem(s);
return s;
});
};
const copyComponent = (store) => (component) => {
store.update((s) => {
const parent = getParent(s.currentPreviewItem.props, component);
const copiedComponent = cloneDeep(component);
walkProps(copiedComponent, (p) => {
p._id = uuid();
});
parent._children = [ ...parent._children, copiedComponent ];
s.curren;
_saveCurrentPreviewItem(s);
s.currentComponentInfo = copiedComponent;
return s;
});
};
const getPathToComponent = (store) => (component) => {
// Gets all the components to needed to construct a path.
const tempStore = get(store);
let pathComponents = [];
let parent = component;
let root = false;
while (!root) {
parent = getParent(tempStore.currentPreviewItem.props, parent);
if (!parent) {
root = true;
} else {
pathComponents.push(parent);
}
}
// Remove root entry since it's the screen or page layout.
// Reverse array since we need the correct order of the IDs
const reversedComponents = pathComponents.reverse().slice(1);
// Add component
const allComponents = [ ...reversedComponents, component ];
// Map IDs
const IdList = allComponents.map((c) => c._id);
// Construct ID Path:
const path = IdList.join('/');
return path;
};
const getParent = (rootProps, child) => {
let parent;
walkProps(rootProps, (p, breakWalk) => {
if (p._children && p._children.includes(child)) {
parent = p;
breakWalk();
}
});
return parent;
};
const walkProps = (props, action, cancelToken = null) => {
cancelToken = cancelToken || { cancelled: false };
action(props, () => {
cancelToken.cancelled = true;
});
if (props._children) {
for (let child of props._children) {
if (cancelToken.cancelled) return;
walkProps(child, action, cancelToken);
}
}
};
const setMetadataProp = (store) => (name, prop) => {
store.update((s) => {
s.currentPreviewItem[name] = prop;
return s;
});
};
const _saveCurrentPreviewItem = (s) => const addTemplatedComponent = store => props => {
s.currentFrontEndType === 'page' ? _savePage(s) : _saveScreenApi(s.currentPreviewItem, s); store.update(state => {
walkProps(props, p => {
p._id = uuid()
})
state.currentComponentInfo._children = state.currentComponentInfo._children.concat(
props
)
state.currentPreviewItem._css = generate_screen_css([
state.currentPreviewItem.props,
])
setCurrentPageFunctions(state)
_saveCurrentPreviewItem(state)
return state
})
}
const selectComponent = store => component => {
store.update(state => {
return _selectComponent(state, component)
})
}
const setComponentProp = store => (name, value) => {
store.update(state => {
const current_component = state.currentComponentInfo
state.currentComponentInfo[name] = value
_saveCurrentPreviewItem(state)
state.currentComponentInfo = current_component
return state
})
}
const setComponentStyle = store => (type, name, value) => {
store.update(state => {
if (!state.currentComponentInfo._styles) {
state.currentComponentInfo._styles = {}
}
state.currentComponentInfo._styles[type][name] = value
state.currentPreviewItem._css = generate_screen_css([
state.currentPreviewItem.props,
])
// save without messing with the store
_saveCurrentPreviewItem(state)
return state
})
}
const setComponentCode = store => code => {
store.update(state => {
state.currentComponentInfo._code = code
setCurrentPageFunctions(state)
// save without messing with the store
_saveScreenApi(state.currentPreviewItem, state)
return state
})
}
const setCurrentPageFunctions = s => {
s.currentPageFunctions = buildPageCode(s.screens, s.pages[s.currentPageName])
insertCodeMetadata(s.currentPreviewItem.props)
}
const buildPageCode = (screens, page) => buildCodeForScreens([page, ...screens])
const setScreenType = store => type => {
store.update(state => {
state.currentFrontEndType = type
const pageOrScreen =
type === "page"
? state.pages[state.currentPageName]
: state.pages[state.currentPageName]._screens[0]
state.currentComponentInfo = pageOrScreen ? pageOrScreen.props : null
state.currentPreviewItem = pageOrScreen
return state
})
}
const getPathToComponent = store => component => {
// Gets all the components to needed to construct a path.
const tempStore = get(store)
let pathComponents = []
let parent = component
let root = false
while (!root) {
parent = getParent(tempStore.currentPreviewItem.props, parent)
if (!parent) {
root = true
} else {
pathComponents.push(parent)
}
}
// Remove root entry since it's the screen or page layout.
// Reverse array since we need the correct order of the IDs
const reversedComponents = pathComponents.reverse().slice(1)
// Add component
const allComponents = [...reversedComponents, component]
// Map IDs
const IdList = allComponents.map(c => c._id)
// Construct ID Path:
const path = IdList.join("/")
return path
}
const setMetadataProp = store => (name, prop) => {
store.update(s => {
s.currentPreviewItem[name] = prop
return s
})
}

59
packages/builder/src/builderStore/storeUtils.js

@ -0,0 +1,59 @@
import { makePropsSafe } from "components/userInterface/pagesParsing/createProps"
import api from "./api"
export const selectComponent = (state, component) => {
const componentDef = component._component.startsWith("##")
? component
: state.components[component._component]
state.currentComponentInfo = makePropsSafe(componentDef, component)
state.currentView = "component"
return state
}
export const getParent = (rootProps, child) => {
let parent
walkProps(rootProps, (p, breakWalk) => {
if (
p._children &&
(p._children.includes(child) || p._children.some(c => c._id === child))
) {
parent = p
breakWalk()
}
})
return parent
}
export const saveCurrentPreviewItem = s =>
s.currentFrontEndType === "page"
? savePage(s)
: saveScreenApi(s.currentPreviewItem, s)
export const savePage = async s => {
const page = s.pages[s.currentPageName]
await api.post(`/_builder/api/${s.appId}/pages/${s.currentPageName}`, {
page: { componentLibraries: s.pages.componentLibraries, ...page },
uiFunctions: s.currentPageFunctions,
screens: page._screens,
})
}
export const saveScreenApi = (screen, s) => {
api
.post(`/_builder/api/${s.appId}/pages/${s.currentPageName}/screen`, screen)
.then(() => savePage(s))
}
export const walkProps = (props, action, cancelToken = null) => {
cancelToken = cancelToken || { cancelled: false }
action(props, () => {
cancelToken.cancelled = true
})
if (props._children) {
for (let child of props._children) {
if (cancelToken.cancelled) return
walkProps(child, action, cancelToken)
}
}
}

29
packages/builder/src/components/common/AppNotification.svelte

@ -5,7 +5,7 @@
UIKit.notification({ UIKit.notification({
message: ` message: `
<div class="message-container"> <div class="message-container">
<i class="ri-information-fill information-icon"></i> <div class="information-icon">🤯</div>
<span class="notification-message"> <span class="notification-message">
${message} ${message}
</span> </span>
@ -21,6 +21,7 @@
<style> <style>
:global(.information-icon) { :global(.information-icon) {
font-size: 24px; font-size: 24px;
margin-right: 8px;
} }
:global(.uk-nofi) { :global(.uk-nofi) {
@ -31,10 +32,9 @@
} }
:global(.message-container) { :global(.message-container) {
display: grid; display: flex;
grid-template-columns: 40px 1fr auto;
grid-gap: 5px;
align-items: center; align-items: center;
justify-content: center;
} }
:global(.uk-notification) { :global(.uk-notification) {
@ -44,7 +44,6 @@
margin-right: auto !important; margin-right: auto !important;
margin-left: auto !important; margin-left: auto !important;
border-radius: 10px; border-radius: 10px;
box-shadow: 0px 3px 6px #00000029;
} }
:global(.uk-notification-message) { :global(.uk-notification-message) {
@ -56,21 +55,23 @@
} }
:global(.uk-notification-message-danger) { :global(.uk-notification-message-danger) {
background: #f2545b !important; background: var(--ink-light) !important;
color: #fff !important; color: #fff !important;
font-family: Roboto; font-family: Roboto;
font-size: 14px !important; font-size: 16px !important;
} }
:global(.refresh-page-button) { :global(.refresh-page-button) {
font-size: 13px; font-size: 14px;
font-weight: 600; border-radius: 3px;
border-radius: 5px;
border: none; border: none;
padding: 5px; padding: 8px 16px;
width: 91px; color: var(--ink);
height: 28px;
color: #f2545b;
background: #ffffff; background: #ffffff;
margin-left: 20px;
}
:global(.refresh-page-button):hover {
background: var(--grey-light);
} }
</style> </style>

12
packages/builder/src/components/common/Icons/More.svelte

@ -0,0 +1,12 @@
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="24">
<path fill="none" d="M0 0h24v24H0z" />
<path
d="M4.5 10.5c-.825 0-1.5.675-1.5 1.5s.675 1.5 1.5 1.5S6 12.825 6
12s-.675-1.5-1.5-1.5zm15 0c-.825 0-1.5.675-1.5 1.5s.675 1.5 1.5 1.5S21
12.825 21 12s-.675-1.5-1.5-1.5zm-7.5 0c-.825 0-1.5.675-1.5 1.5s.675 1.5 1.5
1.5 1.5-.675 1.5-1.5-.675-1.5-1.5-1.5z" />
</svg>

After

Width:  |  Height:  |  Size: 419 B

1
packages/builder/src/components/common/Icons/index.js

@ -31,3 +31,4 @@ export { default as EmailIcon } from "./Email.svelte"
export { default as TwitterIcon } from "./Twitter.svelte" export { default as TwitterIcon } from "./Twitter.svelte"
export { default as InfoIcon } from "./Info.svelte" export { default as InfoIcon } from "./Info.svelte"
export { default as CloseIcon } from "./Close.svelte" export { default as CloseIcon } from "./Close.svelte"
export { default as MoreIcon } from "./More.svelte"

5
packages/builder/src/components/common/Input.svelte

@ -15,7 +15,7 @@
<style> <style>
input { input {
width: 32px; /* width: 32px; */
height: 32px; height: 32px;
font-size: 12px; font-size: 12px;
font-weight: 700; font-weight: 700;
@ -23,6 +23,9 @@
color: var(--ink); color: var(--ink);
opacity: 0.7; opacity: 0.7;
padding: 0px 4px; padding: 0px 4px;
line-height: 1.3;
/* padding: 12px; */
width: 164px;
box-sizing: border-box; box-sizing: border-box;
border: 1px solid var(--grey); border: 1px solid var(--grey);
border-radius: 2px; border-radius: 2px;

2
packages/builder/src/components/common/Inputs/InputGroup.svelte

@ -6,6 +6,7 @@
export let label = "" export let label = ""
export let value = ["0", "0", "0", "0"] export let value = ["0", "0", "0", "0"]
export let suffix = "" export let suffix = ""
export let onChange = () => {} export let onChange = () => {}
function handleChange(val, idx) { function handleChange(val, idx) {
@ -42,4 +43,5 @@
.inputs-group { .inputs-group {
flex: 1; flex: 1;
} }
</style> </style>

77
packages/builder/src/components/common/Switcher.svelte

@ -0,0 +1,77 @@
<script>
export let tabs = []
export const selectTab = tabName => {
selected = tabName
selectedIndex = tabs.indexOf(selected)
}
let selected = tabs.length > 0 && tabs[0]
let selectedIndex = 0
const isSelected = tab => selected === tab
</script>
<div class="root">
<div class="switcher">
{#each tabs as tab}
<button class:selected={selected === tab} on:click={() => selectTab(tab)}>
{tab}
</button>
{/each}
</div>
<div class="panel">
{#if selectedIndex === 0}
<slot name="0" />
{:else if selectedIndex === 1}
<slot name="1" />
{:else if selectedIndex === 2}
<slot name="2" />
{:else if selectedIndex === 3}
<slot name="3" />
{/if}
</div>
</div>
<style>
.root {
height: 100%;
display: flex;
flex-direction: column;
padding: 20px 20px;
border-left: solid 1px var(--grey);
box-sizing: border-box;
}
.switcher {
display: flex;
margin: 0px 20px 20px 0px;
box-sizing: border-box;
}
.switcher > button {
display: inline-block;
border: none;
margin: 0;
padding: 0;
cursor: pointer;
font-size: 18px;
font-weight: 700;
color: var(--ink-lighter);
margin-right: 20px;
background: none;
}
.switcher > .selected {
color: var(--ink);
}
.panel {
min-height: 0;
overflow-y: auto;
}
</style>

64
packages/builder/src/components/database/ModelDataTable/modals/CreateEditModel/FieldView.svelte

@ -13,17 +13,41 @@
const FIELD_TYPES = ["string", "number", "boolean"] const FIELD_TYPES = ["string", "number", "boolean"]
export let field = { type: "string" } export let field = {
type: "string",
constraints: { type: "string", presence: false },
}
export let schema export let schema
export let goBack export let goBack
let errors = [] let errors = []
let draftField = cloneDeep(field) let draftField = cloneDeep(field)
let type = field.type
let constraints = field.constraints
let required =
field.constraints.presence && !field.constraints.presence.allowEmpty
const save = () => { const save = () => {
constraints.presence = required ? { allowEmpty: false } : false
draftField.constraints = constraints
draftField.type = type
schema[field.name] = draftField schema[field.name] = draftField
goBack() goBack()
} }
$: constraints =
type === "string"
? { type: "string", length: {}, presence: false }
: type === "number"
? { type: "number", presence: false, numericality: {} }
: type === "boolean"
? { type: "boolean", presence: false }
: type === "datetime"
? { type: "date", datetime: {}, presence: false }
: type.startsWith("array")
? { type: "array", presence: false }
: { type: "string", presence: false }
</script> </script>
<div class="root"> <div class="root">
@ -32,32 +56,26 @@
<form on:submit|preventDefault class="uk-form-stacked"> <form on:submit|preventDefault class="uk-form-stacked">
<Textbox label="Name" bind:text={field.name} /> <Textbox label="Name" bind:text={field.name} />
<Dropdown <Dropdown label="Type" bind:selected={type} options={FIELD_TYPES} />
label="Type"
bind:selected={draftField.type}
options={FIELD_TYPES} />
{#if field.type === 'string'} <Checkbox label="Required" bind:checked={required} />
<NumberBox label="Max Length" bind:value={draftField.maxLength} />
<ValuesList label="Categories" bind:values={draftField.values} /> {#if type === 'string'}
{:else if field.type === 'boolean'} <NumberBox label="Max Length" bind:value={constraints.length.maximum} />
<!-- TODO: revisit and fix with JSON schema --> <ValuesList label="Categories" bind:values={constraints.inclusion} />
<Checkbox label="Allow Null" bind:checked={draftField.allowNulls} /> {:else if type === 'datetime'}
{:else if field.format === 'datetime'}
<!-- TODO: revisit and fix with JSON schema -->
<DatePicker label="Min Value" bind:value={draftField.minValue} />
<DatePicker label="Max Value" bind:value={draftField.maxValue} />
{:else if field.type === 'number'}
<NumberBox label="Min Value" bind:value={draftField.minimum} />
<NumberBox label="Max Value" bind:value={draftField.maximum} />
{:else if draftField.type.startsWith('array')}
<!-- TODO: revisit and fix with JSON schema --> <!-- TODO: revisit and fix with JSON schema -->
<DatePicker
label="Min Value"
bind:value={constraints.datetime.earliest} />
<DatePicker label="Max Value" bind:value={constraints.datetime.latest} />
{:else if type === 'number'}
<NumberBox <NumberBox
label="Min Length" label="Min Value"
bind:value={draftField.typeOptions.minLength} /> bind:value={constraints.numericality.greaterThanOrEqualTo} />
<NumberBox <NumberBox
label="Max Length" label="Max Value"
bind:value={draftField.typeOptions.maxLength} /> bind:value={constraints.numericality.lessThanOrEqualTo} />
{/if} {/if}
</form> </form>
</div> </div>

21
packages/builder/src/components/database/ModelDataTable/modals/CreateEditRecord.svelte

@ -8,10 +8,6 @@
import * as api from "../api" import * as api from "../api"
import ErrorsBox from "components/common/ErrorsBox.svelte" import ErrorsBox from "components/common/ErrorsBox.svelte"
const CLASS_NAME_MAP = {
boolean: "uk-checkbox",
}
export let record = {} export let record = {}
export let onClosed export let onClosed
@ -28,14 +24,25 @@
onClosed() onClosed()
} }
const isSelect = meta =>
meta.type === "string" &&
meta.constraints &&
meta.constraints.inclusion &&
meta.constraints.inclusion.length > 0
function determineInputType(meta) { function determineInputType(meta) {
if (meta.type === "datetime") return "date" if (meta.type === "datetime") return "date"
if (meta.type === "number") return "number" if (meta.type === "number") return "number"
if (meta.type === "boolean") return "checkbox" if (meta.type === "boolean") return "checkbox"
if (isSelect(meta)) return "select"
return "text" return "text"
} }
function determineOptions(meta) {
return isSelect(meta) ? meta.constraints.inclusion : []
}
async function saveRecord() { async function saveRecord() {
const recordResponse = await api.saveRecord( const recordResponse = await api.saveRecord(
{ {
@ -46,7 +53,9 @@
$backendUiStore.selectedModel._id $backendUiStore.selectedModel._id
) )
if (recordResponse.errors) { if (recordResponse.errors) {
errors = recordResponse.errors errors = Object.keys(recordResponse.errors)
.map(k => ({ dataPath: k, message: recordResponse.errors[k] }))
.flat()
return return
} }
@ -65,8 +74,8 @@
{#each modelSchema as [key, meta]} {#each modelSchema as [key, meta]}
<div class="uk-margin"> <div class="uk-margin">
<RecordFieldControl <RecordFieldControl
className={CLASS_NAME_MAP[meta.type]}
type={determineInputType(meta)} type={determineInputType(meta)}
options={determineOptions(meta)}
label={key} label={key}
bind:value={record[key]} /> bind:value={record[key]} />
</div> </div>

36
packages/builder/src/components/database/ModelDataTable/modals/RecordFieldControl.svelte

@ -3,10 +3,16 @@
export let value = "" export let value = ""
export let label export let label
export let errors = [] export let errors = []
export let className = "uk-input" export let options = []
let checked = type === "checkbox" ? value : false let checked = type === "checkbox" ? value : false
const determineClassName = type => {
if (type === "checkbox") return "uk-checkbox"
if (type === "select") return "uk-select"
return "uk-input"
}
const handleInput = event => { const handleInput = event => {
if (event.target.type === "checkbox") { if (event.target.type === "checkbox") {
value = event.target.checked value = event.target.checked
@ -23,11 +29,23 @@
</script> </script>
<label>{label}</label> <label>{label}</label>
<input
class={className} {#if type === 'select'}
class:uk-form-danger={errors.length > 0} <select
{checked} class={determineClassName(type)}
{type} bind:value
{value} class:uk-form-danger={errors.length > 0}>
on:input={handleInput} {#each options as opt}
on:change={handleInput} /> <option value={opt}>{opt}</option>
{/each}
</select>
{:else}
<input
class={determineClassName(type)}
class:uk-form-danger={errors.length > 0}
{checked}
{type}
{value}
on:input={handleInput}
on:change={handleInput} />
{/if}

1
packages/builder/src/components/nav/BackendNav.svelte

@ -45,7 +45,6 @@
<DatabasesList /> <DatabasesList />
</div> </div>
</div> </div>
<hr />
{#if $backendUiStore.selectedDatabase._id} {#if $backendUiStore.selectedDatabase._id}
<div class="hierarchy"> <div class="hierarchy">
<div class="components-list-container"> <div class="components-list-container">

276
packages/builder/src/components/userInterface/ComponentDropdownMenu.svelte

@ -0,0 +1,276 @@
<script>
import { MoreIcon } from "components/common/Icons"
import { store } from "builderStore"
import { getComponentDefinition } from "builderStore/store"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import { last, cloneDeep } from "lodash/fp"
import UIkit from "uikit"
import {
selectComponent,
getParent,
walkProps,
saveCurrentPreviewItem,
} from "builderStore/storeUtils"
import { uuid } from "builderStore/uuid"
export let component
let confirmDeleteDialog
let dropdownEl
$: dropdown = UIkit.dropdown(dropdownEl, {
mode: "click",
offset: 0,
pos: "bottom-right",
"delay-hide": 0,
animation: false,
})
$: dropdown && UIkit.util.on(dropdown, "shown", () => (hidden = false))
$: noChildrenAllowed =
!component ||
getComponentDefinition($store, component._component).children === false
$: noPaste =
!$store.componentToPaste || $store.componentToPaste._id === component._id
const lastPartOfName = c => (c ? last(c._component.split("/")) : "")
const hideDropdown = () => {
dropdown.hide()
}
const moveUpComponent = () => {
store.update(s => {
const parent = getParent(s.currentPreviewItem.props, component)
if (parent) {
const currentIndex = parent._children.indexOf(component)
if (currentIndex === 0) return s
const newChildren = parent._children.filter(c => c !== component)
newChildren.splice(currentIndex - 1, 0, component)
parent._children = newChildren
}
s.currentComponentInfo = component
saveCurrentPreviewItem(s)
return s
})
}
const moveDownComponent = () => {
store.update(s => {
const parent = getParent(s.currentPreviewItem.props, component)
if (parent) {
const currentIndex = parent._children.indexOf(component)
if (currentIndex === parent._children.length - 1) return s
const newChildren = parent._children.filter(c => c !== component)
newChildren.splice(currentIndex + 1, 0, component)
parent._children = newChildren
}
s.currentComponentInfo = component
saveCurrentPreviewItem(s)
return s
})
}
const copyComponent = () => {
store.update(s => {
const parent = getParent(s.currentPreviewItem.props, component)
const copiedComponent = cloneDeep(component)
walkProps(copiedComponent, p => {
p._id = uuid()
})
parent._children = [...parent._children, copiedComponent]
saveCurrentPreviewItem(s)
s.currentComponentInfo = copiedComponent
return s
})
}
const deleteComponent = () => {
store.update(state => {
const parent = getParent(state.currentPreviewItem.props, component)
if (parent) {
parent._children = parent._children.filter(c => c !== component)
}
saveCurrentPreviewItem(state)
return state
})
}
const generateNewIdsForComponent = c =>
walkProps(c, p => {
p._id = uuid()
})
const storeComponentForCopy = (cut = false) => {
store.update(s => {
const copiedComponent = cloneDeep(component)
s.componentToPaste = copiedComponent
if (cut) {
const parent = getParent(s.currentPreviewItem.props, component._id)
parent._children = parent._children.filter(c => c._id !== component._id)
selectComponent(s, parent)
}
return s
})
}
const pasteComponent = mode => {
store.update(s => {
if (!s.componentToPaste) return s
const componentToPaste = cloneDeep(s.componentToPaste)
generateNewIdsForComponent(componentToPaste)
delete componentToPaste._cutId
if (mode === "inside") {
component._children.push(componentToPaste)
return s
}
const parent = getParent(s.currentPreviewItem.props, component)
const targetIndex = parent._children.indexOf(component)
const index = mode === "above" ? targetIndex : targetIndex + 1
parent._children.splice(index, 0, cloneDeep(componentToPaste))
saveCurrentPreviewItem(s)
selectComponent(s, componentToPaste)
return s
})
}
</script>
<div class="root" on:click|stopPropagation={() => {}}>
<button>
<MoreIcon />
</button>
<ul class="menu" bind:this={dropdownEl} on:click={hideDropdown}>
<li class="item" on:click={() => confirmDeleteDialog.show()}>
<i class="icon ri-delete-bin-2-line" />
Delete
</li>
<li class="item" on:click={moveUpComponent}>
<i class="icon ri-arrow-up-line" />
Move up
</li>
<li class="item" on:click={moveDownComponent}>
<i class="icon ri-arrow-down-line" />
Move down
</li>
<li class="item" on:click={copyComponent}>
<i class="icon ri-repeat-one-line" />
Duplicate
</li>
<li class="item" on:click={() => storeComponentForCopy(true)}>
<i class="icon ri-scissors-cut-line" />
Cut
</li>
<li class="item" on:click={() => storeComponentForCopy(false)}>
<i class="icon ri-file-copy-line" />
Copy
</li>
<hr class="hr-style" />
<li
class="item"
class:disabled={noPaste}
on:click={() => pasteComponent('above')}>
<i class="icon ri-insert-row-top" />
Paste above
</li>
<li
class="item"
class:disabled={noPaste}
on:click={() => pasteComponent('below')}>
<i class="icon ri-insert-row-bottom" />
Paste below
</li>
<li
class="item"
class:disabled={noPaste || noChildrenAllowed}
on:click={() => pasteComponent('inside')}>
<i class="icon ri-insert-column-right" />
Paste inside
</li>
</ul>
</div>
<ConfirmDialog
bind:this={confirmDeleteDialog}
title="Confirm Delete"
body={`Are you sure you wish to delete this '${lastPartOfName(component)}' component?`}
okText="Delete Component"
onOk={deleteComponent} />
<style>
.root {
overflow: hidden;
z-index: 9;
}
.root button {
border-style: none;
border-radius: 2px;
padding: 5px;
background: transparent;
cursor: pointer;
color: var(--ink);
outline: none;
}
.menu {
z-index: 100000;
overflow: visible;
padding: 12px 0px;
border-radius: 5px;
}
.menu li {
border-style: none;
background-color: transparent;
list-style-type: none;
padding: 4px 16px;
margin: 0;
width: 100%;
box-sizing: border-box;
}
.item {
display: flex;
align-items: center;
font-size: 14px;
}
.icon {
margin-right: 8px;
}
.menu li:not(.disabled) {
cursor: pointer;
color: var(--ink-light);
}
.menu li:not(.disabled):hover {
color: var(--ink);
background-color: var(--grey-light);
}
.disabled {
color: var(--grey-dark);
cursor: default;
}
.hr-style {
margin: 8px 0;
color: var(--grey-dark);
}
</style>

8
packages/builder/src/components/userInterface/ComponentPropertiesPanel.svelte

@ -104,6 +104,12 @@
height: 100%; height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
/* Merge Check */
overflow-x: hidden;
overflow-y: hidden;
padding: 20px;
box-sizing: border-box;
/* Merge Check */
} }
.title > div:nth-child(1) { .title > div:nth-child(1) {
@ -118,5 +124,7 @@
.component-props-container { .component-props-container {
margin-top: 20px; margin-top: 20px;
flex: 1 1 auto; flex: 1 1 auto;
min-height: 0;
overflow-y: auto;
} }
</style> </style>

10
packages/builder/src/components/userInterface/ComponentSelectionList.svelte

@ -1,4 +1,5 @@
<script> <script>
import { goto } from "@sveltech/routify"
import { splitName } from "./pagesParsing/splitRootComponentName.js" import { splitName } from "./pagesParsing/splitRootComponentName.js"
import components from "./temporaryPanelStructure.js" import components from "./temporaryPanelStructure.js"
import ConfirmDialog from "components/common/ConfirmDialog.svelte" import ConfirmDialog from "components/common/ConfirmDialog.svelte"
@ -32,7 +33,14 @@
const onComponentChosen = component => { const onComponentChosen = component => {
store.addChildComponent(component._component) store.addChildComponent(component._component)
toggleTab()
toggleTab("Navigate")
// Get ID path
const path = store.getPathToComponent($store.currentComponentInfo)
// Go to correct URL
$goto(`./:page/:screen/${path}`)
} }
</script> </script>

31
packages/builder/src/components/userInterface/ComponentsHierarchy.svelte

@ -1,7 +1,6 @@
<script> <script>
import { params, goto } from "@sveltech/routify" import { params, goto } from "@sveltech/routify"
import ComponentsHierarchyChildren from "./ComponentsHierarchyChildren.svelte" import ComponentsHierarchyChildren from "./ComponentsHierarchyChildren.svelte"
import { last, sortBy, map, trimCharsStart, trimChars, join } from "lodash/fp" import { last, sortBy, map, trimCharsStart, trimChars, join } from "lodash/fp"
import ConfirmDialog from "components/common/ConfirmDialog.svelte" import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import { pipe } from "components/common/core" import { pipe } from "components/common/core"
@ -36,11 +35,6 @@
sortBy("title"), sortBy("title"),
]) ])
const confirmDeleteComponent = component => {
componentToDelete = component
confirmDeleteDialog.show()
}
const changeScreen = screen => { const changeScreen = screen => {
store.setCurrentScreen(screen.title) store.setCurrentScreen(screen.title)
$goto(`./:page/${screen.title}`) $goto(`./:page/${screen.title}`)
@ -62,9 +56,7 @@
{/if} {/if}
</span> </span>
<span class="icon"> <i class="ri-artboard-2-fill icon" />
<ShapeIcon />
</span>
<span class="title">{screen.title}</span> <span class="title">{screen.title}</span>
</div> </div>
@ -72,41 +64,32 @@
{#if $store.currentPreviewItem.name === screen.title && screen.component.props._children} {#if $store.currentPreviewItem.name === screen.title && screen.component.props._children}
<ComponentsHierarchyChildren <ComponentsHierarchyChildren
components={screen.component.props._children} components={screen.component.props._children}
currentComponent={$store.currentComponentInfo} currentComponent={$store.currentComponentInfo} />
onDeleteComponent={confirmDeleteComponent}
onMoveUpComponent={store.moveUpComponent}
onMoveDownComponent={store.moveDownComponent}
onCopyComponent={store.copyComponent} />
{/if} {/if}
{/each} {/each}
</div> </div>
<ConfirmDialog
bind:this={confirmDeleteDialog}
title="Confirm Delete"
body={`Are you sure you wish to delete this '${lastPartOfName(componentToDelete)}' component?`}
okText="Delete Component"
onOk={() => store.deleteComponent(componentToDelete)} />
<style> <style>
.root { .root {
font-weight: 400; font-weight: 400;
color: #000333; color: var(--ink);
} }
.title { .title {
margin-left: 10px; margin-left: 10px;
margin-top: 2px; margin-top: 2px;
font-size: 13px; font-size: 14px;
font-weight: 400;
} }
.icon { .icon {
display: inline-block; display: inline-block;
transition: 0.2s; transition: 0.2s;
font-size: 24px;
width: 20px; width: 20px;
margin-top: 2px; margin-top: 2px;
color: #333; color: var(--ink-light);
} }
.icon:nth-of-type(2) { .icon:nth-of-type(2) {

89
packages/builder/src/components/userInterface/ComponentsHierarchyChildren.svelte

@ -3,6 +3,7 @@
import { store } from "builderStore" import { store } from "builderStore"
import { last } from "lodash/fp" import { last } from "lodash/fp"
import { pipe } from "components/common/core" import { pipe } from "components/common/core"
import ComponentDropdownMenu from "./ComponentDropdownMenu.svelte"
import { import {
XCircleIcon, XCircleIcon,
ChevronUpIcon, ChevronUpIcon,
@ -14,23 +15,12 @@
export let currentComponent export let currentComponent
export let onSelect = () => {} export let onSelect = () => {}
export let level = 0 export let level = 0
export let onDeleteComponent
export let onMoveUpComponent
export let onMoveDownComponent
export let onCopyComponent
const capitalise = s => s.substring(0, 1).toUpperCase() + s.substring(1) const capitalise = s => s.substring(0, 1).toUpperCase() + s.substring(1)
const get_name = s => (!s ? "" : last(s.split("/"))) const get_name = s => (!s ? "" : last(s.split("/")))
const get_capitalised_name = name => pipe(name, [get_name, capitalise]) const get_capitalised_name = name => pipe(name, [get_name, capitalise])
const moveDownComponent = component => {
const c = component
return () => {
return onMoveDownComponent(c)
}
}
const selectComponent = component => { const selectComponent = component => {
// Set current component // Set current component
store.selectComponent(component) store.selectComponent(component)
@ -50,31 +40,13 @@
class="budibase__nav-item item" class="budibase__nav-item item"
class:selected={currentComponent === component} class:selected={currentComponent === component}
style="padding-left: {level * 20 + 53}px"> style="padding-left: {level * 20 + 53}px">
<div>{get_capitalised_name(component._component)}</div> <div class="nav-item">
<div class="reorder-buttons"> <i class="icon ri-arrow-right-circle-fill" />
{#if index > 0} {get_capitalised_name(component._component)}
<button </div>
class:solo={index === components.length - 1} <div class="actions">
on:click|stopPropagation={() => onMoveUpComponent(component)}> <ComponentDropdownMenu {component} />
<ChevronUpIcon />
</button>
{/if}
{#if index < components.length - 1}
<button
class:solo={index === 0}
on:click|stopPropagation={moveDownComponent(component)}>
<ChevronDownIcon />
</button>
{/if}
</div> </div>
<button
class="copy"
on:click|stopPropagation={() => onCopyComponent(component)}>
<CopyIcon />
</button>
<button on:click|stopPropagation={() => onDeleteComponent(component)}>
<XCircleIcon />
</button>
</div> </div>
{#if component._children} {#if component._children}
@ -82,11 +54,7 @@
components={component._children} components={component._children}
{currentComponent} {currentComponent}
{onSelect} {onSelect}
level={level + 1} level={level + 1} />
{onDeleteComponent}
{onMoveUpComponent}
{onMoveDownComponent}
{onCopyComponent} />
{/if} {/if}
</li> </li>
{/each} {/each}
@ -107,50 +75,37 @@
border-radius: 3px; border-radius: 3px;
height: 35px; height: 35px;
align-items: center; align-items: center;
font-weight: 400;
font-size: 13px;
} }
.item button { .actions {
display: none; display: none;
height: 20px; height: 24px;
width: 28px; width: 24px;
color: var(--slate); color: var(--ink);
padding: 0px 5px; padding: 0px 5px;
border-style: none; border-style: none;
background: rgba(0, 0, 0, 0); background: rgba(0, 0, 0, 0);
cursor: pointer; cursor: pointer;
} position: relative;
.item button.copy {
width: 26px;
} }
.item:hover { .item:hover {
background: #fafafa; background: var(--grey-light);
cursor: pointer; cursor: pointer;
} }
.item:hover button { .item:hover .actions {
display: block; display: block;
} }
.item:hover button:hover { .nav-item {
color: var(--button-text);
}
.reorder-buttons {
display: flex; display: flex;
flex-direction: column; align-items: center;
height: 100%; font-size: 14px;
} color: var(--ink);
.reorder-buttons > button {
flex: 1 1 auto;
width: 30px;
height: 15px;
} }
.reorder-buttons > button.solo { .icon {
padding-top: 2px; color: var(--ink-light);
margin-right: 8px;
} }
</style> </style>

57
packages/builder/src/components/userInterface/FrontendNavigatePane.svelte

@ -0,0 +1,57 @@
<script>
import { store, backendUiStore } from "builderStore"
import ComponentsHierarchy from "components/userInterface/ComponentsHierarchy.svelte"
import PageLayout from "components/userInterface/PageLayout.svelte"
import PagesList from "components/userInterface/PagesList.svelte"
import NewScreen from "components/userInterface/NewScreen.svelte"
const newScreen = () => {
newScreenPicker.show()
}
let newScreenPicker
</script>
<PagesList />
<button class="newscreen" on:click={newScreen}>
<i class="icon ri-add-circle-fill" />
Create New Screen
</button>
<PageLayout layout={$store.pages[$store.currentPageName]} />
<div class="nav-items-container">
<ComponentsHierarchy screens={$store.screens} />
</div>
<NewScreen bind:this={newScreenPicker} />
<style>
.newscreen {
cursor: pointer;
border: 1px solid var(--grey-dark);
border-radius: 3px;
width: 100%;
padding: 8px 16px;
margin: 20px 0px 12px 0px;
display: flex;
justify-content: center;
align-items: center;
background: white;
color: var(--ink);
font-size: 14px;
font-weight: 500;
transition: all 2ms;
}
.newscreen:hover {
background: var(--grey-light);
}
.icon {
color: var(--ink);
font-size: 16px;
margin-right: 4px;
}
</style>

2
packages/builder/src/components/userInterface/ItemTab/Item.svelte

@ -3,7 +3,7 @@
export let item export let item
</script> </script>
<div class="item-item" transition:fly={{ y: 100, duration: 1000 }} on:click> <div class="item-item" in:fly={{ y: 100, duration: 1000 }} on:click>
<div class="item-icon"> <div class="item-icon">
<i class={item.icon} /> <i class={item.icon} />
</div> </div>

1
packages/builder/src/components/userInterface/ItemTab/Tab.svelte

@ -3,7 +3,6 @@
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
import Item from "./Item.svelte" import Item from "./Item.svelte"
import { store } from "builderStore"
export let list export let list
let category = list let category = list

85
packages/builder/src/components/userInterface/PageLayout.svelte

@ -34,86 +34,51 @@
title: lastPartOfName(layout), title: lastPartOfName(layout),
} }
const confirmDeleteComponent = async component => {
componentToDelete = component
confirmDeleteDialog.show()
}
const setCurrentScreenToLayout = () => { const setCurrentScreenToLayout = () => {
store.setScreenType("page") store.setScreenType("page")
$goto("./:page/page-layout") $goto("./:page/page-layout")
} }
</script> </script>
<div class="pagelayoutSection"> <div
<div class="components-nav-page">Page Layout</div> class="budibase__nav-item root"
<div class:selected={$store.currentComponentInfo._id === _layout.component.props._id}
class="budibase__nav-item root" on:click|stopPropagation={setCurrentScreenToLayout}>
class:selected={$store.currentComponentInfo._id === _layout.component.props._id} <span
on:click|stopPropagation={setCurrentScreenToLayout}> class="icon"
<span class:rotate={$store.currentPreviewItem.name !== _layout.title}>
class="icon" <ArrowDownIcon />
class:rotate={$store.currentPreviewItem.name !== _layout.title}> </span>
<ArrowDownIcon /> <i class="ri-layout-3-fill icon-big" />
</span> <span class="title">Master Screen</span>
<span class="icon">
<GridIcon />
</span>
<span class="title">Page Layout</span>
</div>
{#if $store.currentPreviewItem.name === _layout.title && _layout.component.props._children}
<ComponentsHierarchyChildren
thisComponent={_layout.component.props}
components={_layout.component.props._children}
currentComponent={$store.currentComponentInfo}
onDeleteComponent={confirmDeleteComponent}
onMoveUpComponent={store.moveUpComponent}
onMoveDownComponent={store.moveDownComponent}
onCopyComponent={store.copyComponent} />
{/if}
</div> </div>
<ConfirmDialog {#if $store.currentPreviewItem.name === _layout.title && _layout.component.props._children}
bind:this={confirmDeleteDialog} <ComponentsHierarchyChildren
title="Confirm Delete" thisComponent={_layout.component.props}
body={`Are you sure you wish to delete this '${lastPartOfName(componentToDelete)}' component?`} components={_layout.component.props._children}
okText="Delete Component" currentComponent={$store.currentComponentInfo} />
onOk={() => store.deleteComponent(componentToDelete)} /> {/if}
<style> <style>
.components-nav-page {
font-size: 13px;
color: #000333;
text-transform: uppercase;
margin-bottom: 10px;
padding-left: 20px;
font-weight: 600;
opacity: 0.4;
letter-spacing: 1px;
}
.pagelayoutSection {
margin: 20px 0px 20px 0px;
}
.title { .title {
margin-left: 10px; margin-left: 10px;
font-size: 13px; font-size: 14px;
font-weight: 400;
color: var(--ink);
} }
.icon { .icon {
width: 24px;
display: inline-block; display: inline-block;
transition: 0.2s; transition: 0.2s;
width: 20px; width: 20px;
margin-top: 2px; color: var(--ink-light);
color: #000333;
} }
.icon:nth-of-type(2) { .icon-big {
width: 14px; font-size: 24px;
margin: 0 0 0 5px; color: var(--ink-light);
} }
:global(svg) { :global(svg) {

73
packages/builder/src/components/userInterface/PagesList.svelte

@ -1,8 +1,6 @@
<script> <script>
import { params, goto } from "@sveltech/routify" import { params, goto } from "@sveltech/routify"
import { store } from "builderStore" import { store } from "builderStore"
import getIcon from "components/common/icon"
import { CheckIcon } from "components/common/Icons"
const getPage = (s, name) => { const getPage = (s, name) => {
const props = s.pages[name] const props = s.pages[name]
@ -20,7 +18,8 @@
}, },
] ]
store.setCurrentPage($params.page ? $params.page : "main") if (!$store.currentPageName)
store.setCurrentPage($params.page ? $params.page : "main")
const changePage = id => { const changePage = id => {
store.setCurrentPage(id) store.setCurrentPage(id)
@ -29,63 +28,37 @@
</script> </script>
<div class="root"> <div class="root">
<ul> {#each pages as { title, id }}
{#each pages as { title, id }} <button class:active={id === $params.page} on:click={() => changePage(id)}>
<li> {title}
<span class="icon"> </button>
{#if id === $params.page} {/each}
<CheckIcon />
{/if}
</span>
<button
class:active={id === $params.page}
on:click={() => changePage(id)}>
{title}
</button>
</li>
{/each}
</ul>
</div> </div>
<style> <style>
.root { .root {
padding-bottom: 10px; display: flex;
font-size: 0.9rem; flex-direction: row;
color: #000333;
font-weight: bold;
position: relative;
padding-left: 1.8rem;
}
ul {
margin: 0;
padding: 0;
list-style: none;
}
li {
margin: 0.5rem 0;
} }
button { button {
margin: 0 0 0 6px;
padding: 0;
border: none;
font-family: Roboto;
font-size: 13px;
outline: none;
cursor: pointer; cursor: pointer;
background: rgba(0, 0, 0, 0); padding: 8px 16px;
text-align: center;
background: #ffffff;
color: var(--ink-light);
border-radius: 5px;
font-family: Roboto;
font-size: 14px;
font-weight: 400;
transition: all 0.3s;
text-rendering: optimizeLegibility;
border: none !important;
transition: 0.2s;
} }
.active { .active {
font-weight: 500; background: var(--ink-light);
} color: var(--white);
.icon {
display: inline-block;
width: 14px;
color: #000333;
} }
</style> </style>

36
packages/builder/src/components/userInterface/UserInterfaceRoot.svelte

@ -27,11 +27,6 @@
settingsView.show() settingsView.show()
} }
const confirmDeleteComponent = component => {
componentToDelete = component
confirmDeleteDialog.show()
}
const lastPartOfName = c => (c ? last(c.split("/")) : "") const lastPartOfName = c => (c ? last(c.split("/")) : "")
</script> </script>
@ -42,7 +37,6 @@
<div class="pages-list-container"> <div class="pages-list-container">
<div class="nav-header"> <div class="nav-header">
<span class="navigator-title">Navigator</span> <span class="navigator-title">Navigator</span>
<div class="border-line" />
<span class="components-nav-page">Pages</span> <span class="components-nav-page">Pages</span>
</div> </div>
@ -52,12 +46,8 @@
</div> </div>
</div> </div>
<div class="border-line" />
<PageLayout layout={$store.pages[$store.currentPageName]} /> <PageLayout layout={$store.pages[$store.currentPageName]} />
<div class="border-line" />
<div class="components-list-container"> <div class="components-list-container">
<div class="nav-group-header"> <div class="nav-group-header">
<span class="components-nav-header" style="margin-top: 0;"> <span class="components-nav-header" style="margin-top: 0;">
@ -91,13 +81,6 @@
<NewScreen bind:this={newScreenPicker} /> <NewScreen bind:this={newScreenPicker} />
<SettingsView bind:this={settingsView} /> <SettingsView bind:this={settingsView} />
<ConfirmDialog
bind:this={confirmDeleteDialog}
title="Confirm Delete"
body={`Are you sure you wish to delete this '${lastPartOfName(componentToDelete)}' component`}
okText="Delete Component"
onOk={() => store.deleteComponent(componentToDelete)} />
<style> <style>
button { button {
cursor: pointer; cursor: pointer;
@ -114,20 +97,10 @@
.root { .root {
display: grid; display: grid;
grid-template-columns: 275px 1fr 300px; grid-template-columns: 300px 1fr 300px;
height: 100%; height: 100%;
width: 100%; width: 100%;
background: #fafafa; background: #fbfbfb;
}
@media only screen and (min-width: 1800px) {
.root {
display: grid;
grid-template-columns: 300px 1fr 300px;
height: 100%;
width: 100%;
background: #fafafa;
}
} }
.ui-nav { .ui-nav {
@ -135,7 +108,6 @@
background-color: var(--white); background-color: var(--white);
height: calc(100vh - 49px); height: calc(100vh - 49px);
padding: 0; padding: 0;
overflow: scroll;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
@ -229,10 +201,6 @@
letter-spacing: 1px; letter-spacing: 1px;
} }
.border-line {
border-bottom: 1px solid #d8d8d8;
}
.components-list-container { .components-list-container {
padding: 20px 0px 0 0; padding: 20px 0px 0 0;
} }

758
packages/builder/src/components/userInterface/propertyCategories.js

@ -1,407 +1,429 @@
import Input from '../common/Input.svelte'; import Input from "../common/Input.svelte"
import OptionSelect from './OptionSelect.svelte'; import OptionSelect from "./OptionSelect.svelte"
import InputGroup from '../common/Inputs/InputGroup.svelte'; import InputGroup from "../common/Inputs/InputGroup.svelte"
import FlatButtonGroup from './FlatButtonGroup.svelte'; import FlatButtonGroup from "./FlatButtonGroup.svelte"
// import Colorpicker from "../common/Colorpicker.svelte" // import Colorpicker from "../common/Colorpicker.svelte"
/* /*
TODO: Allow for default values for all properties TODO: Allow for default values for all properties
*/ */
export const layout = [ export const layout = [
{ {
label: 'Display', label: "Display",
key: 'display', key: "display",
control: OptionSelect, control: OptionSelect,
initialValue: 'Flex', initialValue: "Flex",
options: [ { label: 'Flex', value: 'flex' }, { label: 'Inline Flex', value: 'inline-flex' } ] options: [
}, { label: "Flex", value: "flex" },
{ { label: "Inline Flex", value: "inline-flex" },
label: 'Direction', ],
key: 'flex-direction', },
control: FlatButtonGroup, {
buttonProps: [ label: "Direction",
{ icon: 'ri-arrow-right-line', padding: '0px 5px', value: 'row' }, key: "flex-direction",
{ icon: 'ri-arrow-left-line', padding: '0px 5px', value: 'rowReverse' }, control: FlatButtonGroup,
{ icon: 'ri-arrow-down-line', padding: '0px 5px', value: 'column' }, buttonProps: [
{ { icon: "ri-arrow-right-line", padding: "0px 5px", value: "row" },
icon: 'ri-arrow-up-line', { icon: "ri-arrow-left-line", padding: "0px 5px", value: "rowReverse" },
padding: '0px 5px', { icon: "ri-arrow-down-line", padding: "0px 5px", value: "column" },
value: 'columnReverse' {
} icon: "ri-arrow-up-line",
] padding: "0px 5px",
}, value: "columnReverse",
{ },
label: 'Justify', ],
key: 'justify-content', },
control: OptionSelect, {
initialValue: 'Flex Start', label: "Justify",
options: [ key: "justify-content",
{ label: 'Flex Start', value: 'flex-start' }, control: OptionSelect,
{ label: 'Flex End', value: 'flex-end' }, initialValue: "Flex Start",
{ label: 'Center', value: 'center' }, options: [
{ label: 'Space Between', value: 'space-between' }, { label: "Flex Start", value: "flex-start" },
{ label: 'Space Around', value: 'space-around' }, { label: "Flex End", value: "flex-end" },
{ label: 'Space Evenly', value: 'space-evenly' } { label: "Center", value: "center" },
] { label: "Space Between", value: "space-between" },
}, { label: "Space Around", value: "space-around" },
{ { label: "Space Evenly", value: "space-evenly" },
label: 'Align', ],
key: 'align-items', },
control: OptionSelect, {
initialValue: 'Flex Start', label: "Align",
options: [ key: "align-items",
{ label: 'Flex Start', value: 'flex-start' }, control: OptionSelect,
{ label: 'Flex End', value: 'flex-end' }, initialValue: "Flex Start",
{ label: 'Center', value: 'center' }, options: [
{ label: 'Baseline', value: 'baseline' }, { label: "Flex Start", value: "flex-start" },
{ label: 'Stretch', value: 'stretch' } { label: "Flex End", value: "flex-end" },
] { label: "Center", value: "center" },
}, { label: "Baseline", value: "baseline" },
{ { label: "Stretch", value: "stretch" },
label: 'Wrap', ],
key: 'flex-wrap', },
control: OptionSelect, {
options: [ { label: 'wrap', value: 'wrap' }, { label: 'no wrap', value: 'noWrap' } ] label: "Wrap",
} key: "flex-wrap",
]; control: OptionSelect,
options: [
{ label: "wrap", value: "wrap" },
{ label: "no wrap", value: "noWrap" },
],
},
]
const spacingMeta = [ { placeholder: 'T' }, { placeholder: 'R' }, { placeholder: 'B' }, { placeholder: 'L' } ]; const spacingMeta = [
{ placeholder: "T" },
{ placeholder: "R" },
{ placeholder: "B" },
{ placeholder: "L" },
]
export const spacing = [ export const spacing = [
{ {
label: 'Margin', label: "Margin",
key: 'margin', key: "margin",
control: InputGroup, control: InputGroup,
meta: spacingMeta, meta: spacingMeta,
suffix: 'px', suffix: "px",
defaultValue: [ '0', '0', '0', '0' ] defaultValue: ["0", "0", "0", "0"],
}, },
{ {
label: 'Padding', label: "Padding",
key: 'padding', key: "padding",
control: InputGroup, control: InputGroup,
meta: spacingMeta, meta: spacingMeta,
suffix: 'px', suffix: "px",
defaultValue: [ '0', '0', '0', '0' ] defaultValue: ["0", "0", "0", "0"],
} },
]; ]
export const size = [ export const size = [
{ {
label: 'Width', label: "Width",
key: 'width', key: "width",
control: Input, control: Input,
placeholder: 'px', placeholder: "px",
width: '48px', width: "48px",
textAlign: 'center' textAlign: "center",
}, },
{ {
label: 'Height', label: "Height",
key: 'height', key: "height",
control: Input, control: Input,
placeholder: 'px', placeholder: "px",
width: '48px', width: "48px",
textAlign: 'center' textAlign: "center",
}, },
{ {
label: 'Min W', label: "Min W",
key: 'min-width', key: "min-width",
control: Input, control: Input,
placeholder: 'px', placeholder: "px",
width: '48px', width: "48px",
textAlign: 'center' textAlign: "center",
}, },
{ {
label: 'Min H', label: "Min H",
key: 'min-height', key: "min-height",
control: Input, control: Input,
placeholder: 'px', placeholder: "px",
width: '48px', width: "48px",
textAlign: 'center' textAlign: "center",
}, },
{ {
label: 'Max W', label: "Max W",
key: 'max-width', key: "max-width",
control: Input, control: Input,
placeholder: 'px', placeholder: "px",
width: '48px', width: "48px",
textAlign: 'center' textAlign: "center",
}, },
{ {
label: 'Max H', label: "Max H",
key: 'max-height', key: "max-height",
control: Input, control: Input,
placeholder: 'px', placeholder: "px",
width: '48px', width: "48px",
textAlign: 'center' textAlign: "center",
} },
]; ]
export const position = [ export const position = [
{ {
label: 'Position', label: "Position",
key: 'position', key: "position",
control: OptionSelect, control: OptionSelect,
initialValue: 'Wrap', initialValue: "Wrap",
options: [ options: [
{ label: 'Static', value: 'static' }, { label: "Static", value: "static" },
{ label: 'Relative', value: 'relative' }, { label: "Relative", value: "relative" },
{ label: 'Fixed', value: 'fixed' }, { label: "Fixed", value: "fixed" },
{ label: 'Absolute', value: 'absolute' }, { label: "Absolute", value: "absolute" },
{ label: 'Sticky', value: 'sticky' } { label: "Sticky", value: "sticky" },
] ],
}, },
{ {
label: 'Top', label: "Top",
key: 'top', key: "top",
control: Input, control: Input,
placeholder: 'px', placeholder: "px",
width: '48px', width: "48px",
textAlign: 'center' textAlign: "center",
}, },
{ {
label: 'Right', label: "Right",
key: 'right', key: "right",
control: Input, control: Input,
placeholder: 'px', placeholder: "px",
width: '48px', width: "48px",
textAlign: 'center' textAlign: "center",
}, },
{ {
label: 'Bottom', label: "Bottom",
key: 'bottom', key: "bottom",
control: Input, control: Input,
placeholder: 'px', placeholder: "px",
width: '48px', width: "48px",
textAlign: 'center' textAlign: "center",
}, },
{ {
label: 'Left', label: "Left",
key: 'left', key: "left",
control: Input, control: Input,
placeholder: 'px', placeholder: "px",
width: '48px', width: "48px",
textAlign: 'center' textAlign: "center",
}, },
{ {
label: 'Z-index', label: "Z-index",
key: 'z-index', key: "z-index",
control: Input, control: Input,
placeholder: 'num', placeholder: "num",
width: '48px', width: "48px",
textAlign: 'center' textAlign: "center",
} },
]; ]
export const typography = [ export const typography = [
{ {
label: 'Font', label: "Font",
key: 'font-family', key: "font-family",
control: OptionSelect, control: OptionSelect,
defaultValue: 'initial', defaultValue: "initial",
options: [ options: [
'initial', "initial",
'Arial', "Arial",
'Arial Black', "Arial Black",
'Cursive', "Cursive",
'Courier', "Courier",
'Comic Sans MS', "Comic Sans MS",
'Helvetica', "Helvetica",
'Impact', "Impact",
'Inter', "Inter",
'Lucida Sans Unicode', "Lucida Sans Unicode",
'Open Sans', "Open Sans",
'Playfair', "Playfair",
'Roboto', "Roboto",
'Roboto Mono', "Roboto Mono",
'Times New Roman', "Times New Roman",
'Verdana' "Verdana",
], ],
styleBindingProperty: 'font-family' styleBindingProperty: "font-family",
}, },
{ {
label: 'Weight', label: "Weight",
key: 'font-weight', key: "font-weight",
control: OptionSelect, control: OptionSelect,
options: [ 'normal', 'bold', 'bolder', 'lighter' ] options: ["normal", "bold", "bolder", "lighter"],
}, },
{ {
label: 'size', label: "size",
key: 'font-size', key: "font-size",
defaultValue: '', defaultValue: "",
control: Input, control: Input,
placeholder: 'px', placeholder: "px",
width: '48px', width: "48px",
textAlign: 'center' textAlign: "center",
}, },
{ {
label: 'Line H', label: "Line H",
key: 'line-height', key: "line-height",
control: Input, control: Input,
placeholder: 'lh', placeholder: "lh",
width: '48px', width: "48px",
textAlign: 'center' textAlign: "center",
}, },
{ {
label: 'Color', label: "Color",
key: 'color', key: "color",
control: Input, control: Input,
placeholder: "hex", placeholder: "hex",
}, },
{ {
label: 'align', label: "align",
key: 'text-align', key: "text-align",
control: FlatButtonGroup, control: FlatButtonGroup,
buttonProps: [ buttonProps: [
{ icon: 'ri-align-left', padding: '0px 5px', value: 'left' }, { icon: "ri-align-left", padding: "0px 5px", value: "left" },
{ icon: 'ri-align-center', padding: '0px 5px', value: 'center' }, { icon: "ri-align-center", padding: "0px 5px", value: "center" },
{ icon: 'ri-align-right', padding: '0px 5px', value: 'right' }, { icon: "ri-align-right", padding: "0px 5px", value: "right" },
{ icon: 'ri-align-justify', padding: '0px 5px', value: 'justify' } { icon: "ri-align-justify", padding: "0px 5px", value: "justify" },
] ],
}, },
{ {
label: 'transform', label: "transform",
key: 'text-transform', key: "text-transform",
control: FlatButtonGroup, control: FlatButtonGroup,
buttonProps: [ buttonProps: [
{ text: 'BB', padding: '0px 5px', fontWeight: 500, value: 'uppercase' }, { text: "BB", padding: "0px 5px", fontWeight: 500, value: "uppercase" },
{ text: 'Bb', padding: '0px 5px', fontWeight: 500, value: 'capitalize' }, { text: "Bb", padding: "0px 5px", fontWeight: 500, value: "capitalize" },
{ text: 'bb', padding: '0px 5px', fontWeight: 500, value: 'lowercase' }, { text: "bb", padding: "0px 5px", fontWeight: 500, value: "lowercase" },
{ {
text: '&times;', text: "&times;",
padding: '0px 5px', padding: "0px 5px",
fontWeight: 500, fontWeight: 500,
value: 'none' value: "none",
} },
] ],
}, },
{ label: 'style', key: 'font-style', control: Input } { label: "style", key: "font-style", control: Input },
]; ]
export const background = [ export const background = [
{ {
label: 'Background', label: "Background",
key: 'background', key: "background",
control: Input, control: Input,
}, },
{ {
label: "Image", label: "Image",
key: "background-image", key: "background-image",
control: Input, control: Input,
placeholder: "src", placeholder: "src",
}, },
]; ]
export const border = [ export const border = [
{ {
label: 'Radius', label: "Radius",
key: 'border-radius', key: "border-radius",
control: Input, control: Input,
width: '48px', width: "48px",
placeholder: 'px', placeholder: "px",
textAlign: 'center' textAlign: "center",
}, },
{ {
label: 'Width', label: "Width",
key: 'border-width', key: "border-width",
control: Input, control: Input,
width: '48px', width: "48px",
placeholder: 'px', placeholder: "px",
textAlign: 'center' textAlign: "center",
}, //custom }, //custom
{ {
label: 'Color', label: "Color",
key: 'border-color', key: "border-color",
control: Input control: Input,
}, },
{ {
label: 'Style', label: "Style",
key: 'border-style', key: "border-style",
control: OptionSelect, control: OptionSelect,
options: [ 'none', 'hidden', 'dotted', 'dashed', 'solid', 'double', 'groove', 'ridge', 'inset', 'outset' ] options: [
} "none",
]; "hidden",
"dotted",
"dashed",
"solid",
"double",
"groove",
"ridge",
"inset",
"outset",
],
},
]
export const effects = [ export const effects = [
{ {
label: 'Opacity', label: "Opacity",
key: 'opacity', key: "opacity",
control: Input, control: Input,
width: '48px', width: "48px",
textAlign: 'center', textAlign: "center",
placeholder: '%' placeholder: "%",
}, },
{ {
label: 'Rotate', label: "Rotate",
key: 'transform', key: "transform",
control: Input, control: Input,
width: '48px', width: "48px",
textAlign: 'center', textAlign: "center",
placeholder: 'deg' placeholder: "deg",
}, //needs special control }, //needs special control
{ {
label: "Shadow", label: "Shadow",
key: "box-shadow", key: "box-shadow",
control: InputGroup, control: InputGroup,
meta: [{ placeholder: "X" }, { placeholder: "Y" }, { placeholder: "B" }], meta: [{ placeholder: "X" }, { placeholder: "Y" }, { placeholder: "B" }],
}, },
]; ]
export const transitions = [ export const transitions = [
{ {
label: 'Property', label: "Property",
key: 'transition-property', key: "transition-property",
control: OptionSelect, control: OptionSelect,
options: [ options: [
'None', "None",
'All', "All",
'Background Color', "Background Color",
'Color', "Color",
'Font Size', "Font Size",
'Font Weight', "Font Weight",
'Height', "Height",
'Margin', "Margin",
'Opacity', "Opacity",
'Padding', "Padding",
'Rotate', "Rotate",
'Shadow', "Shadow",
'Width' "Width",
] ],
}, },
{ {
label: 'Duration', label: "Duration",
key: 'transition-timing-function', key: "transition-timing-function",
control: Input, control: Input,
width: '48px', width: "48px",
textAlign: 'center', textAlign: "center",
placeholder: 'sec' placeholder: "sec",
}, },
{ {
label: 'Ease', label: "Ease",
key: 'transition-ease', key: "transition-ease",
control: OptionSelect, control: OptionSelect,
options: [ 'linear', 'ease', 'ease-in', 'ease-out', 'ease-in-out' ] options: ["linear", "ease", "ease-in", "ease-out", "ease-in-out"],
} },
]; ]
export const all = { export const all = {
layout, layout,
spacing, spacing,
size, size,
position, position,
typography, typography,
background, background,
border, border,
effects, effects,
transitions transitions,
}; }
export function excludeProps(props, propsToExclude) { export function excludeProps(props, propsToExclude) {
const modifiedProps = {}; const modifiedProps = {}
for (const prop in props) { for (const prop in props) {
if (!propsToExclude.includes(prop)) { if (!propsToExclude.includes(prop)) {
modifiedProps[prop] = props[prop]; modifiedProps[prop] = props[prop]
} }
} }
return modifiedProps; return modifiedProps
} }

2
packages/builder/src/global.css

@ -88,6 +88,8 @@ html, body {
#app { #app {
height: 100%; height: 100%;
box-sizing: border-box;
overflow-y: hidden;
} }
h1 { h1 {

2
packages/builder/src/pages/[application]/_reset.svelte

@ -81,7 +81,7 @@
<style> <style>
.root { .root {
height: 100%; min-height: 100%;
width: 100%; width: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;

11
packages/builder/src/pages/[application]/backend/_layout.svelte

@ -34,16 +34,7 @@
.nav { .nav {
overflow: auto; overflow: auto;
flex: 0 1 auto; flex: 0 1 auto;
width: 275px; width: 300px;
height: 100%; height: 100%;
} }
@media only screen and (min-width: 1800px) {
.nav {
overflow: auto;
flex: 0 1 auto;
width: 300px;
height: 100%;
}
}
</style> </style>

122
packages/builder/src/pages/[application]/frontend/_layout.svelte

@ -2,18 +2,17 @@
import { store, backendUiStore } from "builderStore" import { store, backendUiStore } from "builderStore"
import { goto } from "@sveltech/routify" import { goto } from "@sveltech/routify"
import { onMount } from "svelte" import { onMount } from "svelte"
import ComponentsHierarchy from "components/userInterface/ComponentsHierarchy.svelte"
import ComponentsHierarchyChildren from "components/userInterface/ComponentsHierarchyChildren.svelte" import ComponentsHierarchyChildren from "components/userInterface/ComponentsHierarchyChildren.svelte"
import PageLayout from "components/userInterface/PageLayout.svelte"
import PagesList from "components/userInterface/PagesList.svelte"
import IconButton from "components/common/IconButton.svelte" import IconButton from "components/common/IconButton.svelte"
import NewScreen from "components/userInterface/NewScreen.svelte"
import CurrentItemPreview from "components/userInterface/AppPreview" import CurrentItemPreview from "components/userInterface/AppPreview"
import PageView from "components/userInterface/PageView.svelte" import PageView from "components/userInterface/PageView.svelte"
import ComponentsPaneSwitcher from "components/userInterface/ComponentsPaneSwitcher.svelte" import ComponentPropertiesPanel from "components/userInterface/ComponentPropertiesPanel.svelte"
import ComponentSelectionList from "components/userInterface/ComponentSelectionList.svelte"
import Switcher from "components/common/Switcher.svelte"
import ConfirmDialog from "components/common/ConfirmDialog.svelte" import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import { last } from "lodash/fp" import { last } from "lodash/fp"
import { AddIcon } from "components/common/Icons" import { AddIcon } from "components/common/Icons"
import FrontendNavigatePane from "components/userInterface/FrontendNavigatePane.svelte"
$: instances = $store.appInstances $: instances = $store.appInstances
@ -27,23 +26,15 @@
} }
}) })
let newScreenPicker
let confirmDeleteDialog let confirmDeleteDialog
let componentToDelete = "" let componentToDelete = ""
const newScreen = () => {
newScreenPicker.show()
}
let settingsView let settingsView
const settings = () => { const settings = () => {
settingsView.show() settingsView.show()
} }
const confirmDeleteComponent = component => { let leftNavSwitcher
componentToDelete = component
confirmDeleteDialog.show()
}
const lastPartOfName = c => (c ? last(c.split("/")) : "") const lastPartOfName = c => (c ? last(c.split("/")) : "")
</script> </script>
@ -52,92 +43,42 @@
<div class="ui-nav"> <div class="ui-nav">
<div class="pages-list-container"> <Switcher bind:this={leftNavSwitcher} tabs={['Navigate', 'Add']}>
<div class="nav-header"> <div slot="0">
<span class="navigator-title">Navigate</span> <FrontendNavigatePane />
<span class="components-nav-page">Pages</span>
</div>
<div class="nav-items-container">
<PagesList />
</div>
</div>
<div class="border-line" />
<PageLayout layout={$store.pages[$store.currentPageName]} />
<div class="border-line" />
<div class="components-list-container">
<div class="nav-group-header">
<span class="components-nav-header" style="margin-top: 0;">
Screens
</span>
<div>
<button on:click={newScreen}>
<AddIcon />
</button>
</div>
</div> </div>
<div class="nav-items-container"> <div slot="1">
<ComponentsHierarchy screens={$store.screens} /> <ComponentSelectionList toggleTab={leftNavSwitcher.selectTab} />
</div> </div>
</div> </Switcher>
</div> </div>
<div class="preview-pane"> <div class="preview-pane">
<CurrentItemPreview /> {#if $store.currentPageName && $store.currentPageName.length > 0}
<CurrentItemPreview />
{/if}
</div> </div>
{#if $store.currentFrontEndType === 'screen' || $store.currentFrontEndType === 'page'} {#if $store.currentFrontEndType === 'screen' || $store.currentFrontEndType === 'page'}
<div class="components-pane"> <div class="components-pane">
<ComponentsPaneSwitcher /> <ComponentPropertiesPanel />
</div> </div>
{/if} {/if}
</div> </div>
<NewScreen bind:this={newScreenPicker} />
<ConfirmDialog
bind:this={confirmDeleteDialog}
title="Confirm Delete"
body={`Are you sure you wish to delete this '${lastPartOfName(componentToDelete)}' component`}
okText="Delete Component"
onOk={() => store.deleteComponent(componentToDelete)} />
<slot /> <slot />
<style> <style>
button {
cursor: pointer;
outline: none;
border: none;
border-radius: 5px;
width: 20px;
padding-bottom: 10px;
display: flex;
justify-content: center;
align-items: center;
padding: 0;
}
.root { .root {
display: grid; display: grid;
grid-template-columns: 275px 1fr 275px; grid-template-columns: 300px 1fr 300px;
width: 100%; width: 100%;
background: var(--grey-light); background: var(--grey-light);
} flex: 1;
min-height: 0;
@media only screen and (min-width: 1800px) { align-items: stretch;
.root {
display: grid;
grid-template-columns: 300px 1fr 300px;
width: 100%;
background: var(--grey-light);
}
} }
.ui-nav { .ui-nav {
@ -145,9 +86,9 @@
background-color: var(--white); background-color: var(--white);
height: calc(100vh - 69px); height: calc(100vh - 69px);
padding: 0; padding: 0;
overflow: scroll;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
z-index: 5;
} }
.preview-pane { .preview-pane {
@ -201,6 +142,7 @@
font-weight: bold; font-weight: bold;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
min-height: 0;
} }
.nav-group-header > div:nth-child(1) { .nav-group-header > div:nth-child(1) {
@ -210,13 +152,6 @@
margin-right: 5px; margin-right: 5px;
} }
.nav-group-header > span:nth-child(3) {
margin-left: 5px;
vertical-align: bottom;
grid-column-start: title;
margin-top: auto;
}
.nav-group-header > div:nth-child(3) { .nav-group-header > div:nth-child(3) {
vertical-align: bottom; vertical-align: bottom;
grid-column-start: button; grid-column-start: button;
@ -227,19 +162,4 @@
.nav-group-header > div:nth-child(3):hover { .nav-group-header > div:nth-child(3):hover {
color: var(--primary75); color: var(--primary75);
} }
.navigator-title {
font-size: 18px;
color: var(--ink);
font-weight: bold;
padding: 0 20px 20px 20px;
}
.border-line {
border-bottom: 1px solid #d8d8d8;
}
.components-list-container {
padding: 20px 0px 0 0;
}
</style> </style>

14
packages/builder/src/pages/_layout.svelte

@ -119,22 +119,12 @@
<style> <style>
.root { .root {
display: grid; display: grid;
grid-template-columns: 275px 1fr; grid-template-columns: 300px 1fr;
height: 100%; height: 100%;
width: 100%; width: 100%;
background: var(--grey-light); background: var(--grey-light);
} }
@media only screen and (min-width: 1800px) {
.root {
display: grid;
grid-template-columns: 300px 1fr;
height: 100%;
width: 100%;
background: var(--grey-light);
}
}
.main { .main {
grid-column: 2; grid-column: 2;
} }
@ -207,7 +197,7 @@
.nav-item-title { .nav-item-title {
font-size: 14px; font-size: 14px;
color: var(--ink); color: var(--ink);
font-weight: 500; font-weight: 400;
margin-left: 12px; margin-left: 12px;
} }

4
packages/builder/tests/generate_css.spec.js

@ -1,14 +1,10 @@
import { import {
generate_css, generate_css,
generate_screen_css, generate_screen_css,
generate_array_styles
} from "../src/builderStore/generate_css.js" } from "../src/builderStore/generate_css.js"
describe("generate_css", () => { describe("generate_css", () => {
test("Check how partially empty arrays are handled", () => {
expect(["", "5", "", ""].map(generate_array_styles)).toEqual(["0px", "5px", "0px", "0px"])
})
test("Check how array styles are output", () => { test("Check how array styles are output", () => {
expect(generate_css({ margin: ["0", "10", "0", "15"] })).toBe("margin: 0px 10px 0px 15px;") expect(generate_css({ margin: ["0", "10", "0", "15"] })).toBe("margin: 0px 10px 0px 15px;")

5
packages/server/.env.template

@ -12,4 +12,7 @@ ADMIN_SECRET={{adminSecret}}
JWT_SECRET={{cookieKey1}} JWT_SECRET={{cookieKey1}}
# port to run http server on # port to run http server on
PORT=4001 PORT=4001
# error level for koa-pino
LOG_LEVEL=error

2
packages/server/package.json

@ -44,7 +44,6 @@
"@budibase/client": "^0.0.32", "@budibase/client": "^0.0.32",
"@budibase/core": "^0.0.32", "@budibase/core": "^0.0.32",
"@koa/router": "^8.0.0", "@koa/router": "^8.0.0",
"ajv": "^6.12.2",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"dotenv": "^8.2.0", "dotenv": "^8.2.0",
"electron-is-dev": "^1.2.0", "electron-is-dev": "^1.2.0",
@ -66,6 +65,7 @@
"squirrelly": "^7.5.0", "squirrelly": "^7.5.0",
"tar-fs": "^2.0.0", "tar-fs": "^2.0.0",
"uuid": "^3.3.2", "uuid": "^3.3.2",
"validate.js": "^0.13.1",
"yargs": "^13.2.4", "yargs": "^13.2.4",
"zlib": "^1.0.5" "zlib": "^1.0.5"
}, },

1
packages/server/scripts/jestSetup.js

@ -5,3 +5,4 @@ process.env.JWT_SECRET = "test-jwtsecret"
process.env.CLIENT_ID = "test-client-id" process.env.CLIENT_ID = "test-client-id"
process.env.BUDIBASE_DIR = tmpdir("budibase-unittests") process.env.BUDIBASE_DIR = tmpdir("budibase-unittests")
process.env.ADMIN_SECRET = "test-admin-secret" process.env.ADMIN_SECRET = "test-admin-secret"
process.env.LOG_LEVEL = "silent"

42
packages/server/src/api/controllers/record.js

@ -1,9 +1,7 @@
const CouchDB = require("../../db") const CouchDB = require("../../db")
const Ajv = require("ajv") const validateJs = require("validate.js")
const newid = require("../../db/newid") const newid = require("../../db/newid")
const ajv = new Ajv()
exports.save = async function(ctx) { exports.save = async function(ctx) {
const db = new CouchDB(ctx.params.instanceId) const db = new CouchDB(ctx.params.instanceId)
const record = ctx.request.body const record = ctx.request.body
@ -13,18 +11,18 @@ exports.save = async function(ctx) {
record._id = newid() record._id = newid()
} }
// validation with ajv
const model = await db.get(record.modelId) const model = await db.get(record.modelId)
const validate = ajv.compile({
properties: model.schema, const validateResult = await validate({
record,
model,
}) })
const valid = validate(record)
if (!valid) { if (!validateResult.valid) {
ctx.status = 400 ctx.status = 400
ctx.body = { ctx.body = {
status: 400, status: 400,
errors: validate.errors, errors: validateResult.errors,
} }
return return
} }
@ -84,3 +82,29 @@ exports.destroy = async function(ctx) {
} }
ctx.body = await db.remove(ctx.params.recordId, ctx.params.revId) ctx.body = await db.remove(ctx.params.recordId, ctx.params.revId)
} }
exports.validate = async function(ctx) {
const errors = await validate({
instanceId: ctx.params.instanceId,
modelId: ctx.params.modelId,
record: ctx.request.body,
})
ctx.status = 200
ctx.body = errors
}
async function validate({ instanceId, modelId, record, model }) {
if (!model) {
const db = new CouchDB(instanceId)
model = await db.get(modelId)
}
const errors = {}
for (let fieldName in model.schema) {
const res = validateJs.single(
record[fieldName],
model.schema[fieldName].constraints
)
if (res) errors[fieldName] = res
}
return { valid: Object.keys(errors).length === 0, errors }
}

5
packages/server/src/api/routes/model.js

@ -28,6 +28,11 @@ router
authorized(WRITE_MODEL, ctx => ctx.params.modelId), authorized(WRITE_MODEL, ctx => ctx.params.modelId),
recordController.save recordController.save
) )
.post(
"/api/:instanceId/:modelId/records/validate",
authorized(WRITE_MODEL, ctx => ctx.params.modelId),
recordController.validate
)
.delete( .delete(
"/api/:instanceId/:modelId/records/:recordId/:revId", "/api/:instanceId/:modelId/records/:recordId/:revId",
authorized(WRITE_MODEL, ctx => ctx.params.modelId), authorized(WRITE_MODEL, ctx => ctx.params.modelId),

7
packages/server/src/api/routes/tests/couchTestUtils.js

@ -27,7 +27,12 @@ exports.createModel = async (request, instanceId, model) => {
type: "model", type: "model",
key: "name", key: "name",
schema: { schema: {
name: { type: "string" }, name: {
type: "text",
constraints: {
type: "string",
},
},
}, },
} }

47
packages/server/src/api/routes/tests/record.spec.js

@ -25,17 +25,17 @@ describe("/records", () => {
server.close(); server.close();
}) })
describe("save, load, update, delete", () => { beforeEach(async () => {
instance = await createInstance(request, app._id)
model = await createModel(request, instance._id)
record = {
name: "Test Contact",
status: "new",
modelId: model._id
}
})
beforeEach(async () => { describe("save, load, update, delete", () => {
instance = await createInstance(request, app._id)
model = await createModel(request, instance._id)
record = {
name: "Test Contact",
status: "new",
modelId: model._id
}
})
const createRecord = async r => const createRecord = async r =>
await request await request
@ -119,4 +119,31 @@ describe("/records", () => {
.expect(404) .expect(404)
}) })
}) })
describe("validate", () => {
it("should return no errors on valid record", async () => {
const result = await request
.post(`/api/${instance._id}/${model._id}/records/validate`)
.send({ name: "ivan" })
.set(defaultHeaders)
.expect('Content-Type', /json/)
.expect(200)
expect(result.body.valid).toBe(true)
expect(Object.keys(result.body.errors)).toEqual([])
})
it("should errors on invalid record", async () => {
const result = await request
.post(`/api/${instance._id}/${model._id}/records/validate`)
.send({ name: 1 })
.set(defaultHeaders)
.expect('Content-Type', /json/)
.expect(200)
expect(result.body.valid).toBe(false)
expect(Object.keys(result.body.errors)).toEqual(["name"])
})
})
}) })

2
packages/server/src/app.js

@ -15,7 +15,7 @@ app.use(
prettyPrint: { prettyPrint: {
levelFirst: true, levelFirst: true,
}, },
level: process.env.NODE_ENV === "jest" ? "silent" : "info", level: env.LOG_LEVEL || "error",
}) })
) )

197
packages/server/yarn.lock

@ -194,6 +194,20 @@
lodash "^4.17.13" lodash "^4.17.13"
to-fast-properties "^2.0.0" to-fast-properties "^2.0.0"
"@budibase/client@^0.0.32":
version "0.0.32"
resolved "https://registry.yarnpkg.com/@budibase/client/-/client-0.0.32.tgz#76d9f147563a0bf939eae7f32ce75b2a527ba496"
integrity sha512-jmCCLn0CUoQbL6h623S5IqK6+GYLqX3WzUTZInSb1SCBOM3pI0eLP5HwTR6s7r42SfD0v9jTWRdyTnHiElNj8A==
dependencies:
"@nx-js/compiler-util" "^2.0.0"
bcryptjs "^2.4.3"
deep-equal "^2.0.1"
lodash "^4.17.15"
lunr "^2.3.5"
regexparam "^1.3.0"
shortid "^2.2.8"
svelte "^3.9.2"
"@budibase/core@^0.0.32": "@budibase/core@^0.0.32":
version "0.0.32" version "0.0.32"
resolved "https://registry.yarnpkg.com/@budibase/core/-/core-0.0.32.tgz#c5d9ab869c5e9596a1ac337aaf041e795b1cc7fa" resolved "https://registry.yarnpkg.com/@budibase/core/-/core-0.0.32.tgz#c5d9ab869c5e9596a1ac337aaf041e795b1cc7fa"
@ -655,7 +669,7 @@ ajv-keywords@^3.4.1:
resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.4.1.tgz#ef916e271c64ac12171fd8384eaae6b2345854da" resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.4.1.tgz#ef916e271c64ac12171fd8384eaae6b2345854da"
integrity sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ== integrity sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ==
ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.0, ajv@^6.12.2: ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.0:
version "6.12.2" version "6.12.2"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.2.tgz#c629c5eced17baf314437918d2da88c99d5958cd" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.2.tgz#c629c5eced17baf314437918d2da88c99d5958cd"
integrity sha512-k+V+hzjm5q/Mr8ef/1Y9goCmlsK4I6Sm74teeyGvFk1XrOsbsKLjEdrvny42CZ+a8sXbk8KWpY/bDwS+FLL2UQ== integrity sha512-k+V+hzjm5q/Mr8ef/1Y9goCmlsK4I6Sm74teeyGvFk1XrOsbsKLjEdrvny42CZ+a8sXbk8KWpY/bDwS+FLL2UQ==
@ -825,6 +839,11 @@ array-equal@^1.0.0:
resolved "https://registry.yarnpkg.com/array-equal/-/array-equal-1.0.0.tgz#8c2a5ef2472fd9ea742b04c77a75093ba2757c93" resolved "https://registry.yarnpkg.com/array-equal/-/array-equal-1.0.0.tgz#8c2a5ef2472fd9ea742b04c77a75093ba2757c93"
integrity sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM= integrity sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=
array-filter@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/array-filter/-/array-filter-1.0.0.tgz#baf79e62e6ef4c2a4c0b831232daffec251f9d83"
integrity sha1-uveeYubvTCpMC4MSMtr/7CUfnYM=
array-unique@^0.3.2: array-unique@^0.3.2:
version "0.3.2" version "0.3.2"
resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428"
@ -897,6 +916,13 @@ atomic-sleep@^1.0.0:
resolved "https://registry.yarnpkg.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz#eb85b77a601fc932cfe432c5acd364a9e2c9075b" resolved "https://registry.yarnpkg.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz#eb85b77a601fc932cfe432c5acd364a9e2c9075b"
integrity sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ== integrity sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==
available-typed-arrays@^1.0.0, available-typed-arrays@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.2.tgz#6b098ca9d8039079ee3f77f7b783c4480ba513f5"
integrity sha512-XWX3OX8Onv97LMk/ftVyBibpGwY5a8SmuxZPzeOxqmuEqUCOM9ZE+uIaD1VNJ5QnvU2UQusvmKbuM1FR8QWGfQ==
dependencies:
array-filter "^1.0.0"
aws-sign2@~0.7.0: aws-sign2@~0.7.0:
version "0.7.0" version "0.7.0"
resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8"
@ -1612,6 +1638,26 @@ decompress-response@^3.3.0:
dependencies: dependencies:
mimic-response "^1.0.0" mimic-response "^1.0.0"
deep-equal@^2.0.1:
version "2.0.3"
resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-2.0.3.tgz#cad1c15277ad78a5c01c49c2dee0f54de8a6a7b0"
integrity sha512-Spqdl4H+ky45I9ByyJtXteOm9CaIrPmnIPmOhrkKGNYWeDgCvJ8jNYVCTjChxW4FqGuZnLHADc8EKRMX6+CgvA==
dependencies:
es-abstract "^1.17.5"
es-get-iterator "^1.1.0"
is-arguments "^1.0.4"
is-date-object "^1.0.2"
is-regex "^1.0.5"
isarray "^2.0.5"
object-is "^1.1.2"
object-keys "^1.1.1"
object.assign "^4.1.0"
regexp.prototype.flags "^1.3.0"
side-channel "^1.0.2"
which-boxed-primitive "^1.0.1"
which-collection "^1.0.1"
which-typed-array "^1.1.2"
deep-equal@~1.0.1: deep-equal@~1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5" resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5"
@ -1965,7 +2011,7 @@ error-inject@^1.0.0:
resolved "https://registry.yarnpkg.com/error-inject/-/error-inject-1.0.0.tgz#e2b3d91b54aed672f309d950d154850fa11d4f37" resolved "https://registry.yarnpkg.com/error-inject/-/error-inject-1.0.0.tgz#e2b3d91b54aed672f309d950d154850fa11d4f37"
integrity sha1-4rPZG1Su1nLzCdlQ0VSFD6EdTzc= integrity sha1-4rPZG1Su1nLzCdlQ0VSFD6EdTzc=
es-abstract@^1.17.0-next.1, es-abstract@^1.17.2, es-abstract@^1.17.5: es-abstract@^1.17.0-next.1, es-abstract@^1.17.2, es-abstract@^1.17.4, es-abstract@^1.17.5:
version "1.17.5" version "1.17.5"
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.5.tgz#d8c9d1d66c8981fb9200e2251d799eee92774ae9" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.5.tgz#d8c9d1d66c8981fb9200e2251d799eee92774ae9"
integrity sha512-BR9auzDbySxOcfog0tLECW8l28eRGpDpU3Dm3Hp4q/N+VtLTmyj4EUN088XZWQDW/hzj6sYRDXeOFsaAODKvpg== integrity sha512-BR9auzDbySxOcfog0tLECW8l28eRGpDpU3Dm3Hp4q/N+VtLTmyj4EUN088XZWQDW/hzj6sYRDXeOFsaAODKvpg==
@ -1982,6 +2028,19 @@ es-abstract@^1.17.0-next.1, es-abstract@^1.17.2, es-abstract@^1.17.5:
string.prototype.trimleft "^2.1.1" string.prototype.trimleft "^2.1.1"
string.prototype.trimright "^2.1.1" string.prototype.trimright "^2.1.1"
es-get-iterator@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.0.tgz#bb98ad9d6d63b31aacdc8f89d5d0ee57bcb5b4c8"
integrity sha512-UfrmHuWQlNMTs35e1ypnvikg6jCz3SK8v8ImvmDsh36fCVUR1MqoFDiyn0/k52C8NqO3YsO8Oe0azeesNuqSsQ==
dependencies:
es-abstract "^1.17.4"
has-symbols "^1.0.1"
is-arguments "^1.0.4"
is-map "^2.0.1"
is-set "^2.0.1"
is-string "^1.0.5"
isarray "^2.0.5"
es-to-primitive@^1.2.1: es-to-primitive@^1.2.1:
version "1.2.1" version "1.2.1"
resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a"
@ -2967,11 +3026,21 @@ is-accessor-descriptor@^1.0.0:
dependencies: dependencies:
kind-of "^6.0.0" kind-of "^6.0.0"
is-arguments@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.0.4.tgz#3faf966c7cba0ff437fb31f6250082fcf0448cf3"
integrity sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA==
is-arrayish@^0.2.1: is-arrayish@^0.2.1:
version "0.2.1" version "0.2.1"
resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=
is-bigint@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.0.tgz#73da8c33208d00f130e9b5e15d23eac9215601c4"
integrity sha512-t5mGUXC/xRheCK431ylNiSkGGpBp8bHENBcENTkDT6ppwPzEVxNGZRvgvmOEfbWkFhA7D2GEuE2mmQTr78sl2g==
is-binary-path@~2.1.0: is-binary-path@~2.1.0:
version "2.1.0" version "2.1.0"
resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
@ -2979,6 +3048,11 @@ is-binary-path@~2.1.0:
dependencies: dependencies:
binary-extensions "^2.0.0" binary-extensions "^2.0.0"
is-boolean-object@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.0.1.tgz#10edc0900dd127697a92f6f9807c7617d68ac48e"
integrity sha512-TqZuVwa/sppcrhUCAYkGBk7w0yxfQQnxq28fjkO53tnK9FQXmdwz2JS5+GjsWQ6RByES1K40nI+yDic5c9/aAQ==
is-buffer@^1.1.5: is-buffer@^1.1.5:
version "1.1.6" version "1.1.6"
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
@ -3015,7 +3089,7 @@ is-data-descriptor@^1.0.0:
dependencies: dependencies:
kind-of "^6.0.0" kind-of "^6.0.0"
is-date-object@^1.0.1: is-date-object@^1.0.1, is-date-object@^1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.2.tgz#bda736f2cd8fd06d32844e7743bfa7494c3bfd7e" resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.2.tgz#bda736f2cd8fd06d32844e7743bfa7494c3bfd7e"
integrity sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g== integrity sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==
@ -3090,11 +3164,21 @@ is-installed-globally@^0.3.1:
global-dirs "^2.0.1" global-dirs "^2.0.1"
is-path-inside "^3.0.1" is-path-inside "^3.0.1"
is-map@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.1.tgz#520dafc4307bb8ebc33b813de5ce7c9400d644a1"
integrity sha512-T/S49scO8plUiAOA2DBTBG3JHpn1yiw0kRp6dgiZ0v2/6twi5eiB0rHtHFH9ZIrvlWc6+4O+m4zg5+Z833aXgw==
is-npm@^4.0.0: is-npm@^4.0.0:
version "4.0.0" version "4.0.0"
resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-4.0.0.tgz#c90dd8380696df87a7a6d823c20d0b12bbe3c84d" resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-4.0.0.tgz#c90dd8380696df87a7a6d823c20d0b12bbe3c84d"
integrity sha512-96ECIfh9xtDDlPylNPXhzjsykHsMJZ18ASpaWzQyBr4YRTcVjUvzaHayDAES2oU/3KpljhHUjtSRNiDwi0F0ig== integrity sha512-96ECIfh9xtDDlPylNPXhzjsykHsMJZ18ASpaWzQyBr4YRTcVjUvzaHayDAES2oU/3KpljhHUjtSRNiDwi0F0ig==
is-number-object@^1.0.3:
version "1.0.4"
resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.4.tgz#36ac95e741cf18b283fc1ddf5e83da798e3ec197"
integrity sha512-zohwelOAur+5uXtk8O3GPQ1eAcu4ZX3UwxQhUlfFFMNpUd83gXgjbhJh6HmB6LUNV/ieOLQuDwJO3dWJosUeMw==
is-number@^3.0.0: is-number@^3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195"
@ -3131,11 +3215,21 @@ is-regex@^1.0.5:
dependencies: dependencies:
has "^1.0.3" has "^1.0.3"
is-set@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.1.tgz#d1604afdab1724986d30091575f54945da7e5f43"
integrity sha512-eJEzOtVyenDs1TMzSQ3kU3K+E0GUS9sno+F0OBT97xsgcJsF9nXMBtkT9/kut5JEpM7oL7X/0qxR17K3mcwIAA==
is-stream@^1.1.0: is-stream@^1.1.0:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ=
is-string@^1.0.4, is-string@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.5.tgz#40493ed198ef3ff477b8c7f92f644ec82a5cd3a6"
integrity sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==
is-symbol@^1.0.2: is-symbol@^1.0.2:
version "1.0.3" version "1.0.3"
resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.3.tgz#38e1014b9e6329be0de9d24a414fd7441ec61937" resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.3.tgz#38e1014b9e6329be0de9d24a414fd7441ec61937"
@ -3152,11 +3246,31 @@ is-type-of@^1.0.0:
is-class-hotfix "~0.0.6" is-class-hotfix "~0.0.6"
isstream "~0.1.2" isstream "~0.1.2"
is-typed-array@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.3.tgz#a4ff5a5e672e1a55f99c7f54e59597af5c1df04d"
integrity sha512-BSYUBOK/HJibQ30wWkWold5txYwMUXQct9YHAQJr8fSwvZoiglcqB0pd7vEN23+Tsi9IUEjztdOSzl4qLVYGTQ==
dependencies:
available-typed-arrays "^1.0.0"
es-abstract "^1.17.4"
foreach "^2.0.5"
has-symbols "^1.0.1"
is-typedarray@^1.0.0, is-typedarray@~1.0.0: is-typedarray@^1.0.0, is-typedarray@~1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=
is-weakmap@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.1.tgz#5008b59bdc43b698201d18f62b37b2ca243e8cf2"
integrity sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==
is-weakset@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.1.tgz#e9a0af88dbd751589f5e50d80f4c98b780884f83"
integrity sha512-pi4vhbhVHGLxohUw7PhGsueT4vRGFoXhP7+RGN0jKIv9+8PWYCQTqtADngrxOm2g46hoH0+g8uZZBzMrvVGDmw==
is-windows@^1.0.2: is-windows@^1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
@ -3182,6 +3296,11 @@ isarray@1.0.0, isarray@~1.0.0:
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=
isarray@^2.0.5:
version "2.0.5"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723"
integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==
isbinaryfile@^4.0.6: isbinaryfile@^4.0.6:
version "4.0.6" version "4.0.6"
resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-4.0.6.tgz#edcb62b224e2b4710830b67498c8e4e5a4d2610b" resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-4.0.6.tgz#edcb62b224e2b4710830b67498c8e4e5a4d2610b"
@ -4589,6 +4708,14 @@ object-inspect@^1.7.0:
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.7.0.tgz#f4f6bd181ad77f006b5ece60bd0b6f398ff74a67" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.7.0.tgz#f4f6bd181ad77f006b5ece60bd0b6f398ff74a67"
integrity sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw== integrity sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==
object-is@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.2.tgz#c5d2e87ff9e119f78b7a088441519e2eec1573b6"
integrity sha512-5lHCz+0uufF6wZ7CRFWJN3hp8Jqblpgve06U5CMQ3f//6iDjPr2PEo9MWCjEssDsa+UZEL4PkFpr+BMop6aKzQ==
dependencies:
define-properties "^1.1.3"
es-abstract "^1.17.5"
object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.0.6, object-keys@^1.1.1: object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.0.6, object-keys@^1.1.1:
version "1.1.1" version "1.1.1"
resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
@ -5310,6 +5437,19 @@ regex-not@^1.0.0, regex-not@^1.0.2:
extend-shallow "^3.0.2" extend-shallow "^3.0.2"
safe-regex "^1.1.0" safe-regex "^1.1.0"
regexp.prototype.flags@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz#7aba89b3c13a64509dabcf3ca8d9fbb9bdf5cb75"
integrity sha512-2+Q0C5g951OlYlJz6yu5/M33IcsESLlLfsyIaLJaG4FA2r4yP8MvVMJUUP/fVBkSpbbbZlS5gynbEWLipiiXiQ==
dependencies:
define-properties "^1.1.3"
es-abstract "^1.17.0-next.1"
regexparam@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/regexparam/-/regexparam-1.3.0.tgz#2fe42c93e32a40eff6235d635e0ffa344b92965f"
integrity sha512-6IQpFBv6e5vz1QAqI+V4k8P2e/3gRrqfCJ9FI+O1FLQTO+Uz6RXZEZOPmTJ6hlGj7gkERzY5BRCv09whKP96/g==
regexpp@^2.0.1: regexpp@^2.0.1:
version "2.0.1" version "2.0.1"
resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-2.0.1.tgz#8d19d31cf632482b589049f8281f93dbcba4d07f" resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-2.0.1.tgz#8d19d31cf632482b589049f8281f93dbcba4d07f"
@ -5638,6 +5778,14 @@ shortid@^2.2.8:
dependencies: dependencies:
nanoid "^2.1.0" nanoid "^2.1.0"
side-channel@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.2.tgz#df5d1abadb4e4bf4af1cd8852bf132d2f7876947"
integrity sha512-7rL9YlPHg7Ancea1S96Pa8/QWb4BtXL/TZvS6B8XFetGBeuhAsfmUspK6DokBeZ64+Kj9TCNRD/30pVz1BvQNA==
dependencies:
es-abstract "^1.17.0-next.1"
object-inspect "^1.7.0"
signal-exit@^3.0.0, signal-exit@^3.0.2: signal-exit@^3.0.0, signal-exit@^3.0.2:
version "3.0.3" version "3.0.3"
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c"
@ -6046,6 +6194,11 @@ supports-color@^7.1.0:
dependencies: dependencies:
has-flag "^4.0.0" has-flag "^4.0.0"
svelte@^3.9.2:
version "3.23.0"
resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.23.0.tgz#bbcd6887cf588c24a975b14467455abfff9acd3f"
integrity sha512-cnyd96bK/Nw5DnYuB1hzm5cl6+I1fpmdKOteA7KLzU9KGLsLmvWsSkSKbcntzODCLmSySN3HjcgTHRo6/rJNTw==
symbol-tree@^3.2.2: symbol-tree@^3.2.2:
version "3.2.4" version "3.2.4"
resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
@ -6458,6 +6611,11 @@ validate-npm-package-license@^3.0.1:
spdx-correct "^3.0.0" spdx-correct "^3.0.0"
spdx-expression-parse "^3.0.0" spdx-expression-parse "^3.0.0"
validate.js@^0.13.1:
version "0.13.1"
resolved "https://registry.yarnpkg.com/validate.js/-/validate.js-0.13.1.tgz#b58bfac04a0f600a340f62e5227e70d95971e92a"
integrity sha512-PnFM3xiZ+kYmLyTiMgTYmU7ZHkjBZz2/+F0DaALc/uUtVzdCt1wAosvYJ5hFQi/hz8O4zb52FQhHZRC+uVkJ+g==
vary@^1.1.2: vary@^1.1.2:
version "1.1.2" version "1.1.2"
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
@ -6526,11 +6684,44 @@ whatwg-url@^7.0.0:
tr46 "^1.0.1" tr46 "^1.0.1"
webidl-conversions "^4.0.2" webidl-conversions "^4.0.2"
which-boxed-primitive@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.1.tgz#cbe8f838ebe91ba2471bb69e9edbda67ab5a5ec1"
integrity sha512-7BT4TwISdDGBgaemWU0N0OU7FeAEJ9Oo2P1PHRm/FCWoEi2VLWC9b6xvxAA3C/NMpxg3HXVgi0sMmGbNUbNepQ==
dependencies:
is-bigint "^1.0.0"
is-boolean-object "^1.0.0"
is-number-object "^1.0.3"
is-string "^1.0.4"
is-symbol "^1.0.2"
which-collection@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/which-collection/-/which-collection-1.0.1.tgz#70eab71ebbbd2aefaf32f917082fc62cdcb70906"
integrity sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==
dependencies:
is-map "^2.0.1"
is-set "^2.0.1"
is-weakmap "^2.0.1"
is-weakset "^2.0.1"
which-module@^2.0.0: which-module@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=
which-typed-array@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.2.tgz#e5f98e56bda93e3dac196b01d47c1156679c00b2"
integrity sha512-KT6okrd1tE6JdZAy3o2VhMoYPh3+J6EMZLyrxBQsZflI1QCZIxMrIYLkosd8Twf+YfknVIHmYQPgJt238p8dnQ==
dependencies:
available-typed-arrays "^1.0.2"
es-abstract "^1.17.5"
foreach "^2.0.5"
function-bind "^1.1.1"
has-symbols "^1.0.1"
is-typed-array "^1.1.3"
which@^1.2.9, which@^1.3.0: which@^1.2.9, which@^1.3.0:
version "1.3.1" version "1.3.1"
resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"

Loading…
Cancel
Save