Browse Source

merge

master
Martin McKeaveney 6 years ago
parent
commit
fe0b95d627
  1. 3
      packages/builder/package.json
  2. 4
      packages/builder/src/App.svelte
  3. 14
      packages/builder/src/budibase.css
  4. 143
      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. 10
      packages/builder/src/components/common/Icons/Close.svelte
  8. 6
      packages/builder/src/components/common/Icons/Info.svelte
  9. 12
      packages/builder/src/components/common/Icons/More.svelte
  10. 3
      packages/builder/src/components/common/Icons/index.js
  11. 69
      packages/builder/src/components/common/Switcher.svelte
  12. 6
      packages/builder/src/components/database/ModelDataTable/api.js
  13. 64
      packages/builder/src/components/database/ModelDataTable/modals/CreateEditModel/FieldView.svelte
  14. 24
      packages/builder/src/components/database/ModelDataTable/modals/CreateEditRecord.svelte
  15. 36
      packages/builder/src/components/database/ModelDataTable/modals/RecordFieldControl.svelte
  16. 1
      packages/builder/src/components/nav/BackendNav.svelte
  17. 2
      packages/builder/src/components/nav/SchemaManagementDrawer.svelte
  18. 68
      packages/builder/src/components/start/AppCard.svelte
  19. 83
      packages/builder/src/components/start/AppList.svelte
  20. 197
      packages/builder/src/components/start/CreateAppModal.svelte
  21. 232
      packages/builder/src/components/userInterface/ComponentDropdownMenu.svelte
  22. 3
      packages/builder/src/components/userInterface/ComponentPropertiesPanel.svelte
  23. 39
      packages/builder/src/components/userInterface/ComponentSelectionList.svelte
  24. 31
      packages/builder/src/components/userInterface/ComponentsHierarchy.svelte
  25. 72
      packages/builder/src/components/userInterface/ComponentsHierarchyChildren.svelte
  26. 2
      packages/builder/src/components/userInterface/ComponentsPaneSwitcher.svelte
  27. 10
      packages/builder/src/components/userInterface/FlatButton.svelte
  28. 1
      packages/builder/src/components/userInterface/FlatButtonGroup.svelte
  29. 48
      packages/builder/src/components/userInterface/FrontendNavigatePane.svelte
  30. 46
      packages/builder/src/components/userInterface/ItemTab/Item.svelte
  31. 1
      packages/builder/src/components/userInterface/ItemTab/Tab.svelte
  32. 55
      packages/builder/src/components/userInterface/PageLayout.svelte
  33. 73
      packages/builder/src/components/userInterface/PagesList.svelte
  34. 36
      packages/builder/src/components/userInterface/UserInterfaceRoot.svelte
  35. 183
      packages/builder/src/components/userInterface/propertyCategories.js
  36. 81
      packages/builder/src/components/userInterface/temporaryPanelStructure.js
  37. 5
      packages/builder/src/global.css
  38. 92
      packages/builder/src/pages/[application]/_reset.svelte
  39. 11
      packages/builder/src/pages/[application]/backend/_layout.svelte
  40. 156
      packages/builder/src/pages/[application]/frontend/_layout.svelte
  41. 207
      packages/builder/src/pages/_layout.svelte
  42. 271
      packages/builder/src/pages/index.svelte
  43. 5
      packages/server/.env.template
  44. 2
      packages/server/package.json
  45. 1
      packages/server/scripts/jestSetup.js
  46. 71
      packages/server/src/api/controllers/application.js
  47. 42
      packages/server/src/api/controllers/record.js
  48. 2
      packages/server/src/api/controllers/user.js
  49. 10
      packages/server/src/api/controllers/workflow/index.js
  50. 5
      packages/server/src/api/routes/model.js
  51. 20
      packages/server/src/api/routes/tests/couchTestUtils.js
  52. 27
      packages/server/src/api/routes/tests/record.spec.js
  53. 2
      packages/server/src/api/routes/view.js
  54. 2
      packages/server/src/app.js
  55. 40
      packages/server/src/schemas/index.js
  56. 1
      packages/server/src/utilities/appDirectoryTemplate/.gitignore
  57. 11
      packages/server/src/utilities/appDirectoryTemplate/package.json
  58. 19
      packages/server/src/utilities/appDirectoryTemplate/pages/main/page.json
  59. 0
      packages/server/src/utilities/appDirectoryTemplate/pages/main/screens/.gitkeep
  60. 19
      packages/server/src/utilities/appDirectoryTemplate/pages/unauthenticated/page.json
  61. 0
      packages/server/src/utilities/appDirectoryTemplate/pages/unauthenticated/screens/.gitkeep
  62. 1
      packages/server/src/utilities/appDirectoryTemplate/plugins.js
  63. 7
      packages/server/yarn.lock

3
packages/builder/package.json

@ -39,6 +39,7 @@
}, },
"dependencies": { "dependencies": {
"@beyonk/svelte-notifications": "^2.0.3", "@beyonk/svelte-notifications": "^2.0.3",
"@budibase/bbui": "^0.3.5",
"@budibase/client": "^0.0.32", "@budibase/client": "^0.0.32",
"@nx-js/compiler-util": "^2.0.0", "@nx-js/compiler-util": "^2.0.0",
"codemirror": "^5.51.0", "codemirror": "^5.51.0",
@ -83,7 +84,7 @@
"rollup-plugin-svelte": "^5.0.3", "rollup-plugin-svelte": "^5.0.3",
"rollup-plugin-terser": "^4.0.4", "rollup-plugin-terser": "^4.0.4",
"rollup-plugin-url": "^2.2.2", "rollup-plugin-url": "^2.2.2",
"svelte": "^3.0.0" "svelte": "3.23.x"
}, },
"gitHead": "115189f72a850bfb52b65ec61d932531bf327072" "gitHead": "115189f72a850bfb52b65ec61d932531bf327072"
} }

4
packages/builder/src/App.svelte

