merge master

This commit is contained in:
kevmodrome 2020-08-26 09:11:16 +02:00
commit 300f18927f
250 changed files with 3483 additions and 10660 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -23,11 +23,7 @@
const capitalise = s => s.substring(0, 1).toUpperCase() + s.substring(1) const capitalise = s => s.substring(0, 1).toUpperCase() + s.substring(1)
const get_name = s => (!s ? "" : last(s.split("/"))) const get_name = s => (!s ? "" : last(s.split("/")))
const get_capitalised_name = name => const get_capitalised_name = name => pipe(name, [get_name, capitalise])
pipe(
name,
[get_name, capitalise]
)
const isScreenslot = name => name === "##builtin/screenslot" const isScreenslot = name => name === "##builtin/screenslot"
const selectComponent = component => { const selectComponent = component => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -8,7 +8,6 @@
import NewScreen from "./NewScreen.svelte" import NewScreen from "./NewScreen.svelte"
import CurrentItemPreview from "./CurrentItemPreview.svelte" import CurrentItemPreview from "./CurrentItemPreview.svelte"
import SettingsView from "./SettingsView.svelte" import SettingsView from "./SettingsView.svelte"
import PageView from "./PageView.svelte"
import ComponentsPaneSwitcher from "./ComponentsPaneSwitcher.svelte" import ComponentsPaneSwitcher from "./ComponentsPaneSwitcher.svelte"
import ConfirmDialog from "components/common/ConfirmDialog.svelte" import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import { last } from "lodash/fp" import { last } from "lodash/fp"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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