Browse Source

Merge branch 'master' of github.com:Budibase/budibase into feature/settings-modal

gh-pages
kevmodrome 6 years ago
parent
commit
df1c1463b3
  1. BIN
      packages/builder/assets/Inter-Black.woff
  2. BIN
      packages/builder/assets/Inter-Black.woff2
  3. BIN
      packages/builder/assets/Inter-Bold.woff
  4. BIN
      packages/builder/assets/Inter-Bold.woff2
  5. BIN
      packages/builder/assets/Inter-ExtraBold.woff
  6. BIN
      packages/builder/assets/Inter-ExtraBold.woff2
  7. BIN
      packages/builder/assets/Inter-Medium.woff
  8. BIN
      packages/builder/assets/Inter-Medium.woff2
  9. BIN
      packages/builder/assets/Inter-Regular.woff
  10. BIN
      packages/builder/assets/Inter-Regular.woff2
  11. BIN
      packages/builder/assets/Inter-SemiBold.woff
  12. BIN
      packages/builder/assets/Inter-SemiBold.woff2
  13. BIN
      packages/builder/assets/roboto-v20-latin-ext_latin-300.woff
  14. BIN
      packages/builder/assets/roboto-v20-latin-ext_latin-300.woff2
  15. BIN
      packages/builder/assets/roboto-v20-latin-ext_latin-500.woff
  16. BIN
      packages/builder/assets/roboto-v20-latin-ext_latin-500.woff2
  17. BIN
      packages/builder/assets/roboto-v20-latin-ext_latin-700.woff
  18. BIN
      packages/builder/assets/roboto-v20-latin-ext_latin-700.woff2
  19. BIN
      packages/builder/assets/roboto-v20-latin-ext_latin-900.woff
  20. BIN
      packages/builder/assets/roboto-v20-latin-ext_latin-900.woff2
  21. BIN
      packages/builder/assets/roboto-v20-latin-ext_latin-regular.woff
  22. BIN
      packages/builder/assets/roboto-v20-latin-ext_latin-regular.woff2
  23. 22
      packages/builder/cypress/integration/screens.spec.js
  24. 20
      packages/builder/cypress/support/commands.js
  25. 5
      packages/builder/package.json
  26. 4
      packages/builder/rollup.config.js
  27. 22
      packages/builder/src/App.svelte
  28. 0
      packages/builder/src/actions/index.js
  29. 29
      packages/builder/src/budibase.css
  30. 77
      packages/builder/src/builderStore/store/backend.js
  31. 40
      packages/builder/src/builderStore/store/index.js
  32. 23
      packages/builder/src/builderStore/store/notifications.js
  33. 5
      packages/builder/src/builderStore/storeUtils.js
  34. 2
      packages/builder/src/components/common/ActionButton.svelte
  35. 77
      packages/builder/src/components/common/AppNotification.svelte
  36. 56
      packages/builder/src/components/common/Block.svelte
  37. 44
      packages/builder/src/components/common/Button.svelte
  38. 2
      packages/builder/src/components/common/Checkbox.svelte
  39. 2
      packages/builder/src/components/common/CodeArea.svelte
  40. 2
      packages/builder/src/components/common/ConfirmDialog.svelte
  41. 2
      packages/builder/src/components/common/IconButton.svelte
  42. 19
      packages/builder/src/components/common/Input.svelte
  43. 101
      packages/builder/src/components/common/LinkedRecordSelector.svelte
  44. 65
      packages/builder/src/components/common/Notification/NotificationDisplay.svelte
  45. 30
      packages/builder/src/components/common/NumberBox.svelte
  46. 4
      packages/builder/src/components/common/PlusButton.svelte
  47. 2
      packages/builder/src/components/common/Select.svelte
  48. 7
      packages/builder/src/components/common/Switcher.svelte
  49. 26
      packages/builder/src/components/common/ValuesList.svelte
  50. 124
      packages/builder/src/components/database/ModelDataTable/LinkedRecord.svelte
  51. 75
      packages/builder/src/components/database/ModelDataTable/ModelDataTable.svelte
  52. 7
      packages/builder/src/components/database/ModelDataTable/TablePagination.svelte
  53. 2
      packages/builder/src/components/database/ModelDataTable/api.js
  54. 2
      packages/builder/src/components/database/ModelDataTable/modals/CreateDatabase.svelte
  55. 4
      packages/builder/src/components/database/ModelDataTable/modals/CreateEditModel/CreateEditModel.svelte
  56. 106
      packages/builder/src/components/database/ModelDataTable/modals/CreateEditModel/FieldView.svelte
  57. 84
      packages/builder/src/components/database/ModelDataTable/modals/CreateEditRecord.svelte
  58. 4
      packages/builder/src/components/database/ModelDataTable/modals/CreateEditView.svelte
  59. 4
      packages/builder/src/components/database/ModelDataTable/modals/CreateUser.svelte
  60. 7
      packages/builder/src/components/database/ModelDataTable/modals/DeleteRecord.svelte
  61. 13
      packages/builder/src/components/database/ModelDataTable/modals/RecordFieldControl.svelte
  62. 1
      packages/builder/src/components/database/ModelDataTable/modals/index.js
  63. 87
      packages/builder/src/components/nav/BackendNav.svelte
  64. 3
      packages/builder/src/components/nav/DatabasesList.svelte
  65. 14
      packages/builder/src/components/nav/HierarchyRow.svelte
  66. 80
      packages/builder/src/components/nav/ModelNavigator/BlockNavigator.svelte
  67. 112
      packages/builder/src/components/nav/ModelNavigator/EmptyModel.svelte
  68. 47
      packages/builder/src/components/nav/ModelNavigator/ListItem.svelte
  69. 107
      packages/builder/src/components/nav/ModelNavigator/ModelNavigator.svelte
  70. 132
      packages/builder/src/components/nav/ModelSetupNav/ModelFieldEditor.svelte
  71. 127
      packages/builder/src/components/nav/ModelSetupNav/ModelSetupNav.svelte
  72. 1
      packages/builder/src/components/nav/ModelSetupNav/index.js
  73. 143
      packages/builder/src/components/nav/SchemaManagementDrawer.svelte
  74. 1
      packages/builder/src/components/nav/UsersList.svelte
  75. 12
      packages/builder/src/components/start/AppCard.svelte
  76. 10
      packages/builder/src/components/start/CreateAppModal.svelte
  77. 4
      packages/builder/src/components/userInterface/CategoryTab.svelte
  78. 26
      packages/builder/src/components/userInterface/Colorpicker/ButtonGroup.svelte
  79. 25
      packages/builder/src/components/userInterface/Colorpicker/CheckedBackground.svelte
  80. 276
      packages/builder/src/components/userInterface/Colorpicker/Colorpicker.svelte
  81. 310
      packages/builder/src/components/userInterface/Colorpicker/Colorpreview.svelte
  82. 8
      packages/builder/src/components/userInterface/Colorpicker/FlatButton.svelte
  83. 13
      packages/builder/src/components/userInterface/Colorpicker/Input.svelte
  84. 60
      packages/builder/src/components/userInterface/Colorpicker/Palette.svelte
  85. 78
      packages/builder/src/components/userInterface/Colorpicker/Slider.svelte
  86. 54
      packages/builder/src/components/userInterface/Colorpicker/Swatch.svelte
  87. 8
      packages/builder/src/components/userInterface/ComponentDropdownMenu.svelte
  88. 38
      packages/builder/src/components/userInterface/ComponentPropertiesPanel.svelte
  89. 6
      packages/builder/src/components/userInterface/ComponentSearch.svelte
  90. 37
      packages/builder/src/components/userInterface/ComponentsHierarchy.svelte
  91. 11
      packages/builder/src/components/userInterface/ComponentsHierarchyChildren.svelte
  92. 6
      packages/builder/src/components/userInterface/ComponentsPaneSwitcher.svelte
  93. 39
      packages/builder/src/components/userInterface/DesignView.svelte
  94. 2
      packages/builder/src/components/userInterface/EditComponentProps.svelte
  95. 2
      packages/builder/src/components/userInterface/EventsEditor/EventEditorModal.svelte
  96. 8
      packages/builder/src/components/userInterface/EventsEditor/EventsEditor.svelte
  97. 2
      packages/builder/src/components/userInterface/EventsEditor/StateBindingCascader.svelte
  98. 23
      packages/builder/src/components/userInterface/FlatButton.svelte
  99. 1
      packages/builder/src/components/userInterface/FlatButtonGroup.svelte
  100. 20
      packages/builder/src/components/userInterface/FrontendNavigatePane.svelte

BIN
packages/builder/assets/Inter-Black.woff

Binary file not shown.

BIN
packages/builder/assets/Inter-Black.woff2

Binary file not shown.

BIN
packages/builder/assets/Inter-Bold.woff

Binary file not shown.

BIN
packages/builder/assets/Inter-Bold.woff2

Binary file not shown.

BIN
packages/builder/assets/Inter-ExtraBold.woff

Binary file not shown.

BIN
packages/builder/assets/Inter-ExtraBold.woff2

Binary file not shown.

BIN
packages/builder/assets/Inter-Medium.woff

Binary file not shown.

BIN
packages/builder/assets/Inter-Medium.woff2

Binary file not shown.

BIN
packages/builder/assets/Inter-Regular.woff

Binary file not shown.

BIN
packages/builder/assets/Inter-Regular.woff2

Binary file not shown.

BIN
packages/builder/assets/Inter-SemiBold.woff

Binary file not shown.

BIN
packages/builder/assets/Inter-SemiBold.woff2

Binary file not shown.

BIN
packages/builder/assets/roboto-v20-latin-ext_latin-300.woff

Binary file not shown.

BIN
packages/builder/assets/roboto-v20-latin-ext_latin-300.woff2

Binary file not shown.

BIN
packages/builder/assets/roboto-v20-latin-ext_latin-500.woff

Binary file not shown.

BIN
packages/builder/assets/roboto-v20-latin-ext_latin-500.woff2

Binary file not shown.

BIN
packages/builder/assets/roboto-v20-latin-ext_latin-700.woff

Binary file not shown.

BIN
packages/builder/assets/roboto-v20-latin-ext_latin-700.woff2

Binary file not shown.

BIN
packages/builder/assets/roboto-v20-latin-ext_latin-900.woff

Binary file not shown.

BIN
packages/builder/assets/roboto-v20-latin-ext_latin-900.woff2

Binary file not shown.

BIN
packages/builder/assets/roboto-v20-latin-ext_latin-regular.woff

Binary file not shown.

BIN
packages/builder/assets/roboto-v20-latin-ext_latin-regular.woff2

Binary file not shown.

22
packages/builder/cypress/integration/screens.spec.js

@ -0,0 +1,22 @@
context('Screen Tests', () => {
before(() => {
cy.visit('localhost:4001/_builder')
cy.createApp('Conor Cy App', 'Model App Description')
cy.navigateToFrontend()
})
it('Should successful create a screen', () => {
cy.createScreen("test Screen")
})
it('Should rename a screen', () => {
cy.get(".components-pane").within(() => {
cy.contains("Settings").click()
cy.get("input[name=_instanceName]").clear().type("About Us").blur()
})
cy.get('.nav-items-container').within(() => {
cy.contains("About Us").should('exist')
})
})
})

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

@ -103,3 +103,23 @@ Cypress.Commands.add("addButtonComponent", () => {
cy.get("[data-cy=Button]").click()
})
Cypress.Commands.add("navigateToFrontend", () => {
cy.get(".close", { timeout: 10000 }).click()
cy.contains("frontend").click()
cy.get(".close", { timeout: 10000 }).click()
})
Cypress.Commands.add("createScreen", (screenName, route) => {
cy.get(".newscreen").click()
cy.get(".uk-input:first").type(screenName)
if (route) {
cy.get(".uk-input:last").type(route)
}
cy.get(".uk-modal-footer").within(() => {
cy.contains("Create Screen").click()
})
cy.get(".nav-items-container").within(() => {
cy.contains(screenName).should("exist")
})
})

5
packages/builder/package.json

@ -50,8 +50,7 @@
]
},
"dependencies": {
"@beyonk/svelte-notifications": "^2.0.3",
"@budibase/bbui": "^1.12.0",
"@budibase/bbui": "^1.13.0",
"@budibase/client": "^0.0.32",
"@nx-js/compiler-util": "^2.0.0",
"codemirror": "^5.51.0",
@ -101,4 +100,4 @@
"svelte": "3.23.x"
},
"gitHead": "115189f72a850bfb52b65ec61d932531bf327072"
}
}