@ -30,6 +30,4 @@
<!-- svelte-notifications --> <!-- svelte-notifications -->
<NotificationDisplay /> <NotificationDisplay />
<Modal> <Router {routes} />
<Router {routes} />
</Modal>

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 {

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

@ -1,4 +1,4 @@
import { cloneDeep, values } from "lodash/fp" import { values } from "lodash/fp"
import { backendUiStore } from "builderStore" import { backendUiStore } from "builderStore"
import * as backendStoreActions from "./backend" import * as backendStoreActions from "./backend"
import { writable, get } from "svelte/store" import { writable, get } from "svelte/store"
@ -16,6 +16,14 @@ import { buildCodeForScreens } from "../buildCodeForScreens"
import { generate_screen_css } from "../generate_css" import { generate_screen_css } from "../generate_css"
import { insertCodeMetadata } from "../insertCodeMetadata" import { insertCodeMetadata } from "../insertCodeMetadata"
import { uuid } from "../uuid" import { uuid } from "../uuid"
import {
selectComponent as _selectComponent,
getParent,
walkProps,
savePage as _savePage,
saveCurrentPreviewItem as _saveCurrentPreviewItem,
saveScreenApi as _saveScreenApi,
} from "../storeUtils"
export const getStore = () => { export const getStore = () => {
const initial = { const initial = {
@ -57,10 +65,6 @@ export const getStore = () => {
store.setComponentStyle = setComponentStyle(store) store.setComponentStyle = setComponentStyle(store)
store.setComponentCode = setComponentCode(store) store.setComponentCode = setComponentCode(store)
store.setScreenType = setScreenType(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.getPathToComponent = getPathToComponent(store)
store.addTemplatedComponent = addTemplatedComponent(store) store.addTemplatedComponent = addTemplatedComponent(store)
store.setMetadataProp = setMetadataProp(store) store.setMetadataProp = setMetadataProp(store)
@ -69,6 +73,9 @@ export const getStore = () => {
export default getStore export default getStore
export const getComponentDefinition = (state, name) =>
name.startsWith("##") ? getBuiltin(name) : state.components[name]
const setPackage = (store, initial) => async pkg => { const setPackage = (store, initial) => async pkg => {
const [main_screens, unauth_screens] = await Promise.all([ const [main_screens, unauth_screens] = await Promise.all([
api api
@ -140,12 +147,6 @@ const _saveScreen = async (store, s, screen) => {
return s return s
} }
const _saveScreenApi = (screen, s) => {
api
.post(`/_builder/api/${s.appId}/pages/${s.currentPageName}/screen`, screen)
.then(() => _savePage(s))
}
const createScreen = store => (screenName, route, layoutComponentName) => { const createScreen = store => (screenName, route, layoutComponentName) => {
store.update(state => { store.update(state => {
const rootComponent = state.components[layoutComponentName] const rootComponent = state.components[layoutComponentName]
@ -276,14 +277,6 @@ const removeStylesheet = store => stylesheet => {
}) })
} }
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 },
screens: page._screens,
})
}
const setCurrentPage = store => pageName => { const setCurrentPage = store => pageName => {
store.update(state => { store.update(state => {
const current_screens = state.pages[pageName]._screens const current_screens = state.pages[pageName]._screens
@ -315,8 +308,6 @@ const setCurrentPage = store => pageName => {
}) })
} }
// const getComponentDefinition = (components, name) => components.find(c => c.name === name)
/** /**
* @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
@ -342,9 +333,7 @@ const addChildComponent = store => (componentToAdd, presetName) => {
return state 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] : {}
@ -398,12 +387,7 @@ const addTemplatedComponent = store => props => {
const selectComponent = store => component => { const selectComponent = store => component => {
store.update(state => { store.update(state => {
const componentDef = component._component.startsWith("##") return _selectComponent(state, component)
? component
: state.components[component._component]
state.currentComponentInfo = makePropsSafe(componentDef, component)
state.currentView = "component"
return state
}) })
} }
@ -470,75 +454,6 @@ const setScreenType = store => type => {
}) })
} }
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 => { const getPathToComponent = store => component => {
// Gets all the components to needed to construct a path. // Gets all the components to needed to construct a path.
const tempStore = get(store) const tempStore = get(store)
@ -570,39 +485,9 @@ const getPathToComponent = store => component => {
return path 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) => { const setMetadataProp = store => (name, prop) => {
store.update(s => { store.update(s => {
s.currentPreviewItem[name] = prop s.currentPreviewItem[name] = prop
return s return s
}) })
} }
const _saveCurrentPreviewItem = s =>
s.currentFrontEndType === "page"
? _savePage(s)
: _saveScreenApi(s.currentPreviewItem, 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>

10
packages/builder/src/components/common/Icons/Close.svelte

@ -0,0 +1,10 @@
<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="M12 10.586l4.95-4.95 1.414 1.414-4.95 4.95 4.95 4.95-1.414
1.414-4.95-4.95-4.95 4.95-1.414-1.414 4.95-4.95-4.95-4.95L7.05 5.636z" />
</svg>

After

Width:  |  Height:  |  Size: 292 B

6
packages/builder/src/components/common/Icons/Info.svelte

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path fill="none" d="M0 0h24v24H0z" />
<path
d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10
10zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16zM11 7h2v2h-2V7zm0 4h2v6h-2v-6z" />
</svg>

After

Width:  |  Height:  |  Size: 271 B

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

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

@ -29,3 +29,6 @@ export { default as ContributionIcon } from "./Contribution.svelte"
export { default as BugIcon } from "./Bug.svelte" export { default as BugIcon } from "./Bug.svelte"
export { default as EmailIcon } from "./Email.svelte" 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 CloseIcon } from "./Close.svelte"
export { default as MoreIcon } from "./More.svelte"

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

@ -0,0 +1,69 @@
<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);
}
.switcher {
display: flex;
margin: 0px 20px 20px 0px;
}
.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;
}
.switcher > .selected {
color: var(--ink);
}
</style>

6
packages/builder/src/components/database/ModelDataTable/api.js

@ -15,13 +15,13 @@ export async function createDatabase(appname, instanceName) {
} }
export async function deleteRecord(record, instanceId) { export async function deleteRecord(record, instanceId) {
const DELETE_RECORDS_URL = `/api/${instanceId}/${record.modelId}/records/${record._id}/${record._rev}` const DELETE_RECORDS_URL = `/api/${instanceId}/${record._modelId}/records/${record._id}/${record._rev}`
const response = await api.delete(DELETE_RECORDS_URL) const response = await api.delete(DELETE_RECORDS_URL)
return response return response
} }
export async function saveRecord(record, instanceId) { export async function saveRecord(record, instanceId, modelId) {
const SAVE_RECORDS_URL = `/api/${instanceId}/${record.modelId}/records` const SAVE_RECORDS_URL = `/api/${instanceId}/${modelId}/records`
const response = await api.post(SAVE_RECORDS_URL, record) const response = await api.post(SAVE_RECORDS_URL, record)
return await response.json() return await response.json()

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>

24
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,24 +24,38 @@
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(
{ {
...record, ...record,
modelId: $backendUiStore.selectedModel._id, modelId: $backendUiStore.selectedModel._id,
}, },
instanceId instanceId,
$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
} }
@ -64,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">

2
packages/builder/src/components/nav/SchemaManagementDrawer.svelte

@ -46,7 +46,7 @@
function selectModel(model) { function selectModel(model) {
backendUiStore.update(state => { backendUiStore.update(state => {
state.selectedModel = model state.selectedModel = model
state.selectedView = `all_${model._id}` state.selectedView = `${model._id}`
return state return state
}) })
} }

68
packages/builder/src/components/start/AppCard.svelte

@ -0,0 +1,68 @@
<script>
import Button from "components/common/Button.svelte"
export let name,
description = `A minimalist CRM which removes the noise and allows you to focus
on your business.`,
_id
</script>
<div class="apps-card">
<h3 class="app-title">{name}</h3>
<p class="app-desc">{description}</p>
<div class="card-footer">
<div class="modified-date">Last Edited - 25th May 2020</div>
<a href={`/_builder/${_id}`} class="app-button">Open Web App</a>
</div>
</div>
<style>
.apps-card {
background-color: var(--white);
padding: 20px;
max-width: 400px;
max-height: 150px;
border-radius: 5px;
border: 1px solid var(--grey-medium);
}
.app-button:hover {
background-color: var(--grey-light);
text-decoration: none;
}
.app-title {
font-size: 18px;
font-weight: 700;
color: var(--ink);
text-transform: capitalize;
}
.app-desc {
color: var(--ink-light);
}
.card-footer {
display: flex;
flex-direction: row;
align-items: baseline;
justify-content: space-between;
}
.modified-date {
font-size: 14px;
color: var(--ink-light);
}
.app-button {
background-color: var(--white);
color: var(--ink);
padding: 12px 20px;
border-radius: 5px;
border: 1px var(--grey) solid;
font-size: 14px;
font-weight: 400;
cursor: pointer;
transition: all 0.2s;
box-sizing: border-box;
}
</style>

83
packages/builder/src/components/start/AppList.svelte

@ -1,5 +1,5 @@
<script> <script>
import Button from "components/common/Button.svelte" import AppCard from "./AppCard.svelte"
export let apps export let apps
function myFunction() { function myFunction() {
@ -13,27 +13,23 @@
<div> <div>
<div> <div>
<div class="app-section-title">Your Web Apps</div> <div class="app-section-title">Your Web Apps</div>
{#each apps as app} <div class="apps">
<div class="apps-card"> {#each apps as app}
<h3 class="app-title">{app.name}</h3> <AppCard {...app} />
<p class="app-desc"> {/each}
A minimalist CRM which removes the noise and allows you to focus </div>
on your business.
</p>
<div class="card-footer">
<div class="modified-date">Last Edited - 25th May 2020</div>
<a href={`/_builder/${app._id}`} class="app-button">
Open Web App
</a>
</div>
</div>
{/each}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<style> <style>
.apps {
display: grid;
grid-template-columns: repeat(auto-fill, 400px);
grid-gap: 40px 85px;
justify-content: start;
}
.root { .root {
margin: 40px 80px; margin: 40px 80px;
} }
@ -44,59 +40,4 @@
font-weight: 700; font-weight: 700;
margin-bottom: 20px; margin-bottom: 20px;
} }
.apps {
display: flex;
flex-wrap: wrap;
gap: 40px;
}
.apps-card {
background-color: var(--white);
padding: 20px;
max-width: 400px;
max-height: 150px;
border-radius: 5px;
border: 1px solid var(--grey-dark);
}
.app-button:hover {
background-color: var(--grey-light);
text-decoration: none;
}
.app-title {
font-size: 18px;
font-weight: 700;
color: var(--ink);
text-transform: capitalize;
}
.app-desc {
color: var(--ink-light);
}
.card-footer {
display: flex;
flex-direction: row;
align-items: baseline;
justify-content: space-between;
}
.modified-date {
font-size: 14px;
color: var(--ink-light);
}
.app-button {
background-color: var(--white);
color: var(--ink);
padding: 12px 20px;
border-radius: 5px;
border: 1px var(--grey) solid;
font-size: 14px;
font-weight: 400;
cursor: pointer;
transition: all 0.2s;
box-sizing: border-box;
}
</style> </style>

197
packages/builder/src/components/start/CreateAppModal.svelte

@ -0,0 +1,197 @@
<script>
import Spinner from "components/common/Spinner.svelte"
import { Input, TextArea, Button } from "@budibase/bbui"
import { goto } from "@sveltech/routify"
import { AppsIcon, InfoIcon, CloseIcon } from "components/common/Icons/"
import { getContext } from "svelte"
import { fade } from "svelte/transition"
const { open, close } = getContext("simple-modal")
let name = ""
let description = ""
let loading = false
let error = {}
const createNewApp = async () => {
if ((name.length > 100 || name.length < 1) && description.length < 1) {
error = {
name: true,
description: true,
}
} else if (description.length < 1) {
error = {
name: false,
description: true,
}
} else if (name.length > 100 || name.length < 1) {
error = {
name: true,
}
} else {
error = {}
const data = { name, description }
loading = true
try {
const response = await fetch("/api/applications", {
method: "POST", // *GET, POST, PUT, DELETE, etc.
credentials: "same-origin", // include, *same-origin, omit
headers: {
"Content-Type": "application/json",
// 'Content-Type': 'application/x-www-form-urlencoded',
},
body: JSON.stringify(data), // body data type must match "Content-Type" header
})
const res = await response.json()
$goto(`./${res._id}`)
} catch (error) {
console.error(error)
}
}
}
let value
let onChange = () => {}
function _onCancel() {
close()
}
async function _onOkay() {
await createNewApp()
}
</script>
<div class="container">
<div class="body">
<div class="heading">
<span class="icon">
<AppsIcon />
</span>
<h3>Create new web app</h3>
</div>
<Input
name="name"
label="Name"
placeholder="Enter application name"
on:change={e => (name = e.target.value)}
on:input={e => (name = e.target.value)} />
{#if error.name}
<span class="error">You need to enter a name for your application.</span>
{/if}
<TextArea
bind:value={description}
name="description"
label="Description"
placeholder="Describe your application" />
{#if error.description}
<span class="error">
Please enter a short description of your application
</span>
{/if}
</div>
<div class="footer">
<a href="./#" class="info">
<InfoIcon />
How to get started
</a>
<Button outline thin on:click={_onCancel}>Cancel</Button>
<Button primary thin on:click={_onOkay}>Save</Button>
</div>
<div class="close-button" on:click={_onCancel}>
<CloseIcon />
</div>
{#if loading}
<div in:fade class="spinner-container">
<Spinner />
<span class="spinner-text">Creating your app...</span>
</div>
{/if}
</div>
<style>
.container {
position: relative;
}
.close-button {
cursor: pointer;
position: absolute;
top: 20px;
right: 20px;
}
.close-button :global(svg) {
width: 24px;
height: 24px;
}
.heading {
display: flex;
flex-direction: row;
align-items: center;
margin-bottom: 20px;
}
h3 {
margin: 0;
font-size: 24px;
font-weight: bold;
}
.icon {
display: grid;
border-radius: 3px;
align-content: center;
justify-content: center;
margin-right: 12px;
height: 20px;
width: 20px;
padding: 10px;
background-color: var(--blue-light);
}
.info {
color: var(--primary100);
text-decoration-color: var(--primary100);
}
.info :global(svg) {
fill: var(--primary100);
margin-right: 8px;
width: 24px;
height: 24px;
}
.body {
padding: 40px 40px 80px 40px;
display: grid;
grid-gap: 20px;
}
.footer {
display: grid;
grid-gap: 20px;
align-items: center;
grid-template-columns: 1fr auto auto;
padding: 30px 40px;
border-bottom-left-radius: 5px;
border-bottom-right-radius: 50px;
background-color: var(--grey-light);
}
.spinner-container {
background: white;
position: absolute;
border-radius: 5px;
left: 0;
top: 0;
right: 0;
bottom: 0;
display: grid;
justify-items: center;
align-content: center;
grid-gap: 50px;
}
.spinner-text {
font-size: 2em;
}
.error {
color: var(--deletion100);
font-weight: bold;
font-size: 0.8em;
}
</style>

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

@ -0,0 +1,232 @@
<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 on:click={() => confirmDeleteDialog.show()}>Delete</li>
<li on:click={moveUpComponent}>Move up</li>
<li on:click={moveDownComponent}>Move down</li>
<li on:click={copyComponent}>Duplicate</li>
<li on:click={() => storeComponentForCopy(true)}>Cut</li>
<li on:click={() => storeComponentForCopy(false)}>Copy</li>
<hr />
<li class:disabled={noPaste} on:click={() => pasteComponent('above')}>
Paste above
</li>
<li class:disabled={noPaste} on:click={() => pasteComponent('below')}>
Paste below
</li>
<li
class:disabled={noPaste || noChildrenAllowed}
on:click={() => pasteComponent('inside')}>
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(--button-text);
outline: none;
}
.menu {
z-index: 100000;
overflow: visible;
padding: 10px 0;
}
.menu li {
border-style: none;
background-color: transparent;
list-style-type: none;
padding: 4px 5px 4px 15px;
margin: 0;
width: 100%;
box-sizing: border-box;
}
.menu li:not(.disabled) {
cursor: pointer;
color: var(--ink);
}
.menu li:not(.disabled):hover {
color: var(--button-text);
background-color: var(--grey-light);
}
.disabled {
color: var(--grey-dark);
cursor: default;
}
</style>

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

@ -107,11 +107,12 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow-x: hidden; overflow-x: hidden;
padding: 20px;
} }
.title > div:nth-child(1) { .title > div:nth-child(1) {
grid-column-start: name; grid-column-start: name;
color: var(--secondary100); color: var(--ink);
} }
.title > div:nth-child(2) { .title > div:nth-child(2) {

39
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>
@ -52,32 +60,9 @@
</div> </div>
<style> <style>
.tabs {
display: flex;
justify-content: center;
list-style: none;
margin: 0 auto;
padding: 0 30px;
border-bottom: 1px solid #d8d8d8;
font-size: 14px;
font-weight: 500;
letter-spacing: 0.14px;
}
li {
color: #808192;
margin: 0 5px;
padding: 0 8px;
cursor: pointer;
}
.panel { .panel {
padding: 20px; padding: 20px 0px;
} display: flex;
flex-wrap: wrap;
.active {
border-bottom: solid 3px #0055ff;
color: #393c44;
} }
</style> </style>

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: 500;
} }
.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) {

72
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)
@ -51,30 +41,9 @@
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>{get_capitalised_name(component._component)}</div>
<div class="reorder-buttons"> <div class="actions">
{#if index > 0} <ComponentDropdownMenu {component} />
<button
class:solo={index === components.length - 1}
on:click|stopPropagation={() => onMoveUpComponent(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 +51,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}
@ -111,7 +76,7 @@
font-size: 13px; font-size: 13px;
} }
.item button { .actions {
display: none; display: none;
height: 20px; height: 20px;
width: 28px; width: 28px;
@ -120,37 +85,14 @@
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: #fafafa;
cursor: pointer; cursor: pointer;
} }
.item:hover button { .item:hover .actions {
display: block; display: block;
} }
.item:hover button:hover {
color: var(--button-text);
}
.reorder-buttons {
display: flex;
flex-direction: column;
height: 100%;
}
.reorder-buttons > button {
flex: 1 1 auto;
width: 30px;
height: 15px;
}
.reorder-buttons > button.solo {
padding-top: 2px;
}
</style> </style>

2
packages/builder/src/components/userInterface/ComponentsPaneSwitcher.svelte

@ -55,7 +55,7 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: 20px 20px; padding: 20px 20px;
border-left: solid 1px #e8e8ef; border-left: solid 1px var(--grey);
} }
.switcher { .switcher {

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

@ -19,20 +19,20 @@
<style> <style>
.flatbutton { .flatbutton {
cursor: pointer; cursor: pointer;
padding: 8px 4px; padding: 8px 2px;
text-align: center; text-align: center;
background: #ffffff; background: #ffffff;
color: var(--ink-light); color: var(--ink-light);
border-radius: 5px; border-radius: 5px;
font-family: Roboto; font-family: Roboto;
font-size: 13px; font-size: 14px;
font-weight: 500; font-weight: 400;
transition: background 0.5s, color 0.5s ease; transition: all 0.3s;
text-rendering: optimizeLegibility; text-rendering: optimizeLegibility;
} }
.selected { .selected {
background: #808192; background: var(--ink-light);
color: #ffffff; color: #ffffff;
} }
</style> </style>

1
packages/builder/src/components/userInterface/FlatButtonGroup.svelte

@ -1,6 +1,5 @@
<script> <script>
import { onMount } from "svelte" import { onMount } from "svelte"
import FlatButton from "./FlatButton.svelte" import FlatButton from "./FlatButton.svelte"
export let buttonProps = [] export let buttonProps = []
export let isMultiSelect = false export let isMultiSelect = false

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

@ -0,0 +1,48 @@
<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}>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: 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);
}
</style>

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

@ -1,66 +1,56 @@
<script> <script>
import { fly } from "svelte/transition"
export let item export let item
</script> </script>
<div class="item-item" 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>
<div class="item-text"> <div class="item-text">
<div class="item-name">{item.name}</div> <div class="item-name">{item.name}</div>
<div class="item-description">
<p>{item.description}</p>
</div>
</div> </div>
</div> </div>
<style> <style>
.item-item { .item-item {
display: flex; display: flex;
flex-direction: row; flex-direction: column;
padding: 10px 0px 8px 10px;
align-items: center;
cursor: pointer; cursor: pointer;
margin-bottom: 8px;
padding: 8px 0px 16px 0px;
width: 120px;
height: 80px;
justify-content: center;
align-items: center;
margin-right: 8px;
background-color: var(--grey-light);
border-radius: 3px;
} }
.item-item:hover { .item-item:hover {
background: #fbfbfb; background: var(--grey);
border-radius: 5px; border-radius: 3px;
transition: all 0.2s;
} }
.item-icon { .item-icon {
flex: 0 0 40px; border-radius: 3px;
background: #f1f4fc;
height: 40px;
border-radius: 5px;
display: flex; display: flex;
justify-content: center;
align-items: center;
} }
.item-text { .item-text {
display: flex; display: flex;
padding-left: 16px;
padding-top: 8px;
flex-direction: column; flex-direction: column;
} }
.item-name { .item-name {
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 400;
}
.item-description {
font-size: 12px;
color: #808192;
}
p {
line-height: 15px;
} }
i { i {
font-size: 24px; font-size: 24px;
color: #808192; color: var(--ink-light);
} }
</style> </style>

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

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

@ -34,11 +34,6 @@
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")
@ -46,7 +41,6 @@
</script> </script>
<div class="pagelayoutSection"> <div class="pagelayoutSection">
<div class="components-nav-page">Page Layout</div>
<div <div
class="budibase__nav-item root" class="budibase__nav-item root"
class:selected={$store.currentComponentInfo._id === _layout.component.props._id} class:selected={$store.currentComponentInfo._id === _layout.component.props._id}
@ -56,64 +50,41 @@
class:rotate={$store.currentPreviewItem.name !== _layout.title}> class:rotate={$store.currentPreviewItem.name !== _layout.title}>
<ArrowDownIcon /> <ArrowDownIcon />
</span> </span>
<i class="ri-layout-3-fill icon-big" />
<span class="icon"> <span class="title">Master Screen</span>
<GridIcon />
</span>
<span class="title">Page Layout</span>
</div> </div>
{#if $store.currentPreviewItem.name === _layout.title && _layout.component.props._children} {#if $store.currentPreviewItem.name === _layout.title && _layout.component.props._children}
<ComponentsHierarchyChildren <ComponentsHierarchyChildren
thisComponent={_layout.component.props} thisComponent={_layout.component.props}
components={_layout.component.props._children} components={_layout.component.props._children}
currentComponent={$store.currentComponentInfo} currentComponent={$store.currentComponentInfo} />
onDeleteComponent={confirmDeleteComponent}
onMoveUpComponent={store.moveUpComponent}
onMoveDownComponent={store.moveDownComponent}
onCopyComponent={store.copyComponent} />
{/if} {/if}
</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>
.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 { .pagelayoutSection {
margin: 20px 0px 20px 0px; margin: 20px 0px 0px 0px;
} }
.title { .title {
margin-left: 10px; margin-left: 10px;
font-size: 13px; font-size: 14px;
font-weight: 500;
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;
} }
@ -230,10 +202,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;
} }

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

@ -7,42 +7,84 @@ import InputGroup from "../common/Inputs/InputGroup.svelte"
*/ */
export const layout = [ export const layout = [
{
label: "Display",
key: "display",
control: OptionSelect,
initialValue: "Select Option",
options: [
{ label: "Select Option", value: "" },
{ label: "Flex", value: "flex" },
{ label: "Inline Flex", value: "inline-flex" },
],
},
{ {
label: "Direction", label: "Direction",
key: "flex-direction", key: "flex-direction",
control: OptionSelect, control: OptionSelect,
initialValue: "columnReverse", initialValue: "Row",
options: [
{ label: "Row", value: "row" },
{ label: "Row Reverse", value: "rowReverse" },
{ label: "column", value: "column" },
{ label: "Column Reverse", value: "columnReverse" },
],
},
{
label: "Justify",
key: "justify-content",
control: OptionSelect,
initialValue: "Flex Start",
options: [ options: [
{ label: "row" }, { label: "Flex Start", value: "flex-start" },
{ label: "row-reverse", value: "rowReverse" }, { label: "Flex End", value: "flex-end" },
{ label: "column" }, { label: "Center", value: "center" },
{ label: "column-reverse", value: "columnReverse" }, { 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",
options: [
{ label: "Flex Start", value: "flex-start" },
{ label: "Flex End", value: "flex-end" },
{ label: "Center", value: "center" },
{ label: "Baseline", value: "baseline" },
{ label: "Stretch", value: "stretch" },
], ],
}, },
{ label: "Justify", key: "justify-content", control: Input },
{ label: "Align", key: "align-items", control: Input },
{ {
label: "Wrap", label: "Wrap",
key: "flex-wrap", key: "flex-wrap",
control: OptionSelect, control: OptionSelect,
options: [{ label: "wrap" }, { label: "no wrap", value: "noWrap" }], initialValue: "NoWrap",
options: [
{ label: "No Wrap", value: "nowrap" },
{ label: "Wrap", value: "wrap" },
{ label: "Wrap Reverse", value: "wrap-reverse" },
],
}, },
] ]
const spacingMeta = [ const spacingMeta = [
{ placeholder: "T" },
{ placeholder: "R" },
{ placeholder: "B" },
{ placeholder: "L" }, { placeholder: "L" },
{ placeholder: "B" },
{ placeholder: "R" },
{ placeholder: "T" },
] ]
export const spacing = [ export const spacing = [
{ label: "Margin", key: "margin", control: InputGroup, meta: spacingMeta },
{ {
label: "Padding", label: "Padding",
key: "padding", key: "padding",
control: InputGroup, control: InputGroup,
meta: spacingMeta, meta: spacingMeta,
}, },
{ label: "Margin", key: "margin", control: InputGroup, meta: spacingMeta },
] ]
export const size = [ export const size = [
@ -59,14 +101,40 @@ export const position = [
label: "Position", label: "Position",
key: "position", key: "position",
control: OptionSelect, control: OptionSelect,
initialValue: "Wrap",
options: [ options: [
{ label: "static" }, { label: "Static", value: "static" },
{ label: "relative" }, { label: "Relative", value: "relative" },
{ label: "fixed" }, { label: "Fixed", value: "fixed" },
{ label: "absolute" }, { label: "Absolute", value: "absolute" },
{ label: "sticky" }, { label: "Sticky", value: "sticky" },
], ],
}, },
{
label: "Top",
key: "top",
control: Input,
},
{
label: "Right",
key: "right",
control: Input,
},
{
label: "Bottom",
key: "bottom",
control: Input,
},
{
label: "Left",
key: "left",
control: Input,
},
{
label: "Z-index",
key: "z-index",
control: Input,
},
] ]
export const typography = [ export const typography = [
@ -77,13 +145,21 @@ export const typography = [
defaultValue: "initial", defaultValue: "initial",
options: [ options: [
"initial", "initial",
"Times New Roman",
"Georgia",
"Arial", "Arial",
"Arial Black", "Arial Black",
"Cursive",
"Courier",
"Comic Sans MS", "Comic Sans MS",
"Helvetica",
"Impact", "Impact",
"Inter",
"Lucida Sans Unicode", "Lucida Sans Unicode",
"Open Sans",
"Playfair",
"Roboto",
"Roboto Mono",
"Times New Roman",
"Verdana",
], ],
styleBindingProperty: "font-family", styleBindingProperty: "font-family",
}, },
@ -92,10 +168,15 @@ export const typography = [
key: "font-weight", key: "font-weight",
control: OptionSelect, control: OptionSelect,
options: [ options: [
{ label: "normal" }, { label: "100", value: "100" },
{ label: "bold" }, { label: "200", value: "200" },
{ label: "bolder" }, { label: "300", value: "300" },
{ label: "lighter" }, { label: "400", value: "400" },
{ label: "500", value: "500" },
{ label: "600", value: "600" },
{ label: "700", value: "700" },
{ label: "800", value: "800" },
{ label: "900", value: "900" },
], ],
}, },
{ label: "size", key: "font-size", defaultValue: "", control: Input }, { label: "size", key: "font-size", defaultValue: "", control: Input },
@ -103,8 +184,7 @@ export const typography = [
{ {
label: "Color", label: "Color",
key: "color", key: "color",
control: OptionSelect, control: Input,
options: ["black", "white", "red", "blue", "green"],
}, },
{ {
label: "align", label: "align",
@ -112,6 +192,20 @@ export const typography = [
control: OptionSelect, control: OptionSelect,
options: ["initial", "left", "right", "center", "justify"], options: ["initial", "left", "right", "center", "justify"],
}, //custom }, //custom
{
label: "Decoration",
key: "text-decoration-line",
control: OptionSelect,
defaultValue: "None",
options: [
{ label: "None", value: "none" },
{ label: "Underline", value: "underline" },
{ label: "Overline", value: "overline" },
{ label: "Line-through", value: "line-through" },
{ label: "Under Over", value: "underline overline" },
],
},
{ label: "transform", key: "text-transform", control: Input }, //custom { label: "transform", key: "text-transform", control: Input }, //custom
{ label: "style", key: "font-style", control: Input }, //custom { label: "style", key: "font-style", control: Input }, //custom
] ]
@ -120,8 +214,7 @@ export const background = [
{ {
label: "Background", label: "Background",
key: "background", key: "background",
control: OptionSelect, control: Input,
options: ["black", "white", "red", "blue", "green"],
}, },
{ label: "Image", key: "image", control: Input }, //custom { label: "Image", key: "image", control: Input }, //custom
] ]
@ -132,15 +225,45 @@ export const border = [
{ {
label: "Color", label: "Color",
key: "border-color", key: "border-color",
control: Input,
},
{
label: "Style",
key: "border-style",
control: OptionSelect, control: OptionSelect,
options: ["black", "white", "red", "blue", "green"], options: [
"none",
"hidden",
"dotted",
"dashed",
"solid",
"double",
"groove",
"ridge",
"inset",
"outset",
],
}, },
{ label: "Style", key: "border-style", control: Input },
] ]
export const effects = [ export const effects = [
{ label: "Opacity", key: "opacity", control: Input }, { label: "Opacity", key: "opacity", control: Input },
{ label: "Rotate", key: "transform", control: Input }, //needs special control {
label: "Rotate",
key: "transform",
control: OptionSelect,
options: [
{ label: "None", value: "rotate(0deg)" },
{ label: "45 degrees", value: "rotate(45deg)" },
{ label: "90 degrees", value: "rotate(90deg)" },
{ label: "135 degrees", value: "rotate(135deg)" },
{ label: "180 degrees", value: "rotate(180deg)" },
{ label: "225 degrees", value: "rotate(225deg)" },
{ label: "270 degrees", value: "rotate(270deg)" },
{ label: "315 degrees", value: "rotate(315deg)" },
{ label: "360 degrees", value: "rotate(360deg)" },
],
}, //needs special control
{ label: "Shadow", key: "box-shadow", control: Input }, { label: "Shadow", key: "box-shadow", control: Input },
] ]

81
packages/builder/src/components/userInterface/temporaryPanelStructure.js

@ -10,15 +10,6 @@ export default {
name: "Basic", name: "Basic",
isCategory: true, isCategory: true,
children: [ children: [
{
_component: "##builtin/screenslot",
name: "Screenslot",
description:
"This component is a placeholder for the rendering of a screen within a page.",
icon: "ri-crop-2-line",
commonProps: {},
children: [],
},
{ {
_component: "@budibase/standard-components/container", _component: "@budibase/standard-components/container",
name: "Container", name: "Container",
@ -119,7 +110,7 @@ export default {
{ {
name: "Input", name: "Input",
description: "These components handle user input.", description: "These components handle user input.",
icon: "ri-edit-box-line", icon: "ri-edit-box-fill",
commonProps: {}, commonProps: {},
children: [ children: [
{ {
@ -127,7 +118,7 @@ export default {
name: "Textfield", name: "Textfield",
description: description:
"A textfield component that allows the user to input text.", "A textfield component that allows the user to input text.",
icon: "ri-edit-box-line", icon: "ri-edit-box-fill",
properties: { properties: {
design: { ...all }, design: { ...all },
settings: [ settings: [
@ -145,7 +136,7 @@ export default {
_component: "@budibase/standard-components/checkbox", _component: "@budibase/standard-components/checkbox",
name: "Checkbox", name: "Checkbox",
description: "A selectable checkbox component", description: "A selectable checkbox component",
icon: "ri-checkbox-line", icon: "ri-checkbox-fill",
properties: { properties: {
design: { ...all }, design: { ...all },
settings: [{ label: "Label", key: "label", control: Input }], settings: [{ label: "Label", key: "label", control: Input }],
@ -166,7 +157,7 @@ export default {
name: "Select", name: "Select",
description: description:
"A select component for choosing from different options", "A select component for choosing from different options",
icon: "ri-file-list-line", icon: "ri-file-list-fill",
properties: { properties: {
design: { ...all }, design: { ...all },
settings: [], settings: [],
@ -236,7 +227,7 @@ export default {
name: "Card", name: "Card",
description: description:
"A basic card component that can contain content and actions.", "A basic card component that can contain content and actions.",
icon: "ri-layout-bottom-line", icon: "ri-layout-bottom-fill",
children: [], children: [],
properties: { design: { ...all } }, properties: { design: { ...all } },
}, },
@ -248,21 +239,6 @@ export default {
children: [], children: [],
properties: { design: { ...all } }, properties: { design: { ...all } },
}, },
{
name: "Navigation Bar",
_component: "@budibase/standard-components/Navigation",
description:
"A component for handling the navigation within your app.",
icon: "ri-navigation-fill",
children: [],
properties: { design: { ...all } },
},
],
},
{
name: "Data",
isCategory: true,
children: [
{ {
name: "Table", name: "Table",
description: "A component that generates a table from your data.", description: "A component that generates a table from your data.",
@ -283,27 +259,11 @@ export default {
}, },
children: [], children: [],
}, },
{
_component: "@budibase/standard-components/datatable",
name: "DataTable",
description: "A table for displaying data from the backend.",
icon: "ri-archive-drawer-fill",
properties: { design: { ...all } },
children: [],
},
{
_component: "@budibase/standard-components/dataform",
name: "DataForm",
description: "Form stuff",
icon: "ri-file-edit-fill",
properties: { design: { ...all } },
children: [],
},
{ {
name: "Chart", name: "Chart",
_component: "@budibase/standard-components/datachart", _component: "@budibase/standard-components/datachart",
description: "Shiny chart", description: "Shiny chart",
icon: "ri-bar-chart-line", icon: "ri-bar-chart-fill",
properties: { design: { ...all } }, properties: { design: { ...all } },
children: [], children: [],
}, },
@ -311,7 +271,7 @@ export default {
name: "List", name: "List",
_component: "@budibase/standard-components/datalist", _component: "@budibase/standard-components/datalist",
description: "Shiny list", description: "Shiny list",
icon: "ri-file-list-line", icon: "ri-file-list-fill",
properties: { design: { ...all } }, properties: { design: { ...all } },
children: [], children: [],
}, },
@ -319,10 +279,35 @@ export default {
name: "Map", name: "Map",
_component: "@budibase/standard-components/datamap", _component: "@budibase/standard-components/datamap",
description: "Shiny map", description: "Shiny map",
icon: "ri-map-pin-line", icon: "ri-map-pin-fill",
properties: { design: { ...all } },
children: [],
},
],
},
{
name: "Layouts",
isCategory: true,
children: [
{
_component: "##builtin/screenslot",
name: "Screenslot",
description:
"This component is a placeholder for the rendering of a screen within a page.",
icon: "ri-crop-2-fill",
properties: { design: { ...all } }, properties: { design: { ...all } },
commonProps: {},
children: [], children: [],
}, },
{
name: "Nav Bar",
_component: "@budibase/standard-components/Navigation",
description:
"A component for handling the navigation within your app.",
icon: "ri-navigation-fill",
children: [],
properties: { design: { ...all } },
},
], ],
}, },
], ],

5
packages/builder/src/global.css

@ -13,6 +13,7 @@
--grey: #F2F2F2; --grey: #F2F2F2;
--grey-light: #FBFBFB; --grey-light: #FBFBFB;
--grey-medium: #e8e8ef;
--grey-dark: #E6E6E6; --grey-dark: #E6E6E6;
--primary100: #0055ff; --primary100: #0055ff;
@ -136,6 +137,10 @@ h5 {
color: var(--darkslate); color: var(--darkslate);
} }
textarea {
font-family: var(--fontnormal);
}
.hoverable:hover { .hoverable:hover {
cursor: pointer; cursor: pointer;
} }

92
packages/builder/src/pages/[application]/_layout.svelte → packages/builder/src/pages/[application]/_reset.svelte

@ -1,4 +1,5 @@
<script> <script>
import Modal from "svelte-simple-modal"
import { store } from "builderStore" import { store } from "builderStore"
import { fade } from "svelte/transition" import { fade } from "svelte/transition"
@ -25,55 +26,58 @@
} }
</script> </script>
<div class="root"> <Modal>
<div class="root">
<div class="top-nav">
<div class="topleftnav"> <div class="top-nav">
<button class="home-logo"> <div class="topleftnav">
<img <button class="home-logo">
src="/_builder/assets/bb-logo.svg" <img
alt="budibase icon" src="/_builder/assets/bb-logo.svg"
on:click={() => $goto(`/`)} /> alt="budibase icon"
</button> on:click={() => $goto(`/`)} />
</button>
<!-- This gets all indexable subroutes and sticks them in the top nav. -->
{#each $layout.children as { path, title }} <!-- This gets all indexable subroutes and sticks them in the top nav. -->
<span {#each $layout.children as { path, title }}
class:active={$isActive(path)} <span
class="topnavitem" class:active={$isActive(path)}
on:click={() => $goto(path)}> class="topnavitem"
{title} on:click={() => $goto(path)}>
</span> {title}
{/each} </span>
<!-- <IconButton icon="home" {/each}
<!-- <IconButton icon="home"
color="var(--slate)" color="var(--slate)"
hoverColor="var(--secondary75)"/> --> hoverColor="var(--secondary75)"/> -->
</div> </div>
<div class="toprightnav"> <div class="toprightnav">
<span <span
class:active={$isActive(`/settings`)} class:active={$isActive(`/settings`)}
class="topnavitemright" class="topnavitemright"
on:click={() => $goto(`/settings`)}> on:click={() => $goto(`/settings`)}>
<SettingsIcon /> <SettingsIcon />
</span> </span>
<span class:active={false} class="topnavitemright"> <span
<a href={`/${application}`} target="_blank"> class:active={false}
class="topnavitemright"
on:click={() => (location = `/${application}`)}>
<PreviewIcon /> <PreviewIcon />
</a> </span>
</span> </div>
</div> </div>
</div>
{#await promise} {#await promise}
<!-- This should probably be some kind of loading state? --> <!-- This should probably be some kind of loading state? -->
<div /> <div />
{:then} {:then}
<slot /> <slot />
{:catch error} {:catch error}
<p>Something went wrong: {error.message}</p> <p>Something went wrong: {error.message}</p>
{/await} {/await}
</div> </div>
</Modal>
<style> <style>
.root { .root {
@ -151,7 +155,7 @@
} }
.topnavitemright:hover { .topnavitemright:hover {
color: rgb(255, 255, 255, 0.8); color: var(--ink);
font-weight: 500; font-weight: 500;
} }

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>

156
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,102 +43,49 @@
<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);
} }
@media only screen and (min-width: 1800px) {
.root {
display: grid;
grid-template-columns: 300px 1fr 300px;
width: 100%;
background: var(--grey-light);
}
}
.ui-nav { .ui-nav {
grid-column: 1; grid-column: 1;
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;
z-index: 5;
} }
.preview-pane { .preview-pane {
@ -162,44 +100,6 @@
background-color: var(--white); background-color: var(--white);
} }
.components-nav-page {
font-size: 13px;
color: var(--ink);
padding-left: 20px;
margin-top: 20px;
font-weight: 600;
opacity: 0.4;
letter-spacing: 1px;
}
.components-nav-header {
font-size: 13px;
color: var(--ink);
margin-top: 20px;
font-weight: 600;
opacity: 0.4;
letter-spacing: 1px;
}
.nav-header {
display: flex;
flex-direction: column;
margin-top: 20px;
}
.nav-items-container {
padding: 1rem 0rem 0rem 0rem;
}
.nav-group-header {
display: flex;
padding: 0px 20px 0px 20px;
font-size: 0.9rem;
font-weight: bold;
justify-content: space-between;
align-items: center;
}
.nav-group-header > div:nth-child(1) { .nav-group-header > div:nth-child(1) {
padding: 0rem 0.5rem 0rem 0rem; padding: 0rem 0.5rem 0rem 0rem;
vertical-align: bottom; vertical-align: bottom;
@ -207,13 +107,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;
@ -224,19 +117,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>

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

@ -0,0 +1,207 @@
<script>
import Modal from "svelte-simple-modal"
import {
SettingsIcon,
AppsIcon,
UpdatesIcon,
HostingIcon,
DocumentationIcon,
TutorialsIcon,
CommunityIcon,
ContributionIcon,
BugIcon,
EmailIcon,
TwitterIcon,
} from "components/common/Icons/"
</script>
<Modal>
<div class="root">
<div class="ui-nav">
<div class="home-logo">
<img src="/_builder/assets/bb-logo.svg" alt="Budibase icon" />
</div>
<div class="nav-section">
<div class="nav-section-title">Build</div>
<div class="nav-item-home">
<span class="nav-item-icon">
<AppsIcon />
</span>
<div class="nav-item-title">Apps</div>
</div>
<div class="nav-item">
<span class="nav-item-icon">
<SettingsIcon />
</span>
<div class="nav-item-title">Settings</div>
</div>
<a href="https://budibase.con/login" target="_blank" class="nav-item">
<span class="nav-item-icon">
<UpdatesIcon />
</span>
<div class="nav-item-title">Updates</div>
</a>
<a href="https://budibase.con/login" target="_blank" class="nav-item">
<span class="nav-item-icon">
<HostingIcon />
</span>
<div class="nav-item-title">Hosting</div>
</a>
</div>
<div class="nav-section">
<div class="nav-section-title">Learn</div>
<a href="https://docs.budibase.com/" target="_blank" class="nav-item">
<span class="nav-item-icon">
<DocumentationIcon />
</span>
<div class="nav-item-title">Documentation</div>
</a>
<a
href="https://docs.budibase.com/tutorial/quick-start"
target="_blank"
class="nav-item">
<span class="nav-item-icon">
<TutorialsIcon />
</span>
<div class="nav-item-title">Tutorials</div>
</a>
<a href="https://forum.budibase.com/" target="_blank" class="nav-item">
<span class="nav-item-icon">
<CommunityIcon />
</span>
<div class="nav-item-title">Community</div>
</a>
</div>
<div class="nav-section">
<div class="nav-section-title">Contact</div>
<a
href="https://github.com/Budibase/budibase/blob/master/CONTRIBUTING.md"
target="_blank"
class="nav-item">
<span class="nav-item-icon">
<ContributionIcon />
</span>
<div class="nav-item-title">Contribute to our product</div>
</a>
<a
href="https://github.com/Budibase/budibase/issues"
target="_blank"
class="nav-item">
<span class="nav-item-icon">
<BugIcon />
</span>
<div class="nav-item-title">Report bug</div>
</a>
<a href="mailto:support@budibase.com" target="_blank" class="nav-item">
<span class="nav-item-icon">
<EmailIcon />
</span>
<div class="nav-item-title">Email</div>
</a>
<a href="https://twitter.com/budibase" target="_blank" class="nav-item">
<span class="nav-item-icon">
<TwitterIcon />
</span>
<div class="nav-item-title">Twitter</div>
</a>
</div>
</div>
<div class="main">
<slot />
</div>
</div>
</Modal>
<style>
.root {
display: grid;
grid-template-columns: 300px 1fr;
height: 100%;
width: 100%;
background: var(--grey-light);
}
.main {
grid-column: 2;
}
.ui-nav {
grid-column: 1;
background-color: var(--white);
padding: 20px;
display: flex;
flex-direction: column;
border-right: 1px solid var(--grey-medium);
}
.home-logo {
cursor: pointer;
height: 40px;
margin-bottom: 20px;
}
.home-logo img {
height: 40px;
}
.nav-section {
margin: 20px 0px;
display: flex;
flex-direction: column;
}
.nav-section-title {
font-size: 20px;
color: var(--ink);
font-weight: 700;
margin-bottom: 12px;
}
.nav-item {
cursor: pointer;
margin: 0px 0px 4px 0px;
padding: 0px 0px 0px 12px;
height: 40px;
display: flex;
flex-direction: row;
align-items: center;
box-sizing: border-box;
}
.nav-item-home {
cursor: pointer;
margin: 0px 0px 4px 0px;
padding: 0px 0px 0px 12px;
height: 40px;
display: flex;
flex-direction: row;
align-items: center;
box-sizing: border-box;
background-color: var(--blue-light);
}
.nav-item:hover {
background-color: var(--grey-light);
border-radius: 3px;
}
.nav-item::selection {
background-color: var(--blue-light);
border-radius: 3px;
}
.nav-item-title {
font-size: 14px;
color: var(--ink);
font-weight: 500;
margin-left: 12px;
}
.nav-item-icon {
color: var(--ink-light);
}
</style>

271
packages/builder/src/pages/index.svelte

@ -1,23 +1,13 @@
<script> <script>
import { getContext } from "svelte"
import { store } from "builderStore" import { store } from "builderStore"
import AppList from "components/start/AppList.svelte" import AppList from "components/start/AppList.svelte"
import { onMount } from "svelte" import { onMount } from "svelte"
import ActionButton from "components/common/ActionButton.svelte" import ActionButton from "components/common/ActionButton.svelte"
import IconButton from "components/common/IconButton.svelte" import IconButton from "components/common/IconButton.svelte"
import {
SettingsIcon,
AppsIcon,
UpdatesIcon,
HostingIcon,
DocumentationIcon,
TutorialsIcon,
CommunityIcon,
ContributionIcon,
BugIcon,
EmailIcon,
TwitterIcon,
} from "components/common/Icons/"
import Spinner from "components/common/Spinner.svelte" import Spinner from "components/common/Spinner.svelte"
import CreateAppModal from "components/start/CreateAppModal.svelte"
let promise = getApps() let promise = getApps()
@ -31,229 +21,54 @@
throw new Error(json) throw new Error(json)
} }
} }
</script>
<div class="root">
<div class="ui-nav">
<div class="home-logo">
<img src="/_builder/assets/bb-logo.svg" alt="Budibase icon" />
</div>
<div class="nav-section"> // Handle create app modal
<div class="nav-section-title">Build</div> const { open } = getContext("simple-modal")
<div class="nav-item-home">
<span class="nav-item-icon"> const showCreateAppModal = () => {
<AppsIcon /> open(
</span> CreateAppModal,
<div class="nav-item-title">Apps</div> {
</div> message: "What is your name?",
<div class="nav-item"> hasForm: true,
<span class="nav-item-icon"> },
<SettingsIcon /> {
</span> closeButton: false,
<div class="nav-item-title">Settings</div> closeOnEsc: false,
</div> closeOnOuterClick: false,
<a href="https://budibase.con/login" target="_blank" class="nav-item"> styleContent: { padding: 0 },
<span class="nav-item-icon"> closeOnOuterClick: true,
<UpdatesIcon /> }
</span> )
<div class="nav-item-title">Updates</div> }
</a> </script>
<a href="https://budibase.con/login" target="_blank" class="nav-item">
<span class="nav-item-icon">
<HostingIcon />
</span>
<div class="nav-item-title">Hosting</div>
</a>
</div>
<div class="nav-section">
<div class="nav-section-title">Learn</div>
<a href="https://docs.budibase.com/" target="_blank" class="nav-item">
<span class="nav-item-icon">
<DocumentationIcon />
</span>
<div class="nav-item-title">Documentation</div>
</a>
<a
href="https://docs.budibase.com/tutorial/quick-start"
target="_blank"
class="nav-item">
<span class="nav-item-icon">
<TutorialsIcon />
</span>
<div class="nav-item-title">Tutorials</div>
</a>
<a href="https://forum.budibase.com/" target="_blank" class="nav-item">
<span class="nav-item-icon">
<CommunityIcon />
</span>
<div class="nav-item-title">Community</div>
</a>
</div>
<div class="nav-section"> <div class="welcome">Welcome to Budibase</div>
<div class="nav-section-title">Contact</div> <div class="banner">
<a <div class="banner-content">
href="https://github.com/Budibase/budibase/blob/master/CONTRIBUTING.md" <div class="banner-header">
target="_blank" Every accomplishment starts with a decision to try.
class="nav-item">
<span class="nav-item-icon">
<ContributionIcon />
</span>
<div class="nav-item-title">Contribute to our product</div>
</a>
<a
href="https://github.com/Budibase/budibase/issues"
target="_blank"
class="nav-item">
<span class="nav-item-icon">
<BugIcon />
</span>
<div class="nav-item-title">Report bug</div>
</a>
<a href="mailto:support@budibase.com" target="_blank" class="nav-item">
<span class="nav-item-icon">
<EmailIcon />
</span>
<div class="nav-item-title">Email</div>
</a>
<a href="https://twitter.com/budibase" target="_blank" class="nav-item">
<span class="nav-item-icon">
<TwitterIcon />
</span>
<div class="nav-item-title">Twitter</div>
</a>
</div> </div>
<button class="banner-button" type="button" on:click={showCreateAppModal}>
<i class="ri-add-circle-fill" />
Create New Web App
</button>
</div> </div>
<div class="banner-image">
<div class="main"> <img src="/_builder/assets/banner-image.png" alt="Bannerimage" />
<div class="welcome">Welcome to Budibase</div>
<div class="banner">
<div class="banner-content">
<div class="banner-header">
Every accomplishment starts with a decision to try.
</div>
<button class="banner-button" type="button">
<i class="ri-add-circle-fill" />
Create New Web App
</button>
</div>
<div class="banner-image">
<img src="/_builder/assets/banner-image.png" alt="Bannerimage" />
</div>
</div>
{#await promise}
<div class="spinner-container">
<Spinner />
</div>
{:then result}
<AppList apps={result} />
{:catch err}
<h1 style="color:red">{err}</h1>
{/await}
</div> </div>
</div> </div>
{#await promise}
<div class="spinner-container">
<Spinner />
</div>
{:then result}
<AppList apps={result} />
{:catch err}
<h1 style="color:red">{err}</h1>
{/await}
<style> <style>
.root {
display: grid;
grid-template-columns: 275px 1fr;
height: 100%;
width: 100%;
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 {
grid-column: 2;
}
.ui-nav {
grid-column: 1;
background-color: var(--white);
padding: 20px;
display: flex;
flex-direction: column;
border-right: 1px solid var(--grey-dark);
}
.home-logo {
cursor: pointer;
height: 40px;
margin-bottom: 20px;
}
.home-logo img {
height: 40px;
}
.nav-section {
margin: 20px 0px;
display: flex;
flex-direction: column;
}
.nav-section-title {
font-size: 20px;
color: var(--ink);
font-weight: 700;
margin-bottom: 12px;
}
.nav-item {
cursor: pointer;
margin: 0px 0px 4px 0px;
padding: 0px 0px 0px 12px;
height: 40px;
display: flex;
flex-direction: row;
align-items: center;
box-sizing: border-box;
}
.nav-item-home {
cursor: pointer;
margin: 0px 0px 4px 0px;
padding: 0px 0px 0px 12px;
height: 40px;
display: flex;
flex-direction: row;
align-items: center;
box-sizing: border-box;
background-color: var(--blue-light);
}
.nav-item:hover {
background-color: var(--grey-light);
border-radius: 3px;
}
.nav-item::selection {
background-color: var(--blue-light);
border-radius: 3px;
}
.nav-item-title {
font-size: 14px;
color: var(--ink);
font-weight: 500;
margin-left: 12px;
}
.nav-item-icon {
color: var(--ink-light);
}
.welcome { .welcome {
margin: 60px 80px 0px 80px; margin: 60px 80px 0px 80px;
font-size: 42px; font-size: 42px;

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

@ -45,7 +45,6 @@
"@budibase/core": "^0.0.32", "@budibase/core": "^0.0.32",
"@koa/router": "^8.0.0", "@koa/router": "^8.0.0",
"@sendgrid/mail": "^7.1.1", "@sendgrid/mail": "^7.1.1",
"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",
@ -68,6 +67,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"

71
packages/server/src/api/controllers/application.js

@ -3,6 +3,10 @@ const ClientDb = require("../../db/clientDb")
const { getPackageForBuilder } = require("../../utilities/builder") const { getPackageForBuilder } = require("../../utilities/builder")
const newid = require("../../db/newid") const newid = require("../../db/newid")
const env = require("../../environment") const env = require("../../environment")
const instanceController = require("./instance")
const { resolve, join } = require("path")
const { copy, readJSON, writeJSON, exists } = require("fs-extra")
const { exec } = require("child_process")
exports.fetch = async function(ctx) { exports.fetch = async function(ctx) {
const db = new CouchDB(ClientDb.name(env.CLIENT_ID)) const db = new CouchDB(ClientDb.name(env.CLIENT_ID))
@ -32,12 +36,77 @@ exports.create = async function(ctx) {
"@budibase/standard-components", "@budibase/standard-components",
"@budibase/materialdesign-components", "@budibase/materialdesign-components",
], ],
...ctx.request.body, name: ctx.request.body.name,
description: ctx.request.body.description,
} }
const { rev } = await db.post(newApplication) const { rev } = await db.post(newApplication)
newApplication._rev = rev newApplication._rev = rev
const createInstCtx = {
params: {
clientId: env.CLIENT_ID,
applicationId: newApplication._id,
},
request: {
body: { name: `dev-${env.CLIENT_ID}` },
},
}
await instanceController.create(createInstCtx)
if (ctx.isDev) {
const newAppFolder = await createEmptyAppPackage(ctx, newApplication)
await runNpmInstall(newAppFolder)
}
ctx.body = newApplication ctx.body = newApplication
ctx.message = `Application ${ctx.request.body.name} created successfully` ctx.message = `Application ${ctx.request.body.name} created successfully`
} }
const createEmptyAppPackage = async (ctx, app) => {
const templateFolder = resolve(
__dirname,
"..",
"..",
"utilities",
"appDirectoryTemplate"
)
const appsFolder = env.BUDIBASE_DIR
const newAppFolder = resolve(appsFolder, app._id)
if (await exists(newAppFolder)) {
ctx.throw(400, "App folder already exists for this application")
return
}
await copy(templateFolder, newAppFolder)
const packageJsonPath = join(appsFolder, app._id, "package.json")
const packageJson = await readJSON(packageJsonPath)
packageJson.name = npmFriendlyAppName(app.name)
await writeJSON(packageJsonPath, packageJson)
return newAppFolder
}
const runNpmInstall = async newAppFolder => {
return new Promise((resolve, reject) => {
const cmd = `cd ${newAppFolder} && npm install`
exec(cmd, (error, stdout, stderr) => {
if (error) {
reject(error)
}
resolve(stdout ? stdout : stderr)
})
})
}
const npmFriendlyAppName = name =>
name
.replace(/_/g, "")
.replace(/./g, "")
.replace(/ /g, "")
.toLowerCase()

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
} }
@ -90,3 +88,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)
ctx.eventEmitter.emit(`record:delete`, record) ctx.eventEmitter.emit(`record:delete`, record)
} }
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 }
}

2
packages/server/src/api/controllers/user.js

@ -61,7 +61,7 @@ exports.create = async function(ctx) {
} }
} }
exports.update = async function(ctx) {} exports.update = async function() {}
exports.destroy = async function(ctx) { exports.destroy = async function(ctx) {
const database = new CouchDB(ctx.params.instanceId) const database = new CouchDB(ctx.params.instanceId)

10
packages/server/src/api/controllers/workflow/index.js

@ -1,8 +1,5 @@
const CouchDB = require("../../../db") const CouchDB = require("../../db")
const Ajv = require("ajv") const newid = require("../../db/newid")
const newid = require("../../../db/newid")
const ajv = new Ajv()
exports.create = async function(ctx) { exports.create = async function(ctx) {
const db = new CouchDB(ctx.params.instanceId) const db = new CouchDB(ctx.params.instanceId)
@ -19,8 +16,7 @@ exports.create = async function(ctx) {
message: "Workflow created successfully", message: "Workflow created successfully",
workflow: { workflow: {
...workflow, ...workflow,
_rev: response.rev, ...response,
_id: response.id,
}, },
} }
} }

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),

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

@ -30,7 +30,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",
},
},
}, },
} }
@ -41,19 +46,6 @@ exports.createModel = async (request, instanceId, model) => {
return res.body return res.body
} }
exports.createRecord = async ({ request, instanceId, modelId, record }) => {
record = record || {
modelId,
name: "test name",
}
const res = await request
.post(`/api/${instanceId}/${modelId}/records`)
.send(record)
.set(exports.defaultHeaders)
return res.body
}
exports.createView = async (request, instanceId, view) => { exports.createView = async (request, instanceId, view) => {
view = view || { view = view || {
map: "function(doc) { emit(doc[doc.key], doc._id); } ", map: "function(doc) { emit(doc[doc.key], doc._id); } ",

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

@ -197,4 +197,31 @@ describe("/records", () => {
}) })
}) })
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/api/routes/view.js

