Browse Source

merge master

master
kevmodrome 6 years ago
parent
commit
300f18927f
  1. 2
      lerna.json
  2. 4
      package.json
  3. 6
      packages/builder/cypress/integration/createTable.spec.js
  4. 85
      packages/builder/cypress/integration/createView.spec.js
  5. 4
      packages/builder/cypress/support/commands.js
  6. 18
      packages/builder/package.json
  7. 42
      packages/builder/src/builderStore/store/backend.js
  8. 59
      packages/builder/src/components/common/Block.svelte
  9. 20
      packages/builder/src/components/common/CodeArea.svelte
  10. 13
      packages/builder/src/components/common/ComingSoon.svelte
  11. 4
      packages/builder/src/components/common/ConfirmDialog.svelte
  12. 2
      packages/builder/src/components/common/DatePicker.svelte
  13. 45
      packages/builder/src/components/common/Dropdown.svelte
  14. 62
      packages/builder/src/components/common/Input.svelte
  15. 50
      packages/builder/src/components/common/Inputs/InputGroup.svelte
  16. 23
      packages/builder/src/components/common/PlusButton.svelte
  17. 65
      packages/builder/src/components/common/Select.svelte
  18. 33
      packages/builder/src/components/common/Textbox.svelte
  19. 16
      packages/builder/src/components/database/CollectionView.svelte
  20. 0
      packages/builder/src/components/database/DataTable/LinkedRecord.svelte
  21. 35
      packages/builder/src/components/database/DataTable/ModelDataTable.svelte
  22. 144
      packages/builder/src/components/database/DataTable/Table.svelte
  23. 0
      packages/builder/src/components/database/DataTable/TablePagination.svelte
  24. 74
      packages/builder/src/components/database/DataTable/ViewDataTable.svelte
  25. 20
      packages/builder/src/components/database/DataTable/api.js
  26. 0
      packages/builder/src/components/database/DataTable/index.js
  27. 36
      packages/builder/src/components/database/DataTable/modals/CreateEditColumn.svelte
  28. 3
      packages/builder/src/components/database/DataTable/modals/CreateEditRecord.svelte
  29. 6
      packages/builder/src/components/database/DataTable/modals/DeleteRecord.svelte
  30. 6
      packages/builder/src/components/database/DataTable/modals/DeleteTable.svelte
  31. 62
      packages/builder/src/components/database/DataTable/modals/DeleteView.svelte
  32. 6
      packages/builder/src/components/database/DataTable/modals/RecordFieldControl.svelte
  33. 2
      packages/builder/src/components/database/DataTable/modals/index.js
  34. 95
      packages/builder/src/components/database/DataTable/popovers/Calculate.svelte
  35. 10
      packages/builder/src/components/database/DataTable/popovers/Column.svelte
  36. 3
      packages/builder/src/components/database/DataTable/popovers/ColumnHeader.svelte
  37. 0
      packages/builder/src/components/database/DataTable/popovers/EditRow.svelte
  38. 86
      packages/builder/src/components/database/DataTable/popovers/GroupBy.svelte
  39. 2
      packages/builder/src/components/database/DataTable/popovers/Row.svelte
  40. 84
      packages/builder/src/components/database/DataTable/popovers/View.svelte
  41. 127
      packages/builder/src/components/database/ModelDataTable/modals/CreateEditView.svelte
  42. 94
      packages/builder/src/components/database/ModelDataTable/modals/CreateUser.svelte
  43. 0
      packages/builder/src/components/database/ModelDataTable/popovers/View.svelte
  44. 95
      packages/builder/src/components/nav/DatabasesList.svelte
  45. 45
      packages/builder/src/components/nav/HierarchyRow.svelte
  46. 8
      packages/builder/src/components/nav/ModelNavigator/CreateTable.svelte
  47. 2
      packages/builder/src/components/nav/ModelNavigator/EditTable.svelte
  48. 143
      packages/builder/src/components/nav/ModelNavigator/EditView.svelte
  49. 15
      packages/builder/src/components/nav/ModelNavigator/ListItem.svelte
  50. 33
      packages/builder/src/components/nav/ModelNavigator/ModelNavigator.svelte
  51. 120
      packages/builder/src/components/nav/ModelSetupNav/ModelFieldEditor.svelte
  52. 156
      packages/builder/src/components/nav/ModelSetupNav/ModelSetupNav.svelte
  53. 1
      packages/builder/src/components/nav/ModelSetupNav/index.js
  54. 82
      packages/builder/src/components/nav/UsersList.svelte
  55. 38
      packages/builder/src/components/start/AppCard.svelte
  56. 4
      packages/builder/src/components/start/AppList.svelte
  57. 7
      packages/builder/src/components/start/CreateAppModal.svelte
  58. 2
      packages/builder/src/components/start/Steps/Info.svelte
  59. 2
      packages/builder/src/components/start/Steps/User.svelte
  60. 1
      packages/builder/src/components/userInterface/BindingDropdown.svelte
  61. 111
      packages/builder/src/components/userInterface/CodeEditor.svelte
  62. 320
      packages/builder/src/components/userInterface/Colorpicker/Colorpicker.svelte
  63. 157
      packages/builder/src/components/userInterface/Colorpicker/Colorpreview.svelte
  64. 37
      packages/builder/src/components/userInterface/Colorpicker/Portal.svelte
  65. 7
      packages/builder/src/components/userInterface/ComponentPropertiesPanel.svelte
  66. 59
      packages/builder/src/components/userInterface/ComponentSearch.svelte
  67. 15
      packages/builder/src/components/userInterface/ComponentsHierarchy.svelte
  68. 6
      packages/builder/src/components/userInterface/ComponentsHierarchyChildren.svelte
  69. 90
      packages/builder/src/components/userInterface/EditComponentProps.svelte
  70. 94
      packages/builder/src/components/userInterface/EventsEditor/EventEditorModal.svelte
  71. 158
      packages/builder/src/components/userInterface/EventsEditor/EventsEditor.svelte
  72. 3
      packages/builder/src/components/userInterface/EventsEditor/HandlerSelector.svelte
  73. 4
      packages/builder/src/components/userInterface/EventsEditor/StateBindingCascader.svelte
  74. 170
      packages/builder/src/components/userInterface/LayoutEditor.svelte
  75. 2
      packages/builder/src/components/userInterface/LayoutTemplateControls.svelte
  76. 17
      packages/builder/src/components/userInterface/ModelSelect.svelte
  77. 110
      packages/builder/src/components/userInterface/NewScreen.svelte
  78. 33
      packages/builder/src/components/userInterface/OptionSelectOld.svelte
  79. 65
      packages/builder/src/components/userInterface/PageView.svelte
  80. 46
      packages/builder/src/components/userInterface/PropControl.svelte
  81. 15
      packages/builder/src/components/userInterface/PropertyControl.svelte
  82. 11
      packages/builder/src/components/userInterface/PropertyPanelControls/Input.svelte
  83. 54
      packages/builder/src/components/userInterface/PropsView.svelte
  84. 3
      packages/builder/src/components/userInterface/SettingsView.svelte
  85. 63
      packages/builder/src/components/userInterface/StateBindingControl.svelte
  86. 1
      packages/builder/src/components/userInterface/UserInterfaceRoot.svelte
  87. 2
      packages/builder/src/components/userInterface/propertyCategories.js
  88. 615
      packages/builder/src/components/userInterface/temporaryPanelStructure.js
  89. 2
      packages/builder/src/components/workflow/SetupPanel/ParamInputs/ComponentSelector.svelte
  90. 2
      packages/builder/src/components/workflow/SetupPanel/ParamInputs/ModelSelector.svelte
  91. 10
      packages/builder/src/components/workflow/SetupPanel/ParamInputs/RecordSelector.svelte
  92. 32
      packages/builder/src/components/workflow/SetupPanel/SetupPanel.svelte
  93. 42
      packages/builder/src/components/workflow/SetupPanel/WorkflowBlockSetup.svelte
  94. 4
      packages/builder/src/components/workflow/WorkflowPanel/WorkflowList/CreateWorkflowModal.svelte
  95. 147
      packages/builder/src/constants/backend/index.js
  96. 15
      packages/builder/src/global.css
  97. 7
      packages/builder/src/index.html
  98. 6
      packages/builder/src/pages/[application]/backend/_layout.svelte
  99. 6
      packages/builder/src/pages/[application]/backend/model/[selectedModel]/index.svelte
  100. 22
      packages/builder/src/pages/[application]/backend/view/[selectedView]/_layout.svelte

2
lerna.json

@ -1,5 +1,5 @@
{ {
"version": "0.1.17", "version": "0.1.19",
"npmClient": "yarn", "npmClient": "yarn",
"packages": [ "packages": [
"packages/*" "packages/*"

4
package.json

@ -28,9 +28,5 @@
"format": "prettier --write \"{,!(node_modules)/**/}*.{js,jsx,svelte}\"", "format": "prettier --write \"{,!(node_modules)/**/}*.{js,jsx,svelte}\"",
"test:e2e": "lerna run cy:test", "test:e2e": "lerna run cy:test",
"test:e2e:ci": "lerna run cy:ci" "test:e2e:ci": "lerna run cy:ci"
},
"dependencies": {
"@material/icon-button": "4.0.0",
"date-fns": "^2.10.0"
} }
} }

6
packages/builder/cypress/integration/createTable.spec.js