4
packages/builder/rollup.config.js

@ -153,6 +153,10 @@ export default {
find: "builderStore",
replacement: path.resolve(projectRootDir, "src/builderStore"),
},
{
find: "constants",
replacement: path.resolve(projectRootDir, "src/constants"),
},
],
customResolver,
}),

22
packages/builder/src/App.svelte

@ -4,30 +4,10 @@
import { Router, basepath } from "@sveltech/routify"
import { routes } from "../routify/routes"
import { store, initialise } from "builderStore"
import AppNotification, {
showAppNotification,
} from "components/common/AppNotification.svelte"
import { NotificationDisplay } from "@beyonk/svelte-notifications"
function showErrorBanner() {
showAppNotification({
status: "danger",
message:
"Whoops! Looks like we're having trouble. Please refresh the page.",
})
}
onMount(async () => {
window.addEventListener("error", showErrorBanner)
window.addEventListener("unhandledrejection", showErrorBanner)
})
import NotificationDisplay from "components/common/Notification/NotificationDisplay.svelte"
$basepath = "/_builder"
</script>
<AppNotification />
<!-- svelte-notifications -->
<NotificationDisplay />
<Router {routes} />

0
packages/builder/src/actions/index.js

29
packages/builder/src/budibase.css

@ -57,13 +57,14 @@
.budibase__nav-item {
cursor: pointer;
padding: 0 4px 0 2px;
height: 35px;
margin: 5px 0px 4px 0px;
border-radius: 0 5px 5px 0;
height: 36px;
margin: 0px 0px 0px 0px;
border-radius: 5px;
display: flex;
align-items: center;
font-size: 14px;
transition: 0.2s;
border-top: var(--grey-1) .5px solid;
}
.budibase__nav-item.selected {
@ -72,18 +73,22 @@
}
.budibase__nav-item:hover {
background: var(--grey-light);
background: var(--grey-1);
}
.budibase__input {
height: 35px;
width: 220px;
border-radius: 3px;
border: 1px solid var(--grey-dark);
height: 36px;
background-color: var(--grey-2);
border: none;
border-radius: 5px;
width: 100%;
text-align: left;
color: var(--ink);
font-size: 14px;
padding-left: 12px;
padding-left: 8px;
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
}
.uk-text-right {
@ -100,7 +105,7 @@
}
.budibase__table {
border: 1px solid var(--grey-dark);
border: 1px solid var(--grey-4);
background: #fff;
border-radius: 2px;
}
@ -116,12 +121,12 @@
}
.budibase__table tr {
border-bottom: 1px solid var(--grey-light);
border-bottom: 1px solid var(--grey-1);
}
.button--toggled {
background: var(--blue-light);
color: var(--ink-light);
color: var(--grey-7);
width: 40px;
height: 40px;
display: flex;

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

@ -1,22 +1,20 @@
import { writable } from "svelte/store"
import { cloneDeep } from "lodash/fp"
import { uuid } from "builderStore/uuid"
import api from "../api"
import { getContext } from "svelte"
/** TODO: DEMO SOLUTION
* this section should not be here, it is a quick fix for a demo
* when we reorg the backend UI, this should disappear
* **/
import { CreateEditModelModal } from "components/database/ModelDataTable/modals"
/** DEMO SOLUTION END **/
export const getBackendUiStore = () => {
const INITIAL_BACKEND_UI_STATE = {
breadcrumbs: [],
models: [],
views: [],
users: [],
selectedDatabase: {},
selectedModel: {},
draftModel: {},
tabs: {
SETUP_PANEL: "SETUP",
NAVIGATION_PANEL: "NAVIGATE",
},
}
const store = writable(INITIAL_BACKEND_UI_STATE)
@ -31,26 +29,12 @@ export const getBackendUiStore = () => {
store.update(state => {
state.selectedDatabase = db
if (models && models.length > 0) {
state.selectedModel = models[0]
state.selectedView = `all_${models[0]._id}`
store.actions.models.select(models[0])
}
state.breadcrumbs = [db.name]
state.models = models
state.views = views
return state
})
/** TODO: DEMO SOLUTION**/
if (!models || models.length === 0) {
const { open, close } = getContext("simple-modal")
open(
CreateEditModelModal,
{
onClosed: close,
},
{ styleContent: { padding: "0" } }
)
}
/** DEMO SOLUTION END **/
},
},
records: {
@ -59,11 +43,6 @@ export const getBackendUiStore = () => {
state.selectedView = state.selectedView
return state
}),
view: record =>
store.update(state => {
state.breadcrumbs = [state.selectedDatabase.name, record._id]
return state
}),
select: record =>
store.update(state => {
state.selectedRecord = record
@ -71,14 +50,48 @@ export const getBackendUiStore = () => {
}),
},
models: {
create: model =>
fetch: async () => {
const modelsResponse = await api.get(`/api/models`)
const models = await modelsResponse.json()
store.update(state => {
state.models = models
return state
})
},
select: model =>
store.update(state => {
state.models.push(model)
state.models = state.models
state.selectedModel = model
state.draftModel = cloneDeep(model)
state.selectedField = ""
state.selectedView = `all_${model._id}`
state.tabs.SETUP_PANEL = "SETUP"
return state
}),
save: async ({ model }) => {
const updatedModel = cloneDeep(model)
const SAVE_MODEL_URL = `/api/models`
await api.post(SAVE_MODEL_URL, updatedModel)
await store.actions.models.fetch()
},
addField: field => {
store.update(state => {
if (!state.draftModel.schema) {
state.draftModel.schema = {}
}
const id = uuid()
state.draftModel.schema = {
...state.draftModel.schema,
[id]: field,
}
state.selectedField = id
state.tabs.NAVIGATION_PANEL = "NAVIGATE"
return state
})
},
},
views: {
select: view =>

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

@ -1,4 +1,6 @@
import { values } from "lodash/fp"
import { get_capitalised_name } from "../../helpers"
import { backendUiStore } from "builderStore"
import * as backendStoreActions from "./backend"
import { writable, get } from "svelte/store"
import api from "../api"
@ -22,7 +24,6 @@ import {
saveCurrentPreviewItem as _saveCurrentPreviewItem,
saveScreenApi as _saveScreenApi,
regenerateCssForCurrentScreen,
renameCurrentScreen,
} from "../storeUtils"
export const getStore = () => {
@ -68,6 +69,7 @@ export const getStore = () => {
store.getPathToComponent = getPathToComponent(store)
store.addTemplatedComponent = addTemplatedComponent(store)
store.setMetadataProp = setMetadataProp(store)
store.editPageOrScreen = editPageOrScreen(store)
return store
}
@ -152,14 +154,13 @@ const createScreen = store => (screenName, route, layoutComponentName) => {
const rootComponent = state.components[layoutComponentName]
const newScreen = {
name: screenName || "",
description: "",
url: "",
_css: "",
props: createProps(rootComponent).props,
}
newScreen.route = route
newScreen.props._instanceName = screenName || ""
state.currentPreviewItem = newScreen
state.currentComponentInfo = newScreen.props
state.currentFrontEndType = "screen"
@ -172,7 +173,7 @@ const createScreen = store => (screenName, route, layoutComponentName) => {
const setCurrentScreen = store => screenName => {
store.update(s => {
const screen = getExactComponent(s.screens, screenName)
const screen = getExactComponent(s.screens, screenName, true)
s.currentPreviewItem = screen
s.currentFrontEndType = "screen"
s.currentView = "detail"
@ -243,6 +244,7 @@ const setCurrentPage = store => pageName => {
const currentPage = state.pages[pageName]
state.currentFrontEndType = "page"
state.currentView = "detail"
state.currentPageName = pageName
state.screens = Array.isArray(current_screens)
? current_screens
@ -294,10 +296,15 @@ const addChildComponent = store => (componentToAdd, presetName) => {
const presetProps = presetName ? component.presets[presetName] : {}
const instanceId = get(backendUiStore).selectedDatabase._id
const instanceName = get_capitalised_name(componentToAdd)
const newComponent = createProps(
component,
{
...presetProps,
_instanceId: instanceId,
_instanceName: instanceName,
},
state
)
@ -346,24 +353,23 @@ const selectComponent = store => component => {
const setComponentProp = store => (name, value) => {
store.update(state => {
const current_component = state.currentComponentInfo
state.currentComponentInfo[name] = value
_saveCurrentPreviewItem(state)
let current_component = state.currentComponentInfo
current_component[name] = value
state.currentComponentInfo = current_component
_saveCurrentPreviewItem(state)
return state
})
}
const setPageOrScreenProp = store => (name, value) => {
store.update(state => {
if (name === "name" && state.currentFrontEndType === "screen") {
state = renameCurrentScreen(value, state)
if (name === "_instanceName" && state.currentFrontEndType === "screen") {
state.currentPreviewItem.props[name] = value
} else {
state.currentPreviewItem[name] = value
_saveCurrentPreviewItem(state)
}
_saveCurrentPreviewItem(state)
return state
})
}
@ -413,6 +419,18 @@ const setScreenType = store => type => {
state.currentComponentInfo = pageOrScreen ? pageOrScreen.props : null
state.currentPreviewItem = pageOrScreen
state.currentView = "detail"
return state
})
}
const editPageOrScreen = store => (key, value, setOnComponent = false) => {
store.update(state => {
setOnComponent
? (state.currentPreviewItem.props[key] = value)
: (state.currentPreviewItem[key] = value)
_saveCurrentPreviewItem(state)
return state
})
}

23
packages/builder/src/builderStore/store/notifications.js

@ -0,0 +1,23 @@
import { writable } from "svelte/store"
import { generate } from "shortid"
export const notificationStore = writable({
notifications: [],
})
export function send(message, type = "default") {
notificationStore.update(state => {
state.notifications = [
...state.notifications,
{ id: generate(), type, message },
]
return state
})
}
export const notifier = {
danger: msg => send(msg, "danger"),
warning: msg => send(msg, "warning"),
info: msg => send(msg, "info"),
success: msg => send(msg, "success"),
}

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

@ -46,8 +46,9 @@ export const saveScreenApi = (screen, s) => {
}
export const renameCurrentScreen = (newname, state) => {
const oldname = state.currentPreviewItem.name
state.currentPreviewItem.name = newname
const oldname = state.currentPreviewItem.props._instanceName
state.currentPreviewItem.props._instanceName = newname
api.patch(
`/_builder/api/${state.appId}/pages/${state.currentPageName}/screen`,
{

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

@ -41,7 +41,7 @@
.secondary {
color: var(--ink);
border: solid 1px var(--grey-dark);
border: solid 1px var(--grey-4);
background: white;
}

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

@ -1,77 +0,0 @@
<script context="module">
import UIKit from "uikit"
export function showAppNotification({ message, status }) {
UIKit.notification({
message: `
<div class="message-container">
<div class="information-icon">🤯</div>
<span class="notification-message">
${message}
</span>
<button class="hoverable refresh-page-button" onclick="window.location.reload()">Refresh Page</button>
</div>
`,
status,
timeout: 100000,
})
}
</script>
<style>
:global(.information-icon) {
font-size: 24px;
margin-right: 8px;
}
:global(.uk-nofi) {
display: grid;
grid-template-columns: 40px 1fr auto;
grid-gap: 5px;
align-items: center;
}
:global(.message-container) {
display: flex;
align-items: center;
justify-content: center;
}
:global(.uk-notification) {
width: 50% !important;
left: 0 !important;
right: 0 !important;
margin-right: auto !important;
margin-left: auto !important;
border-radius: 10px;
}
:global(.uk-notification-message) {
border-radius: 5px;
}
:global(.uk-notification-message:hover .uk-notification-close) {
visibility: hidden;
}
:global(.uk-notification-message-danger) {
background: var(--ink-light) !important;
color: #fff !important;
font-family: Roboto;
font-size: 16px !important;
}
:global(.refresh-page-button) {
font-size: 14px;
border-radius: 3px;
border: none;
padding: 8px 16px;
color: var(--ink);
background: #ffffff;
margin-left: 20px;
}
:global(.refresh-page-button):hover {
background: var(--grey-light);
}
</style>

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

@ -0,0 +1,56 @@
<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: 30px;
}
span {
font-size: 14px;
text-align: center;
}
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>

44
packages/builder/src/components/common/Button.svelte

@ -42,14 +42,14 @@
/* ---- PRIMARY ----*/
.primary {
background-color: var(--primary100);
border-color: var(--primary100);
background-color: var(--blue);
border-color: var(--blue);
color: var(--white);
}
.primary:hover {
background-color: var(--primary75);
border-color: var(--primary75);
background-color: var(--blue);
border-color: var(--blue);
}
.primary:active {
@ -59,8 +59,8 @@
.primary-outline {
background-color: var(--white);
border-color: var(--primary100);
color: var(--primary100);
border-color: var(--blue);
color: var(--blue);
}
.primary-outline:hover {
@ -74,8 +74,8 @@
/* ---- secondary ----*/
.secondary {
background-color: var(--secondary100);
border-color: var(--secondary100);
background-color: var(--ink);
border-color: var(--ink);
color: var(--white);
}
@ -91,8 +91,8 @@
.secondary-outline {
background-color: var(--white);
border-color: var(--secondary100);
color: var(--secondary100);
border-color: var(--ink);
color: var(--ink);
}
.secondary-outline:hover {
@ -136,32 +136,36 @@
/* ---- deletion ----*/
.deletion {
background-color: var(--deletion100);
border-color: var(--deletion100);
background-color: var(--red);
border-color: var(--red);
color: var(--white);
}
.deletion:hover {
background-color: var(--deletion75);
border-color: var(--deletion75);
background-color: var(--red-light);
border-color: var(--red);
color: var(--red);
}
.deletion:pressed {
background-color: var(--deletiondark);
border-color: var(--deletiondark);
background-color: var(--red-dark);
border-color: var(--red-dark);
color: var(--white);
}
.deletion-outline {
background-color: var(--white);
border-color: var(--deletion100);
color: var(--deletion100);
border-color: var(--red);
color: var(--red);
}
.deletion-outline:hover {
background-color: var(--deletion10);
background-color: var(--red-light);
color: var(--red);
}
.deletion-outline:pressed {
background-color: var(--deletion25);
background-color: var(--red-dark);
color: var(--white);
}
</style>

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

@ -3,8 +3,8 @@
export let label = ""
</script>
<input class="uk-checkbox" type="checkbox" bind:checked on:change />
{label}
<input class="uk-checkbox" type="checkbox" bind:checked on:change />
<style>
input {

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

@ -11,7 +11,7 @@
padding: 10px;
margin-top: 5px;
margin-bottom: 10px;
background: var(--secondary80);
background: var(--grey-7);
color: var(--white);
font-family: "Courier New", Courier, monospace;
height: 200px;

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

@ -54,7 +54,7 @@
<style>
.uk-modal-footer {
background: var(--lightslate);
background: var(--grey-1);
}
.uk-modal-dialog {

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

@ -4,7 +4,7 @@
export let size = 18
export let icon = ""
export let style = ""
export let color = "var(--secondary100)"
export let color = "var(--ink)"
export let hoverColor = "var(--secondary75)"
export let attributes = {}

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

@ -37,19 +37,18 @@
<style>
input {
/* width: 32px; */
height: 32px;
font-size: 12px;
font-weight: 700;
margin: 0px 0px 0px 1px;
height: 36px;
font-size: 14px;
font-weight: 400;
margin: 0px 0px 0px 2px;
color: var(--ink);
opacity: 0.7;
padding: 0px 4px;
line-height: 1.3;
/* padding: 12px; */
padding: 0px 8px;
font-family: inter;
width: 164px;
box-sizing: border-box;
border: 1px solid var(--grey);
border-radius: 2px;
background-color: var(--grey-2);
border-radius: 4px;
border: 1px solid var(--grey-2);
outline: none;
}

101
packages/builder/src/components/common/LinkedRecordSelector.svelte

@ -0,0 +1,101 @@
<script>
import { onMount } from "svelte"
import { backendUiStore } from "builderStore"
import api from "builderStore/api"
export let modelId
export let linkName
export let linked = []
let records = []
let model = {}
let linkedRecords = new Set(linked)
$: linked = [...linkedRecords]
$: FIELDS_TO_HIDE = [$backendUiStore.selectedModel._id]
$: schema = $backendUiStore.selectedModel.schema
async function fetchRecords() {
const FETCH_RECORDS_URL = `/api/${modelId}/records`
const response = await api.get(FETCH_RECORDS_URL)
const modelResponse = await api.get(`/api/models/${modelId}`)
model = await modelResponse.json()
records = await response.json()
}
function linkRecord(id) {
if (linkedRecords.has(id)) {
linkedRecords.delete(id)
} else {
linkedRecords.add(id)
}
linkedRecords = linkedRecords
}
onMount(() => {
fetchRecords()
})
</script>
<section>
<header>
<h3>{linkName}</h3>
</header>
{#each records as record}
<div class="linked-record" on:click={() => linkRecord(record._id)}>
<div class="fields" class:selected={linkedRecords.has(record._id)}>
{#each Object.keys(model.schema).filter(key => !FIELDS_TO_HIDE.includes(key)) as key}
<div class="field">
<span>{model.schema[key].name}</span>
<p>{record[key]}</p>
</div>
{/each}
</div>
</div>
{/each}
</section>
<style>
.fields.selected {
background: var(--light-grey);
}
h3 {
font-size: 18px;
font-weight: 600;
margin-bottom: 12px;
color: var(--ink);
}
.fields {
padding: 15px;
display: grid;
grid-template-columns: 1fr 1fr 1fr;
grid-gap: 20px;
background: var(--white);
border: 1px solid var(--grey);
border-radius: 5px;
transition: 0.5s all;
margin-bottom: 8px;
}
.fields:hover {
cursor: pointer;
}
.field span {
color: var(--ink-lighter);
font-size: 12px;
}
.field p {
color: var(--ink);
font-size: 14px;
word-break: break-word;
font-weight: 500;
margin-top: 4px;
}
</style>

65
packages/builder/src/components/common/Notification/NotificationDisplay.svelte

@ -0,0 +1,65 @@
<script>
import { notificationStore } from "builderStore/store/notifications"
import { onMount, onDestroy } from "svelte"
import { fade } from "svelte/transition"
export let themes = {
danger: "#E26D69",
success: "#84C991",
warning: "#f0ad4e",
info: "#5bc0de",
default: "#aaaaaa",
}
export let timeout = 3000
$: if ($notificationStore.notifications.length) {
setTimeout(() => {
notificationStore.update(state => {
state.notifications.shift()
state.notifications = state.notifications
return state
})
}, timeout)
}
</script>
<ul class="notifications">
{#each $notificationStore.notifications as notification (notification.id)}
<li
class="toast"
style="background: {themes[notification.type]};"
transition:fade>
<div class="content">{notification.message}</div>
{#if notification.icon}
<i class={notification.icon} />
{/if}
</li>
{/each}
</ul>
<style>
.notifications {
width: 40vw;
list-style: none;
position: fixed;
top: 0;
left: 0;
right: 0;
margin-left: auto;
margin-right: auto;
padding: 0;
z-index: 9999;
}
.toast {
margin-bottom: 10px;
}
.content {
padding: 10px;
display: block;
color: var(--white);
font-weight: 500;
}
</style>

30
packages/builder/src/components/common/NumberBox.svelte

@ -13,13 +13,25 @@
let numberText = value === null || value === undefined ? "" : value.toString()
</script>
<div class="uk-margin">
<label class="uk-form-label">{label}</label>
<div class="uk-form-controls">
<input
class="budibase__input"
type="number"
{value}
on:change={inputChanged} />
</div>
<div class="numberbox">
<label>{label}</label>
<input
class="budibase__input"
type="number"
{value}
on:change={inputChanged} />
</div>
<style>
.numberbox {
display: grid;
align-items: center;
margin-bottom: 16px;
}
label {
font-size: 14px;
font-weight: 500;
margin-bottom: 8px;
}
</style>

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

@ -17,7 +17,7 @@
align-items: center;
font-size: 1.2rem;
font-weight: 700;
color: var(--secondary100);
font-weight: 600;
color: var(--ink);
}
</style>

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

@ -21,7 +21,7 @@
.select-container {
font-size: 14px;
position: relative;
border: var(--grey-dark) 1px solid;
border: var(--grey-4) 1px solid;
}
.adjusted {

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

@ -43,7 +43,7 @@
display: flex;
flex-direction: column;
padding: 20px 20px;
border-left: solid 1px var(--grey);
border-left: solid 1px var(--grey-2);
box-sizing: border-box;
}
@ -60,10 +60,11 @@
padding: 0;
cursor: pointer;
font-size: 18px;
font-weight: 700;
color: var(--ink-lighter);
font-weight: 600;
color: var(--grey-5);
margin-right: 20px;
background: none;
outline: none;
}
.switcher > .selected {

26
packages/builder/src/components/common/ValuesList.svelte

@ -15,16 +15,34 @@
$: valuesText = join("\n")(values)
</script>
<div class="uk-margin">
<label class="uk-form-label">{label}</label>
<div class="margin">
<label class="label">{label}</label>
<div class="uk-form-controls">
<textarea value={valuesText} on:change={inputChanged} />
</div>
</div>
<style>
.margin {
margin-bottom: 16px;
display: grid;
}
.label {
font-size: 14px;
font-weight: 500;
margin-bottom: 8px;
}
textarea {
width: 300px;
height: 100px;
font-size: 14px;
height: 200px;
width: 100%;
border-radius: 5px;
border: none;
cursor: text;
background: var(--grey-2);
padding: 12px;
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
}
</style>

124
packages/builder/src/components/database/ModelDataTable/LinkedRecord.svelte

@ -0,0 +1,124 @@
<script>
import { onMount } from "svelte"
import { fade } from "svelte/transition"
import { backendUiStore } from "builderStore"
import api from "builderStore/api"
export let ids = []
export let field
let records = []
let open = false
let model
$: FIELDS_TO_HIDE = [$backendUiStore.selectedModel._id, field.modelId]
async function fetchRecords() {
const response = await api.post("/api/records/search", {
keys: ids,
})
const modelResponse = await api.get(`/api/models/${field.modelId}`)
records = await response.json()
model = await modelResponse.json()
}
$: ids && fetchRecords()
function toggleOpen() {
open = !open
}
onMount(() => {
fetchRecords()
})
</script>
<section>
<a on:click={toggleOpen}>{records.length}</a>
{#if open}
<div class="popover" transition:fade>
<header>
<h3>{field.name}</h3>
<i class="ri-close-circle-fill" on:click={toggleOpen} />
</header>
{#each records as record}
<div class="linked-record">
<div class="fields">
{#each Object.keys(model.schema).filter(key => !FIELDS_TO_HIDE.includes(key)) as key}
<div class="field">
<span>{model.schema[key].name}</span>
<p>{record[key]}</p>
</div>
{/each}
</div>
</div>
{/each}
</div>
{/if}
</section>
<style>
section {
display: relative;
}
header {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
i {
font-size: 24px;
color: var(--ink-lighter);
}
i:hover {
cursor: pointer;
}
a {
font-size: 14px;
}
.popover {
width: 500px;
position: absolute;
right: 15%;
padding: 20px;
background: var(--light-grey);
border: 1px solid var(--grey);
}
h3 {
font-size: 20px;
font-weight: bold;
margin: 0;
}
.fields {
padding: 15px;
display: grid;
grid-template-columns: 1fr 1fr 1fr;
grid-gap: 20px;
background: var(--white);
border: 1px solid var(--grey);
border-radius: 5px;
margin-bottom: 8px;
}
.field span {
color: var(--ink-lighter);
font-size: 12px;
}
.field p {
color: var(--ink);
font-size: 14px;
word-break: break-word;
font-weight: 500;
margin-top: 4px;
}
</style>

75
packages/builder/src/components/database/ModelDataTable/ModelDataTable.svelte

@ -1,20 +1,10 @@
<script>
import { onMount, getContext } from "svelte"
import { store, backendUiStore } from "builderStore"
import {
tap,
get,
find,
last,
compose,
flatten,
map,
remove,
keys,
takeRight,
} from "lodash/fp"
import { Button } from "@budibase/bbui"
import Select from "components/common/Select.svelte"
import ActionButton from "components/common/ActionButton.svelte"
import LinkedRecord from "./LinkedRecord.svelte"
import TablePagination from "./TablePagination.svelte"
import { DeleteRecordModal, CreateEditRecordModal } from "./modals"
import * as api from "./api"
@ -52,25 +42,39 @@
let headers = []
let views = []
let currentPage = 0
let search
$: {
if ($backendUiStore.selectedView) {
api
.fetchDataForView($backendUiStore.selectedView)
.then(records => {
data = records || []
headers = Object.keys($backendUiStore.selectedModel.schema).filter(
key => !INTERNAL_HEADERS.includes(key)
)
})
api.fetchDataForView($backendUiStore.selectedView).then(records => {
data = records || []
})
}
}
$: paginatedData = data.slice(
currentPage * ITEMS_PER_PAGE,
currentPage * ITEMS_PER_PAGE + ITEMS_PER_PAGE
$: paginatedData = data
? data.slice(
currentPage * ITEMS_PER_PAGE,
currentPage * ITEMS_PER_PAGE + ITEMS_PER_PAGE
)
: []
$: headers = Object.keys($backendUiStore.selectedModel.schema).filter(
id => !INTERNAL_HEADERS.includes(id)
)
$: schema = $backendUiStore.selectedModel.schema
const createNewRecord = () => {
open(
CreateEditRecordModal,
{
onClosed: close,
},
{ styleContent: { padding: "0" } }
)
}
onMount(() => {
if (views.length) {
backendUiStore.actions.views.select(views[0])
@ -81,13 +85,16 @@
<section>
<div class="table-controls">
<h2 class="title">{$backendUiStore.selectedModel.name}</h2>
<Button primary on:click={createNewRecord}>
<span class="button-inner">Create New Record</span>
</Button>
</div>
<table class="uk-table">
<thead>
<tr>
<th>Edit</th>
{#each headers as header}
<th>{header}</th>
<th>{$backendUiStore.selectedModel.schema[header].name}</th>
{/each}
</tr>
</thead>
@ -121,7 +128,11 @@
</div>
</td>
{#each headers as header}
<td>{row[header]}</td>
<td>
{#if schema[header].type === 'link'}
<LinkedRecord field={schema[header]} ids={row[header]} />
{:else}{row[header]}{/if}
</td>
{/each}
</tr>
{/each}
@ -143,7 +154,7 @@
}
table {
border: 1px solid var(--grey-dark);
border: 1px solid var(--grey-4);
background: #fff;
border-radius: 3px;
border-collapse: collapse;
@ -151,7 +162,7 @@
thead {
background: var(--blue-light);
border: 1px solid var(--grey-dark);
border: 1px solid var(--grey-4);
}
thead th {
@ -160,18 +171,17 @@
font-weight: 500;
font-size: 14px;
text-rendering: optimizeLegibility;
letter-spacing: 1px;
}
tbody tr {
border-bottom: 1px solid var(--grey-dark);
border-bottom: 1px solid var(--grey-4);
transition: 0.3s background-color;
color: var(--ink);
font-size: 14px;
}
tbody tr:hover {
background: var(--grey-light);
background: var(--grey-1);
}
.table-controls {
@ -189,4 +199,9 @@
.no-data {
padding: 20px;
}
.button-inner {
display: flex;
align-items: center;
}
</style>

7
packages/builder/src/components/database/ModelDataTable/TablePagination.svelte

@ -58,20 +58,19 @@
padding: 10px;
margin: 0;
background: #fff;
border: 1px solid #ccc;
border: 1px solid var(--grey-4);
text-transform: capitalize;
border-radius: 3px;
font-family: Roboto;
min-width: 20px;
transition: 0.3s background-color;
}
.pagination__buttons button:hover {
cursor: pointer;
background-color: #fafafa;
background-color: var(--grey-1);
}
.selected {
color: var(--button-text);
color: var(--blue);
}
</style>

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

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

2
packages/builder/src/components/database/ModelDataTable/modals/CreateDatabase.svelte

@ -34,7 +34,7 @@
}
footer {
padding: 20px;
background: #fafafa;
background: var(--grey-1);
border-radius: 0.5rem;
}
</style>

4
packages/builder/src/components/database/ModelDataTable/modals/CreateEditModel/CreateEditModel.svelte

@ -130,7 +130,7 @@
}
tbody > tr:hover {
background-color: var(--grey-light);
background-color: var(--grey-1);
}
.table-controls {
@ -156,7 +156,7 @@
}
footer {
background-color: var(--grey-light);
background-color: var(--grey-1);
margin-top: 40px;
padding: 20px 40px 20px 40px;
display: flex;

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

@ -1,106 +0,0 @@
<script>
import Dropdown from "components/common/Dropdown.svelte"
import Textbox from "components/common/Textbox.svelte"
import Button from "components/common/Button.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"]
export let field = {
type: "string",
constraints: { type: "string", presence: false },
}
export let schema
export let goBack
let errors = []
let draftField = cloneDeep(field)
let type = field.type
let constraints = field.constraints
let required =
field.constraints.presence && !field.constraints.presence.allowEmpty
const save = () => {
constraints.presence = required ? { allowEmpty: false } : false
draftField.constraints = constraints
draftField.type = type
schema[field.name] = draftField
goBack()
}
$: constraints =
type === "string"
? { type: "string", length: {}, presence: false }
: type === "number"
? { type: "number", presence: false, numericality: {} }
: type === "boolean"
? { type: "boolean", presence: false }
: type === "datetime"
? { type: "date", datetime: {}, presence: false }
: type.startsWith("array")
? { type: "array", presence: false }
: { type: "string", presence: false }
</script>
<div class="root">
<ErrorsBox {errors} />
<form on:submit|preventDefault class="uk-form-stacked">
<Textbox label="Name" bind:text={field.name} />
<Dropdown label="Type" bind:selected={type} options={FIELD_TYPES} />
<Checkbox label="Required" bind:checked={required} />
{#if type === 'string'}
<NumberBox label="Max Length" bind:value={constraints.length.maximum} />
<ValuesList label="Categories" bind:values={constraints.inclusion} />
{:else if type === 'datetime'}
<DatePicker
label="Min Value"
bind:value={constraints.datetime.earliest} />
<DatePicker label="Max Value" bind:value={constraints.datetime.latest} />
{:else if type === 'number'}
<NumberBox
label="Min Value"
bind:value={constraints.numericality.greaterThanOrEqualTo} />
<NumberBox
label="Max Value"
bind:value={constraints.numericality.lessThanOrEqualTo} />
{/if}
</form>
</div>
<footer>
<div class="button">
<ActionButton secondary on:click={goBack}>Cancel</ActionButton>
</div>
<ActionButton primary on:click={save}>Save</ActionButton>
</footer>
<style>
.root {
margin: 40px;
}
footer {
padding: 20px 40px;
border-radius: 0 0 5px 5px;
bottom: 0;
left: 0;
background: var(--grey-light);
display: flex;
align-items: center;
justify-content: flex-end;
}
.button {
margin-right: 20px;
}
</style>

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

@ -1,8 +1,10 @@
<script>
import { onMount } from "svelte"
import { store, backendUiStore } from "builderStore"
import { notifier } from "builderStore/store/notifications"
import { compose, map, get, flatten } from "lodash/fp"
import ActionButton from "components/common/ActionButton.svelte"
import { Button } from "@budibase/bbui"
import LinkedRecordSelector from "components/common/LinkedRecordSelector.svelte"
import Select from "components/common/Select.svelte"
import RecordFieldControl from "./RecordFieldControl.svelte"
import * as api from "../api"
@ -59,38 +61,96 @@
backendUiStore.update(state => {
state.selectedView = state.selectedView
onClosed()
notifier.success("Record created successfully.")
return state
})
}
</script>
<div class="actions">
<h4 class="budibase__title--4">Create / Edit Record</h4>
<header>
<i class="ri-file-user-fill" />
<h4 class="budibase__title--4">Create / Edit Record</h4>
</header>
<ErrorsBox {errors} />
<form on:submit|preventDefault class="uk-form-stacked">
{#each modelSchema as [key, meta]}
<div class="uk-margin">
<RecordFieldControl
type={determineInputType(meta)}
options={determineOptions(meta)}
label={key}
bind:value={record[key]} />
{#if meta.type === 'link'}
<LinkedRecordSelector
bind:linked={record[key]}
linkName={meta.name}
modelId={meta.modelId} />
{:else}
<RecordFieldControl
type={determineInputType(meta)}
options={determineOptions(meta)}
label={meta.name}
bind:value={record[key]} />
{/if}
</div>
{/each}
</form>
</div>
<footer>
<ActionButton alert on:click={onClosed}>Cancel</ActionButton>
<ActionButton on:click={saveRecord}>Save</ActionButton>
<div class="button-margin-3">
<Button secondary on:click={onClosed}>Cancel</Button>
</div>
<div class="button-margin-4">
<Button blue on:click={saveRecord}>Save</Button>
</div>
</footer>
<style>
header {
margin-bottom: 40px;
display: grid;
grid-gap: 20px;
grid-template-columns: 40px 1fr;
align-items: center;
}
i {
height: 40px;
width: 40px;
display: flex;
align-items: center;
justify-content: center;
background: var(--blue-light);
color: var(--ink);
font-size: 20px;
border-radius: 3px;
}
h4 {
display: inline-block;
font-size: 24px;
font-weight: bold;
color: var(--ink);
margin: 0;
}
.actions {
padding: 30px;
}
footer {
padding: 20px;
background: #fafafa;
border-radius: 0.5rem;
padding: 20px 30px;
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-3 {
grid-column-start: 3;
display: grid;
}
.button-margin-4 {
grid-column-start: 4;
display: grid;
}
</style>

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

@ -96,7 +96,7 @@
.snippet-selector__heading {
margin-right: 20px;
font-size: 14px;
color: var(--ink-lighter);
color: var(--grey-5);
}
.header {
@ -116,7 +116,7 @@
.buttons {
display: flex;
justify-content: flex-end;
background-color: var(--grey-light);
background-color: var(--grey-1);
margin: 0 40px;
padding: 20px 0;
}

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

@ -69,7 +69,7 @@
.title {
font-size: 24px;
font-weight: 700;
font-weight: 600;
color: var(--ink);
margin-left: 12px;
}
@ -84,7 +84,7 @@
align-items: center;
justify-content: flex-end;
padding: 20px;
background: var(--grey-light);
background: var(--grey-1);
border-radius: 0 0 5px 5px;
}

7
packages/builder/src/components/database/ModelDataTable/modals/DeleteRecord.svelte

@ -1,11 +1,11 @@
<script>
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 record
export let onClosed
</script>
<section>
@ -25,6 +25,7 @@
alert
on:click={async () => {
await api.deleteRecord(record)
notifier.danger('Record deleted')
backendUiStore.actions.records.delete(record)
onClosed()
}}>
@ -36,13 +37,13 @@
<style>
.alert {
color: rgba(255, 0, 31, 1);
background: #fafafa;
background: var(--grey-1);
padding: 5px;
}
.modal-actions {
padding: 10px;
background: #fafafa;
background: var(--grey-1);
border-top: 1px solid #ccc;
}

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

@ -51,3 +51,16 @@
on:input={handleInput}
on:change={handleInput} />
{/if}
<style>
label {
display: block;
font-size: 18px;
font-weight: 500;
margin-bottom: 12px;
}
input {
color: var(--dark-grey);
}
</style>

1
packages/builder/src/components/database/ModelDataTable/modals/index.js

@ -1,6 +1,5 @@
export { default as DeleteRecordModal } from "./DeleteRecord.svelte"
export { default as CreateEditRecordModal } from "./CreateEditRecord.svelte"
export { default as CreateEditModelModal } from "./CreateEditModel/CreateEditModel.svelte"
export { default as CreateEditViewModal } from "./CreateEditView.svelte"
export { default as CreateDatabaseModal } from "./CreateDatabase.svelte"
export { default as CreateUserModal } from "./CreateUser.svelte"

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

@ -1,87 +0,0 @@
<script>
import { getContext } from "svelte"
import { store, backendUiStore } from "builderStore"
import HierarchyRow from "./HierarchyRow.svelte"
import DatabasesList from "./DatabasesList.svelte"
import UsersList from "./UsersList.svelte"
import NavItem from "./NavItem.svelte"
import getIcon from "components/common/icon"
import {
CreateDatabaseModal,
CreateUserModal,
} from "components/database/ModelDataTable/modals"
const { open, close } = getContext("simple-modal")
const openDatabaseCreator = () => {
open(
CreateDatabaseModal,
{
onClosed: close,
},
{ styleContent: { padding: "0" } }
)
}
const openUserCreator = () => {
open(
CreateUserModal,
{
onClosed: close,
},
{ styleContent: { padding: "0" } }
)
}
</script>
<div class="items-root">
<div class="hierarchy" />
{#if $backendUiStore.selectedDatabase._id}
<div class="hierarchy">
<div class="components-list-container">
<div class="nav-group-header">
<div class="hierarchy-title">Users</div>
<i class="ri-add-line hoverable" on:click={openUserCreator} />
</div>
</div>
<div class="hierarchy-items-container">
<UsersList />
</div>
</div>
{/if}
</div>
<style>
.items-root {
display: flex;
flex-direction: column;
max-height: 100%;
height: 100%;
background: var(--white);
}
.nav-group-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 20px 10px 20px;
}
.hierarchy-title {
align-items: center;
font-size: 18px;
font-weight: 700;
text-rendering: optimizeLegibility;
color: var(--ink);
}
.hierarchy {
display: flex;
flex-direction: column;
}
.hierarchy-items-container {
flex: 1 1 auto;
overflow-y: auto;
}
</style>

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

@ -50,7 +50,7 @@
<style>
.root {
font-size: 13px;
color: var(--secondary100);
color: var(--ink);
position: relative;
padding-left: 20px;
}
@ -76,7 +76,6 @@
margin: 0 0 0 6px;
padding: 0;
border: none;
font-family: Roboto;
font-size: 13px;
outline: none;
cursor: pointer;

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

@ -3,10 +3,8 @@
import { store, backendUiStore } from "builderStore"
import { cloneDeep } from "lodash/fp"
import getIcon from "../common/icon"
import {
CreateEditModelModal,
CreateEditViewModal,
} from "components/database/ModelDataTable/modals"
import { CreateEditViewModal } from "components/database/ModelDataTable/modals"
import api from "builderStore/api"
const { open, close } = getContext("simple-modal")
@ -30,14 +28,6 @@
class:selected={$backendUiStore.selectedView === `all_${node._id}`}>
<i class={ICON_MAP[type]} />
<span style="margin-left: 1rem">{node.name}</span>
<!-- <i
class="ri-edit-line hoverable"
on:click={editModel}
/>
<i
class="ri-delete-bin-7-line hoverable"
on:click={deleteModel}
/> -->
</div>
</div>

80
packages/builder/src/components/nav/ModelNavigator/BlockNavigator.svelte

@ -0,0 +1,80 @@
<script>
import * as blockDefinitions from "constants/backend"
import { backendUiStore } from "builderStore"
import Block from "components/common/Block.svelte"
const HEADINGS = [
{
title: "Fields",
key: "FIELDS",
},
{
title: "Blocks",
key: "BLOCKS",
},
]
let selectedTab = "FIELDS"
function addField(blockDefinition) {
backendUiStore.actions.models.addField(blockDefinition)
backendUiStore.actions.models.fetch()
}
</script>
<section>
<header>
{#each HEADINGS as tab}
<span
class:selected={selectedTab === tab.key}
on:click={() => (selectedTab = tab.key)}>
{tab.title}
</span>
{/each}
</header>
<div class="block-grid">
{#each Object.values(blockDefinitions[selectedTab]) as blockDefinition}
<Block
on:click={() => addField(blockDefinition)}
title={blockDefinition.name}
icon={blockDefinition.icon} />
{/each}
</div>
</section>
<style>
header {
margin-top: 20px;
margin-bottom: 20px;
display: grid;
grid-template-columns: repeat(3, 1fr);
}
span {
text-align: center;
padding: 10px;
font-weight: 500;
border-radius: 3px;
color: var(--ink-lighter);
font-size: 14px;
background: var(--light-grey);
}
span:hover {
background: var(--blue-light);
cursor: pointer;
}
.selected {
background: var(--grey-3);
color: var(--ink);
}
.block-grid {
margin-top: 20px;
display: grid;
grid-template-columns: 1fr 1fr;
grid-gap: 20px;
}
</style>

112
packages/builder/src/components/nav/ModelNavigator/EmptyModel.svelte

@ -0,0 +1,112 @@
<script>
import { backendUiStore } from "builderStore"
import { uuid } from "builderStore/uuid"
import { fade } from "svelte/transition"
import { FIELDS, BLOCKS, MODELS } from "constants/backend"
import Block from "components/common/Block.svelte"
function addNewField(field) {
backendUiStore.actions.models.addField(field)
}
function createModel(model) {
const { schema, ...rest } = $backendUiStore.selectedModel
const newModel = { ...model, schema: {} }
// TODO: could be better
for (let key in model.schema) {
newModel.schema[uuid()] = model.schema[key]
}
backendUiStore.actions.models.save({
model: {
...newModel,
...rest,
},
})
}
</script>
<section transition:fade>
<header>
<h2>Create New Model</h2>
<p>Before you can view your model, you need to set it up.</p>
</header>
<div class="block-row">
<span class="block-row-title">Fields</span>
<p>Blocks are pre-made fields and help you build your model quicker.</p>
<div class="blocks">
{#each Object.values(FIELDS) as field}
<Block
primary
title={field.name}
icon={field.icon}
on:click={() => addNewField(field)} />
{/each}
</div>
</div>
<div class="block-row">
<span class="block-row-title">Blocks</span>
<p>Blocks are pre-made fields and help you build your model quicker.</p>
<div class="blocks">
{#each Object.values(BLOCKS) as field}
<Block
secondary
title={field.name}
icon={field.icon}
on:click={() => addNewField(field)} />
{/each}
</div>
</div>
<div class="block-row">
<span class="block-row-title">Models</span>
<p>Blocks are pre-made fields and help you build your model quicker.</p>
<div class="blocks">
{#each Object.values(MODELS) as model}
<Block
tertiary
title={model.name}
icon={model.icon}
on:click={() => createModel(model)} />
{/each}
</div>
</div>
</section>
<style>
section {
height: 100vh;
}
h2 {
font-size: 20px;
font-weight: bold;
margin: 0;
}
.block-row-title {
font-weight: 500;
font-size: 16px;
}
p {
margin-top: 8px;
margin-bottom: 20px;
font-size: 14px;
}
.block-row {
margin-top: 40px;
}
.block-row .blocks {
display: grid;
grid-auto-flow: column;
grid-auto-columns: 110px;
grid-gap: 20px;
}
</style>

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

@ -0,0 +1,47 @@
<script>
export let icon
export let className
export let title
export let selected
export let indented
</script>
<div class:selected on:click class={className}>
<i class:indented class={icon} />
<span>{title}</span>
</div>
<style>
.indented {
margin-left: 10px;
}
div {
padding: 0 10px 0 10px;
height: 36px;
border-radius: 5px;
display: flex;
align-items: center;
transition: 0.3s background-color;
color: var(--ink);
font-weight: 400;
font-size: 14px;
margin-top: 4px;
margin-bottom: 4px;
}
.selected {
background-color: var(--blue-light);
}
div:hover {
background-color: var(--blue-light);
cursor: pointer;
}
i {
color: var(--grey-7);
font-size: 20px;
margin-right: 8px;
}
</style>

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

@ -0,0 +1,107 @@
<script>
import { getContext } from "svelte"
import { slide } from "svelte/transition"
import { Switcher } from "@budibase/bbui"
import { goto } from "@sveltech/routify"
import { store, backendUiStore } from "builderStore"
import BlockNavigator from "./BlockNavigator.svelte"
import ListItem from "./ListItem.svelte"
import { Button } from "@budibase/bbui"
const { open, close } = getContext("simple-modal")
let HEADINGS = [
{
title: "Navigate",
key: "NAVIGATE",
},
{
title: "Add",
key: "ADD",
},
]
$: selectedTab = $backendUiStore.tabs.NAVIGATION_PANEL
function selectModel(model, fieldId) {
backendUiStore.actions.models.select(model)
if (fieldId) {
backendUiStore.update(state => {
state.selectedField = fieldId
return state
})
}
}
function setupForNewModel() {
backendUiStore.update(state => {
state.selectedModel = {}
state.draftModel = { schema: {} }
state.tabs.SETUP_PANEL = "SETUP"
return state
})
}
</script>
<div class="items-root">
{#if $backendUiStore.selectedDatabase && $backendUiStore.selectedDatabase._id}
<div class="hierarchy">
<div class="components-list-container">
<Switcher
headings={HEADINGS}
bind:value={$backendUiStore.tabs.NAVIGATION_PANEL}>
{#if selectedTab === 'NAVIGATE'}
<Button purple wide on:click={setupForNewModel}>
Create New Model
</Button>
<div class="hierarchy-items-container">
{#each $backendUiStore.models as model}
<ListItem
selected={!$backendUiStore.selectedField && model._id === $backendUiStore.selectedModel._id}
title={model.name}
icon="ri-table-fill"
on:click={() => selectModel(model)} />
{#if model._id === $backendUiStore.selectedModel._id}
<div in:slide>
{#each Object.keys(model.schema) as fieldId}
<ListItem
selected={model._id === $backendUiStore.selectedModel._id && fieldId === $backendUiStore.selectedField}
indented
icon="ri-layout-column-fill"
title={model.schema[fieldId].name}
on:click={() => selectModel(model, fieldId)} />
{/each}
</div>
{/if}
{/each}
</div>
{:else if selectedTab === 'ADD'}
<BlockNavigator />
{/if}
</Switcher>
</div>
</div>
{/if}
</div>
<style>
.items-root {
display: flex;
flex-direction: column;
max-height: 100%;
height: 100%;
background: var(--white);
padding: 20px;
}
.hierarchy {
display: flex;
flex-direction: column;
}
.hierarchy-items-container {
margin-top: 20px;
flex: 1 1 auto;
}
</style>

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

@ -0,0 +1,132 @@
<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 &&
!constraints.presence.allowEmpty
function attachModelIdToSchema(evt) {
const { draftModel } = $backendUiStore
if ($backendUiStore.selectedField !== evt.target.value) {
delete draftModel.schema[$backendUiStore.selectedField]
draftModel.schema[evt.target.value] = field
backendUiStore.update(state => {
state.selectedField = evt.target.value
return state
})
}
}
</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" />
</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}
on:change={attachModelIdToSchema}>
<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>

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

@ -0,0 +1,127 @@
<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
})
}
async function saveModel() {
await backendUiStore.actions.models.save({
model: $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
type="text"
class="budibase__input"
bind:value={$backendUiStore.draftModel.name} />
</div>
<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: 100%;
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

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

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

@ -1,143 +0,0 @@
<script>
import { getContext, onMount } from "svelte"
import { store, backendUiStore } from "builderStore"
import HierarchyRow from "./HierarchyRow.svelte"
import NavItem from "./NavItem.svelte"
import getIcon from "components/common/icon"
import api from "builderStore/api"
import {
CreateEditModelModal,
CreateEditViewModal,
} from "components/database/ModelDataTable/modals"
const { open, close } = getContext("simple-modal")
function editModel() {
open(
CreateEditModelModal,
{
model: node,
onClosed: close,
},
{ styleContent: { padding: "0" } }
)
}
function newModel() {
open(
CreateEditModelModal,
{
onClosed: close,
},
{ styleContent: { padding: "0" } }
)
}
function newView() {
open(
CreateEditViewModal,
{
onClosed: close,
},
{ styleContent: { padding: "0" } }
)
}
function selectModel(model) {
backendUiStore.update(state => {
state.selectedModel = model
state.selectedView = `all_${model._id}`
return state
})
}
async function deleteModel(modelToDelete) {
const DELETE_MODEL_URL = `/api/models/${node._id}/${node._rev}`
const response = await api.delete(DELETE_MODEL_URL)
backendUiStore.update(state => {
state.models = state.models.filter(
model => model._id !== modelToDelete._id
)
state.selectedView = {}
return state
})
}
function selectView(view) {
backendUiStore.update(state => {
state.selectedView = view.name
return state
})
}
</script>
<div class="items-root">
<div class="hierarchy">
<div class="components-list-container">
<div class="nav-group-header">
<div class="hierarchy-title">Models</div>
<div class="uk-inline">
<i class="ri-add-line hoverable" on:click={newModel} />
</div>
</div>
</div>
<div class="hierarchy-items-container">
{#each $backendUiStore.models as model}
<HierarchyRow onSelect={selectModel} node={model} type="model" />
{/each}
</div>
</div>
<div class="hierarchy">
<div class="components-list-container">
<div class="nav-group-header">
<div class="hierarchy-title">Views</div>
<div class="uk-inline">
<i class="ri-add-line hoverable" on:click={newView} />
</div>
</div>
</div>
<div class="hierarchy-items-container">
{#each $backendUiStore.views as view}
<HierarchyRow onSelect={selectView} node={view} type="view" />
{/each}
</div>
</div>
</div>
<style>
.items-root {
display: flex;
flex-direction: column;
max-height: 100%;
height: 100%;
background-color: var(--white);
}
.nav-group-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 20px 10px 20px;
}
.hierarchy-title {
align-items: center;
font-size: 18px;
font-weight: 700;
text-rendering: optimizeLegibility;
color: var(--ink);
}
.hierarchy {
display: flex;
flex-direction: column;
}
.hierarchy-items-container {
flex: 1 1 auto;
overflow-y: auto;
}
</style>

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

@ -64,7 +64,6 @@
margin: 0 0 0 6px;
padding: 0;
border: none;
font-family: Roboto;
font-size: 13px;
outline: none;
cursor: pointer;

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

@ -21,23 +21,25 @@
max-width: 400px;
max-height: 150px;
border-radius: 5px;
border: 1px solid var(--grey-medium);
border: 1px solid var(--grey-4);
}
.app-button:hover {
background-color: var(--grey-light);
background-color: var(--grey-1);
text-decoration: none;
}
.app-title {
font-size: 18px;
font-weight: 700;
font-weight: 600;
color: var(--ink);
text-transform: capitalize;
font-family: Inter;
}
.app-desc {
color: var(--ink-light);
color: var(--grey-7);
font-family: Inter;
}
.card-footer {
@ -56,7 +58,7 @@
justify-content: center;
padding: 12px 20px;
border-radius: 5px;
border: 1px var(--grey) solid;
border: 1px var(--grey-2) solid;
font-size: 14px;
font-weight: 400;
cursor: pointer;

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

@ -142,11 +142,11 @@
background-color: var(--blue-light);
}
.info {
color: var(--primary100);
text-decoration-color: var(--primary100);
color: var(--blue);
text-decoration-color: var(--blue);
}
.info :global(svg) {
fill: var(--primary100);
fill: var(--blue);
margin-right: 8px;
width: 24px;
height: 24px;
@ -164,7 +164,7 @@
padding: 30px 40px;
border-bottom-left-radius: 5px;
border-bottom-right-radius: 50px;
background-color: var(--grey-light);
background-color: var(--grey-1);
}
.spinner-container {
background: white;
@ -183,7 +183,7 @@
font-size: 2em;
}
.error {
color: var(--deletion100);
color: var(--red);
font-weight: bold;
font-size: 0.8em;
}

4
packages/builder/src/components/userInterface/CategoryTab.svelte

@ -21,11 +21,11 @@
display: flex;
list-style: none;
font-size: 18px;
font-weight: 700;
font-weight: 600;
}
li {
color: var(--ink-lighter);
color: var(--grey-5);
cursor: pointer;
margin-right: 20px;
}

26
packages/builder/src/components/userInterface/Colorpicker/ButtonGroup.svelte

@ -1,12 +1,21 @@
<script>
import FlatButton from "./FlatButton.svelte";
import FlatButton from "./FlatButton.svelte"
export let format = "hex";
export let onclick = format => {};
export let format = "hex"
export let onclick = format => {}
let colorFormats = ["hex", "rgb", "hsl"];
let colorFormats = ["hex", "rgb", "hsl"]
</script>
<div class="flatbutton-group">
{#each colorFormats as text}
<FlatButton
selected={format === text}
{text}
on:click={() => onclick(text)} />
{/each}
</div>
<style>
.flatbutton-group {
font-weight: 500;
@ -18,12 +27,3 @@
align-self: center;
}
</style>
<div class="flatbutton-group">
{#each colorFormats as text}
<FlatButton
selected={format === text}
{text}
on:click={() => onclick(text)} />
{/each}
</div>

25
packages/builder/src/components/userInterface/Colorpicker/CheckedBackground.svelte

@ -1,17 +1,20 @@
<script>
import {buildStyle} from "./helpers.js"
import {fade} from "svelte/transition"
import { buildStyle } from "./helpers.js"
import { fade } from "svelte/transition"
export let backgroundSize = "10px"
export let borderRadius = ""
export let height = ""
export let width = ""
export let margin = ""
$: style = buildStyle({backgroundSize, borderRadius, height, width, margin})
export let backgroundSize = "10px"
export let borderRadius = ""
export let height = ""
export let width = ""
export let margin = ""
$: style = buildStyle({ backgroundSize, borderRadius, height, width, margin })
</script>
<div in:fade {style}>
<slot />
</div>
<style>
div {
background-image: url('data:image/svg+xml;utf8, <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2 2"><path fill="white" d="M1,0H2V1H1V0ZM0,1H1V2H0V1Z"/><path fill="gray" d="M0,0H1V1H0V0ZM1,1H2V2H1V1Z"/></svg>');
@ -19,7 +22,3 @@
width: fit-content;
}
</style>
<div in:fade {style}>
<slot />
</div>

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

@ -1,39 +1,39 @@
<script>
import { onMount, createEventDispatcher } from "svelte";
import { fade } from 'svelte/transition';
import Swatch from "./Swatch.svelte";
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 { 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";
export let value = "#3ec1d3ff";
convertHsvaToFormat,
} from "./utils.js"
import Slider from "./Slider.svelte"
import Palette from "./Palette.svelte"
import ButtonGroup from "./ButtonGroup.svelte"
import Input from "./Input.svelte"
export let value = "#3ec1d3ff"
export let swatches = [] //TODO: Safe swatches - limit to 12. warn in console
export let disableSwatches = false
export let format = "hexa";
export let open = false;
export let pickerHeight = 0;
export let pickerWidth = 0;
export let format = "hexa"
export let open = false
let adder = null;
export let pickerHeight = 0
export let pickerWidth = 0
let h = null;
let s = null;
let v = null;
let a = null;
let adder = null
const dispatch = createEventDispatcher();
let h = null
let s = null
let v = null
let a = null
const dispatch = createEventDispatcher()
onMount(() => {
if(!swatches.length > 0) {
if (!swatches.length > 0) {
//Don't use locally stored recent colors if swatches have been passed as props
getRecentColors()
}
@ -41,12 +41,12 @@
if (format) {
convertAndSetHSVA()
}
});
})
function getRecentColors() {
let colorStore = localStorage.getItem("cp:recent-colors")
if (colorStore) {
swatches = JSON.parse(colorStore)
swatches = JSON.parse(colorStore)
}
}
@ -61,39 +61,39 @@
}
function convertAndSetHSVA() {
let hsva = convertToHSVA(value, format);
setHSVA(hsva);
let hsva = convertToHSVA(value, format)
setHSVA(hsva)
}
function setHSVA([hue, sat, val, alpha]) {
h = hue;
s = sat;
v = val;
a = 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);
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) {
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 setAlpha({ color, isDrag }) {
a = color === "1.00" ? "1" : color
value = convertHsvaToFormat([h, s, v, a], format)
if (!isDrag) {
dispatchValue()
}
}
function dispatchValue() {
@ -101,43 +101,42 @@
}
function changeFormatAndConvert(f) {
format = f;
value = convertHsvaToFormat([h, s, v, a], format);
format = f
value = convertHsvaToFormat([h, s, v, a], format)
}
function handleColorInput(text) {
let format = getColorFormat(text)
if(format) {
if (format) {
value = text
convertAndSetHSVA()
}
}
function dispatchInputChange() {
if(format) {
if (format) {
dispatchValue()
}
}
function addSwatch() {
if(format) {
if (format) {
dispatch("addswatch", value)
setRecentColor(value)
}
}
function removeSwatch(idx) {
let removedSwatch = swatches.splice(idx, 1);
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) {
if (value !== color) {
format = getColorFormat(color)
if(format) {
if (format) {
value = color
convertAndSetHSVA()
dispatchValue()
@ -145,11 +144,112 @@
}
}
$: border = (v > 90 && s < 5) ? "1px dashed #dedada" : ""
$: style = buildStyle({background: value, border})
$: shrink = swatches.length > 0
$: border = v > 90 && s < 5 ? "1px dashed #dedada" : ""
$: style = buildStyle({ background: value, border })
$: shrink = swatches.length > 0
</script>
<div class="colorpicker-container">
<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} />
</CheckedBackground>
</div>
<div>
<Slider type="hue" value={h} on:change={hue => setHue(hue.detail)} />
<CheckedBackground borderRadius="10px" backgroundSize="7px">
<Slider
type="alpha"
value={a}
on:change={alpha => setAlpha(alpha.detail)} />
</CheckedBackground>
</div>
</div>
<div class="format-input-panel">
<ButtonGroup {format} onclick={changeFormatAndConvert} />
<Input {value} on:input={event => handleColorInput(event.target.value)} />
</div>
</div>
</div>
<div
class="colorpicker-container"
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} />
</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>
<style>
.colorpicker-container {
display: flex;
@ -161,7 +261,8 @@
width: 220px;
background: #ffffff;
border-radius: 2px;
box-shadow: 0 0.15em 1.5em 0 rgba(0,0,0,.1), 0 0 1em 0 rgba(0,0,0,.03);
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 {
@ -192,7 +293,7 @@
border-radius: 50%;
}
.swatch-panel {
.swatch-panel {
flex: 0 0 15px;
display: flex;
flex-flow: row wrap;
@ -203,7 +304,7 @@
.adder {
flex: 1;
height: 20px;
height: 20px;
display: flex;
transition: flex 0.5s;
justify-content: center;
@ -217,7 +318,7 @@
font-weight: 500;
}
.shrink {
.shrink {
flex: 0 0 20px;
}
@ -228,54 +329,3 @@
padding-top: 3px;
}
</style>
<div class="colorpicker-container" 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} />
</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>

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

@ -1,152 +1,184 @@
<script>
import Colorpicker from "./Colorpicker.svelte"
import CheckedBackground from "./CheckedBackground.svelte"
import {createEventDispatcher, afterUpdate, 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, left: 0}
let colorPreview = null
let previewHeight = null
let previewWidth = null
let pickerWidth = 0
let pickerHeight = 0
let anchorEl = null
let parentNodes = [];
let errorMsg = null
$: previewStyle = buildStyle({width, height, background: value})
$: errorPreviewStyle = buildStyle({width, height})
$: pickerStyle = buildStyle({top: `${dimensions.top}px`, left: `${dimensions.left}px`})
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
}
})
afterUpdate(() => {
if(colorPreview && colorPreview.offsetParent && !anchorEl) {
//Anchor relative to closest positioned ancestor element. If none, then anchor to body
anchorEl = colorPreview.offsetParent
let curEl = colorPreview
let els = []
//Travel up dom tree from preview element to find parent elements that scroll
while(!anchorEl.isSameNode(curEl)) {
curEl = curEl.parentNode
let elOverflow = window.getComputedStyle(curEl).getPropertyValue("overflow")
if(/scroll|auto/.test(elOverflow)) {
els.push(curEl)
}
}
parentNodes = els
}
})
function openColorpicker(event) {
if(colorPreview) {
open = true;
}
import Colorpicker from "./Colorpicker.svelte"
import CheckedBackground from "./CheckedBackground.svelte"
import { createEventDispatcher, afterUpdate, 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, left: 0 }
let colorPreview = null
let previewHeight = null
let previewWidth = null
let pickerWidth = 0
let pickerHeight = 0
let anchorEl = null
let parentNodes = []
let errorMsg = null
$: previewStyle = buildStyle({ width, height, background: value })
$: errorPreviewStyle = buildStyle({ width, height })
$: pickerStyle = buildStyle({
top: `${dimensions.top}px`,
left: `${dimensions.left}px`,
})
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
}
$: if(open && colorPreview) {
const {top: spaceAbove, width, bottom, right, left: spaceLeft} = colorPreview.getBoundingClientRect()
const {innerHeight, innerWidth} = window
const {offsetLeft, offsetTop} = colorPreview
//get the scrollTop value for all scrollable parent elements
let scrollTop = parentNodes.reduce((scrollAcc, el) => scrollAcc += el.scrollTop, 0);
const spaceBelow = (innerHeight - spaceAbove) - previewHeight
const top = spaceAbove > spaceBelow ? (offsetTop - pickerHeight) - scrollTop : (offsetTop + previewHeight) - scrollTop
//TOO: Testing and Scroll Awareness for x Scroll
const spaceRight = (innerWidth - spaceLeft) + previewWidth
const left = spaceRight > spaceLeft ? (offsetLeft + previewWidth) : offsetLeft - pickerWidth
dimensions = {top, left}
})
afterUpdate(() => {
if (colorPreview && colorPreview.offsetParent && !anchorEl) {
//Anchor relative to closest positioned ancestor element. If none, then anchor to body
anchorEl = colorPreview.offsetParent
let curEl = colorPreview
let els = []
//Travel up dom tree from preview element to find parent elements that scroll
while (!anchorEl.isSameNode(curEl)) {
curEl = curEl.parentNode
let elOverflow = window
.getComputedStyle(curEl)
.getPropertyValue("overflow")
if (/scroll|auto/.test(elOverflow)) {
els.push(curEl)
}
}
parentNodes = els
}
})
function onColorChange(color) {
value = color.detail;
dispatch("change", color.detail)
function openColorpicker(event) {
if (colorPreview) {
open = true
}
}
$: if (open && colorPreview) {
const {
top: spaceAbove,
width,
bottom,
right,
left: spaceLeft,
} = colorPreview.getBoundingClientRect()
const { innerHeight, innerWidth } = window
const { offsetLeft, offsetTop } = colorPreview
//get the scrollTop value for all scrollable parent elements
let scrollTop = parentNodes.reduce(
(scrollAcc, el) => (scrollAcc += el.scrollTop),
0
)
const spaceBelow = innerHeight - spaceAbove - previewHeight
const top =
spaceAbove > spaceBelow
? offsetTop - pickerHeight - scrollTop
: offsetTop + previewHeight - scrollTop
//TOO: Testing and Scroll Awareness for x Scroll
const spaceRight = innerWidth - spaceLeft + previewWidth
const left =
spaceRight > spaceLeft
? offsetLeft + previewWidth
: offsetLeft - pickerWidth
dimensions = { top, left }
}
function onColorChange(color) {
value = color.detail
dispatch("change", color.detail)
}
</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}
<div transition:fade class="picker-container" style={pickerStyle}>
<Colorpicker on:change={onColorChange} on:addswatch on:removeswatch bind:format bind:value bind:pickerHeight bind:pickerWidth {swatches} {disableSwatches} {open} />
</div>
<div on:click|self={() => open = false} class="overlay"></div>
{/if}
{:else}
<div class="color-preview preview-error" style={errorPreviewStyle}>
<span>&times;</span>
</div>
{#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}
<div transition:fade class="picker-container" style={pickerStyle}>
<Colorpicker
on:change={onColorChange}
on:addswatch
on:removeswatch
bind:format
bind:value
bind:pickerHeight
bind:pickerWidth
{swatches}
{disableSwatches}
{open} />
</div>
<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 {
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;
}
.color-preview-container {
display: flex;
flex-flow: row nowrap;
height: fit-content;
}
.color-preview {
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>

8
packages/builder/src/components/userInterface/Colorpicker/FlatButton.svelte

@ -1,8 +1,10 @@
<script>
export let text = "";
export let selected = false;
export let text = ""
export let selected = false
</script>
<div class="flatbutton" class:selected on:click>{text}</div>
<style>
.flatbutton {
cursor: pointer;
@ -25,5 +27,3 @@
border: none;
}
</style>
<div class="flatbutton" class:selected on:click>{text}</div>

13
packages/builder/src/components/userInterface/Colorpicker/Input.svelte

@ -1,7 +1,14 @@
<script>
export let value = "";
export let value = ""
</script>
<div>
<input on:input type="text" {value} maxlength="25" />
</div>
<div>
<input on:input on:change type="text" {value} maxlength="25" />
</div>
<style>
div {
display: flex;
@ -22,7 +29,3 @@
font-weight: 550;
}
</style>
<div>
<input on:input on:change type="text" {value} maxlength="25" />
</div>

60
packages/builder/src/components/userInterface/Colorpicker/Palette.svelte

@ -1,42 +1,58 @@
<script>
import { onMount, createEventDispatcher } from "svelte";
import { onMount, createEventDispatcher } from "svelte"
import CheckedBackground from "./CheckedBackground.svelte"
const dispatch = createEventDispatcher()
export let h = 0;
export let s = 0;
export let v = 0;
export let a = 1;
export let h = 0
export let s = 0
export let v = 0
export let a = 1
let palette;
let paletteHeight, paletteWidth = 0;
let palette
let paletteHeight,
paletteWidth = 0
function handleClick(event) {
const { left, top } = palette.getBoundingClientRect();
let clickX = (event.clientX - left)
let clickY = (event.clientY - top)
if((clickX > 0 && clickY > 0) && (clickX < paletteWidth && clickY < paletteHeight)) {
const { left, top } = palette.getBoundingClientRect()
let clickX = event.clientX - left
let clickY = event.clientY - top
if (
clickX > 0 &&
clickY > 0 &&
clickX < paletteWidth &&
clickY < paletteHeight
) {
let s = (clickX / paletteWidth) * 100
let v = 100 - ((clickY / paletteHeight) * 100)
dispatch("change", {s, v})
let v = 100 - (clickY / paletteHeight) * 100
dispatch("change", { s, v })
}
}
$: pickerX = (s * paletteWidth) / 100;
$: pickerY = paletteHeight * ((100 - v) / 100)
$: pickerX = (s * paletteWidth) / 100
$: pickerY = paletteHeight * ((100 - v) / 100)
$: paletteGradient = `linear-gradient(to top, rgba(0, 0, 0, 1), transparent),
linear-gradient(to left, hsla(${h}, 100%, 50%, ${a}), rgba(255, 255, 255, ${a}))
`;
$: style = `background: ${paletteGradient};`;
`
$: style = `background: ${paletteGradient};`
$: pickerStyle = `transform: translate(${pickerX - 8}px, ${pickerY - 8}px);`
</script>
<CheckedBackground width="100%">
<div
bind:this={palette}
bind:clientHeight={paletteHeight}
bind:clientWidth={paletteWidth}
on:click={handleClick}
class="palette"
{style}>
<div class="picker" style={pickerStyle} />
</div>
</CheckedBackground>
<style>
.palette {
position: relative;
@ -55,9 +71,3 @@
border-radius: 50%;
}
</style>
<CheckedBackground width="100%">
<div bind:this={palette} bind:clientHeight={paletteHeight} bind:clientWidth={paletteWidth} on:click={handleClick} class="palette" {style}>
<div class="picker" style={pickerStyle} />
</div>
</CheckedBackground>

78
packages/builder/src/components/userInterface/Colorpicker/Slider.svelte

@ -1,37 +1,62 @@
<script>
import { onMount, createEventDispatcher } from "svelte";
import dragable from "./drag.js";
import { onMount, createEventDispatcher } from "svelte"
import dragable from "./drag.js"
export let value = 1;
export let type = "hue";
export let value = 1
export let type = "hue"
const dispatch = createEventDispatcher();
const dispatch = createEventDispatcher()
let slider;
let sliderWidth = 0;
let slider
let sliderWidth = 0
function onSliderChange(mouseX, isDrag = false) {
const { left, width } = slider.getBoundingClientRect();
let clickPosition = mouseX - left;
const { left, width } = slider.getBoundingClientRect()
let clickPosition = mouseX - left
let percentageClick = (clickPosition / sliderWidth).toFixed(2)
if (percentageClick >= 0 && percentageClick <= 1) {
let value =
type === "hue"
? 360 * percentageClick
: percentageClick;
dispatch("change", {color: value, isDrag});
let value = type === "hue" ? 360 * percentageClick : percentageClick
dispatch("change", { color: value, isDrag })
}
}
$: thumbPosition =
type === "hue" ? sliderWidth * (value / 360) : sliderWidth * value;
type === "hue" ? sliderWidth * (value / 360) : sliderWidth * value
$: style = `transform: translateX(${thumbPosition - 6}px);`;
$: style = `transform: translateX(${thumbPosition - 6}px);`
</script>
<div
bind:this={slider}
bind:clientWidth={sliderWidth}
on:click={event => handleClick(event.clientX)}
class="color-format-slider"
class:hue={type === 'hue'}
class:alpha={type === 'alpha'}>
<div
use:dragable
on:drag={e => handleClick(e.detail)}
class="slider-thumb"
{style} />
</div>
<div
bind:this={slider}
bind:clientWidth={sliderWidth}
on:click={event => onSliderChange(event.clientX)}
class="color-format-slider"
class:hue={type === 'hue'}
class:alpha={type === 'alpha'}>
<div
use:dragable
on:drag={e => onSliderChange(e.detail, true)}
on:dragend
class="slider-thumb"
{style} />
</div>
<style>
.color-format-slider {
position: relative;
@ -69,21 +94,6 @@
border: 1px solid #777676;
border-radius: 50%;
background-color: #ffffff;
cursor:grab;
cursor: grab;
}
</style>
<div
bind:this={slider}
bind:clientWidth={sliderWidth}
on:click={event => onSliderChange(event.clientX)}
class="color-format-slider"
class:hue={type === 'hue'}
class:alpha={type === 'alpha'}>
<div
use:dragable
on:drag={e => onSliderChange(e.detail, true)}
on:dragend
class="slider-thumb"
{style} />
</div>

54
packages/builder/src/components/userInterface/Colorpicker/Swatch.svelte

@ -1,27 +1,47 @@
<script>
import {createEventDispatcher} from "svelte"
import { fade } from 'svelte/transition';
import CheckedBackground from "./CheckedBackground.svelte"
import { createEventDispatcher } from "svelte"
import { fade } from "svelte/transition"
import CheckedBackground from "./CheckedBackground.svelte"
export let hovered = false
export let color = "#fff"
const dispatch = createEventDispatcher()
export let hovered = false
export let color = "#fff"
const dispatch = createEventDispatcher()
</script>
<div class="space">
<CheckedBackground borderRadius="6px">
<div
in:fade
class="swatch"
style={`background: ${color};`}
on:click|self
on:mouseover={() => (hovered = true)}
on:mouseleave={() => (hovered = false)}>
{#if hovered}
<div
in:fade
class="remove-icon"
on:click|self={() => dispatch('removeswatch')}>
<span on:click|self={() => dispatch('removeswatch')}>&times;</span>
</div>
{/if}
</div>
</CheckedBackground>
</div>
<style>
.swatch {
position: relative;
cursor: pointer;
border-radius: 6px;
border: 1px solid #dedada;
height: 20px;
border: 1px solid #dedada;
height: 20px;
width: 20px;
}
.space {
padding: 3px 5px;
padding: 3px 5px;
}
.remove-icon {
@ -29,7 +49,7 @@
right: 0;
top: -5px;
right: -4px;
width:10px;
width: 10px;
height: 10px;
border-radius: 50%;
background-color: #800000;
@ -39,15 +59,3 @@
align-items: center;
}
</style>
<div class="space">
<CheckedBackground borderRadius="6px">
<div in:fade class="swatch" style={`background: ${color};`} on:click|self on:mouseover={() => hovered = true} on:mouseleave={() => hovered = false}>
{#if hovered}
<div in:fade class="remove-icon" on:click|self={()=> dispatch("removeswatch")}>
<span on:click|self={()=> dispatch("removeswatch")}>&times;</span>
</div>
{/if}
</div>
</CheckedBackground>
</div>

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

@ -257,21 +257,21 @@
.menu li:not(.disabled) {
cursor: pointer;
color: var(--ink-light);
color: var(--grey-7);
}
.menu li:not(.disabled):hover {
color: var(--ink);
background-color: var(--grey-light);
background-color: var(--grey-1);
}
.disabled {
color: var(--grey-dark);
color: var(--grey-4);
cursor: default;
}
.hr-style {
margin: 8px 0;
color: var(--grey-dark);
color: var(--grey-4);
}
</style>

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

@ -1,6 +1,7 @@
<script>
import { setContext, onMount } from "svelte"
import PropsView from "./PropsView.svelte"
import { store } from "builderStore"
import IconButton from "components/common/IconButton.svelte"
import {
@ -29,7 +30,10 @@
let selectedCategory = categories[0]
$: components = $store.components
$: componentInstance = $store.currentComponentInfo
$: componentInstance =
$store.currentView !== "component"
? { ...$store.currentPreviewItem, ...$store.currentComponentInfo }
: $store.currentComponentInfo
$: componentDefinition = $store.components[componentInstance._component]
$: componentPropDefinition =
flattenedPanel.find(
@ -44,7 +48,22 @@
componentPropDefinition.properties[selectedCategory.value]
const onStyleChanged = store.setComponentStyle
const onPropChanged = store.setComponentProp
function onPropChanged(key, value) {
if ($store.currentView !== "component") {
store.setPageOrScreenProp(key, value)
return
}
store.setComponentProp(key, value)
}
$: isComponentOrScreen =
$store.currentView === "component" ||
$store.currentFrontEndType === "screen"
$: isNotScreenslot = componentInstance._component !== "##builtin/screenslot"
$: displayName =
isComponentOrScreen && componentInstance._instanceName && isNotScreenslot
function walkProps(component, action) {
action(component)
@ -79,6 +98,12 @@
{categories}
{selectedCategory} />
{#if displayName}
<div class="instance-name">
<strong>{componentInstance._instanceName}</strong>
</div>
{/if}
<div class="component-props-container">
{#if selectedCategory.value === 'design'}
<DesignView {panelDefinition} {componentInstance} {onStyleChanged} />
@ -87,8 +112,8 @@
{componentInstance}
{componentDefinition}
{panelDefinition}
displayNameField={displayName}
onChange={onPropChanged}
onScreenPropChange={store.setPageOrScreenProp}
screenOrPageInstance={$store.currentView !== 'component' && $store.currentPreviewItem} />
{:else if selectedCategory.value === 'events'}
<EventsEditor component={componentInstance} />
@ -117,8 +142,13 @@
}
.component-props-container {
margin-top: 20px;
margin-top: 10px;
flex: 1 1 auto;
min-height: 0;
}
.instance-name {
margin-top: 10px;
font-size: 12px;
}
</style>

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

@ -38,7 +38,7 @@
padding: 5px;
border-style: solid;
border-width: 0 0 1px 0;
border-color: var(--lightslate);
border-color: var(--grey-1);
cursor: pointer;
}
@ -48,12 +48,12 @@
.component > .title {
font-size: 13pt;
color: var(--secondary100);
color: var(--ink);
}
.component > .description {
font-size: 10pt;
color: var(--primary75);
color: var(--blue);
font-style: italic;
}
</style>

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

@ -22,48 +22,35 @@
trimChars(" "),
])
const lastPartOfName = c => {
if (!c) return ""
const name = c.name ? c.name : c._component ? c._component : c
return last(name.split("/"))
}
const isComponentSelected = (current, comp) => current === comp
$: _screens = pipe(screens, [
map(c => ({ component: c, title: lastPartOfName(c) })),
sortBy("title"),
])
const changeScreen = screen => {
store.setCurrentScreen(screen.title)
store.setCurrentScreen(screen.props._instanceName)
$goto(`./:page/${screen.title}`)
}
</script>
<div class="root">
{#each _screens as screen}
{#each screens as screen}
<div
class="budibase__nav-item component"
class:selected={$store.currentComponentInfo._id === screen.component.props._id}
class:selected={$store.currentComponentInfo._id === screen.props._id}
on:click|stopPropagation={() => changeScreen(screen)}>
<span
class="icon"
class:rotate={$store.currentPreviewItem.name !== screen.title}>
{#if screen.component.props._children.length}
class:rotate={$store.currentPreviewItem.name !== screen.props._instanceName}>
{#if screen.props._children.length}
<ArrowDownIcon />
{/if}
</span>
<i class="ri-artboard-2-fill icon" />
<span class="title">{screen.title}</span>
<span class="title">{screen.props._instanceName}</span>
</div>
{#if $store.currentPreviewItem.name === screen.title && screen.component.props._children}
{#if $store.currentPreviewItem.props._instanceName && $store.currentPreviewItem.props._instanceName === screen.props._instanceName && screen.props._children}
<ComponentsHierarchyChildren
components={screen.component.props._children}
components={screen.props._children}
currentComponent={$store.currentComponentInfo} />
{/if}
{/each}
@ -77,8 +64,7 @@
}
.title {
margin-left: 10px;
margin-top: 2px;
margin-left: 14px;
font-size: 14px;
font-weight: 400;
}
@ -87,9 +73,8 @@
display: inline-block;
transition: 0.2s;
font-size: 24px;
width: 20px;
margin-top: 2px;
color: var(--ink-light);
width: 18px;
color: var(--grey-7);
}
.icon:nth-of-type(2) {

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

@ -20,6 +20,7 @@
const get_name = s => (!s ? "" : last(s.split("/")))
const get_capitalised_name = name => pipe(name, [get_name, capitalise])
const isScreenslot = name => name === "##builtin/screenslot"
const selectComponent = component => {
// Set current component
@ -39,10 +40,10 @@
<div
class="budibase__nav-item item"
class:selected={currentComponent === component}
style="padding-left: {level * 20 + 53}px">
style="padding-left: {level * 20 + 40}px">
<div class="nav-item">
<i class="icon ri-arrow-right-circle-fill" />
{get_capitalised_name(component._component)}
{isScreenslot(component._component) ? 'Screenslot' : component._instanceName}
</div>
<div class="actions">
<ComponentDropdownMenu {component} />
@ -73,7 +74,7 @@
padding: 0px 5px 0px 15px;
margin: auto 0px;
border-radius: 3px;
height: 35px;
height: 36px;
align-items: center;
}
@ -90,7 +91,7 @@
}
.item:hover {
background: var(--grey-light);
background: var(--grey-1);
cursor: pointer;
}
.item:hover .actions {
@ -105,7 +106,7 @@
}
.icon {
color: var(--ink-light);
color: var(--grey-7);
margin-right: 8px;
}
</style>

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

@ -55,7 +55,7 @@
display: flex;
flex-direction: column;
padding: 20px 5px 20px 10px;
border-left: solid 1px var(--grey);
border-left: solid 1px var(--grey-2);
}
.switcher {
@ -70,8 +70,8 @@
padding: 0;
cursor: pointer;
font-size: 18px;
font-weight: 700;
color: var(--ink-lighter);
font-weight: 600;
color: var(--grey-5);
margin-right: 20px;
}

39
packages/builder/src/components/userInterface/DesignView.svelte

@ -19,7 +19,6 @@
{ value: "normal", text: "Normal" },
{ value: "hover", text: "Hover" },
{ value: "active", text: "Active" },
{ value: "selected", text: "Selected" },
]
$: propertyGroupNames = Object.keys(panelDefinition)
@ -31,26 +30,26 @@
<FlatButtonGroup value={selectedCategory} {buttonProps} {onChange} />
</div>
<div class="positioned-wrapper">
<div class="design-view-property-groups">
{#if propertyGroupNames.length > 0}
{#each propertyGroupNames as groupName}
<PropertyGroup
name={groupName}
properties={getProperties(groupName)}
styleCategory={selectedCategory}
{onStyleChanged}
{componentDefinition}
{componentInstance} />
{/each}
{:else}
<div class="no-design">
<span>This component does not have any design properties</span>
</div>
{/if}
<div class="positioned-wrapper">
<div class="design-view-property-groups">
{#if propertyGroupNames.length > 0}
{#each propertyGroupNames as groupName}
<PropertyGroup
name={groupName}
properties={getProperties(groupName)}
styleCategory={selectedCategory}
{onStyleChanged}
{componentDefinition}
{componentInstance} />
{/each}
{:else}
<div class="no-design">
<span>This component does not have any design properties</span>
</div>
{/if}
</div>
</div>
</div>
</div>
<style>
.design-view-container {
@ -64,7 +63,7 @@
flex: 0 0 50px;
}
.positioned-wrapper{
.positioned-wrapper {
position: relative;
display: flex;
min-height: 0;

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

@ -53,7 +53,7 @@
.title > div:nth-child(1) {
grid-column-start: name;
color: var(--secondary100);
color: var(--ink);
}
.title > div:nth-child(2) {

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

@ -165,7 +165,7 @@
padding: 30px 40px;
border-bottom-left-radius: 5px;
border-bottom-right-radius: 50px;
background-color: var(--grey-light);
background-color: var(--grey-1);
}
.save {
margin-left: 20px;

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

@ -93,7 +93,7 @@
.newevent {
cursor: pointer;
border: 1px solid var(--grey-dark);
border: 1px solid var(--grey-4);
border-radius: 3px;
width: 100%;
padding: 8px 16px;
@ -109,7 +109,7 @@
}
.newevent:hover {
background: var(--grey-light);
background: var(--grey-1);
}
.icon {
@ -155,7 +155,7 @@
}
.selected {
color: var(--button-text);
background: var(--background-button) !important;
color: var(--blue);
background: var(--grey-1) !important;
}
</style>

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

@ -8,6 +8,7 @@
import { EVENT_TYPE_MEMBER_NAME } from "components/common/eventHandlers"
import { store, workflowStore } from "builderStore"
import { ArrowDownIcon } from "components/common/Icons/"
import { createEventDispatcher } from "svelte"
export let parameter
@ -25,6 +26,7 @@
{/if}
{#if parameter.name === 'workflow'}
<Select on:change bind:value={parameter.value}>
<option value="" />
{#each $workflowStore.workflows.filter(wf => wf.live) as workflow}
<option value={workflow._id}>{workflow.name}</option>
{/each}

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

@ -3,20 +3,13 @@
export let value = ""
export let text = ""
export let icon = ""
export let padding = "8px 5px;"
export let onClick = value => {}
export let selected = false
export let fontWeight = ""
$: style = buildStyle({ padding, fontWeight })
$: useIcon = !!icon
</script>
<div
class="flatbutton"
{style}
class:selected
on:click={() => onClick(value || text)}>
<div class="flatbutton" class:selected on:click={() => onClick(value || text)}>
{#if useIcon}
<i class={icon} />
{:else}
@ -29,25 +22,27 @@
<style>
.flatbutton {
cursor: pointer;
max-height: 36px;
padding: 8px 2px;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
background: #ffffff;
color: var(--ink-light);
color: var(--grey-7);
border-radius: 5px;
font-family: Roboto;
font-size: 14px;
font-weight: 400;
transition: all 0.3s;
margin-left: 5px;
text-rendering: optimizeLegibility;
}
.selected {
background: var(--ink-light);
color: #ffffff;
background: var(--grey-3);
color: var(--ink);
}
i {
font-size: 20px;
font-size: 16px;
}
</style>

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

@ -46,6 +46,7 @@
<style>
.flatbutton-group {
display: flex;
width: 100%;
}
.button-container {

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

@ -14,10 +14,7 @@
<PagesList />
<button class="newscreen" on:click={newScreen}>
<i class="icon ri-add-circle-fill" />
Create New Screen
</button>
<button class="newscreen" on:click={newScreen}>Create New Screen</button>
<PageLayout layout={$store.pages[$store.currentPageName]} />
@ -30,23 +27,26 @@
<style>
.newscreen {
cursor: pointer;
border: 1px solid var(--grey-dark);
border-radius: 3px;
border: 1px solid var(--purple);
border-radius: 5px;
width: 100%;
height: 36px;
padding: 8px 16px;
margin: 20px 0px 12px 0px;
display: flex;
justify-content: center;
align-items: center;
background: white;
color: var(--ink);
background: var(--purple);
color: var(--white);
font-size: 14px;
font-weight: 500;
transition: all 2ms;
transition: all 3ms;
outline: none;
}
.newscreen:hover {
background: var(--grey-light);
background: var(--purple-light);
color: var(--purple);
}
.icon {

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

Loading…
Cancel
Save