@ -8,7 +8,7 @@ const router = Router()
router router
.get( .get(
"/api/:instanceId/views/:viewName", "/api/:instanceId/view/:viewName",
authorized(READ_VIEW, ctx => ctx.params.viewName), authorized(READ_VIEW, ctx => ctx.params.viewName),
recordController.fetchView recordController.fetchView
) )

2
packages/server/src/app.js

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

40
packages/server/src/schemas/index.js

@ -1,40 +0,0 @@
const WORKFLOW_SCHEMA = {
properties: {
type: "workflow",
pageId: {
type: "string",
},
screenId: {
type: "string",
},
live: {
type: "boolean",
},
uiTree: {
type: "object",
},
definition: {
type: "object",
properties: {
triggers: { type: "array" },
steps: { type: "array" },
// next: {
// type: "object",
// properties: {
// environment: { environment: "string" },
// type: { type: "string" },
// actionId: { type: "string" },
// args: { type: "object" },
// conditions: { type: "array" },
// errorHandling: { type: "object" },
// next: { type: "object" },
// },
// },
},
},
},
}
module.exports = {
WORKFLOW_SCHEMA,
}

1
packages/server/src/utilities/appDirectoryTemplate/.gitignore

@ -0,0 +1 @@
dist/

11
packages/server/src/utilities/appDirectoryTemplate/package.json