@ -38,10 +38,10 @@ context('Create a Table', () => {
it('edits a record', () => { it('edits a record', () => {
cy.get("tbody .ri-more-line").click() cy.get("tbody .ri-more-line").click()
cy.get("[data-cy=edit-row]").click() cy.get("[data-cy=edit-row]").click()
cy.get(".actions input").type("updatedRecord") cy.get(".actions input").type("Updated")
cy.contains("Save").click() cy.contains("Save").click()
cy.contains('updatedRecord').should('have.text', 'updatedRecord') cy.contains('RoverUpdated').should('have.text', 'RoverUpdated')
}) })
it('deletes a record', () => { it('deletes a record', () => {
@ -49,7 +49,7 @@ context('Create a Table', () => {
cy.get("[data-cy=delete-row]").click() cy.get("[data-cy=delete-row]").click()
cy.get(".modal-actions").contains("Delete").click() cy.get(".modal-actions").contains("Delete").click()
cy.contains('updatedRecord').should('not.exist') cy.contains('RoverUpdated').should('not.exist')
}) })
it('deletes a column', () => { it('deletes a column', () => {

85
packages/builder/cypress/integration/createView.spec.js

@ -0,0 +1,85 @@
context('Create a View', () => {
before(() => {
cy.visit('localhost:4001/_builder')
cy.createApp('View App', 'View App Description')
cy.createTable('data')
cy.addColumn('data', 'group', 'Plain Text')
cy.addColumn('data', 'age', 'Number')
cy.addColumn('data', 'rating', 'Number')
cy.addRecord(["Students", 25, 1])
cy.addRecord(["Students", 20, 3])
cy.addRecord(["Students", 18, 6])
cy.addRecord(["Students", 25, 2])
cy.addRecord(["Teachers", 49, 5])
cy.addRecord(["Teachers", 36, 3])
})
it('creates a stats view based on age', () => {
cy.contains("Create New View").click()
cy.get("[placeholder='View Name']").type("Test View")
cy.contains("Save View").click()
cy.get("thead th").should(($headers) => {
expect($headers).to.have.length(7)
const headers = $headers.map((i, header) => Cypress.$(header).text())
expect(headers.get()).to.deep.eq([
"group",
"sum",
"min",
"max",
"sumsqr",
"count",
"avg",
])
})
cy.get("tbody td").should(($values) => {
const values = $values.map((i, value) => Cypress.$(value).text())
expect(values.get()).to.deep.eq([
"null",
"173",
"18",
"49",
"5671",
"6",
"28.833333333333332"
])
})
})
it('groups the stats view by group', () => {
cy.contains("Group By").click()
cy.get("select").select("group")
cy.contains("Save").click()
cy.contains("Students").should("be.visible")
cy.contains("Teachers").should("be.visible")
cy.get("tbody tr").first().find("td").should(($values) => {
const values = $values.map((i, value) => Cypress.$(value).text())
expect(values.get()).to.deep.eq([
"Students",
"88",
"18",
"25",
"1974",
"4",
"22"
])
})
})
it('renames a view', () => {
cy.contains("[data-cy=model-nav-item]", "Test View").find(".ri-more-line").click()
cy.contains("Edit").click()
cy.get("[placeholder='View Name']").type(" Updated")
cy.contains("Save").click()
cy.contains("Test View Updated").should("be.visible")
})
it('deletes a view', () => {
cy.contains("[data-cy=model-nav-item]", "Test View Updated").find(".ri-more-line").click()
cy.contains("Delete").click()
cy.get(".content").contains("button", "Delete").click()
cy.contains("TestView Updated").should("not.be.visible")
})
})

4
packages/builder/cypress/support/commands.js

@ -131,9 +131,9 @@ Cypress.Commands.add("navigateToFrontend", () => {
Cypress.Commands.add("createScreen", (screenName, route) => { Cypress.Commands.add("createScreen", (screenName, route) => {
cy.get(".newscreen").click() cy.get(".newscreen").click()
cy.get(".uk-input:first").type(screenName) cy.get("[data-cy=new-screen-dialog] input:first").type(screenName)
if (route) { if (route) {
cy.get(".uk-input:last").type(route) cy.get("[data-cy=new-screen-dialog] input:last").type(route)
} }
cy.get(".uk-modal-footer").within(() => { cy.get(".uk-modal-footer").within(() => {
cy.contains("Create Screen").click() cy.contains("Create Screen").click()

18
packages/builder/package.json

@ -1,6 +1,6 @@
{ {
"name": "@budibase/builder", "name": "@budibase/builder",
"version": "0.1.17", "version": "0.1.19",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"private": true, "private": true,
"scripts": { "scripts": {
@ -58,26 +58,20 @@
}, },
"dependencies": { "dependencies": {
"@budibase/bbui": "^1.27.0", "@budibase/bbui": "^1.27.0",
"@budibase/client": "^0.1.17", "@budibase/client": "^0.1.19",
"@budibase/colorpicker": "^1.0.1", "@budibase/colorpicker": "^1.0.1",
"@nx-js/compiler-util": "^2.0.0",
"@sentry/browser": "5.19.1", "@sentry/browser": "5.19.1",
"@svelteschool/svelte-forms": "^0.7.0", "@svelteschool/svelte-forms": "^0.7.0",
"britecharts": "^2.16.0", "britecharts": "^2.16.0",
"codemirror": "^5.51.0",
"d3-selection": "^1.4.1", "d3-selection": "^1.4.1",
"date-fns": "^1.29.0",
"deepmerge": "^4.2.2", "deepmerge": "^4.2.2",
"fast-sort": "^2.2.0", "fast-sort": "^2.2.0",
"feather-icons": "^4.21.0", "feather-icons": "^4.21.0",
"flatpickr": "^4.5.7", "flatpickr": "^4.5.7",
"lodash": "^4.17.13", "lodash": "^4.17.13",
"lunr": "^2.3.5",
"mustache": "^4.0.1", "mustache": "^4.0.1",
"posthog-js": "1.3.1", "posthog-js": "1.3.1",
"safe-buffer": "^5.1.2",
"shortid": "^2.2.15", "shortid": "^2.2.15",
"string_decoder": "^1.2.0",
"svelte-portal": "^0.1.0", "svelte-portal": "^0.1.0",
"svelte-simple-modal": "^0.4.2", "svelte-simple-modal": "^0.4.2",
"uikit": "^3.1.7", "uikit": "^3.1.7",
@ -93,20 +87,16 @@
"@sveltech/routify": "1.7.11", "@sveltech/routify": "1.7.11",
"@testing-library/jest-dom": "^5.11.0", "@testing-library/jest-dom": "^5.11.0",
"@testing-library/svelte": "^3.0.0", "@testing-library/svelte": "^3.0.0",
"babel-jest": "^26.2.2", "babel-jest": "^24.8.0",
"browser-sync": "^2.26.7",
"cypress": "^4.8.0", "cypress": "^4.8.0",
"cypress-terminal-report": "^1.4.1", "cypress-terminal-report": "^1.4.1",
"eslint-plugin-cypress": "^2.11.1", "eslint-plugin-cypress": "^2.11.1",
"http-proxy-middleware": "^0.19.1",
"identity-obj-proxy": "^3.0.0", "identity-obj-proxy": "^3.0.0",
"jest": "^26.2.2", "jest": "^24.8.0",
"ncp": "^2.0.0", "ncp": "^2.0.0",
"npm-run-all": "^4.1.5",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"rollup": "^1.12.0", "rollup": "^1.12.0",
"rollup-plugin-alias": "^1.5.2", "rollup-plugin-alias": "^1.5.2",
"rollup-plugin-browsersync": "^1.0.0",
"rollup-plugin-commonjs": "^10.0.0", "rollup-plugin-commonjs": "^10.0.0",
"rollup-plugin-copy": "^3.0.0", "rollup-plugin-copy": "^3.0.0",
"rollup-plugin-livereload": "^1.0.0", "rollup-plugin-livereload": "^1.0.0",

42
packages/builder/src/builderStore/store/backend.js

@ -23,13 +23,10 @@ export const getBackendUiStore = () => {
database: { database: {
select: async db => { select: async db => {
const modelsResponse = await api.get(`/api/models`) const modelsResponse = await api.get(`/api/models`)
const viewsResponse = await api.get(`/api/views`)
const models = await modelsResponse.json() const models = await modelsResponse.json()
const views = await viewsResponse.json()
store.update(state => { store.update(state => {
state.selectedDatabase = db state.selectedDatabase = db
state.models = models state.models = models
state.views = views
return state return state
}) })
}, },
@ -59,8 +56,7 @@ export const getBackendUiStore = () => {
store.update(state => { store.update(state => {
state.selectedModel = model state.selectedModel = model
state.draftModel = cloneDeep(model) state.draftModel = cloneDeep(model)
state.selectedField = "" state.selectedView = { name: `all_${model._id}` }
state.selectedView = `all_${model._id}`
return state return state
}), }),
save: async model => { save: async model => {
@ -97,12 +93,16 @@ export const getBackendUiStore = () => {
saveField: ({ originalName, field }) => { saveField: ({ originalName, field }) => {
store.update(state => { store.update(state => {
// delete the original if renaming // delete the original if renaming
delete state.draftModel.schema[originalName] if (originalName) {
delete state.draftModel.schema[originalName]
state.draftModel.schema = { state.draftModel._rename = {
...state.draftModel.schema, old: originalName,
[field.name]: cloneDeep(field), updated: field.name,
}
} }
state.draftModel.schema[field.name] = cloneDeep(field)
store.actions.models.save(state.draftModel) store.actions.models.save(state.draftModel)
return state return state
}) })
@ -119,8 +119,30 @@ export const getBackendUiStore = () => {
select: view => select: view =>
store.update(state => { store.update(state => {
state.selectedView = view state.selectedView = view
state.selectedModel = {}
return state return state
}), }),
delete: async view => {
await api.delete(`/api/views/${view}`)
await store.actions.models.fetch()
},
save: async view => {
await api.post(`/api/views`, view)
store.update(state => {
const viewModel = state.models.find(
model => model._id === view.modelId
)
// TODO: Cleaner?
if (!viewModel.views) viewModel.views = {}
if (view.originalName) delete viewModel.views[view.originalName]
viewModel.views[view.name] = view
state.models = state.models
state.selectedView = view
return state
})
},
}, },
users: { users: {
create: user => create: user =>

59
packages/builder/src/components/common/Block.svelte

@ -1,59 +0,0 @@
<script>
export let title
export let icon
export let primary
export let secondary
export let tertiary
</script>
<div on:click class:primary class:secondary class:tertiary>
<i class={icon} />
<span>{title}</span>
</div>
<style>
div {
height: 80px;
border-radius: 5px;
color: var(--ink);
font-weight: 400;
padding: 15px;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
transition: 0.3s transform;
background: var(--grey-1);
}
i {
font-size: 24px;
color: var(--grey-7);
}
span {
font-size: 14px;
text-align: center;
margin-top: 8px;
line-height: 1.25;
}
div:hover {
cursor: pointer;
background: var(--grey-2);
}
.primary {
background: var(--ink);
color: var(--white);
}
.secondary {
background: var(--blue-light);
}
.tertiary {
background: var(--white);
}
</style>

20
packages/builder/src/components/common/CodeArea.svelte

@ -1,20 +0,0 @@
<script>
import { JavaScriptIcon } from "../common/Icons"
// todo: use https://ace.c9.io
export let text = ""
</script>
<textarea class="uk-textarea" bind:value={text} />
<style>
textarea {
padding: 10px;
margin-top: 5px;
margin-bottom: 10px;
background: var(--grey-7);
color: var(--white);
font-family: "Courier New", Courier, monospace;
height: 200px;
border-radius: 5px;
}
</style>

13
packages/builder/src/components/common/ComingSoon.svelte

@ -1,13 +0,0 @@
<script>
export let name = ""
</script>
<div>
<h4>Coming Sometime: {name}</h4>
</div>
<style>
h4 {
margin-top: 20px;
}
</style>

4
packages/builder/src/components/common/ConfirmDialog.svelte

@ -29,8 +29,10 @@
} }
const ok = () => { const ok = () => {
const result = onOk()
// allow caller to return false, to cancel the "ok"
if (result === false) return
hide() hide()
onOk()
} }
</script> </script>

2
packages/builder/src/components/common/DatePicker.svelte

@ -24,7 +24,7 @@
}) })
</script> </script>
<div class="uk-margin"> <div class="bb-margin-m">
<label class="uk-form-label">{label}</label> <label class="uk-form-label">{label}</label>
<div class="uk-form-controls"> <div class="uk-form-controls">
<input <input

45
packages/builder/src/components/common/Dropdown.svelte

@ -1,45 +0,0 @@
<script>
import { createEventDispatcher } from "svelte"
import Select from "../common/Select.svelte"
export let selected
export let label
export let options
export let valueMember
export let textMember
export let multiple = false
export let width = "medium"
export let size = "small"
const dispatch = createEventDispatcher()
</script>
<div class="uk-margin">
<label class="uk-form-label">{label}</label>
<div class="uk-form-controls">
{#if multiple}
<Select
class="uk-select uk-form-width-{width} uk-form-{size}"
multiple
bind:value={selected}
on:change>
{#each options as option}
<option value={!valueMember ? option : valueMember(option)}>
{!textMember ? option : textMember(option)}
</option>
{/each}
</Select>
{:else}
<Select
class="uk-select uk-form-width-{width} uk-form-{size}"
bind:value={selected}
on:change>
{#each options as option}
<option value={!valueMember ? option : valueMember(option)}>
{!textMember ? option : textMember(option)}
</option>
{/each}
</Select>
{/if}
</div>
</div>

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

@ -1,62 +0,0 @@
<script>
import { onMount } from "svelte"
import { buildStyle } from "../../helpers.js"
export let value = ""
export let name = ""
export let textAlign = "left"
export let width = "160px"
export let placeholder = ""
export let suffix = ""
export let onChange = val => {}
let centerPlaceholder = textAlign === "center"
let style = buildStyle({ width, textAlign })
function handleChange(val) {
value = val
let _value = value !== "auto" ? value + suffix : value
onChange(_value)
}
$: displayValue =
suffix && value && value.endsWith(suffix)
? value.replace(new RegExp(`${suffix}$`), "")
: value || ""
</script>
<input
{name}
class:centerPlaceholder
type="text"
value={displayValue}
{placeholder}
{style}
on:change={e => handleChange(e.target.value)} />
<style>
input {
/* width: 32px; */
height: 36px;
font-size: 14px;
font-weight: 400;
margin: 0px 0px 0px 2px;
color: var(--ink);
padding: 0px 8px;
font-family: inter;
width: 164px;
box-sizing: border-box;
background-color: var(--grey-2);
border-radius: 4px;
border: 1px solid var(--grey-2);
outline: none;
}
input::placeholder {
text-align: left;
}
.centerPlaceholder::placeholder {
text-align: center;
}
</style>

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

@ -1,50 +0,0 @@
<script>
import { onMount } from "svelte"
import Input from "../Input.svelte"
export let meta = []
export let label = ""
export let value = ["0", "0", "0", "0"]
export let suffix = ""
export let onChange = () => {}
function handleChange(val, idx) {
value.splice(idx, 1, val !== "auto" && suffix ? val + suffix : val)
value = value
let _value = value.map(v =>
suffix && !v.endsWith(suffix) && v !== "auto" ? v + suffix : v
)
onChange(_value)
}
$: displayValues =
value && suffix
? value.map(v => v.replace(new RegExp(`${suffix}$`), ""))
: value || []
</script>
<div class="input-container">
<div class="label">{label}</div>
<div class="inputs-group">
{#each meta as m, i}
<Input
width="37px"
textAlign="center"
placeholder={m.placeholder || ''}
value={!displayValues || displayValues[i] === '0' ? '' : displayValues[i]}
onChange={value => handleChange(value || 0, i)} />
{/each}
</div>
</div>
<style>
.label {
flex: 0;
}
.inputs-group {
flex: 1;
}
</style>

23
packages/builder/src/components/common/PlusButton.svelte

@ -1,23 +0,0 @@
<button on:click>
<slot>+</slot>
</button>
<style>
button {
cursor: pointer;
outline: none;
border: none;
border-radius: 5px;
min-width: 1.8rem;
min-height: 1.8rem;
padding-bottom: 10px;
display: flex;
justify-content: center;
align-items: center;
font-size: 1.2rem;
font-weight: 600;
color: var(--ink);
}
</style>

65
packages/builder/src/components/common/Select.svelte

@ -1,65 +0,0 @@
<script>
import getIcon from "./icon"
export let icon
export let value
</script>
<div class="select-container">
{#if icon}
<i class={icon} />
{/if}
<select class:adjusted={icon} on:change bind:value>
<slot />
</select>
<span class="arrow">
{@html getIcon('chevron-down', '24')}
</span>
</div>
<style>
.select-container {
font-size: 14px;
position: relative;
border: var(--grey-4) 1px solid;
}
.adjusted {
padding-left: 30px;
}
i {
position: absolute;
left: 10px;
top: 10px;
}
select {
height: 40px;
display: block;
font-family: sans-serif;
font-weight: 400;
font-size: 14px;
color: var(--ink);
padding: 0 40px 0px 20px;
width: 100%;
max-width: 100%;
box-sizing: border-box;
margin: 0;
-moz-appearance: none;
-webkit-appearance: none;
appearance: none;
background: var(--white);
}
.arrow {
position: absolute;
right: 10px;
bottom: 0;
margin: auto;
width: 30px;
height: 30px;
pointer-events: none;
color: var(--ink);
}
</style>

33
packages/builder/src/components/common/Textbox.svelte

@ -1,33 +0,0 @@
<script>
export let text = ""
export let label = ""
export let width = "medium"
export let size = "small"
export let margin = true
export let infoText = ""
export let hasError = false
export let disabled = false
</script>
<div class:uk-margin={margin}>
<label class="uk-form-label">{label}</label>
<div class="uk-form-controls">
<input
data-cy={label}
class="budibase__input"
class:uk-form-danger={hasError}
on:change
bind:value={text}
{disabled} />
</div>
{#if infoText}
<div class="info-text">{infoText}</div>
{/if}
</div>
<style>
.info-text {
font-size: 0.7rem;
color: var(--secondary50);
}
</style>

16
packages/builder/src/components/database/CollectionView.svelte

@ -1,16 +0,0 @@
<script>
import RecordCard from "./RecordCard.svelte"
export let record = {}
</script>
<div class="root">
<div class="title">{record.name}</div>
<div class="inner">
<div class="node-path">{record.nodeKey()}</div>
</div>
</div>
<style>
</style>

0
packages/builder/src/components/database/ModelDataTable/LinkedRecord.svelte → packages/builder/src/components/database/DataTable/LinkedRecord.svelte

35
packages/builder/src/components/database/ModelDataTable/ModelDataTable.svelte → packages/builder/src/components/database/DataTable/ModelDataTable.svelte

@ -1,15 +1,16 @@
<script> <script>
import { onMount } from "svelte" import { onMount } from "svelte"
import fsort from "fast-sort" import fsort from "fast-sort"
import getOr from "lodash/fp/getOr"
import { store, backendUiStore } from "builderStore" import { store, backendUiStore } from "builderStore"
import { Button, Icon } from "@budibase/bbui" import { Button, Icon } from "@budibase/bbui"
import Select from "components/common/Select.svelte"
import ActionButton from "components/common/ActionButton.svelte" import ActionButton from "components/common/ActionButton.svelte"
import LinkedRecord from "./LinkedRecord.svelte" import LinkedRecord from "./LinkedRecord.svelte"
import TablePagination from "./TablePagination.svelte" import TablePagination from "./TablePagination.svelte"
import { DeleteRecordModal, CreateEditRecordModal } from "./modals" import { DeleteRecordModal, CreateEditRecordModal } from "./modals"
import RowPopover from "./popovers/Row.svelte" import RowPopover from "./popovers/Row.svelte"
import ColumnPopover from "./popovers/Column.svelte" import ColumnPopover from "./popovers/Column.svelte"
import ViewPopover from "./popovers/View.svelte"
import ColumnHeaderPopover from "./popovers/ColumnHeader.svelte" import ColumnHeaderPopover from "./popovers/ColumnHeader.svelte"
import EditRowPopover from "./popovers/EditRow.svelte" import EditRowPopover from "./popovers/EditRow.svelte"
import * as api from "./api" import * as api from "./api"
@ -21,12 +22,14 @@
let modalOpen = false let modalOpen = false
let data = [] let data = []
let headers = [] let headers = []
let views = []
let currentPage = 0 let currentPage = 0
let search let search
$: { $: {
if ($backendUiStore.selectedView) { if (
$backendUiStore.selectedView &&
$backendUiStore.selectedView.name.startsWith("all_")
) {
api.fetchDataForView($backendUiStore.selectedView).then(records => { api.fetchDataForView($backendUiStore.selectedView).then(records => {
data = records || [] data = records || []
}) })
@ -42,27 +45,11 @@
$: sort = $backendUiStore.sort $: sort = $backendUiStore.sort
$: sorted = sort ? fsort(data)[sort.direction](sort.column) : data $: sorted = sort ? fsort(data)[sort.direction](sort.column) : data
$: headers = Object.keys($backendUiStore.selectedModel.schema).filter( $: headers = Object.keys($backendUiStore.selectedModel.schema)
id => !INTERNAL_HEADERS.includes(id) .sort()
) .filter(id => !INTERNAL_HEADERS.includes(id))
$: schema = $backendUiStore.selectedModel.schema $: schema = $backendUiStore.selectedModel.schema
const createNewRecord = () => {
open(
CreateEditRecordModal,
{
onClosed: close,
},
{ styleContent: { padding: "0" } }
)
}
onMount(() => {
if (views.length) {
backendUiStore.actions.views.select(views[0])
}
})
</script> </script>
<section> <section>
@ -72,6 +59,7 @@
<ColumnPopover /> <ColumnPopover />
{#if Object.keys($backendUiStore.selectedModel.schema).length > 0} {#if Object.keys($backendUiStore.selectedModel.schema).length > 0}
<RowPopover /> <RowPopover />
<ViewPopover />
{/if} {/if}
</div> </div>
</div> </div>
@ -102,7 +90,7 @@
<td> <td>
{#if schema[header].type === 'link'} {#if schema[header].type === 'link'}
<LinkedRecord field={schema[header]} ids={row[header]} /> <LinkedRecord field={schema[header]} ids={row[header]} />
{:else}{row[header] || ''}{/if} {:else}{getOr('', header, row)}{/if}
</td> </td>
{/each} {/each}
</tr> </tr>
@ -190,7 +178,6 @@
.popovers { .popovers {
display: flex; display: flex;
gap: var(--spacing-m);
} }
.no-data { .no-data {

144
packages/builder/src/components/database/DataTable/Table.svelte

@ -0,0 +1,144 @@
<script>
import { onMount } from "svelte"
import fsort from "fast-sort"
import getOr from "lodash/fp/getOr"
import { store, backendUiStore } from "builderStore"
import api from "builderStore/api"
import { Button, Icon } from "@budibase/bbui"
import ActionButton from "components/common/ActionButton.svelte"
import LinkedRecord from "./LinkedRecord.svelte"
import TablePagination from "./TablePagination.svelte"
import { DeleteRecordModal, CreateEditRecordModal } from "./modals"
import RowPopover from "./popovers/Row.svelte"
import ColumnPopover from "./popovers/Column.svelte"
import ViewPopover from "./popovers/View.svelte"
import ColumnHeaderPopover from "./popovers/ColumnHeader.svelte"
import EditRowPopover from "./popovers/EditRow.svelte"
import CalculationPopover from "./popovers/Calculate.svelte"
export let columns = []
export let data = []
export let title
const ITEMS_PER_PAGE = 10
let currentPage = 0
$: paginatedData =
data && data.length
? data.slice(
currentPage * ITEMS_PER_PAGE,
currentPage * ITEMS_PER_PAGE + ITEMS_PER_PAGE
)
: []
$: sort = $backendUiStore.sort
$: sorted = sort ? fsort(data)[sort.direction](sort.column) : data
</script>
<section>
<div class="table-controls">
<h2 class="title">{title}</h2>
<div class="popovers">
<slot />
</div>
</div>
<table class="uk-table">
<thead>
<tr>
{#each columns as header}
<th>{header.name}</th>
{/each}
</tr>
</thead>
<tbody>
{#if paginatedData.length === 0}
<div class="no-data">No Data.</div>
{/if}
{#each paginatedData as row}
<tr>
{#each columns as header}
<td>{getOr(row.default || '', header.key, row)}</td>
{/each}
</tr>
{/each}
</tbody>
</table>
<TablePagination
{data}
bind:currentPage
pageItemCount={data.length}
{ITEMS_PER_PAGE} />
</section>
<style>
section {
margin-bottom: 20px;
}
.title {
font-size: 24px;
font-weight: 600;
text-rendering: optimizeLegibility;
text-transform: capitalize;
}
table {
border: 1px solid var(--grey-4);
background: #fff;
border-radius: 3px;
border-collapse: collapse;
}
thead {
height: 40px;
background: var(--grey-3);
border: 1px solid var(--grey-4);
}
thead th {
color: var(--ink);
text-transform: capitalize;
font-weight: 500;
font-size: 14px;
text-rendering: optimizeLegibility;
transition: 0.5s all;
vertical-align: middle;
}
th:hover {
color: var(--blue);
cursor: pointer;
}
td {
max-width: 200px;
text-overflow: ellipsis;
border: 1px solid var(--grey-4);
}
tbody tr {
border-bottom: 1px solid var(--grey-4);
transition: 0.3s background-color;
color: var(--ink);
font-size: 12px;
}
tbody tr:hover {
background: var(--grey-1);
}
.table-controls {
width: 100%;
}
.popovers {
display: flex;
}
:global(.popovers > div) {
margin-right: var(--spacing-m);
}
.no-data {
padding: 14px;
}
</style>

0
packages/builder/src/components/database/ModelDataTable/TablePagination.svelte → packages/builder/src/components/database/DataTable/TablePagination.svelte

74
packages/builder/src/components/database/DataTable/ViewDataTable.svelte

@ -0,0 +1,74 @@
<script>
import { onMount } from "svelte"
import fsort from "fast-sort"
import getOr from "lodash/fp/getOr"
import { store, backendUiStore } from "builderStore"
import api from "builderStore/api"
import { Button, Icon } from "@budibase/bbui"
import Table from "./Table.svelte"
import ActionButton from "components/common/ActionButton.svelte"
import LinkedRecord from "./LinkedRecord.svelte"
import TablePagination from "./TablePagination.svelte"
import { DeleteRecordModal, CreateEditRecordModal } from "./modals"
import RowPopover from "./popovers/Row.svelte"
import ColumnPopover from "./popovers/Column.svelte"
import ViewPopover from "./popovers/View.svelte"
import ColumnHeaderPopover from "./popovers/ColumnHeader.svelte"
import EditRowPopover from "./popovers/EditRow.svelte"
import CalculationPopover from "./popovers/Calculate.svelte"
import GroupByPopover from "./popovers/GroupBy.svelte"
let COLUMNS = [
{
name: "group",
key: "key",
default: "All Records",
},
{
name: "sum",
key: "value.sum",
},
{
name: "min",
key: "value.min",
},
{
name: "max",
key: "value.max",
},
{
name: "sumsqr",
key: "value.sumsqr",
},
{
name: "count",
key: "value.count",
},
{
name: "avg",
key: "value.avg",
},
]
export let view = {}
let data = []
$: ({ name, groupBy } = view)
$: !name.startsWith("all_") && fetchViewData(name, groupBy)
async function fetchViewData(name, groupBy) {
let QUERY_VIEW_URL = `/api/views/${name}?stats=true`
if (groupBy) {
QUERY_VIEW_URL += `&group=${groupBy}`
}
const response = await api.get(QUERY_VIEW_URL)
data = await response.json()
}
</script>
<Table title={decodeURI(view.name)} columns={COLUMNS} {data}>
<CalculationPopover {view} />
<GroupByPopover {view} />
</Table>

20
packages/builder/src/components/database/ModelDataTable/api.js → packages/builder/src/components/database/DataTable/api.js

@ -6,11 +6,10 @@ export async function createUser(user) {
return await response.json() return await response.json()
} }
export async function createDatabase(appname, instanceName) { export async function saveRecord(record, modelId) {
const CREATE_DATABASE_URL = `/api/${appname}/instances` const SAVE_RECORDS_URL = `/api/${modelId}/records`
const response = await api.post(CREATE_DATABASE_URL, { const response = await api.post(SAVE_RECORDS_URL, record)
name: instanceName,
})
return await response.json() return await response.json()
} }
@ -20,15 +19,8 @@ export async function deleteRecord(record) {
return response return response
} }
export async function saveRecord(record, modelId) { export async function fetchDataForView(view) {
const SAVE_RECORDS_URL = `/api/${modelId}/records` const FETCH_RECORDS_URL = `/api/views/${view.name}`
const response = await api.post(SAVE_RECORDS_URL, record)
return await response.json()
}
export async function fetchDataForView(viewName) {
const FETCH_RECORDS_URL = `/api/views/${viewName}`
const response = await api.get(FETCH_RECORDS_URL) const response = await api.get(FETCH_RECORDS_URL)
return await response.json() return await response.json()

0
packages/builder/src/components/database/ModelDataTable/index.js → packages/builder/src/components/database/DataTable/index.js

36
packages/builder/src/components/database/ModelDataTable/modals/CreateEditColumn.svelte → packages/builder/src/components/database/DataTable/modals/CreateEditColumn.svelte

@ -1,11 +1,10 @@
<script> <script>
import { onMount } from "svelte" import { onMount } from "svelte"
import { Input, TextArea, Button, Select } from "@budibase/bbui" import { Input, TextArea, Button, Select } from "@budibase/bbui"
import { cloneDeep, merge } from "lodash/fp"
import { store, backendUiStore } from "builderStore" import { store, backendUiStore } from "builderStore"
import { FIELDS } from "constants/backend" import { FIELDS } from "constants/backend"
import { notifier } from "builderStore/store/notifications" import { notifier } from "builderStore/store/notifications"
import Dropdown from "components/common/Dropdown.svelte"
import Textbox from "components/common/Textbox.svelte"
import ButtonGroup from "components/common/ButtonGroup.svelte" import ButtonGroup from "components/common/ButtonGroup.svelte"
import NumberBox from "components/common/NumberBox.svelte" import NumberBox from "components/common/NumberBox.svelte"
import ValuesList from "components/common/ValuesList.svelte" import ValuesList from "components/common/ValuesList.svelte"
@ -16,8 +15,13 @@
import LinkedRecordSelector from "components/common/LinkedRecordSelector.svelte" import LinkedRecordSelector from "components/common/LinkedRecordSelector.svelte"
import * as api from "../api" import * as api from "../api"
let fieldDefinitions = cloneDeep(FIELDS)
export let onClosed export let onClosed
export let field = {} export let field = {
type: "string",
constraints: fieldDefinitions.STRING.constraints,
}
let originalName = field.name let originalName = field.name
@ -25,9 +29,6 @@
field.constraints && field.constraints &&
field.constraints.presence && field.constraints.presence &&
!field.constraints.presence.allowEmpty !field.constraints.presence.allowEmpty
$: if (field.type) {
field.constraints = FIELDS[field.type.toUpperCase()].constraints
}
async function saveColumn() { async function saveColumn() {
backendUiStore.update(state => { backendUiStore.update(state => {
@ -40,13 +41,26 @@
}) })
onClosed() onClosed()
} }
function handleFieldConstraints(event) {
const { type, constraints } = fieldDefinitions[
event.target.value.toUpperCase()
]
field.type = type
field.constraints = constraints
}
</script> </script>
<div class="actions"> <div class="actions">
<Input placeholder="Name" thin bind:value={field.name} /> <Input placeholder="Name" thin bind:value={field.name} />
<Select secondary thin bind:value={field.type}> <Select
{#each Object.values(FIELDS) as field} secondary
thin
on:change={handleFieldConstraints}
bind:value={field.type}>
{#each Object.values(fieldDefinitions) as field}
<option value={field.type}>{field.name}</option> <option value={field.type}>{field.name}</option>
{/each} {/each}
</Select> </Select>
@ -60,21 +74,21 @@
on:change={() => (field.constraints.presence.allowEmpty = required)} /> on:change={() => (field.constraints.presence.allowEmpty = required)} />
</div> </div>
{#if field.type === 'string'} {#if field.type === 'string' && field.constraints}
<NumberBox <NumberBox
label="Max Length" label="Max Length"
bind:value={field.constraints.length.maximum} /> bind:value={field.constraints.length.maximum} />
<ValuesList <ValuesList
label="Categories" label="Categories"
bind:values={field.constraints.inclusion} /> bind:values={field.constraints.inclusion} />
{:else if field.type === 'datetime'} {:else if field.type === 'datetime' && field.constraints}
<DatePicker <DatePicker
label="Min Value" label="Min Value"
bind:value={field.constraints.datetime.earliest} /> bind:value={field.constraints.datetime.earliest} />
<DatePicker <DatePicker
label="Max Value" label="Max Value"
bind:value={field.constraints.datetime.latest} /> bind:value={field.constraints.datetime.latest} />
{:else if field.type === 'number'} {:else if field.type === 'number' && field.constraints}
<NumberBox <NumberBox
label="Min Value" label="Min Value"
bind:value={field.constraints.numericality.greaterThanOrEqualTo} /> bind:value={field.constraints.numericality.greaterThanOrEqualTo} />

3
packages/builder/src/components/database/ModelDataTable/modals/CreateEditRecord.svelte → packages/builder/src/components/database/DataTable/modals/CreateEditRecord.svelte

@ -5,7 +5,6 @@
import { compose, map, get, flatten } from "lodash/fp" import { compose, map, get, flatten } from "lodash/fp"
import { Input, TextArea, Button } from "@budibase/bbui" import { Input, TextArea, Button } from "@budibase/bbui"
import LinkedRecordSelector from "components/common/LinkedRecordSelector.svelte" import LinkedRecordSelector from "components/common/LinkedRecordSelector.svelte"
import Select from "components/common/Select.svelte"
import RecordFieldControl from "./RecordFieldControl.svelte" import RecordFieldControl from "./RecordFieldControl.svelte"
import * as api from "../api" import * as api from "../api"
import ErrorsBox from "components/common/ErrorsBox.svelte" import ErrorsBox from "components/common/ErrorsBox.svelte"
@ -67,7 +66,7 @@
<ErrorsBox {errors} /> <ErrorsBox {errors} />
<form on:submit|preventDefault class="uk-form-stacked"> <form on:submit|preventDefault class="uk-form-stacked">
{#each modelSchema as [key, meta]} {#each modelSchema as [key, meta]}
<div class="uk-margin"> <div class="bb-margin-xl">
{#if meta.type === 'link'} {#if meta.type === 'link'}
<LinkedRecordSelector <LinkedRecordSelector
bind:linked={record[key]} bind:linked={record[key]}

6
packages/builder/src/components/database/ModelDataTable/modals/DeleteRecord.svelte → packages/builder/src/components/database/DataTable/modals/DeleteRecord.svelte

@ -10,10 +10,10 @@
<section> <section>
<div class="content"> <div class="content">
<heading> <header>
<i class="ri-information-line alert" /> <i class="ri-information-line alert" />
<h4 class="budibase__title--4">Delete Record</h4> <h4 class="budibase__title--4">Delete Record</h4>
</heading> </header>
<p> <p>
Are you sure you want to delete this record? All of your data will be Are you sure you want to delete this record? All of your data will be
permanently removed. This action cannot be undone. permanently removed. This action cannot be undone.
@ -47,7 +47,7 @@
border-top: 1px solid #ccc; border-top: 1px solid #ccc;
} }
heading { header {
display: flex; display: flex;
align-items: center; align-items: center;
} }

6
packages/builder/src/components/database/ModelDataTable/modals/DeleteTable.svelte → packages/builder/src/components/database/DataTable/modals/DeleteTable.svelte

@ -14,10 +14,10 @@
<section> <section>
<div class="content"> <div class="content">
<heading> <header>
<i class="ri-information-line alert" /> <i class="ri-information-line alert" />
<h4 class="budibase__title--4">Delete Table</h4> <h4 class="budibase__title--4">Delete Table</h4>
</heading> </header>
<p> <p>
Are you sure you want to delete this table? All of your data will be Are you sure you want to delete this table? All of your data will be
permanently removed. This action cannot be undone. permanently removed. This action cannot be undone.
@ -50,7 +50,7 @@
border-top: 1px solid #ccc; border-top: 1px solid #ccc;
} }
heading { header {
display: flex; display: flex;
align-items: center; align-items: center;
} }

62
packages/builder/src/components/database/DataTable/modals/DeleteView.svelte

@ -0,0 +1,62 @@
<script>
import { goto } from "@sveltech/routify"
import ActionButton from "components/common/ActionButton.svelte"
import { notifier } from "builderStore/store/notifications"
import { store, backendUiStore } from "builderStore"
import * as api from "../api"
export let viewName
export let onClosed
</script>
<section>
<div class="content">
<header>
<i class="ri-information-line alert" />
<h4 class="budibase__title--4">Delete View</h4>
</header>
<p>
Are you sure you want to delete this view? All of your data will be
permanently removed. This action cannot be undone.
</p>
</div>
<div class="modal-actions">
<ActionButton on:click={onClosed}>Cancel</ActionButton>
<ActionButton
alert
on:click={async () => {
await backendUiStore.actions.views.delete(viewName)
notifier.danger(`View ${viewName} deleted.`)
$goto(`./backend`)
onClosed()
}}>
Delete
</ActionButton>
</div>
</section>
<style>
.alert {
color: rgba(255, 0, 31, 1);
background: var(--grey-1);
padding: 5px;
}
.modal-actions {
padding: 10px;
background: var(--grey-1);
border-top: 1px solid #ccc;
}
header {
display: flex;
align-items: center;
}
.content {
padding: 30px;
}
h4 {
margin: 0 0 0 10px;
}
</style>

6
packages/builder/src/components/database/ModelDataTable/modals/RecordFieldControl.svelte → packages/builder/src/components/database/DataTable/modals/RecordFieldControl.svelte

@ -2,12 +2,10 @@
import { Input, Select } from "@budibase/bbui" import { Input, Select } from "@budibase/bbui"
export let type = "text" export let type = "text"
export let value = "" export let value = type === "checkbox" ? false : ""
export let label export let label
export let options = [] export let options = []
let checked = type === "checkbox" ? value : false
const handleInput = event => { const handleInput = event => {
if (event.target.type === "checkbox") { if (event.target.type === "checkbox") {
value = event.target.checked value = event.target.checked
@ -38,7 +36,7 @@
thin thin
placeholder={label} placeholder={label}
data-cy="{label}-input" data-cy="{label}-input"
{checked} checked={value}
{type} {type}
{value} {value}
on:input={handleInput} on:input={handleInput}

2
packages/builder/src/components/database/ModelDataTable/modals/index.js → packages/builder/src/components/database/DataTable/modals/index.js

@ -1,4 +1,2 @@
export { default as DeleteRecordModal } from "./DeleteRecord.svelte" export { default as DeleteRecordModal } from "./DeleteRecord.svelte"
export { default as CreateEditRecordModal } from "./CreateEditRecord.svelte" export { default as CreateEditRecordModal } from "./CreateEditRecord.svelte"
export { default as CreateEditViewModal } from "./CreateEditView.svelte"
export { default as CreateUserModal } from "./CreateUser.svelte"

95
packages/builder/src/components/database/DataTable/popovers/Calculate.svelte

@ -0,0 +1,95 @@
<script>
import {
Popover,
TextButton,
Button,
Icon,
Input,
Select,
} from "@budibase/bbui"
import { backendUiStore } from "builderStore"
import { notifier } from "builderStore/store/notifications"
import CreateEditRecord from "../modals/CreateEditRecord.svelte"
const CALCULATIONS = [
{
name: "Statistics",
key: "stats",
},
]
export let view = {}
let anchor
let dropdown
$: viewModel = $backendUiStore.models.find(
({ _id }) => _id === $backendUiStore.selectedView.modelId
)
$: fields =
viewModel &&
Object.keys(viewModel.schema).filter(
field => viewModel.schema[field].type === "number"
)
function saveView() {
backendUiStore.actions.views.save(view)
notifier.success(`View ${view.name} saved.`)
dropdown.hide()
}
</script>
<div bind:this={anchor}>
<TextButton text small on:click={dropdown.show} active={!!view.field}>
<Icon name="calculate" />
Calculate
</TextButton>
</div>
<Popover bind:this={dropdown} {anchor} align="left">
<h5>Calculate</h5>
<div class="input-group-row">
<p>The</p>
<Select secondary thin bind:value={view.calculation}>
{#each CALCULATIONS as calculation}
<option value={calculation.key}>{calculation.name}</option>
{/each}
</Select>
<p>of</p>
<Select secondary thin bind:value={view.field}>
{#each fields as field}
<option value={field}>{field}</option>
{/each}
</Select>
</div>
<div class="button-group">
<Button secondary on:click={dropdown.hide}>Cancel</Button>
<Button primary on:click={saveView}>Save</Button>
</div>
</Popover>
<style>
h5 {
margin-bottom: var(--spacing-l);
font-weight: 500;
}
.button-group {
margin-top: var(--spacing-l);
display: flex;
justify-content: flex-end;
gap: var(--spacing-s);
}
.input-group-row {
display: grid;
grid-template-columns: 50px 1fr 20px 1fr;
gap: var(--spacing-s);
margin-bottom: var(--spacing-l);
align-items: center;
}
p {
margin: 0;
font-size: var(--font-size-xs);
}
</style>

10
packages/builder/src/components/database/ModelDataTable/popovers/Column.svelte → packages/builder/src/components/database/DataTable/popovers/Column.svelte

@ -1,9 +1,13 @@
<script> <script>
import { backendUiStore } from "builderStore" import { backendUiStore } from "builderStore"
import { DropdownMenu, Button, Icon, Input, Select } from "@budibase/bbui" import {
DropdownMenu,
TextButton as Button,
Icon,
Input,
Select,
} from "@budibase/bbui"
import { FIELDS } from "constants/backend" import { FIELDS } from "constants/backend"
import { ModelSetupNav } from "components/nav/ModelSetupNav"
import ModelFieldEditor from "components/nav/ModelSetupNav/ModelFieldEditor.svelte"
import CreateEditColumn from "../modals/CreateEditColumn.svelte" import CreateEditColumn from "../modals/CreateEditColumn.svelte"
let anchor let anchor

3
packages/builder/src/components/database/ModelDataTable/popovers/ColumnHeader.svelte → packages/builder/src/components/database/DataTable/popovers/ColumnHeader.svelte

@ -2,8 +2,6 @@
import { backendUiStore } from "builderStore" import { backendUiStore } from "builderStore"
import { DropdownMenu, Button, Icon, Input, Select } from "@budibase/bbui" import { DropdownMenu, Button, Icon, Input, Select } from "@budibase/bbui"
import { FIELDS } from "constants/backend" import { FIELDS } from "constants/backend"
import { ModelSetupNav } from "components/nav/ModelSetupNav"
import ModelFieldEditor from "components/nav/ModelSetupNav/ModelFieldEditor.svelte"
import CreateEditColumn from "../modals/CreateEditColumn.svelte" import CreateEditColumn from "../modals/CreateEditColumn.svelte"
export let field export let field
@ -27,6 +25,7 @@
function deleteField() { function deleteField() {
backendUiStore.actions.models.deleteField(field) backendUiStore.actions.models.deleteField(field)
hideEditor()
} }
function sort(direction, column) { function sort(direction, column) {

0
packages/builder/src/components/database/ModelDataTable/popovers/EditRow.svelte → packages/builder/src/components/database/DataTable/popovers/EditRow.svelte

86
packages/builder/src/components/database/DataTable/popovers/GroupBy.svelte

@ -0,0 +1,86 @@
<script>
import {
Popover,
TextButton,
Button,
Icon,
Input,
Select,
} from "@budibase/bbui"
import { backendUiStore } from "builderStore"
import { notifier } from "builderStore/store/notifications"
import CreateEditRecord from "../modals/CreateEditRecord.svelte"
const CALCULATIONS = [
{
name: "Statistics",
key: "stats",
},
]
export let view = {}
let anchor
let dropdown
$: viewModel = $backendUiStore.models.find(
({ _id }) => _id === $backendUiStore.selectedView.modelId
)
$: fields = viewModel && Object.keys(viewModel.schema)
function saveView() {
backendUiStore.actions.views.save(view)
notifier.success(`View ${view.name} saved.`)
dropdown.hide()
}
</script>
<div bind:this={anchor}>
<TextButton text small active={!!view.groupBy} on:click={dropdown.show}>
<Icon name="group" />
Group By
</TextButton>
</div>
<Popover bind:this={dropdown} {anchor} align="left">
<h5>Group By</h5>
<div class="input-group-row">
<p>Group By</p>
<Select secondary thin bind:value={view.groupBy}>
<option value={false} />
{#each fields as field}
<option value={field}>{field}</option>
{/each}
</Select>
</div>
<div class="button-group">
<Button secondary on:click={dropdown.hide}>Cancel</Button>
<Button primary on:click={saveView}>Save</Button>
</div>
</Popover>
<style>
h5 {
margin-bottom: var(--spacing-l);
font-weight: 500;
}
.button-group {
margin-top: var(--spacing-l);
display: flex;
justify-content: flex-end;
gap: var(--spacing-s);
}
.input-group-row {
display: grid;
grid-template-columns: 50px 1fr 20px 1fr;
gap: var(--spacing-s);
margin-bottom: var(--spacing-l);
align-items: center;
}
p {
margin: 0;
font-size: var(--font-size-xs);
}
</style>

2
packages/builder/src/components/database/ModelDataTable/popovers/Row.svelte → packages/builder/src/components/database/DataTable/popovers/Row.svelte

@ -1,5 +1,5 @@
<script> <script>
import { DropdownMenu, Button, Icon } from "@budibase/bbui" import { DropdownMenu, TextButton as Button, Icon } from "@budibase/bbui"
import CreateEditRecord from "../modals/CreateEditRecord.svelte" import CreateEditRecord from "../modals/CreateEditRecord.svelte"
let anchor let anchor

84
packages/builder/src/components/database/DataTable/popovers/View.svelte

@ -0,0 +1,84 @@
<script>
import {
Popover,
TextButton,
Button,
Icon,
Input,
Select,
} from "@budibase/bbui"
import { goto } from "@sveltech/routify"
import { backendUiStore } from "builderStore"
import { notifier } from "builderStore/store/notifications"
import CreateEditRecord from "../modals/CreateEditRecord.svelte"
let anchor
let dropdown
let name
let field
$: fields = Object.keys($backendUiStore.selectedModel.schema).filter(key => {
return $backendUiStore.selectedModel.schema[key].type === "number"
})
$: views = $backendUiStore.models.flatMap(model =>
Object.keys(model.views || {})
)
function saveView() {
if (views.includes(name)) {
notifier.danger(`View exists with name ${name}.`)
return
}
backendUiStore.actions.views.save({
name,
modelId: $backendUiStore.selectedModel._id,
field,
})
notifier.success(`View ${name} created`)
dropdown.hide()
$goto(`../../../view/${name}`)
}
</script>
<div bind:this={anchor}>
<TextButton text small on:click={dropdown.show}>
<Icon name="view" />
Create New View
</TextButton>
</div>
<Popover bind:this={dropdown} {anchor} align="left">
<h5>Create View</h5>
<div class="input-group-column">
<Input placeholder="View Name" thin bind:value={name} />
<Select thin secondary bind:value={field}>
{#each fields as field}
<option value={field}>{field}</option>
{/each}
</Select>
</div>
<div class="button-group">
<Button secondary on:click={dropdown.hide}>Cancel</Button>
<Button primary on:click={saveView}>Save View</Button>
</div>
</Popover>
<style>
h5 {
margin-bottom: var(--spacing-l);
font-weight: 500;
}
.button-group {
margin-top: var(--spacing-l);
display: flex;
justify-content: flex-end;
gap: var(--spacing-s);
}
.input-group-column {
display: flex;
flex-direction: column;
gap: var(--spacing-s);
}
</style>

127
packages/builder/src/components/database/ModelDataTable/modals/CreateEditView.svelte

@ -1,127 +0,0 @@
<script>
import Textbox from "components/common/Textbox.svelte"
import CodeArea from "components/common/CodeArea.svelte"
import Button from "components/common/Button.svelte"
import Dropdown from "components/common/Dropdown.svelte"
import { store, backendUiStore } from "builderStore"
import { filter, some, map, compose } from "lodash/fp"
import ErrorsBox from "components/common/ErrorsBox.svelte"
import ActionButton from "components/common/ActionButton.svelte"
import api from "builderStore/api"
const SNIPPET_EDITORS = {
MAP: "Map",
FILTER: "Filter",
REDUCE: "Reduce",
}
const COUCHDB_FUNCTION = `function(doc) {
}`
export let onClosed
export let view = {}
let currentSnippetEditor = SNIPPET_EDITORS.MAP
$: instanceId = $backendUiStore.selectedDatabase._id
function deleteView() {}
async function saveView() {
const SAVE_VIEW_URL = `/api/views`
const response = await api.post(SAVE_VIEW_URL, view)
backendUiStore.update(state => {
state.views = [...state.views, response.view]
return state
})
onClosed()
}
</script>
<div class="header">
<i class="ri-eye-line button--toggled" />
<h3 class="budibase__title--3">Create / Edit View</h3>
</div>
<form on:submit|preventDefault class="uk-form-stacked root">
{#if $store.errors && $store.errors.length > 0}
<ErrorsBox errors={$store.errors} />
{/if}
<div class="main">
<div class="uk-grid-small" uk-grid>
<div class="uk-width-1-2@s">
<Textbox bind:text={view.name} label="Name" />
</div>
</div>
<div class="code-snippets">
{#each Object.values(SNIPPET_EDITORS) as snippetType}
<span
class="snippet-selector__heading hoverable"
class:highlighted={currentSnippetEditor === snippetType}
on:click={() => (currentSnippetEditor = snippetType)}>
{snippetType}
</span>
{/each}
{#if currentSnippetEditor === SNIPPET_EDITORS.MAP}
<CodeArea bind:text={view.map} label="Map" />
{:else if currentSnippetEditor === SNIPPET_EDITORS.FILTER}
<CodeArea bind:text={view.filter} label="Filter" />
{:else if currentSnippetEditor === SNIPPET_EDITORS.REDUCE}
<CodeArea bind:text={view.reduce} label="Reduce" />
{/if}
</div>
</div>
<div class="buttons">
<div class="button">
<ActionButton secondary on:click={deleteView}>Delete</ActionButton>
</div>
<ActionButton color="secondary" on:click={saveView}>Save</ActionButton>
</div>
</form>
<style>
.root {
height: 100%;
}
.highlighted {
opacity: 1;
}
h3 {
margin: 0 0 0 10px;
color: var(--ink);
}
.snippet-selector__heading {
margin-right: 20px;
font-size: 14px;
color: var(--grey-5);
}
.header {
padding: 20px 40px 0 40px;
display: flex;
align-items: center;
}
.main {
margin: 20px 40px 0px 40px;
}
.code-snippets {
margin: 20px 0px 20px 0px;
}
.buttons {
display: flex;
justify-content: flex-end;
background-color: var(--grey-1);
margin: 0 40px;
padding: 20px 0;
}
.button {
margin-right: 20px;
}
</style>

94
packages/builder/src/components/database/ModelDataTable/modals/CreateUser.svelte

@ -1,94 +0,0 @@
<script>
import { store, backendUiStore } from "builderStore"
import ActionButton from "components/common/ActionButton.svelte"
import * as api from "../api"
export let onClosed
let username
let password
let accessLevelId
$: valid = username && password && accessLevelId
$: appId = $store.appId
async function createUser() {
const user = { name: username, username, password, accessLevelId }
const response = await api.createUser(user)
backendUiStore.actions.users.create(response)
onClosed()
}
</script>
<form on:submit|preventDefault class="uk-form-stacked">
<div class="main">
<div class="heading">
<i class="ri-list-settings-line button--toggled" />
<div class="title">Create User</div>
</div>
<div class="uk-margin">
<label class="uk-form-label" for="form-stacked-text">Username</label>
<input
data-cy="username"
class="uk-input"
type="text"
bind:value={username} />
</div>
<div class="uk-margin">
<label class="uk-form-label" for="form-stacked-text">Password</label>
<input
data-cy="password"
class="uk-input"
type="password"
bind:value={password} />
</div>
<div class="uk-margin">
<label class="uk-form-label" for="form-stacked-text">Access Level</label>
<select
data-cy="accessLevel"
class="uk-select"
bind:value={accessLevelId}>
<option value="" />
<option value="POWER_USER">Power User</option>
<option value="ADMIN">Admin</option>
</select>
</div>
</div>
<footer>
<div class="button">
<ActionButton secondary on:click={onClosed}>Cancel</ActionButton>
</div>
<ActionButton disabled={!valid} on:click={createUser}>Save</ActionButton>
</footer>
</form>
<style>
.main {
padding: 40px 40px 20px 40px;
}
.title {
font-size: 24px;
font-weight: 600;
color: var(--ink);
margin-left: 12px;
}
.heading {
display: flex;
align-items: baseline;
}
footer {
display: flex;
align-items: center;
justify-content: flex-end;
padding: 20px;
background: var(--grey-1);
border-radius: 0 0 5px 5px;
}
.button {
margin-right: 20px;
}
</style>

0
packages/builder/src/components/database/ModelDataTable/popovers/View.svelte

95
packages/builder/src/components/nav/DatabasesList.svelte

@ -1,95 +0,0 @@
<script>
import { tick, onMount } from "svelte"
import { goto } from "@sveltech/routify"
import { store, backendUiStore } from "builderStore"
import api from "builderStore/api"
import { CheckIcon } from "../common/Icons"
$: instances = $store.appInstances
async function selectDatabase(database) {
backendUiStore.actions.database.select(database)
}
async function deleteDatabase(database) {
const DELETE_DATABASE_URL = `/api/instances/${database.name}`
const response = await api.delete(DELETE_DATABASE_URL)
store.update(state => {
state.appInstances = state.appInstances.filter(
db => db._id !== database._id
)
return state
})
}
</script>
<div class="root">
<ul>
{#each $store.appInstances as database}
<li>
<span class="icon">
{#if database._id === $backendUiStore.selectedDatabase._id}
<CheckIcon />
{/if}
</span>
<button
class:active={database._id === $backendUiStore.selectedDatabase._id}
on:click={() => {
$goto(`./database/${database._id}`), selectDatabase(database)
}}>
{database.name}
</button>
<i
class="ri-delete-bin-7-line hoverable alignment"
on:click={() => deleteDatabase(database)} />
</li>
{/each}
</ul>
</div>
<style>
.root {
font-size: 13px;
color: var(--ink);
position: relative;
padding-left: 20px;
}
ul {
margin: 0;
padding: 0;
list-style: none;
}
.alignment {
margin-left: auto;
padding-right: 20px;
}
li {
margin: 0px 0px 10px 0px;
display: flex;
align-items: center;
}
button {
margin: 0 0 0 6px;
padding: 0;
border: none;
font-size: 13px;
outline: none;
cursor: pointer;
background: rgba(0, 0, 0, 0);
text-rendering: optimizeLegibility;
}
.active {
font-weight: 500;
}
.icon {
display: inline-block;
width: 14px;
color: #333;
}
</style>

45
packages/builder/src/components/nav/HierarchyRow.svelte

@ -1,45 +0,0 @@
<script>
import { getContext } from "svelte"
import { store, backendUiStore } from "builderStore"
import { cloneDeep } from "lodash/fp"
import getIcon from "../common/icon"
import { CreateEditViewModal } from "components/database/ModelDataTable/modals"
import api from "builderStore/api"
const { open, close } = getContext("simple-modal")
export let node
export let type
export let onSelect
let navActive = ""
const ICON_MAP = {
index: "ri-eye-line",
model: "ri-list-settings-line",
}
</script>
<div>
<div
on:click={() => onSelect(node)}
class="budibase__nav-item hierarchy-item"
class:capitalized={type === 'model'}
class:selected={$backendUiStore.selectedView === `all_${node._id}`}>
<i class={ICON_MAP[type]} />
<span style="margin-left: 1rem">{node.name}</span>
</div>
</div>
<style>
.hierarchy-item {
font-size: 13px;
font-weight: 400;
margin-bottom: 10px;
padding-left: 20px;
}
.capitalized {
text-transform: capitalize;
}
</style>

8
packages/builder/src/components/nav/ModelNavigator/CreateTable.svelte

@ -17,6 +17,12 @@
}) })
notifier.success(`Table ${name} created successfully.`) notifier.success(`Table ${name} created successfully.`)
$goto(`./model/${model._id}`) $goto(`./model/${model._id}`)
name = ""
dropdown.hide()
}
const onClosed = () => {
name = ""
dropdown.hide() dropdown.hide()
} }
</script> </script>
@ -35,7 +41,7 @@
</div> </div>
<footer> <footer>
<div class="button-margin-3"> <div class="button-margin-3">
<Button secondary on:click={dropdown.hide}>Cancel</Button> <Button secondary on:click={onClosed}>Cancel</Button>
</div> </div>
<div class="button-margin-4"> <div class="button-margin-4">
<Button primary on:click={saveTable}>Save</Button> <Button primary on:click={saveTable}>Save</Button>

2
packages/builder/src/components/nav/ModelNavigator/EditTable.svelte

@ -3,7 +3,7 @@
import { backendUiStore } from "builderStore" import { backendUiStore } from "builderStore"
import { DropdownMenu, Button, Icon, Input, Select } from "@budibase/bbui" import { DropdownMenu, Button, Icon, Input, Select } from "@budibase/bbui"
import { FIELDS } from "constants/backend" import { FIELDS } from "constants/backend"
import DeleteTableModal from "components/database/ModelDataTable/modals/DeleteTable.svelte" import DeleteTableModal from "components/database/DataTable/modals/DeleteTable.svelte"
const { open, close } = getContext("simple-modal") const { open, close } = getContext("simple-modal")

143
packages/builder/src/components/nav/ModelNavigator/EditView.svelte

@ -0,0 +1,143 @@
<script>
import { getContext } from "svelte"
import { backendUiStore } from "builderStore"
import { notifier } from "builderStore/store/notifications"
import { DropdownMenu, Button, Icon, Input, Select } from "@budibase/bbui"
import { FIELDS } from "constants/backend"
import DeleteViewModal from "components/database/DataTable/modals/DeleteView.svelte"
const { open, close } = getContext("simple-modal")
export let view
let anchor
let dropdown
let editing
let originalName = view.name
function showEditor() {
editing = true
}
function hideEditor() {
dropdown.hide()
editing = false
close()
}
const deleteView = () => {
open(
DeleteViewModal,
{
onClosed: close,
viewName: view.name,
},
{ styleContent: { padding: "0" } }
)
}
function save() {
backendUiStore.actions.views.save({
originalName,
...view,
})
notifier.success("Renamed View Successfully.")
hideEditor()
}
</script>
<div bind:this={anchor} on:click={dropdown.show}>
<i class="ri-more-line" />
</div>
<DropdownMenu bind:this={dropdown} {anchor} align="left">
{#if editing}
<h5>Edit View</h5>
<div class="container">
<Input placeholder="View Name" thin bind:value={view.name} />
</div>
<footer>
<div class="button-margin-3">
<Button secondary on:click={hideEditor}>Cancel</Button>
</div>
<div class="button-margin-4">
<Button primary on:click={save}>Save</Button>
</div>
</footer>
{:else}
<ul>
<li on:click={showEditor}>
<Icon name="edit" />
Edit
</li>
<li data-cy="delete-view" on:click={deleteView}>
<Icon name="delete" />
Delete
</li>
</ul>
{/if}
</DropdownMenu>
<style>
h5 {
padding: var(--spacing-xl) 0 0 var(--spacing-xl);
margin: 0;
font-weight: 500;
}
.container {
padding: var(--spacing-xl);
}
ul {
padding: var(--spacing-xl) 0 0 var(--spacing-xl);
list-style: none;
padding-left: 0;
margin: 0;
padding: var(--spacing-s) 0;
}
li {
display: flex;
font-family: var(--font-sans);
font-size: var(--font-size-xs);
color: var(--ink);
padding: var(--spacing-s) var(--spacing-m);
margin: auto 0px;
align-items: center;
cursor: pointer;
}
li:hover {
background-color: var(--grey-2);
}
li:active {
color: var(--blue);
}
footer {
padding: 20px;
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr;
gap: 20px;
background: var(--grey-1);
border-bottom-left-radius: 0.5rem;
border-bottom-left-radius: 0.5rem;
}
.button-margin-1 {
grid-column-start: 1;
display: grid;
}
.button-margin-3 {
grid-column-start: 3;
display: grid;
}
.button-margin-4 {
grid-column-start: 4;
display: grid;
}
</style>

15
packages/builder/src/components/nav/ModelNavigator/ListItem.svelte

@ -6,15 +6,24 @@
export let indented export let indented
</script> </script>
<div class:selected on:click class={className}> <div
<i class:indented class={icon} /> data-cy="model-nav-item"
class:indented
class:selected
on:click
class={className}>
<i class={icon} />
<span>{title}</span> <span>{title}</span>
<slot /> <slot />
</div> </div>
<style> <style>
.indented { .indented {
margin-left: 10px; grid-template-columns: 50px 1fr 20px;
}
.indented i {
justify-self: end;
} }
div { div {

33
packages/builder/src/components/nav/ModelNavigator/ModelNavigator.svelte

@ -8,20 +8,21 @@
import { Button } from "@budibase/bbui" import { Button } from "@budibase/bbui"
import CreateTablePopover from "./CreateTable.svelte" import CreateTablePopover from "./CreateTable.svelte"
import EditTablePopover from "./EditTable.svelte" import EditTablePopover from "./EditTable.svelte"
import EditViewPopover from "./EditView.svelte"
const { open, close } = getContext("simple-modal") const { open, close } = getContext("simple-modal")
$: selectedTab = $backendUiStore.tabs.NAVIGATION_PANEL $: selectedView =
$backendUiStore.selectedView && $backendUiStore.selectedView.name
function selectModel(model, fieldId) { function selectModel(model) {
backendUiStore.actions.models.select(model) backendUiStore.actions.models.select(model)
$goto(`./model/${model._id}`) $goto(`./model/${model._id}`)
if (fieldId) { }
backendUiStore.update(state => {
state.selectedField = fieldId function selectView(view) {
return state backendUiStore.actions.views.select(view)
}) $goto(`./view/${view.name}`)
}
} }
</script> </script>
@ -34,12 +35,26 @@
<div class="hierarchy-items-container"> <div class="hierarchy-items-container">
{#each $backendUiStore.models as model} {#each $backendUiStore.models as model}
<ListItem <ListItem
selected={!$backendUiStore.selectedField && model._id === $backendUiStore.selectedModel._id} selected={selectedView === `all_${model._id}`}
title={model.name} title={model.name}
icon="ri-table-fill" icon="ri-table-fill"
on:click={() => selectModel(model)}> on:click={() => selectModel(model)}>
<EditTablePopover table={model} /> <EditTablePopover table={model} />
</ListItem> </ListItem>
{#each Object.keys(model.views || {}) as viewName}
<ListItem
indented
selected={selectedView === viewName}
title={viewName}
icon="ri-eye-line"
on:click={() => selectView({
name: viewName,
...model.views[viewName],
})}>
<EditViewPopover
view={{ name: viewName, ...model.views[viewName] }} />
</ListItem>
{/each}
{/each} {/each}
</div> </div>
</div> </div>

120
packages/builder/src/components/nav/ModelSetupNav/ModelFieldEditor.svelte

@ -1,120 +0,0 @@
<script>
import { backendUiStore } from "builderStore"
import { Button } from "@budibase/bbui"
import Dropdown from "components/common/Dropdown.svelte"
import Textbox from "components/common/Textbox.svelte"
import ButtonGroup from "components/common/ButtonGroup.svelte"
import NumberBox from "components/common/NumberBox.svelte"
import ValuesList from "components/common/ValuesList.svelte"
import ErrorsBox from "components/common/ErrorsBox.svelte"
import Checkbox from "components/common/Checkbox.svelte"
import ActionButton from "components/common/ActionButton.svelte"
import DatePicker from "components/common/DatePicker.svelte"
import { keys, cloneDeep } from "lodash/fp"
const FIELD_TYPES = ["string", "number", "boolean", "link"]
let field = {}
$: field =
$backendUiStore.draftModel.schema[$backendUiStore.selectedField] || {}
$: required =
field.constraints &&
field.constraints.presence &&
!field.constraints.presence.allowEmpty
</script>
<div class="info">
<div class="field-box">
<header>Name</header>
<input class="budibase__input" type="text" bind:value={field.name} />
</div>
</div>
<div class="info">
<div class="field-box">
<header>Type</header>
<span>{field.type}</span>
</div>
</div>
<div class="info">
<div class="field">
<label>Required</label>
<input
type="checkbox"
bind:checked={required}
on:change={() => (field.constraints.presence.allowEmpty = required)} />
</div>
{#if field.type === 'string'}
<NumberBox
label="Max Length"
bind:value={field.constraints.length.maximum} />
<ValuesList label="Categories" bind:values={field.constraints.inclusion} />
{:else if field.type === 'datetime'}
<DatePicker
label="Min Value"
bind:value={field.constraints.datetime.earliest} />
<DatePicker
label="Max Value"
bind:value={field.constraints.datetime.latest} />
{:else if field.type === 'number'}
<NumberBox
label="Min Value"
bind:value={field.constraints.numericality.greaterThanOrEqualTo} />
<NumberBox
label="Max Value"
bind:value={field.constraints.numericality.lessThanOrEqualTo} />
{:else if field.type === 'link'}
<div class="field">
<label>Link</label>
<select class="budibase__input" bind:value={field.modelId}>
<option value={''} />
{#each $backendUiStore.models as model}
{#if model._id !== $backendUiStore.draftModel._id}
<option value={model._id}>{model.name}</option>
{/if}
{/each}
</select>
</div>
{/if}
</div>
<style>
.info {
margin-bottom: 16px;
border-radius: 5px;
}
label {
font-size: 14px;
font-weight: 500;
margin-bottom: 8px;
}
.field {
display: grid;
align-items: center;
margin-bottom: 16px;
}
.field-box header {
font-size: 14px;
font-weight: 500;
margin-bottom: 8px;
}
.field-box span {
background: var(--grey-2);
color: var(--grey-6);
font-weight: 400;
height: 36px;
display: grid;
align-items: center;
padding-left: 12px;
text-transform: capitalize;
border-radius: 5px;
cursor: not-allowed;
}
</style>

156
packages/builder/src/components/nav/ModelSetupNav/ModelSetupNav.svelte

@ -1,156 +0,0 @@
<script>
import { getContext, onMount } from "svelte"
import { Button, Switcher } from "@budibase/bbui"
import { notifier } from "builderStore/store/notifications"
import { store, backendUiStore } from "builderStore"
import api from "builderStore/api"
import ModelFieldEditor from "./ModelFieldEditor.svelte"
const { open, close } = getContext("simple-modal")
const ITEMS = [
{
title: "Setup",
key: "SETUP",
},
{
title: "Delete",
key: "DELETE",
},
]
let edited = false
$: selectedTab = $backendUiStore.tabs.SETUP_PANEL
$: edited =
$backendUiStore.selectedField ||
($backendUiStore.draftModel &&
$backendUiStore.draftModel.name !== $backendUiStore.selectedModel.name)
async function deleteModel() {
const model = $backendUiStore.selectedModel
const field = $backendUiStore.selectedField
if (field) {
const name = model.schema[field].name
delete model.schema[field]
backendUiStore.actions.models.save(model)
notifier.danger(`Field ${name} deleted.`)
return
}
const DELETE_MODEL_URL = `/api/models/${model._id}/${model._rev}`
const response = await api.delete(DELETE_MODEL_URL)
backendUiStore.update(state => {
state.selectedView = null
state.selectedModel = {}
state.draftModel = {}
state.models = state.models.filter(({ _id }) => _id !== model._id)
notifier.danger(`${model.name} deleted successfully.`)
return state
})
}
function validate() {
let errors = []
for (let field of Object.values($backendUiStore.draftModel.schema)) {
const restrictedFieldNames = ["type", "modelId"]
if (field.name.startsWith("_")) {
errors.push(`field '${field.name}' - name cannot begin with '_''`)
} else if (restrictedFieldNames.includes(field.name)) {
errors.push(
`field '${field.name}' - is a restricted name, please rename`
)
} else if (!field.name || !field.name.trim()) {
errors.push("field name cannot be blank")
}
}
if (!$backendUiStore.draftModel.name) {
errors.push("Table name cannot be blank")
}
return errors
}
async function saveModel() {
const errors = validate()
if (errors.length > 0) {
notifier.danger(errors.join("/n"))
return
}
await backendUiStore.actions.models.save($backendUiStore.draftModel)
notifier.success(
"Success! Your changes have been saved. Please continue on with your greatness."
)
}
</script>
<div class="items-root">
<Switcher headings={ITEMS} bind:value={$backendUiStore.tabs.SETUP_PANEL}>
{#if selectedTab === 'SETUP'}
{#if $backendUiStore.selectedField}
<ModelFieldEditor />
{:else if $backendUiStore.draftModel.schema}
<div class="titled-input">
<header>Name</header>
<input
data-cy="table-name-input"
type="text"
class="budibase__input"
bind:value={$backendUiStore.draftModel.name} />
</div>
<!-- dont have this capability yet..
<div class="titled-input">
<header>Import Data</header>
<Button wide secondary>Import CSV</Button>
</div>
-->
{/if}
<footer>
<Button disabled={!edited} green={edited} wide on:click={saveModel}>
Save
</Button>
</footer>
{:else if selectedTab === 'DELETE'}
<div class="titled-input">
<header>Danger Zone</header>
<Button red wide on:click={deleteModel}>Delete</Button>
</div>
{/if}
</Switcher>
</div>
<style>
header {
font-weight: 500;
}
footer {
width: 260px;
position: fixed;
bottom: 20px;
}
.items-root {
padding: 20px;
display: flex;
flex-direction: column;
max-height: 100%;
height: 100%;
background-color: var(--white);
}
.titled-input {
margin-bottom: 16px;
display: grid;
}
.titled-input header {
display: block;
font-size: 14px;
margin-bottom: 8px;
}
</style>

1
packages/builder/src/components/nav/ModelSetupNav/index.js

@ -1 +0,0 @@
export { default as ModelSetupNav } from "./ModelSetupNav.svelte"

82
packages/builder/src/components/nav/UsersList.svelte

@ -1,82 +0,0 @@
<script>
import { onMount } from "svelte"
import { store, backendUiStore } from "builderStore"
import api from "builderStore/api"
import getIcon from "../common/icon"
import { CheckIcon } from "../common/Icons"
const getPage = (s, name) => {
const props = s.pages[name]
return { name, props }
}
$: currentAppInfo = {
name: $store.name,
}
async function fetchUsers() {
const FETCH_USERS_URL = `/api/users`
const response = await api.get(FETCH_USERS_URL)
const users = await response.json()
backendUiStore.update(state => {
state.users = users
return state
})
}
onMount(fetchUsers)
</script>
<div class="root">
<ul>
{#each $backendUiStore.users as user}
<li>
<i class="ri-user-4-line" />
<button class:active={user.username === $store.currentUserName}>
{user.name}
</button>
</li>
{/each}
</ul>
</div>
<style>
.root {
padding-bottom: 10px;
font-size: 0.9rem;
color: var(--secondary50);
font-weight: bold;
position: relative;
padding-left: 1.8rem;
}
ul {
margin: 0;
padding: 0;
list-style: none;
}
li {
margin: 0.5rem 0;
}
button {
margin: 0 0 0 6px;
padding: 0;
border: none;
font-size: 13px;
outline: none;
cursor: pointer;
background: rgba(0, 0, 0, 0);
}
.active {
font-weight: 500;
}
.icon {
display: inline-block;
width: 14px;
color: #333;
}
</style>

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

@ -13,35 +13,26 @@
<style> <style>
.apps-card { .apps-card {
background-color: var(--white); background-color: var(--white);
padding: 20px 20px 20px 20px; padding: var(--spacing-xl);
max-width: 400px; max-width: 300px;
max-height: 150px; max-height: 150px;
border-radius: 5px; border-radius: var(--border-radius-m);
border: 1px solid var(--grey-4); border: var(--border-dark);
font-family: Inter;
} }
.app-button:hover { .app-button:hover {
background-color: var(--grey-1); background-color: var(--white);
color: var(--black);
text-decoration: none; text-decoration: none;
} }
.app-title { .app-title {
font-size: 18px; font-size: var(--font-size-l);
font-weight: 600; font-weight: 600;
color: var(--ink); color: var(--ink);
text-transform: capitalize; text-transform: capitalize;
} }
.app-desc {
color: var(--grey-7);
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
.card-footer { .card-footer {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@ -52,17 +43,18 @@
.app-button { .app-button {
align-items: center; align-items: center;
display: flex; display: flex;
background-color: var(--white); background-color: var(--ink);
color: var(--ink); color: var(--white);
border: 1.5px var(--ink) solid;
width: 100%; width: 100%;
justify-content: center; justify-content: center;
padding: 12px 20px; padding: 8px 16px;
border-radius: 5px; border-radius: var(--border-radius-s);
border: 1px var(--grey-2) solid; font-size: var(--font-size-xs);
font-size: 14px; font-weight: 500;
font-weight: 400;
cursor: pointer; cursor: pointer;
transition: all 0.2s; transition: all 0.2s;
box-sizing: border-box; box-sizing: border-box;
font-family: var(--font-sans);
} }
</style> </style>

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

@ -25,8 +25,8 @@
<style> <style>
.apps { .apps {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(360px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
grid-gap: 20px 40px; grid-gap: var(--layout-m);
justify-content: start; justify-content: start;
} }

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

@ -208,16 +208,17 @@
</div> </div>
<div class="footer"> <div class="footer">
{#if $createAppStore.currentStep > 0} {#if $createAppStore.currentStep > 0}
<Button secondary on:click={back}>Back</Button> <Button medium secondary on:click={back}>Back</Button>
{/if} {/if}
{#if $createAppStore.currentStep < steps.length - 1} {#if $createAppStore.currentStep < steps.length - 1}
<Button secondary on:click={next} disabled={!currentStepIsValid}> <Button medium blue on:click={next} disabled={!currentStepIsValid}>
Next Next
</Button> </Button>
{/if} {/if}
{#if $createAppStore.currentStep === steps.length - 1} {#if $createAppStore.currentStep === steps.length - 1}
<Button <Button
secondary medium
blue
on:click={signUp} on:click={signUp}
disabled={!fullFormIsValid || submitting}> disabled={!fullFormIsValid || submitting}>
{submitting ? 'Loading...' : 'Submit'} {submitting ? 'Loading...' : 'Submit'}

2
packages/builder/src/components/start/Steps/Info.svelte

@ -5,7 +5,7 @@
let blurred = { appName: false } let blurred = { appName: false }
</script> </script>
<h2>Create your first web app</h2> <h2>Create your web app</h2>
<div class="container"> <div class="container">
<Input <Input
on:input={() => (blurred.appName = true)} on:input={() => (blurred.appName = true)}

2
packages/builder/src/components/start/Steps/User.svelte

@ -21,7 +21,7 @@
placeholder="Password" placeholder="Password"
type="pasword" type="pasword"
error={blurred.password && validationErrors.password} /> error={blurred.password && validationErrors.password} />
<Select name="accessLevelId"> <Select secondary name="accessLevelId">
<option value="ADMIN">Admin</option> <option value="ADMIN">Admin</option>
<option value="POWER_USER">Power User</option> <option value="POWER_USER">Power User</option>
</Select> </Select>

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

@ -15,7 +15,6 @@
<div class="container"> <div class="container">
<div class="text"> <div class="text">
<TextArea <TextArea
bind:value bind:value
placeholder="" placeholder=""

111
packages/builder/src/components/userInterface/CodeEditor.svelte

@ -1,111 +0,0 @@
<script>
import { store } from "builderStore"
import UIkit from "uikit"
import ActionButton from "components/common/ActionButton.svelte"
import ButtonGroup from "components/common/ButtonGroup.svelte"
import CodeMirror from "codemirror"
import "codemirror/mode/javascript/javascript.js"
export let onCodeChanged
export let code
export const show = () => {
UIkit.modal(codeModal).show()
}
let codeModal
let editor
let cmInstance
$: currentCode = code
$: originalCode = code
$: {
if (editor) {
if (!cmInstance) {
cmInstance = CodeMirror.fromTextArea(editor, {
mode: "javascript",
lineNumbers: false,
lineWrapping: true,
smartIndent: true,
matchBrackets: true,
readOnly: false,
})
cmInstance.on("change", () => (currentCode = cmInstance.getValue()))
}
cmInstance.focus()
cmInstance.setValue(code || "")
}
}
const cancel = () => {
UIkit.modal(codeModal).hide()
currentCode = originalCode
}
const save = () => {
originalCode = currentCode
onCodeChanged(currentCode)
UIkit.modal(codeModal).hide()
}
</script>
<div bind:this={codeModal} uk-modal>
<div class="uk-modal-dialog" uk-overflow-auto>
<div class="uk-modal-header">
<h3>Code</h3>
</div>
<div class="uk-modal-body uk-form-horizontal">
<p>
Use the code box below to control how this component is displayed, with
javascript.
</p>
<div>
<div class="editor-code-surround">
function(render, context, state, route) {'{'}
</div>
<div class="editor">
<textarea bind:this={editor} />
</div>
<div class="editor-code-surround">{'}'}</div>
</div>
</div>
<div class="uk-modal-footer">
<ButtonGroup>
<ActionButton primary on:click={save}>Save</ActionButton>
<ActionButton alert on:click={cancel}>Close</ActionButton>
</ButtonGroup>
</div>
</div>
</div>
<style>
h3 {
text-transform: uppercase;
font-size: 13px;
font-weight: 700;
color: #8997ab;
margin-bottom: 10px;
}
p {
font-size: 13px;
color: #333;
margin-top: 0;
}
.editor {
border-style: dotted;
border-width: 1px;
border-color: gainsboro;
padding: 10px 30px;
}
.editor-code-surround {
font-family: "Courier New", Courier, monospace;
}
</style>

320
packages/builder/src/components/userInterface/Colorpicker/Colorpicker.svelte

@ -1,320 +0,0 @@
<script>
import { onMount, createEventDispatcher } from "svelte"
import { fade } from "svelte/transition"
import Swatch from "./Swatch.svelte"
import CheckedBackground from "./CheckedBackground.svelte"
import { buildStyle } from "./helpers.js"
import {
getColorFormat,
convertToHSVA,
convertHsvaToFormat,
} from "./utils.js"
import Slider from "./Slider.svelte"
import Palette from "./Palette.svelte"
import ButtonGroup from "./ButtonGroup.svelte"
import Input from "./Input.svelte"
import Portal from "./Portal.svelte"
export let value = "#3ec1d3ff"
export let open = false
export let swatches = [] //TODO: Safe swatches - limit to 12. warn in console
export let disableSwatches = false
export let format = "hexa"
export let style = ""
export let pickerHeight = 0
export let pickerWidth = 0
let colorPicker = null
let adder = null
let h = null
let s = null
let v = null
let a = null
const dispatch = createEventDispatcher()
onMount(() => {
if (!swatches.length > 0) {
//Don't use locally stored recent colors if swatches have been passed as props
getRecentColors()
}
if (colorPicker) {
colorPicker.focus()
}
if (format) {
convertAndSetHSVA()
}
})
function getRecentColors() {
let colorStore = localStorage.getItem("cp:recent-colors")
if (colorStore) {
swatches = JSON.parse(colorStore)
}
}
function handleEscape(e) {
if (open && e.key === "Escape") {
open = false
}
}
function setRecentColor(color) {
if (swatches.length === 12) {
swatches.splice(0, 1)
}
if (!swatches.includes(color)) {
swatches = [...swatches, color]
localStorage.setItem("cp:recent-colors", JSON.stringify(swatches))
}
}
function convertAndSetHSVA() {
let hsva = convertToHSVA(value, format)
setHSVA(hsva)
}
function setHSVA([hue, sat, val, alpha]) {
h = hue
s = sat
v = val
a = alpha
}
//fired by choosing a color from the palette
function setSaturationAndValue({ detail }) {
s = detail.s
v = detail.v
value = convertHsvaToFormat([h, s, v, a], format)
dispatchValue()
}
function setHue({ color, isDrag }) {
h = color
value = convertHsvaToFormat([h, s, v, a], format)
if (!isDrag) {
dispatchValue()
}
}
function setAlpha({ color, isDrag }) {
a = color === "1.00" ? "1" : color
value = convertHsvaToFormat([h, s, v, a], format)
if (!isDrag) {
dispatchValue()
}
}
function dispatchValue() {
dispatch("change", value)
}
function changeFormatAndConvert(f) {
format = f
value = convertHsvaToFormat([h, s, v, a], format)
}
function handleColorInput(text) {
let format = getColorFormat(text)
if (format) {
value = text
convertAndSetHSVA()
}
}
function dispatchInputChange() {
if (format) {
dispatchValue()
}
}
function addSwatch() {
if (format) {
dispatch("addswatch", value)
setRecentColor(value)
}
}
function removeSwatch(idx) {
let removedSwatch = swatches.splice(idx, 1)
swatches = swatches
dispatch("removeswatch", removedSwatch)
localStorage.setItem("cp:recent-colors", JSON.stringify(swatches))
}
function applySwatch(color) {
if (value !== color) {
format = getColorFormat(color)
if (format) {
value = color
convertAndSetHSVA()
dispatchValue()
}
}
}
$: border = v > 90 && s < 5 ? "1px dashed #dedada" : ""
$: selectedColorStyle = buildStyle({ background: value, border })
$: shrink = swatches.length > 0
</script>
<Portal>
<div
class="colorpicker-container"
transition:fade
bind:this={colorPicker}
{style}
tabindex="0"
on:keydown={handleEscape}
bind:clientHeight={pickerHeight}
bind:clientWidth={pickerWidth}>
<div class="palette-panel">
<Palette on:change={setSaturationAndValue} {h} {s} {v} {a} />
</div>
<div class="control-panel">
<div class="alpha-hue-panel">
<div>
<CheckedBackground borderRadius="50%" backgroundSize="8px">
<div class="selected-color" style={selectedColorStyle} />
</CheckedBackground>
</div>
<div>
<Slider
type="hue"
value={h}
on:change={hue => setHue(hue.detail)}
on:dragend={dispatchValue} />
<CheckedBackground borderRadius="10px" backgroundSize="7px">
<Slider
type="alpha"
value={a}
on:change={(alpha, isDrag) => setAlpha(alpha.detail, isDrag)}
on:dragend={dispatchValue} />
</CheckedBackground>
</div>
</div>
{#if !disableSwatches}
<div transition:fade class="swatch-panel">
{#if swatches.length > 0}
{#each swatches as color, idx}
<Swatch
{color}
on:click={() => applySwatch(color)}
on:removeswatch={() => removeSwatch(idx)} />
{/each}
{/if}
{#if swatches.length !== 12}
<div
bind:this={adder}
transition:fade
class="adder"
on:click={addSwatch}
class:shrink>
<span>&plus;</span>
</div>
{/if}
</div>
{/if}
<div class="format-input-panel">
<ButtonGroup {format} onclick={changeFormatAndConvert} />
<Input
{value}
on:input={event => handleColorInput(event.target.value)}
on:change={dispatchInputChange} />
</div>
</div>
</div>
</Portal>
<style>
.colorpicker-container {
position: absolute;
outline: none;
z-index: 3;
display: flex;
font-size: 11px;
font-weight: 400;
flex-direction: column;
margin: 5px 0px;
height: auto;
width: 220px;
background: #ffffff;
border-radius: 2px;
box-shadow: 0 0.15em 1.5em 0 rgba(0, 0, 0, 0.1),
0 0 1em 0 rgba(0, 0, 0, 0.03);
}
.palette-panel {
flex: 1;
}
.control-panel {
flex: 1;
display: flex;
flex-direction: column;
padding: 8px;
background: white;
border: 1px solid #d2d2d2;
color: #777373;
}
.alpha-hue-panel {
display: grid;
grid-template-columns: 25px 1fr;
grid-gap: 15px;
justify-content: center;
align-items: center;
}
.selected-color {
width: 30px;
height: 30px;
border-radius: 50%;
}
.swatch-panel {
flex: 0 0 15px;
display: flex;
flex-flow: row wrap;
justify-content: flex-start;
padding: 0 5px;
max-height: 56px;
}
.adder {
flex: 1;
height: 20px;
display: flex;
transition: flex 0.5s;
justify-content: center;
align-items: center;
background: #f1f3f4;
cursor: pointer;
border: 1px solid #d4d4d4;
border-radius: 8px;
margin-left: 5px;
margin-top: 3px;
font-weight: 500;
}
.shrink {
flex: 0 0 20px;
}
.format-input-panel {
display: flex;
flex-direction: column;
justify-content: center;
padding-top: 3px;
}
</style>

157
packages/builder/src/components/userInterface/Colorpicker/Colorpreview.svelte

@ -1,157 +0,0 @@
<script>
import Colorpicker from "./Colorpicker.svelte"
import CheckedBackground from "./CheckedBackground.svelte"
import { createEventDispatcher, beforeUpdate } from "svelte"
import { buildStyle } from "./helpers.js"
import { fade } from "svelte/transition"
import { getColorFormat } from "./utils.js"
export let value = "#3ec1d3ff"
export let swatches = []
export let disableSwatches = false
export let open = false
export let width = "25px"
export let height = "25px"
let format = "hexa"
let dimensions = { top: 0, bottom: 0, right: 0, left: 0 }
let positionSide = "top"
let colorPreview = null
let previewHeight = null
let previewWidth = null
let pickerWidth = 0
let pickerHeight = 0
let errorMsg = null
const dispatch = createEventDispatcher()
beforeUpdate(() => {
format = getColorFormat(value)
if (!format) {
errorMsg = `Colorpicker - ${value} is an unknown color format. Please use a hex, rgb or hsl value`
console.error(errorMsg)
} else {
errorMsg = null
}
})
function openColorpicker(event) {
if (colorPreview) {
open = true
}
}
function onColorChange(color) {
value = color.detail
dispatch("change", color.detail)
}
$: if (open && colorPreview) {
const {
top: spaceAbove,
width,
bottom,
right,
left,
} = colorPreview.getBoundingClientRect()
const spaceBelow = window.innerHeight - bottom
const previewCenter = previewWidth / 2
let y, x
if (spaceAbove > spaceBelow) {
positionSide = "bottom"
y = window.innerHeight - spaceAbove
} else {
positionSide = "top"
y = bottom
}
x = left + previewCenter - pickerWidth / 2
dimensions = { [positionSide]: y.toFixed(1), left: x.toFixed(1) }
}
$: previewStyle = buildStyle({ width, height, background: value })
$: errorPreviewStyle = buildStyle({ width, height })
$: pickerStyle = buildStyle({
[positionSide]: `${dimensions[positionSide]}px`,
left: `${dimensions.left}px`,
})
</script>
<div class="color-preview-container">
{#if !errorMsg}
<CheckedBackground borderRadius="3px" backgroundSize="8px">
<div
bind:this={colorPreview}
bind:clientHeight={previewHeight}
bind:clientWidth={previewWidth}
class="color-preview"
style={previewStyle}
on:click={openColorpicker} />
</CheckedBackground>
{#if open}
<Colorpicker
style={pickerStyle}
on:change={onColorChange}
on:addswatch
on:removeswatch
bind:format
bind:value
bind:pickerHeight
bind:pickerWidth
bind:open
{swatches}
{disableSwatches} />
<div on:click|self={() => (open = false)} class="overlay" />
{/if}
{:else}
<div class="color-preview preview-error" style={errorPreviewStyle}>
<span>&times;</span>
</div>
{/if}
</div>
<style>
.color-preview-container {
display: flex;
flex-flow: row nowrap;
height: fit-content;
}
.color-preview {
cursor: pointer;
border-radius: 3px;
border: 1px solid #dedada;
}
.preview-error {
background: #cccccc;
color: #808080;
text-align: center;
font-size: 18px;
cursor: not-allowed;
}
/* .picker-container {
position: absolute;
z-index: 3;
width: fit-content;
height: fit-content;
} */
.overlay {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
z-index: 2;
}
</style>

37
packages/builder/src/components/userInterface/Colorpicker/Portal.svelte

@ -1,37 +0,0 @@
<script>
import { onMount } from "svelte"
export let target = document.body
let targetEl
let portal
let componentInstance
onMount(() => {
if (typeof target === "string") {
targetEl = document.querySelector(target)
// Force exit
if (targetEl === null) {
return () => {}
}
} else if (target instanceof HTMLElement) {
targetEl = target
} else {
throw new TypeError(
`Unknown target type: ${typeof target}. Allowed types: String (CSS selector), HTMLElement.`
)
}
portal = document.createElement("div")
targetEl.appendChild(portal)
portal.appendChild(componentInstance)
return () => {
targetEl.removeChild(portal)
}
})
</script>
<div bind:this={componentInstance}>
<slot />
</div>

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

@ -1,6 +1,5 @@
<script> <script>
import { setContext, onMount } from "svelte" import { setContext, onMount } from "svelte"
import PropsView from "./PropsView.svelte"
import { store } from "builderStore" import { store } from "builderStore"
import IconButton from "components/common/IconButton.svelte" import IconButton from "components/common/IconButton.svelte"
@ -11,8 +10,7 @@
CircleIndicator, CircleIndicator,
EventsIcon, EventsIcon,
} from "components/common/Icons/" } from "components/common/Icons/"
import CodeEditor from "./CodeEditor.svelte" import EventsEditor from "./EventsEditor"
import LayoutEditor from "./LayoutEditor.svelte"
import panelStructure from "./temporaryPanelStructure.js" import panelStructure from "./temporaryPanelStructure.js"
import CategoryTab from "./CategoryTab.svelte" import CategoryTab from "./CategoryTab.svelte"
import DesignView from "./DesignView.svelte" import DesignView from "./DesignView.svelte"
@ -24,6 +22,7 @@
let categories = [ let categories = [
{ value: "settings", name: "Settings" }, { value: "settings", name: "Settings" },
{ value: "design", name: "Design" }, { value: "design", name: "Design" },
{ value: "events", name: "Events" },
] ]
let selectedCategory = categories[0] let selectedCategory = categories[0]
@ -111,6 +110,8 @@
displayNameField={displayName} displayNameField={displayName}
onChange={onPropChanged} onChange={onPropChanged}
screenOrPageInstance={$store.currentView !== 'component' && $store.currentPreviewItem} /> screenOrPageInstance={$store.currentView !== 'component' && $store.currentPreviewItem} />
{:else if selectedCategory.value === 'events'}
<EventsEditor component={componentInstance} />
{/if} {/if}
</div> </div>

59
packages/builder/src/components/userInterface/ComponentSearch.svelte

@ -1,59 +0,0 @@
<script>
import { searchAllComponents } from "./pagesParsing/searchComponents"
import { store } from "../builderStore"
export let onComponentChosen = () => {}
let phrase = ""
components = $store.components
$: filteredComponents = !phrase ? [] : searchAllComponents(components, phrase)
</script>
<div class="root">
<form on:submit|preventDefault class="uk-search uk-search-large">
<span uk-search-icon />
<input
class="uk-search-input"
type="search"
placeholder="Based on component..."
bind:value={phrase} />
</form>
<div>
{#each filteredComponents as component}
<div class="component" on:click={() => onComponentChosen(component)}>
<div class="title">{component.name}</div>
<div class="description">{component.description}</div>
</div>
{/each}
</div>
</div>
<style>
.component {
padding: 5px;
border-style: solid;
border-width: 0 0 1px 0;
border-color: var(--grey-1);
cursor: pointer;
}
.component:hover {
background-color: var(--primary10);
}
.component > .title {
font-size: 13pt;
color: var(--ink);
}
.component > .description {
font-size: 10pt;
color: var(--blue);
font-style: italic;
}
</style>

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

@ -19,15 +19,12 @@
const joinPath = join("/") const joinPath = join("/")
const normalizedName = name => const normalizedName = name =>
pipe( pipe(name, [
name, trimCharsStart("./"),
[ trimCharsStart("~/"),
trimCharsStart("./"), trimCharsStart("../"),
trimCharsStart("~/"), trimChars(" "),
trimCharsStart("../"), ])
trimChars(" "),
]
)
const changeScreen = screen => { const changeScreen = screen => {
store.setCurrentScreen(screen.props._instanceName) store.setCurrentScreen(screen.props._instanceName)

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

@ -23,11 +23,7 @@
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 => const get_capitalised_name = name => pipe(name, [get_name, capitalise])
pipe(
name,
[get_name, capitalise]
)
const isScreenslot = name => name === "##builtin/screenslot" const isScreenslot = name => name === "##builtin/screenslot"
const selectComponent = component => { const selectComponent = component => {

90
packages/builder/src/components/userInterface/EditComponentProps.svelte

@ -1,90 +0,0 @@
<script>
import PropsView from "./PropsView.svelte"
import { store } from "../builderStore"
import Textbox from "../common/Textbox.svelte"
import Button from "../common/Button.svelte"
import { LayoutIcon, PaintIcon, TerminalIcon } from "../common/Icons/"
import { cloneDeep, join, split, last } from "lodash/fp"
import { assign } from "lodash"
$: component = $store.currentPreviewItem
$: componentInfo = $store.currentComponentInfo
$: components = $store.components
const updateComponent = doChange => doChange(cloneDeep(component))
const onPropsChanged = newProps => {
updateComponent(newComponent => assign(newComponent.props, newProps))
}
</script>
<div class="root">
<ul>
<li>
<button>
<PaintIcon />
</button>
</li>
<li>
<button>
<LayoutIcon />
</button>
</li>
<li>
<button>
<TerminalIcon />
</button>
</li>
</ul>
<div class="component-props-container">
<PropsView {componentInfo} {onPropsChanged} />
</div>
</div>
<style>
.root {
height: 100%;
display: flex;
flex-direction: column;
}
.title > div:nth-child(1) {
grid-column-start: name;
color: var(--ink);
}
.title > div:nth-child(2) {
grid-column-start: actions;
}
.component-props-container {
flex: 1 1 auto;
overflow-y: auto;
}
ul {
list-style: none;
display: flex;
padding: 0;
}
li {
margin-right: 20px;
background: none;
border-radius: 5px;
width: 45px;
height: 45px;
}
li button {
width: 100%;
height: 100%;
background: none;
border: none;
border-radius: 5px;
padding: 13px;
}
</style>

94
packages/builder/src/components/userInterface/EventsEditor/EventEditorModal.svelte

@ -1,28 +1,34 @@
<script> <script>
import { store } from "builderStore" import { store } from "builderStore"
import { Button } from "@budibase/bbui" import { Button, Select } from "@budibase/bbui"
import Modal from "../../common/Modal.svelte"
import HandlerSelector from "./HandlerSelector.svelte" import HandlerSelector from "./HandlerSelector.svelte"
import IconButton from "../../common/IconButton.svelte"
import ActionButton from "../../common/ActionButton.svelte"
import getIcon from "../../common/icon"
import { CloseIcon } from "components/common/Icons/" import { CloseIcon } from "components/common/Icons/"
import { EVENT_TYPE_MEMBER_NAME } from "../../common/eventHandlers" import { EVENT_TYPE_MEMBER_NAME } from "../../common/eventHandlers"
import { createEventDispatcher } from "svelte"
export let event = [] export let event
export let eventType export let eventOptions = []
export let onClose
const dispatch = createEventDispatcher() let eventType = ""
let draftEventHandler = { parameters: [] } let draftEventHandler = { parameters: [] }
$: handlers = (event && [...event]) || [] $: eventData = event || { handlers: [] }
$: if (!eventOptions.includes(eventType) && eventOptions.length > 0)
eventType = eventOptions[0].name
const closeModal = () => { const closeModal = () => {
dispatch("close") onClose()
draftEventHandler = { parameters: [] } draftEventHandler = { parameters: [] }
handlers = [] eventData = { handlers: [] }
} }
const updateEventHandler = (updatedHandler, index) => { const updateEventHandler = (updatedHandler, index) => {
handlers[index] = updatedHandler eventData.handlers[index] = updatedHandler
dispatch("change", handlers)
} }
const updateDraftEventHandler = updatedHandler => { const updateDraftEventHandler = updatedHandler => {
@ -30,8 +36,8 @@
} }
const deleteEventHandler = index => { const deleteEventHandler = index => {
handlers.splice(index, 1) eventData.handlers.splice(index, 1)
dispatch("change", handlers) eventData = eventData
} }
const createNewEventHandler = handler => { const createNewEventHandler = handler => {
@ -39,15 +45,37 @@
parameters: {}, parameters: {},
[EVENT_TYPE_MEMBER_NAME]: "", [EVENT_TYPE_MEMBER_NAME]: "",
} }
handlers.push(newHandler) eventData.handlers.push(newHandler)
dispatch("change", handlers) eventData = eventData
}
const deleteEvent = () => {
store.setComponentProp(eventType, [])
closeModal()
}
const saveEventData = () => {
store.setComponentProp(eventType, eventData.handlers)
closeModal()
} }
</script> </script>
<div class="container"> <div class="container">
<div class="body"> <div class="body">
<div class="heading"> <div class="heading">
<h3>{eventType} Event</h3> <h3>
{eventData.name ? `${eventData.name} Event` : 'Create a New Component Event'}
</h3>
</div>
<div class="event-options">
<div class="section">
<h4>Event Type</h4>
<Select bind:value={eventType}>
{#each eventOptions as option}
<option value={option.name}>{option.name}</option>
{/each}
</Select>
</div>
</div> </div>
<div class="section"> <div class="section">
@ -61,16 +89,35 @@
}} }}
handler={draftEventHandler} /> handler={draftEventHandler} />
</div> </div>
{#each handlers as handler, index} {#if eventData}
<HandlerSelector {#each eventData.handlers as handler, index}
{index} <HandlerSelector
onChanged={updateEventHandler} {index}
onRemoved={() => deleteEventHandler(index)} onChanged={updateEventHandler}
{handler} /> onRemoved={() => deleteEventHandler(index)}
{/each} {handler} />
{/each}
{/if}
</div> </div>
<div class="footer">
{#if eventData.name}
<Button
outline
on:click={deleteEvent}
disabled={eventData.handlers.length === 0}>
Delete
</Button>
{/if}
<div class="save">
<Button
primary
on:click={saveEventData}
disabled={eventData.handlers.length === 0}>
Save
</Button>
</div>
</div>
<div class="close-button" on:click={closeModal}> <div class="close-button" on:click={closeModal}>
<CloseIcon /> <CloseIcon />
</div> </div>
@ -79,7 +126,6 @@
<style> <style>
.container { .container {
position: relative; position: relative;
width: 600px;
} }
.heading { .heading {
margin-bottom: 20px; margin-bottom: 20px;

158
packages/builder/src/components/userInterface/EventsEditor/EventsEditor.svelte

@ -0,0 +1,158 @@
<script>
import { getContext } from "svelte"
import {
keys,
map,
some,
includes,
cloneDeep,
isEqual,
sortBy,
filter,
difference,
} from "lodash/fp"
import { pipe } from "components/common/core"
import Checkbox from "components/common/Checkbox.svelte"
import IconButton from "components/common/IconButton.svelte"
import EventEditorModal from "./EventEditorModal.svelte"
import { PencilIcon } from "components/common/Icons"
import { EVENT_TYPE_MEMBER_NAME } from "components/common/eventHandlers"
export const EVENT_TYPE = "event"
export let component
let events = []
let selectedEvent = null
$: {
events = Object.keys(component)
// TODO: use real events
.filter(propName => ["onChange", "onClick", "onLoad"].includes(propName))
.map(propName => ({
name: propName,
handlers: component[propName] || [],
}))
}
// Handle create app modal
const { open, close } = getContext("simple-modal")
const openModal = event => {
selectedEvent = event
open(
EventEditorModal,
{
eventOptions: events,
event: selectedEvent,
onClose: () => {
close()
selectedEvent = null
},
},
{
closeButton: false,
closeOnEsc: false,
styleContent: { padding: 0 },
closeOnOuterClick: true,
}
)
}
</script>
<button class="newevent" on:click={() => openModal()}>
<i class="icon ri-add-circle-fill" />
Create New Event
</button>
<div class="root">
<form on:submit|preventDefault class="uk-form-stacked form-root">
{#each events as event, index}
{#if event.handlers.length > 0}
<div
class:selected={selectedEvent && selectedEvent.index === index}
class="handler-container budibase__nav-item"
on:click={() => openModal({ ...event, index })}>
<span class="event-name">{event.name}</span>
<span class="edit-text">EDIT</span>
</div>
{/if}
{/each}
</form>
</div>
<style>
.root {
font-size: 10pt;
width: 100%;
}
.newevent {
cursor: pointer;
border: 1px solid var(--grey-4);
border-radius: 3px;
width: 100%;
padding: 8px 16px;
margin: 0px 0px 12px 0px;
display: flex;
justify-content: center;
align-items: center;
background: white;
color: var(--ink);
font-size: 14px;
font-weight: 500;
transition: all 2ms;
}
.newevent:hover {
background: var(--grey-1);
}
.icon {
color: var(--ink);
font-size: 16px;
margin-right: 4px;
}
.form-root {
display: flex;
flex-wrap: wrap;
}
header {
display: flex;
align-items: center;
justify-content: space-between;
}
.handler-container {
display: grid;
grid-template-columns: repeat(2, 1fr);
border: 2px solid #f9f9f9;
height: 80px;
width: 100%;
}
.event-name {
margin-top: 5px;
font-weight: bold;
font-size: 16px;
color: rgba(22, 48, 87, 0.6);
align-self: end;
}
.edit-text {
font-family: Arial, Helvetica, sans-serif;
font-weight: bold;
align-self: end;
justify-self: end;
font-size: 10px;
color: rgba(35, 65, 105, 0.4);
}
.selected {
color: var(--blue);
background: var(--grey-1) !important;
}
</style>

3
packages/builder/src/components/userInterface/EventsEditor/HandlerSelector.svelte

@ -1,5 +1,6 @@
<script> <script>
import { Button, Select } from "@budibase/bbui" import { Button, Select } from "@budibase/bbui"
import IconButton from "components/common/IconButton.svelte"
import StateBindingCascader from "./StateBindingCascader.svelte" import StateBindingCascader from "./StateBindingCascader.svelte"
import { find, map, keys, reduce, keyBy } from "lodash/fp" import { find, map, keys, reduce, keyBy } from "lodash/fp"
import { pipe } from "components/common/core" import { pipe } from "components/common/core"
@ -93,7 +94,7 @@
{#if newHandler} {#if newHandler}
<Button primary thin on:click={onCreate}>Add Action</Button> <Button primary thin on:click={onCreate}>Add Action</Button>
{:else} {:else}
<Button secondary thin on:click={onRemoved}>Remove Action</Button> <Button outline thin on:click={onRemoved}>Remove Action</Button>
{/if} {/if}
</div> </div>
{/if} {/if}

4
packages/builder/src/components/userInterface/EventsEditor/StateBindingCascader.svelte

@ -1,8 +1,6 @@
<script> <script>
import { Input } from "@budibase/bbui" import { Input, Select } from "@budibase/bbui"
import IconButton from "components/common/IconButton.svelte" import IconButton from "components/common/IconButton.svelte"
import PlusButton from "components/common/PlusButton.svelte"
import Select from "components/common/Select.svelte"
import { find, map, keys, reduce, keyBy } from "lodash/fp" import { find, map, keys, reduce, keyBy } from "lodash/fp"
import { pipe } from "components/common/core" import { pipe } from "components/common/core"
import { EVENT_TYPE_MEMBER_NAME } from "components/common/eventHandlers" import { EVENT_TYPE_MEMBER_NAME } from "components/common/eventHandlers"

170
packages/builder/src/components/userInterface/LayoutEditor.svelte

@ -1,170 +0,0 @@
<script>
import InputGroup from "../common/Inputs/InputGroup.svelte"
import LayoutTemplateControls from "./LayoutTemplateControls.svelte"
export let onStyleChanged = () => {}
export let component
const tbrl = [
{ placeholder: "T" },
{ placeholder: "R" },
{ placeholder: "B" },
{ placeholder: "L" },
]
const se = [{ placeholder: "START" }, { placeholder: "END" }]
const single = [{ placeholder: "" }]
$: layout = {
...component._styles.position,
...component._styles.layout,
}
$: layouts = {
templaterows: ["Grid Rows", single],
templatecolumns: ["Grid Columns", single],
}
$: display = {
direction: ["Direction", single],
align: ["Align", single],
justify: ["Justify", single],
}
$: positions = {
column: ["Column", se],
row: ["Row", se],
}
$: spacing = {
margin: ["Margin", tbrl, "small"],
padding: ["Padding", tbrl, "small"],
}
$: size = {
height: ["Height", single],
width: ["Width", single],
}
$: zindex = {
zindex: ["Z-Index", single],
}
const newValue = n => Array(n).fill("")
</script>
<h3>Layout</h3>
<div class="layout-pos">
{#each Object.entries(display) as [key, [name, meta, size]] (component._id + key)}
<div class="grid">
<h5>{name}:</h5>
<LayoutTemplateControls
onStyleChanged={_value => onStyleChanged('layout', key, _value)}
values={layout[key] || newValue(meta.length)}
propertyName={name}
{meta}
{size}
type="text" />
</div>
{/each}
</div>
<!-- <h4>Positioning</h4>
<div class="layout-pos">
{#each Object.entries(positions) as [key, [name, meta, size]] (component._id + key)}
<div class="grid">
<h5>{name}:</h5>
<InputGroup
onStyleChanged={_value => onStyleChanged('position', key, _value)}
values={layout[key] || newValue(meta.length)}
{meta}
{size} />
</div>
{/each}
</div> -->
<h3>Spacing</h3>
<div class="layout-spacing">
{#each Object.entries(spacing) as [key, [name, meta, size]] (component._id + key)}
<div class="grid">
<h5>{name}:</h5>
<InputGroup
onStyleChanged={_value => onStyleChanged('position', key, _value)}
values={layout[key] || newValue(meta.length)}
{meta}
{size}
type="text" />
</div>
{/each}
</div>
<h3>Size</h3>
<div class="layout-layer">
{#each Object.entries(size) as [key, [name, meta, size]] (component._id + key)}
<div class="grid">
<h5>{name}:</h5>
<InputGroup
onStyleChanged={_value => onStyleChanged('position', key, _value)}
values={layout[key] || newValue(meta.length)}
type="text"
{meta}
{size} />
</div>
{/each}
</div>
<h3>Order</h3>
<div class="layout-layer">
{#each Object.entries(zindex) as [key, [name, meta, size]] (component._id + key)}
<div class="grid">
<h5>{name}:</h5>
<InputGroup
onStyleChanged={_value => onStyleChanged('position', key, _value)}
values={layout[key] || newValue(meta.length)}
{meta}
{size} />
</div>
{/each}
</div>
<style>
h3 {
text-transform: uppercase;
font-size: 13px;
font-weight: 600;
color: var(--ink);
opacity: 0.6;
margin-bottom: 10px;
}
h4 {
text-transform: uppercase;
font-size: 10px;
font-weight: 600;
color: var(--ink);
opacity: 0.4;
margin-bottom: 10px;
}
h5 {
font-size: 13px;
font-weight: 400;
color: var(--ink);
opacity: 0.8;
padding-top: 13px;
margin-bottom: 0;
}
div > div {
display: grid;
grid-template-rows: 1fr;
grid-gap: 10px;
height: 40px;
margin-bottom: 15px;
}
.grid {
grid-template-columns: 70px 2fr;
}
</style>

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

@ -1,6 +1,4 @@
<script> <script>
import PlusButton from "../common/PlusButton.svelte"
export let meta = [] export let meta = []
export let size = "" export let size = ""
export let values = [] export let values = []

17
packages/builder/src/components/userInterface/ModelSelect.svelte

@ -1,16 +1,13 @@
<script> <script>
import { Select } from "@budibase/bbui"
import { backendUiStore } from "builderStore" import { backendUiStore } from "builderStore"
export let value export let value
</script> </script>
<div class="uk-margin block-field"> <Select thin secondary wide on:change {value}>
<div class="uk-form-controls"> <option value="" />
<select class="budibase__input" on:change {value}> {#each $backendUiStore.models as model}
<option value="" /> <option value={model._id}>{model.name}</option>
{#each $backendUiStore.models as model} {/each}
<option value={model._id}>{model.name}</option> </Select>
{/each}
</select>
</div>
</div>

110
packages/builder/src/components/userInterface/NewScreen.svelte

@ -1,7 +1,5 @@
<script> <script>
import { store } from "builderStore" import { store } from "builderStore"
import PropsView from "./PropsView.svelte"
import Textbox from "components/common/Textbox.svelte"
import Button from "components/common/Button.svelte" import Button from "components/common/Button.svelte"
import ActionButton from "components/common/ActionButton.svelte" import ActionButton from "components/common/ActionButton.svelte"
import ButtonGroup from "components/common/ButtonGroup.svelte" import ButtonGroup from "components/common/ButtonGroup.svelte"
@ -10,6 +8,7 @@
import UIkit from "uikit" import UIkit from "uikit"
import { isRootComponent } from "./pagesParsing/searchComponents" import { isRootComponent } from "./pagesParsing/searchComponents"
import { splitName } from "./pagesParsing/splitRootComponentName.js" import { splitName } from "./pagesParsing/splitRootComponentName.js"
import { Input, Select } from "@budibase/bbui"
import { find, filter, some, map, includes } from "lodash/fp" import { find, filter, some, map, includes } from "lodash/fp"
import { assign } from "lodash" import { assign } from "lodash"
@ -23,8 +22,7 @@
let layoutComponent let layoutComponent
let screens let screens
let name = "" let name = ""
let routeError
let saveAttempted = false
$: layoutComponents = Object.values($store.components).filter( $: layoutComponents = Object.values($store.components).filter(
componentDefinition => componentDefinition.container componentDefinition => componentDefinition.container
@ -39,18 +37,21 @@
$: route = !route && $store.screens.length === 0 ? "*" : route $: route = !route && $store.screens.length === 0 ? "*" : route
const save = () => { const save = () => {
saveAttempted = true if (!route) {
routeError = "Url is required"
const isValid = } else {
name.length > 0 && if (routeNameExists(route)) {
!screenNameExists(name) && routeError = "This url is already taken"
route.length > 0 && } else {
!routeNameExists(route) && routeError = ""
layoutComponent }
}
if (!isValid) return if (routeError) return false
store.createScreen(name, route, layoutComponent._component) store.createScreen(name, route, layoutComponent._component)
name = ""
route = ""
dialog.hide() dialog.hide()
} }
@ -58,12 +59,6 @@
dialog.hide() dialog.hide()
} }
const screenNameExists = name => {
return $store.screens.some(
screen => screen.name.toLowerCase() === name.toLowerCase()
)
}
const routeNameExists = route => { const routeNameExists = route => {
return $store.screens.some( return $store.screens.some(
screen => screen.route.toLowerCase() === route.toLowerCase() screen => screen.route.toLowerCase() === route.toLowerCase()
@ -84,72 +79,27 @@
onOk={save} onOk={save}
okText="Create Screen"> okText="Create Screen">
<div class="uk-form-horizontal"> <div data-cy="new-screen-dialog">
<div class="uk-margin"> <div class="bb-margin-xl">
<label class="uk-form-label">Name</label> <Input label="Name" bind:value={name} />
<div class="uk-form-controls">
<input
class="uk-input uk-form-small"
class:uk-form-danger={saveAttempted && (name.length === 0 || screenNameExists(name))}
bind:value={name} />
</div>
</div> </div>
<div class="uk-margin"> <div class="bb-margin-xl">
<label class="uk-form-label">Route (URL)</label> <Input
<div class="uk-form-controls"> label="Url"
<input error={routeError}
class="uk-input uk-form-small" bind:value={route}
class:uk-form-danger={saveAttempted && (route.length === 0 || routeNameExists(route))} on:change={routeChanged} />
bind:value={route}
on:change={routeChanged} />
</div>
</div> </div>
<div class="uk-margin"> <div class="bb-margin-xl">
<label class="uk-form-label">Layout Component</label> <label>Layout Component</label>
<div class="uk-form-controls"> <Select bind:value={layoutComponent} secondary>
<select {#each layoutComponents as { _component, name }}
class="uk-select uk-form-small" <option value={_component}>{name}</option>
bind:value={layoutComponent} {/each}
class:uk-form-danger={saveAttempted && !layoutComponent}> </Select>
{#each layoutComponents as { _component, name }}
<option value={_component}>{name}</option>
{/each}
</select>
</div>
</div> </div>
</div> </div>
</ConfirmDialog> </ConfirmDialog>
<style>
.uk-margin {
display: flex;
flex-direction: column;
}
.uk-form-controls {
margin-left: 0 !important;
}
.uk-form-label {
padding-bottom: 10px;
font-weight: 500;
font-size: 16px;
color: var(--grey-7);
}
.uk-input {
height: 40px !important;
border-radius: 3px;
}
.uk-select {
height: 40px !important;
font-weight: 500px;
color: var(--grey-5);
border: 1px solid var(--grey-2);
border-radius: 3px;
}
</style>

33
packages/builder/src/components/userInterface/OptionSelectOld.svelte

@ -1,33 +0,0 @@
<script>
import { onMount } from "svelte"
export let value = ""
export let onChange = value => {}
export let options = []
export let initialValue = ""
export let styleBindingProperty = ""
const handleStyleBind = value =>
!!styleBindingProperty ? { style: `${styleBindingProperty}: ${value}` } : {}
$: isOptionsObject = options.every(o => typeof o === "object")
onMount(() => {
if (!value && !!initialValue) {
value = initialValue
}
})
</script>
<select {value} on:change={ev => onChange(ev.target.value)}>
{#if isOptionsObject}
{#each options as { value, label }}
<option {...handleStyleBind(value || label)} value={value || label}>
{label}
</option>
{/each}
{:else}
{#each options as value}
<option {...handleStyleBind(value)} {value}>{value}</option>
{/each}
{/if}
</select>

65
packages/builder/src/components/userInterface/PageView.svelte

@ -1,65 +0,0 @@
<script>
import Textbox from "components/common/Textbox.svelte"
import Dropdown from "components/common/Dropdown.svelte"
import Button from "components/common/Button.svelte"
import { store } from "builderStore"
import { isRootComponent } from "./pagesParsing/searchComponents"
import { pipe } from "components/common/core"
import { filter, find, concat } from "lodash/fp"
const notSelectedComponent = { name: "(none selected)" }
$: page = $store.pages[$store.currentPageName]
$: title = page.index.title
$: components = pipe($store.components, [
filter(store => !isRootComponent($store)),
concat([notSelectedComponent]),
])
$: entryComponent = components[page.appBody] || notSelectedComponent
const save = () => {
if (!title || !entryComponent || entryComponent === notSeletedComponent)
return
const page = {
index: {
title,
},
appBody: entryComponent.name,
}
store.savePage(page)
}
</script>
<div class="root">
<h3>{$store.currentPageName}</h3>
<form on:submit|preventDefault class="uk-form-horizontal">
<Textbox bind:text={title} label="Title" hasError={!title} />
<div class="help-text">
The title of your page, displayed in the bowser tab
</div>
<Dropdown
label="App Entry Component"
options={components}
bind:selected={entryComponent}
textMember={v => v.name} />
<div class="help-text">
The component that will be loaded into the body of the page
</div>
<div style="margin-top: 20px" />
<Button on:click={save}>Save</Button>
</form>
</div>
<style>
.root {
padding: 15px;
}
.help-text {
color: var(--grey-2);
font-size: 10pt;
}
</style>

46
packages/builder/src/components/userInterface/PropControl.svelte

@ -1,46 +0,0 @@
<script>
import { store } from "builderStore"
import Checkbox from "../common/Checkbox.svelte"
import Textbox from "../common/Textbox.svelte"
import Dropdown from "../common/Dropdown.svelte"
import StateBindingControl from "./StateBindingControl.svelte"
export let index
export let prop_name
export let prop_value
export let prop_definition = {}
</script>
<div class="root">
{#if prop_definition.type !== 'event'}
<h5>{prop_name}</h5>
<StateBindingControl
value={prop_value}
type={prop_definition.type || prop_definition}
options={prop_definition.options}
styleBindingProperty={prop_definition.styleBindingProperty}
onChanged={v => store.setComponentProp(prop_name, v)} />
{/if}
</div>
<style>
.root {
height: 40px;
margin-bottom: 15px;
display: grid;
grid-template-rows: 1fr;
grid-template-columns: 70px 1fr;
grid-gap: 10px;
align-items: baseline;
}
h5 {
word-wrap: break-word;
font-size: 13px;
font-weight: 400;
color: var(--ink);
opacity: 0.8;
padding-top: 13px;
margin-bottom: 0;
}
</style>

15
packages/builder/src/components/userInterface/PropertyControl.svelte

@ -1,6 +1,6 @@
<script> <script>
import { Icon } from "@budibase/bbui" import { Icon } from "@budibase/bbui"
import Input from "components/common/Input.svelte" import Input from "./PropertyPanelControls/Input.svelte"
import { store, backendUiStore } from "builderStore" import { store, backendUiStore } from "builderStore"
import fetchBindableProperties from "builderStore/fetchBindableProperties" import fetchBindableProperties from "builderStore/fetchBindableProperties"
import { DropdownMenu } from "@budibase/bbui" import { DropdownMenu } from "@budibase/bbui"
@ -69,17 +69,14 @@
getBindableProperties() getBindableProperties()
let temp = value let temp = value
const boundValues = (value && value.match(/{{([^}]+)}}/g)) || [] const boundValues = (value && value.match(/{{([^}]+)}}/g)) || []
console.log(boundValues)
// Replace with names: // Replace with names:
boundValues.forEach(v => { boundValues.forEach(v => {
const { readableBinding } = bindableProperties.find( const binding = bindableProperties.find(({ runtimeBinding }) => {
({ runtimeBinding }) => { return v === `{{ ${runtimeBinding} }}`
return v === `{{ ${runtimeBinding} }}` })
} if (binding) {
) temp = temp.replace(v, `{{ ${binding.readableBinding} }}`)
if (readableBinding) {
temp = temp.replace(v, `{{ ${readableBinding} }}`)
} }
}) })
// console.log(temp) // console.log(temp)

11
packages/builder/src/components/userInterface/PropertyPanelControls/Input.svelte

@ -0,0 +1,11 @@
<script>
/*
This file exists because of how we pass this control to the
properties panel - via a JS reference, not using svelte tags
... checkout the use of Input in propertyCategories to see what i mean
*/
import { Input } from "@budibase/bbui"
export let name, value, placeholder, type
</script>
<Input {name} {value} {placeholder} {type} thin on:change />

54
packages/builder/src/components/userInterface/PropsView.svelte

@ -1,54 +0,0 @@
<script>
import { some, includes, filter } from "lodash/fp"
import Textbox from "../common/Textbox.svelte"
import Dropdown from "../common/Dropdown.svelte"
import PropControl from "./PropControl.svelte"
import IconButton from "../common/IconButton.svelte"
export let component
export let components
let errors = []
const props_to_ignore = ["_component", "_children", "_styles", "_code", "_id"]
$: componentDef = components[component._component]
</script>
<div class="root">
<form on:submit|preventDefault class="uk-form-stacked form-root">
{#if componentDef}
{#each Object.entries(componentDef.props) as [prop_name, prop_def], index}
{#if prop_def !== 'event'}
<div class="prop-container">
<PropControl
{prop_name}
prop_value={component[prop_name]}
prop_definition={prop_def}
{index}
disabled={false} />
</div>
{/if}
{/each}
{/if}
</form>
</div>
<style>
.root {
font-size: 10pt;
width: 100%;
}
.form-root {
display: flex;
flex-wrap: wrap;
}
.prop-container {
flex: 1 1 auto;
min-width: 250px;
}
</style>

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

@ -1,7 +1,6 @@
<script> <script>
import PropertyControl from "./PropertyControl.svelte" import PropertyControl from "./PropertyControl.svelte"
import InputGroup from "../common/Inputs/InputGroup.svelte" import Input from "./PropertyPanelControls/Input.svelte"
import Input from "../common/Input.svelte"
import { goto } from "@sveltech/routify" import { goto } from "@sveltech/routify"
import { excludeProps } from "./propertyCategories.js" import { excludeProps } from "./propertyCategories.js"
import { store } from "builderStore" import { store } from "builderStore"

63
packages/builder/src/components/userInterface/StateBindingControl.svelte

@ -1,63 +0,0 @@
<script>
import { backendUiStore } from "builderStore"
import IconButton from "../common/IconButton.svelte"
import Input from "../common/Input.svelte"
export let value = ""
export let onChanged = () => {}
export let type = ""
export let options = []
export let styleBindingProperty = ""
$: bindOptionToStyle = !!styleBindingProperty
</script>
<div class="unbound-container">
{#if type === 'bool'}
<div>
<IconButton
icon={value == true ? 'check-square' : 'square'}
size="19"
on:click={() => onChanged(!value)} />
</div>
{:else if type === 'models'}
<select
class="uk-select uk-form-small"
bind:value
on:change={() => {
onChanged(value)
}}>
{#each $backendUiStore.models || [] as option}
<option value={option}>{option.name}</option>
{/each}
</select>
{:else if type === 'options' || type === 'models'}
<select
class="uk-select uk-form-small"
{value}
on:change={ev => onChanged(ev.target.value)}>
{#each options || [] as option}
{#if bindOptionToStyle}
<option style={`${styleBindingProperty}: ${option};`} value={option}>
{option}
</option>
{:else}
<option value={option}>{option}</option>
{/if}
{/each}
</select>
{/if}
</div>
<style>
.unbound-container {
display: flex;
}
.bound-header > div:nth-child(1) {
flex: 1 0 auto;
width: 30px;
color: var(--secondary50);
padding-left: 5px;
}
</style>

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

@ -8,7 +8,6 @@
import NewScreen from "./NewScreen.svelte" import NewScreen from "./NewScreen.svelte"
import CurrentItemPreview from "./CurrentItemPreview.svelte" import CurrentItemPreview from "./CurrentItemPreview.svelte"
import SettingsView from "./SettingsView.svelte" import SettingsView from "./SettingsView.svelte"
import PageView from "./PageView.svelte"
import ComponentsPaneSwitcher from "./ComponentsPaneSwitcher.svelte" import ComponentsPaneSwitcher from "./ComponentsPaneSwitcher.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"

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

@ -1,4 +1,4 @@
import Input from "../common/Input.svelte" import Input from "./PropertyPanelControls/Input.svelte"
import OptionSelect from "./OptionSelect.svelte" import OptionSelect from "./OptionSelect.svelte"
import FlatButtonGroup from "./FlatButtonGroup.svelte" import FlatButtonGroup from "./FlatButtonGroup.svelte"
import Colorpicker from "@budibase/colorpicker" import Colorpicker from "@budibase/colorpicker"

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

@ -1,4 +1,4 @@
import Input from "../common/Input.svelte" import Input from "./PropertyPanelControls/Input.svelte"
import OptionSelect from "./OptionSelect.svelte" import OptionSelect from "./OptionSelect.svelte"
import Checkbox from "../common/Checkbox.svelte" import Checkbox from "../common/Checkbox.svelte"
import ModelSelect from "components/userInterface/ModelSelect.svelte" import ModelSelect from "components/userInterface/ModelSelect.svelte"
@ -308,7 +308,7 @@ export default {
label: "destinationUrl", label: "destinationUrl",
key: "destinationUrl", key: "destinationUrl",
control: Input, control: Input,
placeholder: "/table/{{context._id}}", placeholder: "/table/_id",
}, },
], ],
}, },
@ -480,44 +480,6 @@ export default {
}, },
], ],
}, },
{
_component: "@budibase/materialdesign-components/BasicCard",
name: "Card",
description:
"A basic card component that can contain content and actions.",
icon: "ri-layout-bottom-line",
children: [],
properties: {
design: { ...all },
settings: [
{
label: "Heading",
key: "heading",
control: Input,
placeholder: "text",
},
{
label: "Subheading",
key: "subheading",
control: Input,
placeholder: "text",
},
{
label: "Content",
key: "content",
control: Input,
placeholder: "text",
},
{
label: "Image",
key: "imageUrl",
control: Input,
placeholder: "src",
},
],
},
},
{ {
name: "Table", name: "Table",
_component: "@budibase/standard-components/datatable", _component: "@budibase/standard-components/datatable",
@ -566,11 +528,6 @@ export default {
}, },
], ],
}, },
template: {
component: "@budibase/materialdesign-components/Form",
description: "Form for saving a record",
name: "@budibase/materialdesign-components/recordForm",
},
}, },
{ {
_component: "@budibase/standard-components/dataformwide", _component: "@budibase/standard-components/dataformwide",
@ -600,129 +557,461 @@ export default {
], ],
}, },
{ {
name: "Donut Chart", name: "Chart",
_component: "@budibase/standard-components/donut", description: "Shiny chart",
description: "Donut chart", icon: "ri-bar-chart-fill",
icon: "ri-pie-chart-fill", children: [
properties: { {
settings: [ name: "Donut",
{ _component: "@budibase/standard-components/donut",
label: "Table", description: "Donut chart",
key: "model", icon: "ri-pie-chart-fill",
control: ModelSelect, properties: {
}, settings: [
{ {
label: "Animate Chart", label: "Table",
key: "isAnimated", key: "model",
valueKey: "checked", control: ModelSelect,
control: Checkbox, },
}, {
{ label: "Animate Chart",
label: "Hover Highlight", key: "isAnimated",
key: "hasHoverAnimation", valueKey: "checked",
valueKey: "checked", control: Checkbox,
control: Checkbox, },
}, {
{ label: "Hover Highlight",
label: "Keep Last Hover", key: "hasHoverAnimation",
key: "hasLastHoverSliceHighlighted", valueKey: "checked",
valueKey: "checked", control: Checkbox,
control: Checkbox, },
}, {
{ label: "Keep Last Hover",
label: "Colors", key: "hasLastHoverSliceHighlighted",
key: "color", valueKey: "checked",
control: OptionSelect, control: Checkbox,
options: [ },
"britecharts", {
"blueGreen", label: "Colors",
"green", key: "color",
"grey", control: OptionSelect,
"orange", options: [
"pink", "britecharts",
"purple", "blueGreen",
"red", "green",
"teal", "grey",
"yellow", "orange",
"pink",
"purple",
"red",
"teal",
"yellow",
],
},
{
label: "Name Field",
key: "nameKey",
control: Input,
},
{
label: "Value Field",
key: "valueKey",
control: Input,
},
{
label: "External Radius",
key: "externalRadius",
control: Input,
},
{
label: "Internal Radius",
key: "internalRadius",
control: Input,
},
{
label: "Radius Offset",
key: "radiusHoverOffset ",
control: Input,
},
{
label: "Show Legend",
key: "useLegend ",
valueKey: "checked",
control: Checkbox,
},
{
label: "Horizontal Legend",
key: "horizontalLegend",
valueKey: "checked",
control: Checkbox,
},
{
label: "Legend Width",
key: "legendWidth",
control: Input,
},
], ],
}, },
{ },
label: "Name Field", {
key: "nameKey", name: "Bar",
control: Input, _component: "@budibase/standard-components/bar",
}, description: "Bar chart",
{ icon: "ri-bar-chart-fill",
label: "Value Field", properties: {
key: "valueKey", settings: [
control: Input, {
}, label: "Table",
{ key: "model",
label: "External Radius", control: ModelSelect,
key: "externalRadius", },
control: Input, {
}, label: "Name Label",
{ key: "nameLabel",
label: "Internal Radius", control: Input,
key: "internalRadius", },
control: Input, {
}, label: "Value Label",
{ key: "valueLabel",
label: "Radius Offset", control: Input,
key: "radiusHoverOffset ", },
control: Input, {
}, label: "Y Axis Label",
{ key: "yAxisLabel",
label: "Show Legend", control: Input,
key: "useLegend ", },
valueKey: "checked", {
control: Checkbox, label: "X Axis Label",
}, key: "xAxisLabel",
{ control: Input,
label: "Horizontal Legend", },
key: "horizontalLegend", {
valueKey: "checked", label: "X Axis Label Offset",
control: Checkbox, key: "xAxisLabelOffset",
}, control: Input,
{ },
label: "Legend Width", {
key: "legendWidth", label: "Y Axis Label Offset",
control: Input, key: "yAxisLabelOffset",
control: Input,
},
{
label: "Enable Labels",
key: "enableLabels",
control: Checkbox,
valueKey: "checked",
},
{
label: "Colors",
key: "color",
control: OptionSelect,
options: [
{ label: "Normal", value: "britecharts" },
{ label: "Blue Green", value: "blueGreen" },
{ label: "Green", value: "green" },
{ label: "Grey", value: "grey" },
{ label: "Orange", value: "orange" },
{ label: "Pink", value: "pink" },
{ label: "Purple", value: "purple" },
{ label: "Red", value: "red" },
{ label: "Teal", value: "teal" },
{ label: "Yellow", value: "yellow" },
],
},
{
label: "Gradients",
key: "gradient",
control: OptionSelect,
options: [
{ value: "", label: "None" },
{ value: "bluePurple", label: "Blue Purple" },
{ value: "greenBlue", label: "Green Blue" },
{ value: "orangePink", label: "Orange Pink" },
],
},
{
label: "Highlight Single Bar",
key: "hasSingleBarHighlight",
control: Checkbox,
valueKey: "checked",
},
{
label: "Width",
key: "width",
control: Input,
},
{
label: "Height",
key: "height",
control: Input,
},
{
label: "Animate",
key: "isAnimate",
control: Checkbox,
valueKey: "checked",
},
{
label: "Horizontal",
key: "isHorizontal",
control: Checkbox,
valueKey: "checked",
},
{
label: "Label Number Format",
key: "labelsNumberFormat",
control: Input,
},
],
}, },
{ },
label: "Legend Height", {
key: "legendHeight", name: "Groupedbar",
control: Input, _component: "@budibase/standard-components/groupedbar",
description: "Groupedbar chart",
icon: "ri-bar-chart-grouped-fill",
properties: {
settings: [
{
label: "Table",
key: "model",
control: ModelSelect,
},
{
label: "Color",
key: "color",
control: OptionSelect,
options: [
"britecharts",
"blueGreen",
"green",
"grey",
"orange",
"pink",
"purple",
"red",
"teal",
"yellow",
],
},
{
label: "Height",
key: "height",
control: Input,
},
{
label: "Width",
key: "width",
control: Input,
},
{
label: "Aspect Ratio",
key: "aspectRatio",
control: Input,
},
{
label: "Grid",
key: "grid",
control: OptionSelect,
options: ["vertical", "horizontal", "full"],
},
{
label: "Group Label",
key: "groupLabel",
control: Input,
},
{
label: "Name Label",
key: "nameLabel",
control: Input,
},
{
label: "Value Label",
key: "valueLabel",
control: Input,
},
{
label: "Y Ticks",
key: "yTicks",
control: Input,
},
{
label: "Y Tick Text Offset",
key: "yTickTextOffset",
control: Input,
},
{
label: "Is Animated",
key: "isAnimated",
valueKey: "checked",
control: Checkbox,
},
{
label: "Is Horizontal",
key: "isHorizontal",
valueKey: "checked",
control: Checkbox,
},
{
label: "Tooltip Title",
key: "tooltipTitle",
control: Input,
},
],
}, },
], },
},
children: [],
},
{
name: "Data List",
_component: "@budibase/standard-components/datalist",
description: "Shiny list",
icon: "ri-file-list-fill",
properties: {
design: { ...all },
settings: [{ label: "Table", key: "model", control: ModelSelect }],
},
children: [],
},
{
name: "List",
_component: "@budibase/standard-components/list",
description: "Renders all children once per record, of a given table",
icon: "ri-file-list-fill",
properties: {
design: { ...all },
settings: [{ label: "Table", key: "model", control: ModelSelect }],
},
children: [
{ {
_component: "@budibase/standard-components/heading", name: "Line",
name: "Headline", _component: "@budibase/standard-components/line",
description: "A component for displaying heading text", description: "Line chart",
icon: "ri-heading", icon: "ri-line-chart-fill",
properties: {
settings: [
{
label: "Table",
key: "model",
control: ModelSelect,
},
{
label: "Colors",
key: "color",
control: OptionSelect,
options: [
"britecharts",
"blueGreen",
"green",
"grey",
"orange",
"pink",
"purple",
"red",
"teal",
"yellow",
],
},
{
label: "Gradients",
key: "lineGradient",
control: OptionSelect,
options: [
{ value: "", label: "None" },
{ value: "bluePurple", label: "Blue Purple" },
{ value: "greenBlue", label: "Green Blue" },
{ value: "orangePink", label: "Orange Pink" },
],
},
{
label: "Line Curve",
key: "lineCurve",
control: OptionSelect,
options: [
"linear",
"basis",
"natural",
"monotoneX",
"monotoneY",
"step",
"stepAfter",
"stepBefore",
"cardinal",
"catmullRom",
],
},
{
label: "X Axis Value Type",
key: "xAxisValueType",
control: OptionSelect,
options: ["date", "number"],
},
{
label: "Grid",
key: "grid",
control: OptionSelect,
options: ["vertical", "horizontal", "full"],
},
{
label: "Date Label",
key: "dateLabel",
control: Input,
},
{
label: "Topic Label",
key: "topicLabel",
control: Input,
},
{
label: "Value Label",
key: "valueLabel",
control: Input,
},
{
label: "X Axis Label",
key: "xAxisLabel",
control: Input,
},
{
label: "Y Axis Label",
key: "yAxisLabel",
control: Input,
},
{
label: "Show All Datapoints",
key: "shouldShowAllDataPoints",
valueKey: "checked",
control: Checkbox,
},
{
label: "Width",
key: "width",
control: Input,
},
{
label: "Height",
key: "height",
control: Input,
},
{
label: "Is Animated",
key: "isAnimated",
control: Checkbox,
valueKey: "checked",
},
{
label: "Locale",
key: "locale",
control: OptionSelect,
options: ["en-GB", "en-US"],
},
{
label: "X Axis Value Type",
key: "xAxisValueType",
control: OptionSelect,
options: ["date", "numeric"],
},
{
label: "X Axis Format",
key: "xAxisFormat",
control: OptionSelect,
options: [
"day-month",
"minute-hour",
"hour-daymonth",
"month-year",
"custom",
],
},
{
label: "X Axis Custom Format",
key: "xAxisCustomFormat",
control: Input,
},
{
label: "Tooltip Title",
key: "tooltipTitle",
control: Input,
},
],
},
}, },
], ],
}, },

2
packages/builder/src/components/workflow/SetupPanel/ParamInputs/ComponentSelector.svelte

@ -23,7 +23,7 @@
} }
</script> </script>
<div class="uk-margin block-field"> <div class="bb-margin-xl block-field">
<label class="uk-form-label">Page</label> <label class="uk-form-label">Page</label>
<div class="uk-form-controls"> <div class="uk-form-controls">
<select class="budibase__input" bind:value={pageName}> <select class="budibase__input" bind:value={pageName}>

2
packages/builder/src/components/workflow/SetupPanel/ParamInputs/ModelSelector.svelte

@ -4,7 +4,7 @@
export let value export let value
</script> </script>
<div class="uk-margin block-field"> <div class="bb-margin-xl block-field">
<div class="uk-form-controls"> <div class="uk-form-controls">
<select class="budibase__input" bind:value> <select class="budibase__input" bind:value>
<option value="" /> <option value="" />

10
packages/builder/src/components/workflow/SetupPanel/ParamInputs/RecordSelector.svelte

@ -1,10 +1,11 @@
<script> <script>
import { backendUiStore } from "builderStore" import { backendUiStore } from "builderStore"
import { Input } from "@budibase/bbui"
export let value export let value
</script> </script>
<div class="uk-margin block-field"> <div class="bb-margin-xl block-field">
<div class="uk-form-controls"> <div class="uk-form-controls">
<select class="budibase__input" bind:value={value.model}> <select class="budibase__input" bind:value={value.model}>
{#each $backendUiStore.models as model} {#each $backendUiStore.models as model}
@ -15,12 +16,11 @@
</div> </div>
{#if value.model} {#if value.model}
<div class="uk-margin block-field"> <div class="bb-margin-xl block-field">
<label class="uk-form-label fields">Fields</label> <label class="uk-form-label fields">Fields</label>
{#each Object.keys(value.model.schema) as field} {#each Object.keys(value.model.schema) as field}
<div class="uk-form-controls uk-margin"> <div class="uk-form-controls bb-margin-xl">
<label class="uk-form-label">{field}</label> <Input bind:value={value[field]} label={field} />
<input type="text" class="budibase__input" bind:value={value[field]} />
</div> </div>
{/each} {/each}
</div> </div>

32
packages/builder/src/components/workflow/SetupPanel/SetupPanel.svelte

@ -5,7 +5,7 @@
import { notifier } from "builderStore/store/notifications" import { notifier } from "builderStore/store/notifications"
import WorkflowBlockSetup from "./WorkflowBlockSetup.svelte" import WorkflowBlockSetup from "./WorkflowBlockSetup.svelte"
import DeleteWorkflowModal from "./DeleteWorkflowModal.svelte" import DeleteWorkflowModal from "./DeleteWorkflowModal.svelte"
import { Button } from "@budibase/bbui" import { Button, Input } from "@budibase/bbui"
const { open, close } = getContext("simple-modal") const { open, close } = getContext("simple-modal")
@ -81,7 +81,7 @@
{/if} {/if}
</header> </header>
{#if selectedTab === 'TEST'} {#if selectedTab === 'TEST'}
<div class="uk-margin config-item"> <div class="bb-margin-m">
{#if testResult} {#if testResult}
<button <button
transition:fade transition:fade
@ -111,18 +111,10 @@
<div class="panel"> <div class="panel">
<div class="panel-body"> <div class="panel-body">
<div class="block-label">Workflow: {workflow.name}</div> <div class="block-label">Workflow: {workflow.name}</div>
<div class="config-item">
<label>Name</label>
<div class="form">
<input
type="text"
class="budibase_input"
bind:value={workflow.name} />
</div>
</div>
<div class="config-item"> <div class="config-item">
<label class="uk-form-label">User Access</label> <label class="uk-form-label">User Access</label>
<div class="access-levels"> <div class="access-levels">
{#each ACCESS_LEVELS as level} {#each ACCESS_LEVELS as level}
<span class="access-level"> <span class="access-level">
<label>{level.name}</label> <label>{level.name}</label>
@ -194,28 +186,12 @@
margin-bottom: 20px; margin-bottom: 20px;
} }
.budibase_input {
height: 36px;
width: 244px;
border-radius: 3px;
background-color: var(--grey-2);
border: 1px solid var(--grey-2);
text-align: left;
color: var(--ink);
font-size: 14px;
padding-left: 12px;
}
header > span { header > span {
color: var(--grey-5); color: var(--grey-5);
margin-right: 20px; margin-right: 20px;
cursor: pointer; cursor: pointer;
} }
.form {
margin-top: 12px;
}
label { label {
font-weight: 500; font-weight: 500;
font-size: 14px; font-size: 14px;
@ -226,7 +202,7 @@
position: absolute; position: absolute;
bottom: 20px; bottom: 20px;
display: grid; display: grid;
width: 100%; width: 260px;
gap: 12px; gap: 12px;
} }

42
packages/builder/src/components/workflow/SetupPanel/WorkflowBlockSetup.svelte

@ -3,6 +3,7 @@
import ComponentSelector from "./ParamInputs/ComponentSelector.svelte" import ComponentSelector from "./ParamInputs/ComponentSelector.svelte"
import ModelSelector from "./ParamInputs/ModelSelector.svelte" import ModelSelector from "./ParamInputs/ModelSelector.svelte"
import RecordSelector from "./ParamInputs/RecordSelector.svelte" import RecordSelector from "./ParamInputs/RecordSelector.svelte"
import { Input, TextArea, Select } from "@budibase/bbui"
export let workflowBlock export let workflowBlock
@ -18,42 +19,34 @@
<div class="block-field"> <div class="block-field">
<label class="label">{parameter}</label> <label class="label">{parameter}</label>
{#if Array.isArray(type)} {#if Array.isArray(type)}
<select class="budibase_input" bind:value={workflowBlock.args[parameter]}> <Select bind:value={workflowBlock.args[parameter]} thin>
{#each type as option} {#each type as option}
<option value={option}>{option}</option> <option value={option}>{option}</option>
{/each} {/each}
</select> </Select>
{:else if type === 'component'} {:else if type === 'component'}
<ComponentSelector bind:value={workflowBlock.args[parameter]} /> <ComponentSelector bind:value={workflowBlock.args[parameter]} />
{:else if type === 'accessLevel'} {:else if type === 'accessLevel'}
<select class="budibase_input" bind:value={workflowBlock.args[parameter]}> <Select bind:value={workflowBlock.args[parameter]} thin>
<option value="ADMIN">Admin</option> <option value="ADMIN">Admin</option>
<option value="POWER_USER">Power User</option> <option value="POWER_USER">Power User</option>
</select> </Select>
{:else if type === 'password'} {:else if type === 'password'}
<input <Input type="password" thin bind:value={workflowBlock.args[parameter]} />
type="password"
class="budibase_input"
bind:value={workflowBlock.args[parameter]} />
{:else if type === 'number'} {:else if type === 'number'}
<input <Input type="number" thin bind:value={workflowBlock.args[parameter]} />
type="number"
class="budibase_input"
bind:value={workflowBlock.args[parameter]} />
{:else if type === 'longText'} {:else if type === 'longText'}
<textarea <TextArea
type="text" type="text"
class="budibase_input" thin
bind:value={workflowBlock.args[parameter]} /> bind:value={workflowBlock.args[parameter]}
label="" />
{:else if type === 'model'} {:else if type === 'model'}
<ModelSelector bind:value={workflowBlock.args[parameter]} /> <ModelSelector bind:value={workflowBlock.args[parameter]} />
{:else if type === 'record'} {:else if type === 'record'}
<RecordSelector bind:value={workflowBlock.args[parameter]} /> <RecordSelector bind:value={workflowBlock.args[parameter]} />
{:else if type === 'string'} {:else if type === 'string'}
<input <Input type="text" thin bind:value={workflowBlock.args[parameter]} />
type="text"
class="budibase_input"
bind:value={workflowBlock.args[parameter]} />
{/if} {/if}
</div> </div>
{/each} {/each}
@ -62,17 +55,6 @@
.block-field { .block-field {
display: grid; display: grid;
} }
.budibase_input {
height: 36px;
border-radius: 5px;
background-color: var(--grey-2);
border: 1px solid var(--grey-2);
text-align: left;
color: var(--ink);
font-size: 14px;
padding-left: 12px;
margin-top: 8px;
}
label { label {
text-transform: capitalize; text-transform: capitalize;

4
packages/builder/src/components/workflow/WorkflowPanel/WorkflowList/CreateWorkflowModal.svelte

@ -2,6 +2,7 @@
import { store, backendUiStore, workflowStore } from "builderStore" import { store, backendUiStore, workflowStore } from "builderStore"
import { notifier } from "builderStore/store/notifications" import { notifier } from "builderStore/store/notifications"
import ActionButton from "components/common/ActionButton.svelte" import ActionButton from "components/common/ActionButton.svelte"
import { Input } from "@budibase/bbui"
export let onClosed export let onClosed
@ -26,8 +27,7 @@
Create Workflow Create Workflow
</header> </header>
<div> <div>
<label class="uk-form-label" for="form-stacked-text">Name</label> <Input bind:value={name} label="Name" />
<input class="uk-input" type="text" bind:value={name} />
</div> </div>
<footer> <footer>
<a href="https://docs.budibase.com"> <a href="https://docs.budibase.com">

147
packages/builder/src/constants/backend/index.js

@ -16,7 +16,7 @@ export const FIELDS = {
constraints: { constraints: {
type: "number", type: "number",
presence: { allowEmpty: true }, presence: { allowEmpty: true },
numericality: {}, numericality: { greaterThanOrEqualTo: "", lessThanOrEqualTo: "" },
}, },
}, },
BOOLEAN: { BOOLEAN: {
@ -37,16 +37,21 @@ export const FIELDS = {
// presence: { allowEmpty: true }, // presence: { allowEmpty: true },
// }, // },
// }, // },
DATETIME: { // DATETIME: {
name: "Date/Time", // name: "Date/Time",
icon: "ri-calendar-event-fill", // icon: "ri-calendar-event-fill",
type: "string", // type: "string",
constraints: { // value: "datetime",
type: "string", // constraints: {
length: {}, // type: "string",
presence: { allowEmpty: true }, // length: {},
}, // presence: { allowEmpty: true },
}, // datetime: {
// latest: "",
// earliest: "",
// },
// },
// },
// IMAGE: { // IMAGE: {
// name: "File", // name: "File",
// icon: "ri-image-line", // icon: "ri-image-line",
@ -75,123 +80,3 @@ export const FIELDS = {
// }, // },
// }, // },
} }
export const BLOCKS = {
NAME: {
name: "Name",
icon: "ri-text",
type: "string",
constraints: {
type: "string",
length: {},
presence: { allowEmpty: true },
},
},
COMPANY: {
name: "Company",
icon: "ri-store-line",
type: "string",
constraints: {
type: "string",
length: {},
presence: { allowEmpty: true },
},
},
EMAIL: {
name: "Email",
icon: "ri-mail-line",
type: "string",
constraints: {
type: "string",
length: {},
presence: { allowEmpty: true },
},
},
PHONE_NUMBER: {
name: "Phone No.",
icon: "ri-phone-line",
type: "number",
constraints: {
type: "number",
presence: { allowEmpty: true },
numericality: {},
},
},
VALUE: {
name: "Value",
icon: "ri-number-5",
type: "number",
constraints: {
type: "number",
presence: { allowEmpty: true },
numericality: {},
},
},
ACTIVE: {
name: "Active",
icon: "ri-toggle-line",
type: "boolean",
constraints: {
type: "boolean",
presence: { allowEmpty: true },
},
},
URL: {
name: "URL",
icon: "ri-link",
type: "string",
constraints: {
type: "string",
length: {},
presence: { allowEmpty: true },
},
},
IMAGE: {
name: "Image URL",
icon: "ri-image-line",
type: "string",
constraints: {
type: "string",
length: {},
presence: { allowEmpty: true },
},
},
// PRIORITY: {
// name: "Options",
// icon: "ri-list-check-2",
// type: "options",
// constraints: {
// type: "string",
// presence: { allowEmpty: true },
// inclusion: ["low", "medium", "high"],
// },
// },
END_DATE: {
name: "End Date",
icon: "ri-calendar-event-fill",
type: "string",
constraints: {
type: "string",
length: {},
presence: { allowEmpty: true },
},
},
// AVATAR: {
// name: "Avatar",
// icon: "ri-image-line",
// type: "image",
// constraints: {
// type: "string",
// presence: { allowEmpty: true },
// },
// },
// PDF: {
// name: "PDF",
// icon: "ri-file-line",
// type: "file",
// constraints: {
// type: "string",
// presence: { allowEmpty: true },
// },
// },
}

15
packages/builder/src/global.css

@ -94,4 +94,19 @@ textarea {
.hoverable:hover { .hoverable:hover {
cursor: pointer; cursor: pointer;
}
.bb-margin-m {
margin-bottom: var(--spacing-m);
}
* + .bb-margin-m {
margin-top: var(--spacing-m);
}
.bb-margin-xl {
margin-bottom: var(--spacing-xl);
}
* + .bb-margin-xl {
margin-top: var(--spacing-xl);
} }

7
packages/builder/src/index.html

@ -8,20 +8,21 @@
<title>Budibase Builder</title> <title>Budibase Builder</title>
<link href="https://cdn.jsdelivr.net/npm/remixicon@2.3.0/fonts/remixicon.css" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/remixicon@2.3.0/fonts/remixicon.css" rel="stylesheet">
<link rel='icon' type='image/png' href='/_builder/favicon.png'> <link rel='icon' type='image/png' href='/_builder/favicon.png'>
<link rel='stylesheet' href='/_builder/global.css'> <link rel='stylesheet' href='/_builder/global.css'>
<link rel='stylesheet' href='/_builder/codemirror.css'>
<link rel='stylesheet' href='/_builder/budibase.css'> <link rel='stylesheet' href='/_builder/budibase.css'>
<link rel='stylesheet' href='/_builder/monokai.css'>
<link rel='stylesheet' href='/_builder/bundle.css'> <link rel='stylesheet' href='/_builder/bundle.css'>
<link rel='stylesheet' href='/_builder/bbui.css'> <link rel='stylesheet' href='/_builder/bbui.css'>
<link rel='stylesheet' href='/_builder/fonts.css'> <link rel='stylesheet' href='/_builder/fonts.css'>
<link rel='stylesheet' href="/_builder/uikit.min.css"> <link rel='stylesheet' href="/_builder/uikit.min.css">
</head> </head>
<body id="app"> <body id="app">
<script src='/_builder/bundle.js'></script> <script src='/_builder/bundle.js'></script>
</body> </body>
</html> </html>

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

@ -1,10 +1,8 @@
<script> <script>
import { getContext } from "svelte" import { getContext } from "svelte"
import { store, backendUiStore } from "builderStore" import { store, backendUiStore } from "builderStore"
import * as api from "components/database/ModelDataTable/api" import * as api from "components/database/DataTable/api"
import ModelNavigator from "components/nav/ModelNavigator/ModelNavigator.svelte" import ModelNavigator from "components/nav/ModelNavigator/ModelNavigator.svelte"
import { ModelSetupNav } from "components/nav/ModelSetupNav"
</script> </script>
<div class="root"> <div class="root">
@ -24,12 +22,10 @@
background: var(--grey-1); background: var(--grey-1);
line-height: 1; line-height: 1;
} }
.content { .content {
flex: 1 1 auto; flex: 1 1 auto;
margin: 20px 40px; margin: 20px 40px;
} }
.nav { .nav {
flex: 0 1 auto; flex: 0 1 auto;
width: 300px; width: 300px;

6
packages/builder/src/pages/[application]/backend/model/[selectedModel]/index.svelte

@ -1,11 +1,11 @@
<script> <script>
import { getContext } from "svelte" import { getContext } from "svelte"
import { Button } from "@budibase/bbui" import { Button } from "@budibase/bbui"
import ModelDataTable from "components/database/ModelDataTable" import ModelDataTable from "components/database/DataTable"
import { backendUiStore } from "builderStore" import { backendUiStore } from "builderStore"
import ActionButton from "components/common/ActionButton.svelte" import ActionButton from "components/common/ActionButton.svelte"
import * as api from "components/database/ModelDataTable/api" import * as api from "components/database/DataTable/api"
import { CreateEditRecordModal } from "components/database/ModelDataTable/modals" import { CreateEditRecordModal } from "components/database/DataTable/modals"
const { open, close } = getContext("simple-modal") const { open, close } = getContext("simple-modal")

22
packages/builder/src/pages/[application]/backend/view/[selectedView]/_layout.svelte

@ -0,0 +1,22 @@
<script>
import { params } from "@sveltech/routify"
import { backendUiStore } from "builderStore"
if ($params.selectedView) {
let view
const viewName = decodeURI($params.selectedView)
for (let model of $backendUiStore.models) {
if (model.views && model.views[viewName]) {
view = model.views[viewName]
}
}
if (view) {
backendUiStore.actions.views.select({
name: viewName,
...view,
})
}
}
</script>
<slot />

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save