@ -0,0 +1,11 @@
{
"name": "name",
"version": "1.0.0",
"description": "",
"author": "",
"license": "ISC",
"dependencies": {
"@budibase/standard-components": "0.x",
"@budibase/materialdesign-components": "0.x"
}
}

19
packages/server/src/utilities/appDirectoryTemplate/pages/main/page.json

@ -0,0 +1,19 @@
{
"title": "Test App",
"favicon": "./_shared/favicon.png",
"stylesheets": [],
"componentLibraries": ["@budibase/standard-components", "@budibase/materialdesign-components"],
"props" : {
"_component": "@budibase/standard-components/container",
"_children": [],
"_id": 0,
"type": "div",
"_styles": {
"layout": {},
"position": {}
},
"_code": ""
},
"_css": "",
"uiFunctions": ""
}

0
packages/server/src/utilities/appDirectoryTemplate/pages/main/screens/.gitkeep

19
packages/server/src/utilities/appDirectoryTemplate/pages/unauthenticated/page.json

@ -0,0 +1,19 @@
{
"title": "Test App",
"favicon": "./_shared/favicon.png",
"stylesheets": [],
"componentLibraries": ["@budibase/standard-components", "@budibase/materialdesign-components"],
"props" : {
"_component": "@budibase/standard-components/container",
"_children": [],
"_id": 1,
"type": "div",
"_styles": {
"layout": {},
"position": {}
},
"_code": ""
},
"_css": "",
"uiFunctions": ""
}

0
packages/server/src/utilities/appDirectoryTemplate/pages/unauthenticated/screens/.gitkeep

1
packages/server/src/utilities/appDirectoryTemplate/plugins.js

@ -0,0 +1 @@
module.exports = () => ({})

7
packages/server/yarn.lock

@ -693,7 +693,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==
@ -6659,6 +6659,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"

Loading…
Cancel
Save