Merge branch 'master' of github.com:Budibase/budibase into deployment

This commit is contained in:
Martin McKeaveney 2020-07-01 21:42:47 +01:00
commit d176aa1d70
70 changed files with 1540 additions and 537 deletions

View File

@ -55,7 +55,7 @@
]
},
"dependencies": {
"@budibase/bbui": "^1.13.0",
"@budibase/bbui": "^1.15.0",
"@budibase/client": "^0.0.32",
"@nx-js/compiler-util": "^2.0.0",
"codemirror": "^5.51.0",
@ -70,6 +70,7 @@
"safe-buffer": "^5.1.2",
"shortid": "^2.2.8",
"string_decoder": "^1.2.0",
"svelte-portal": "^0.1.0",
"svelte-simple-modal": "^0.4.2",
"uikit": "^3.1.7"
},
@ -108,4 +109,4 @@
"svelte-jester": "^1.0.6"
},
"gitHead": "115189f72a850bfb52b65ec61d932531bf327072"
}
}

View File

@ -185,7 +185,11 @@ export default {
svelte({
// enable run-time checks when not in production
dev: !production,
include: ["src/**/*.svelte", "node_modules/**/*.svelte"],
include: [
"src/**/*.svelte",
"node_modules/**/*.svelte",
"../../../bbui/src/**/*.svelte",
],
// we'll extract any component CSS out into
// a separate file — better for performance
css: css => {

View File

@ -27,9 +27,6 @@ export const getBackendUiStore = () => {
const views = await viewsResponse.json()
store.update(state => {
state.selectedDatabase = db
if (models && models.length > 0) {
store.actions.models.select(models[0])
}
state.models = models
state.views = views
return state

View File

@ -109,8 +109,8 @@ const setPackage = (store, initial) => async pkg => {
initial.builtins = [getBuiltin("##builtin/screenslot")]
initial.appInstances = pkg.application.instances
initial.appId = pkg.application._id
store.set(initial)
await backendUiStore.actions.database.select(initial.appInstances[0])
return initial
}

View File

@ -36,6 +36,7 @@
class={determineClassName(type)}
bind:value
class:uk-form-danger={errors.length > 0}>
<option></option>
{#each options as opt}
<option value={opt}>{opt}</option>
{/each}

View File

@ -25,7 +25,7 @@
function selectModel(model, fieldId) {
backendUiStore.actions.models.select(model)
$goto(`./model/${model._id}`)
if (fieldId) {
backendUiStore.update(state => {
state.selectedField = fieldId

View File

@ -0,0 +1,56 @@
<script>
import { Button } from "@budibase/bbui"
export let remove = false
</script>
<div class="container">
<div class="content">
<div class="img">
<img src="https://picsum.photos/60/60" alt="zoom" />
</div>
<div class="body">
<div class="title">Zoom</div>
<div class="description">
Lorem, ipsum dolor sit amet consectetur adipisicing elit
</div>
</div>
</div>
<div class="footer">
<Button wide error={remove} secondary={!remove} on:click>
<span>{remove ? 'Remove' : 'Add'}</span>
</Button>
</div>
</div>
<style>
.container {
display: grid;
padding: 12px;
background: var(--light-grey);
grid-gap: 20px;
}
span {
font-size: 12px;
font-weight: bold;
}
.content {
display: grid;
grid-gap: 12px;
grid-template-columns: 60px auto;
}
.title {
font-size: 14px;
font-weight: bold;
}
.description {
font-size: 12px;
}
.img {
border-radius: 3px;
overflow: hidden;
}
img {
height: 60px;
width: 60px;
}
</style>

View File

@ -0,0 +1,48 @@
<script>
import Modal from "./Modal.svelte"
import { SettingsIcon } from "components/common/Icons/"
import { getContext } from "svelte"
import { isActive, goto, layout } from "@sveltech/routify"
// Handle create app modal
const { open } = getContext("simple-modal")
const showSettingsModal = () => {
open(
Modal,
{
name: "Placeholder App Name",
description: "This is a hardcoded description that needs to change",
},
{
closeButton: false,
closeOnEsc: true,
styleContent: { padding: 0 },
closeOnOuterClick: true,
}
)
}
</script>
<span class="topnavitemright" on:click={showSettingsModal}>
<SettingsIcon />
</span>
<style>
span:first-letter {
text-transform: capitalize;
}
.topnavitemright {
cursor: pointer;
color: var(--ink-light);
margin: 0px 20px 0px 0px;
padding-top: 4px;
font-weight: 500;
font-size: 1rem;
height: 100%;
display: flex;
flex: 1;
align-items: center;
box-sizing: border-box;
}
</style>

View File

@ -0,0 +1,92 @@
<script>
import { General, Users, DangerZone } from "./tabs"
import { Input, TextArea, Button, Switcher } from "@budibase/bbui"
import { SettingsIcon, CloseIcon } from "components/common/Icons/"
import { getContext } from "svelte"
import { post } from "builderStore/api"
const { open, close } = getContext("simple-modal")
export let name = ""
export let description = ""
const tabs = [
{
title: "General",
key: "GENERAL",
component: General,
},
{
title: "Users",
key: "USERS",
component: Users,
},
{
title: "Danger Zone",
key: "DANGERZONE",
component: DangerZone,
},
]
let value = "GENERAL"
$: selectedTab = tabs.find(tab => tab.key === value).component
</script>
<div class="container">
<div class="body">
<div class="heading">
<span class="icon">
<SettingsIcon />
</span>
<h3>Settings</h3>
</div>
<Switcher headings={tabs} bind:value>
<svelte:component this={selectedTab} />
</Switcher>
</div>
<div class="close-button" on:click={close}>
<CloseIcon />
</div>
</div>
<style>
.container {
position: relative;
}
.close-button {
cursor: pointer;
position: absolute;
top: 20px;
right: 20px;
}
.close-button :global(svg) {
width: 24px;
height: 24px;
}
.heading {
display: flex;
flex-direction: row;
align-items: center;
margin-bottom: 20px;
}
h3 {
margin: 0;
font-size: 24px;
font-weight: bold;
}
.icon {
display: grid;
border-radius: 3px;
align-content: center;
justify-content: center;
margin-right: 12px;
height: 20px;
width: 20px;
padding: 10px;
background-color: var(--blue-light);
}
.body {
padding: 40px 40px 80px 40px;
display: grid;
grid-gap: 20px;
}
</style>

View File

@ -0,0 +1,13 @@
<h3>
<slot />
</h3>
<style>
h3 {
font-size: 18px;
font-weight: bold;
color: var(--ink);
margin-top: 40px;
margin-bottom: 20px;
}
</style>

View File

@ -0,0 +1,42 @@
<script>
import { createEventDispatcher } from "svelte"
const dispatch = createEventDispatcher()
import { Input, Select, Button } from "@budibase/bbui"
export let user
let editMode = false
</script>
<div class="inputs">
<Input
disabled
thin
bind:value={user.username}
name="Name"
placeholder="Username" />
<Select disabled={!editMode} bind:value={user.accessLevelId} thin>
<option value="ADMIN">Admin</option>
<option value="POWER_USER">Power User</option>
</Select>
{#if editMode}
<Button
blue
on:click={() => {
dispatch('save', user)
editMode = false
}}>
Save
</Button>
{:else}
<Button secondary on:click={() => (editMode = true)}>Edit</Button>
{/if}
</div>
<style>
.inputs {
display: grid;
justify-items: stretch;
grid-gap: 18px;
grid-template-columns: 1fr 1fr 1fr;
}
</style>

View File

@ -0,0 +1,46 @@
<script>
import { Input, TextArea, Button } from "@budibase/bbui"
import Title from "../TabTitle.svelte"
let value = ""
let loading = false
const deleteApp = () => {
loading = true
// Do stuff here to delete app!
// Navigate to start
}
</script>
<Title>Danger Zone</Title>
<div class="background">
<Input
on:change={e => (value = e.target.value)}
on:input={e => (value = e.target.value)}
thin
disabled={loading}
placeholder="Enter your name"
label="Type DELETE into the textbox, then click the following button to
delete your web app:" />
<Button
disabled={value !== 'DELETE' || loading}
primary
wide
on:click={deleteApp}>
Delete Entire Web App
</Button>
</div>
<style>
.background {
display: grid;
grid-gap: var(--space);
border-radius: 5px;
background-color: var(--light-grey);
padding: 12px 12px 18px 12px;
}
.background :global(button) {
max-width: 100%;
}
</style>

View File

@ -0,0 +1,26 @@
<script>
import { Input, TextArea, Button } from "@budibase/bbui"
import Title from "../TabTitle.svelte"
</script>
<Title>General</Title>
<div class="container">
<div class="background">
<Input thin edit placeholder="Enter your name" label="Name" />
</div>
<div class="background">
<TextArea thin edit placeholder="Enter your name" label="Name" />
</div>
</div>
<style>
.container {
display: grid;
grid-gap: var(--space);
}
.background {
border-radius: 5px;
background-color: var(--light-grey);
padding: 12px 12px 18px 12px;
}
</style>

View File

@ -0,0 +1,45 @@
<script>
import Integration from "../Integration.svelte"
</script>
<div class="container">
<div class="title">Your Integrations</div>
<div class="integrations">
<Integration remove />
<Integration remove />
<Integration remove />
<Integration remove />
</div>
<div class="apps">Recommended apps</div>
<div class="integrations">
<Integration />
<Integration />
<Integration />
<Integration />
</div>
</div>
<style>
.container {
display: grid;
grid-gap: 16px;
}
.integrations {
display: grid;
grid-template-columns: 1fr 1fr;
grid-gap: 20px;
}
.title {
font-size: 18px;
font-weight: bold;
}
.apps {
font-size: 14px;
font-weight: bold;
}
.background {
border-radius: 5px;
background-color: var(--light-grey);
padding: 12px 12px 18px 12px;
}
</style>

View File

@ -0,0 +1 @@
Permissions

View File

@ -0,0 +1,135 @@
<script>
import { Input, Select, Button } from "@budibase/bbui"
import Title from "../TabTitle.svelte"
import UserRow from "../UserRow.svelte"
import { store, backendUiStore } from "builderStore"
import api from "builderStore/api"
// import * as api from "../api"
let username = ""
let password = ""
let accessLevelId
$: valid = username && password && accessLevelId
$: appId = $store.appId
// Create user!
async function createUser() {
if (valid) {
const user = { name: username, username, password, accessLevelId }
const response = await api.post(`/api/users`, user)
const json = await response.json()
backendUiStore.actions.users.create(json)
fetchUsersPromise = fetchUsers()
}
}
// Update user!
async function updateUser(event) {
let data = event.detail
delete data.password
const response = await api.put(`/api/users`, data)
const users = await response.json()
backendUiStore.update(state => {
state.users = users
return state
})
fetchUsersPromise = fetchUsers()
}
// Get users
async function fetchUsers() {
const response = await api.get(`/api/users`)
const users = await response.json()
backendUiStore.update(state => {
state.users = users
return state
})
return users
}
let fetchUsersPromise = fetchUsers()
</script>
<Title>Users</Title>
<div class="container">
<div class="background create">
<div class="title">Create new user</div>
<div class="inputs">
<Input thin bind:value={username} name="Name" placeholder="Username" />
<Input
thin
bind:value={password}
name="Password"
placeholder="Password" />
<Select bind:value={accessLevelId} thin>
<option value="ADMIN">Admin</option>
<option value="POWER_USER">Power User</option>
</Select>
</div>
<div class="create-button">
<Button on:click={createUser} small blue>Create</Button>
</div>
</div>
<div class="background">
<div class="title">Current Users</div>
{#await fetchUsersPromise}
Loading state!
{:then users}
<ul>
{#each users as user}
<li>
<UserRow {user} on:save={updateUser} />
</li>
{:else}
<li>No Users found</li>
{/each}
</ul>
{:catch error}
err0r
{/await}
</div>
</div>
<style>
.container {
display: grid;
grid-gap: 14px;
}
.background {
position: relative;
display: grid;
grid-gap: 12px;
border-radius: 5px;
background-color: var(--grey-2);
padding: 12px 12px 18px 12px;
}
.background.create {
background-color: var(--blue-light);
}
.inputs :global(select) {
padding: 12px 9px;
height: initial;
}
.create-button {
position: absolute;
top: 12px;
right: 12px;
}
.title {
font-size: 14px;
font-weight: 500;
}
.inputs {
display: grid;
grid-gap: 18px;
grid-template-columns: 1fr 1fr 1fr;
}
ul {
list-style: none;
padding: 0;
display: grid;
grid-gap: 8px;
}
</style>

View File

@ -0,0 +1,5 @@
export { default as General } from "./General.svelte"
export { default as Integrations } from "./Integrations.svelte"
export { default as Permissions } from "./Permissions.svelte"
export { default as Users } from "./Users.svelte"
export { default as DangerZone } from "./DangerZone.svelte"

View File

@ -25,38 +25,69 @@
name: "Screen Placeholder",
route: "*",
props: {
_component: "@budibase/standard-components/container",
type: "div",
_children: [
{
_component: "@budibase/standard-components/container",
_styles: { normal: {}, hover: {}, active: {}, selected: {} },
_id: "__screenslot__text",
_code: "",
className: "",
onLoad: [],
type: "div",
_children: [
{
_component: "@budibase/standard-components/text",
_styles: {
normal: {},
hover: {},
active: {},
selected: {},
},
_id: "__screenslot__text_2",
_code: "",
text: "content",
font: "",
color: "",
textAlign: "inline",
verticalAlign: "inline",
formattingTag: "none",
},
],
"_id": "49c3d0a2-7028-46f0-b004-7eddf62ad01c",
"_component": "@budibase/standard-components/container",
"_styles": {
"normal": {
"padding": "0px",
"font-family": "Roboto",
"border-width": "0",
"border-style": "None",
"text-align": "center"
},
"hover": {},
"active": {},
"selected": {}
},
"_code": "",
"className": "",
"onLoad": [],
"type": "div",
"_children": [
{
"_id": "335428f7-f9ca-4acd-9e76-71bc8ad27324",
"_component": "@budibase/standard-components/container",
"_styles": {
"normal": {
"padding": "16px",
"border-style": "Dashed",
"border-width": "2px",
"border-color": "#8a8989fa"
},
"hover": {},
"active": {},
"selected": {}
},
"_code": "",
"className": "",
"onLoad": [],
"type": "div",
"_instanceId": "inst_b3b4e95_ab0df02dda3f4d8eb4b35eea2968bad3",
"_instanceName": "Container",
"_children": [
{
"_id": "ddb6a225-33ba-4ba8-91da-bc6a2697ebf9",
"_component": "@budibase/standard-components/heading",
"_styles": {
"normal": {
"font-family": "Roboto"
},
"hover": {},
"active": {},
"selected": {}
},
"_code": "",
"className": "",
"text": "Your screens go here",
"type": "h1",
"_instanceId": "inst_b3b4e95_ab0df02dda3f4d8eb4b35eea2968bad3",
"_instanceName": "Heading",
"_children": []
}
]
}
],
"_instanceName": "Content Placeholder"
},
}

View File

@ -1,10 +1,15 @@
export default `<html>
<head>
<link rel="stylesheet" href="https://rsms.me/inter/inter.css">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Roboto+Mono">
<style>
body, html {
height: 100%!important;
font-family: Roboto !important;
}
*, *:before, *:after {
box-sizing: border-box;
}
.lay-__screenslot__text {
width: 100%;
height: 100%;

View File

@ -0,0 +1,2 @@
export { default as drag } from "./drag.js"
export { default as keyevents } from "./key-events.js"

View File

@ -0,0 +1,41 @@
//events: Array<{trigger: fn}>
export default function(node, events = []) {
const ev = Object.entries(events)
let fns = []
for (let [trigger, fn] of ev) {
let f = addEvent(trigger, fn)
fns = [...fns, f]
}
function _scaffold(trigger, fn) {
return () => {
let trig = parseInt(trigger)
if (trig) {
if (event.keyCode === trig) {
fn(event)
}
} else {
if (event.key === trigger) {
fn(event)
}
}
}
}
function addEvent(trigger, fn) {
let f = _scaffold(trigger, fn)
node.addEventListener("keydown", f)
return f
}
function removeEvents() {
fns.forEach(f => node.removeEventListener("keypress", f))
}
return {
destroy() {
removeEvents()
},
}
}

View File

@ -1,5 +1,5 @@
<script>
import { buildStyle } from "./helpers.js"
import {buildStyle} from "../helpers.js"
import { fade } from "svelte/transition"
export let backgroundSize = "10px"

View File

@ -3,32 +3,35 @@
import { fade } from "svelte/transition"
import Swatch from "./Swatch.svelte"
import CheckedBackground from "./CheckedBackground.svelte"
import { buildStyle } from "./helpers.js"
import {buildStyle} from "../helpers.js"
import {
getColorFormat,
convertToHSVA,
convertHsvaToFormat,
} from "./utils.js"
} 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"
import {keyevents} from "../actions"
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 open = false
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
let h = 0
let s = 0
let v = 0
let a = 0
const dispatch = createEventDispatcher()
@ -38,6 +41,10 @@
getRecentColors()
}
if(colorPicker) {
colorPicker.focus()
}
if (format) {
convertAndSetHSVA()
}
@ -50,6 +57,12 @@
}
}
function handleEscape() {
if(open) {
open = false;
}
}
function setRecentColor(color) {
if (swatches.length === 12) {
swatches.splice(0, 1)
@ -145,85 +158,99 @@
}
$: border = v > 90 && s < 5 ? "1px dashed #dedada" : ""
$: style = buildStyle({ background: value, border })
$: selectedColorStyle = buildStyle({ background: value, border })
$: shrink = swatches.length > 0
</script>
<div
class="colorpicker-container"
bind:clientHeight={pickerHeight}
bind:clientWidth={pickerWidth}>
<Portal>
<div
class="colorpicker-container"
use:keyevents={{"Escape": handleEscape}}
transition:fade
bind:this={colorPicker}
{style}
tabindex="0"
bind:clientHeight={pickerHeight}
bind:clientWidth={pickerWidth}>
<div class="palette-panel">
<Palette on:change={setSaturationAndValue} {h} {s} {v} {a} />
</div>
<div class="palette-panel">
<Palette on:change={setSaturationAndValue} {h} {s} {v} {a} />
</div>
<div class="control-panel">
<div class="alpha-hue-panel">
<div>
<CheckedBackground borderRadius="50%" backgroundSize="8px">
<div class="selected-color" {style} />
</CheckedBackground>
</div>
<div>
<Slider
type="hue"
value={h}
on:change={hue => setHue(hue.detail)}
on:dragend={dispatchValue} />
<CheckedBackground borderRadius="10px" backgroundSize="7px">
<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="alpha"
value={a}
on:change={(alpha, isDrag) => setAlpha(alpha.detail, isDrag)}
type="hue"
value={h}
on:change={hue => setHue(hue.detail)}
on:dragend={dispatchValue} />
</CheckedBackground>
<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
tabindex="0"
use:keyevents={{"Enter": addSwatch}}
bind:this={adder}
transition:fade
class="adder"
class:shrink
on:click={addSwatch}>
<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>
{#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;
transition: top 0.1s, left 0.1s;
flex-direction: column;
/* height: 265px; */
margin: 5px 0px;
height: auto;
width: 220px;
background: #ffffff;
@ -273,7 +300,7 @@
flex: 1;
height: 20px;
display: flex;
transition: flex 0.5s;
transition: flex 0.3s;
justify-content: center;
align-items: center;
background: #f1f3f4;
@ -283,6 +310,8 @@
margin-left: 5px;
margin-top: 3px;
font-weight: 500;
outline-color: #003cb0;
outline-width: thin;
}
.shrink {

View File

@ -1,11 +1,11 @@
<script>
import Colorpicker from "./Colorpicker.svelte"
import CheckedBackground from "./CheckedBackground.svelte"
import { createEventDispatcher, afterUpdate, beforeUpdate } from "svelte"
import { createEventDispatcher, beforeUpdate, onMount } from "svelte"
import { buildStyle } from "./helpers.js"
import {buildStyle, debounce} from "../helpers.js"
import { fade } from "svelte/transition"
import { getColorFormat } from "./utils.js"
import { getColorFormat } from "../utils.js"
export let value = "#3ec1d3ff"
export let swatches = []
@ -15,7 +15,8 @@
export let height = "25px"
let format = "hexa"
let dimensions = { top: 0, left: 0 }
let dimensions = { top: 0, bottom: 0, right: 0, left: 0 }
let positionSide = "top"
let colorPreview = null
let previewHeight = null
@ -23,17 +24,8 @@
let pickerWidth = 0
let pickerHeight = 0
let anchorEl = null
let parentNodes = []
let errorMsg = null
$: previewStyle = buildStyle({ width, height, background: value })
$: errorPreviewStyle = buildStyle({ width, height })
$: pickerStyle = buildStyle({
top: `${dimensions.top}px`,
left: `${dimensions.left}px`,
})
const dispatch = createEventDispatcher()
beforeUpdate(() => {
@ -46,25 +38,6 @@
}
})
afterUpdate(() => {
if (colorPreview && colorPreview.offsetParent && !anchorEl) {
//Anchor relative to closest positioned ancestor element. If none, then anchor to body
anchorEl = colorPreview.offsetParent
let curEl = colorPreview
let els = []
//Travel up dom tree from preview element to find parent elements that scroll
while (!anchorEl.isSameNode(curEl)) {
curEl = curEl.parentNode
let elOverflow = window
.getComputedStyle(curEl)
.getPropertyValue("overflow")
if (/scroll|auto/.test(elOverflow)) {
els.push(curEl)
}
}
parentNodes = els
}
})
function openColorpicker(event) {
if (colorPreview) {
@ -72,45 +45,53 @@
}
}
$: if (open && colorPreview) {
const {
top: spaceAbove,
width,
bottom,
right,
left: spaceLeft,
} = colorPreview.getBoundingClientRect()
const { innerHeight, innerWidth } = window
const { offsetLeft, offsetTop } = colorPreview
//get the scrollTop value for all scrollable parent elements
let scrollTop = parentNodes.reduce(
(scrollAcc, el) => (scrollAcc += el.scrollTop),
0
)
const spaceBelow = innerHeight - spaceAbove - previewHeight
const top =
spaceAbove > spaceBelow
? offsetTop - pickerHeight - scrollTop
: offsetTop + previewHeight - scrollTop
//TOO: Testing and Scroll Awareness for x Scroll
const spaceRight = innerWidth - spaceLeft + previewWidth
const left =
spaceRight > spaceLeft
? offsetLeft + previewWidth
: offsetLeft - pickerWidth
dimensions = { top, left }
}
function onColorChange(color) {
value = color.detail
dispatch("change", color.detail)
}
function calculateDimensions() {
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) - (220 / 2)
dimensions = { [positionSide]: y.toFixed(1), left: x.toFixed(1) }
}
$: if(open && colorPreview) {
calculateDimensions()
}
$: previewStyle = buildStyle({ width, height, background: value })
$: errorPreviewStyle = buildStyle({ width, height })
$: pickerStyle = buildStyle({
[positionSide]: `${dimensions[positionSide]}px`,
left: `${dimensions.left}px`,
})
</script>
<svelte:window on:resize={debounce(calculateDimensions, 200)} />
<div class="color-preview-container">
{#if !errorMsg}
<CheckedBackground borderRadius="3px" backgroundSize="8px">
@ -124,19 +105,19 @@
</CheckedBackground>
{#if open}
<div transition:fade class="picker-container" style={pickerStyle}>
<Colorpicker
on:change={onColorChange}
on:addswatch
on:removeswatch
bind:format
bind:value
bind:pickerHeight
bind:pickerWidth
{swatches}
{disableSwatches}
{open} />
</div>
<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}
@ -154,6 +135,7 @@
}
.color-preview {
cursor: pointer;
border-radius: 3px;
border: 1px solid #dedada;
}
@ -166,12 +148,12 @@
cursor: not-allowed;
}
.picker-container {
/* .picker-container {
position: absolute;
z-index: 3;
width: fit-content;
height: fit-content;
}
} */
.overlay {
position: fixed;

View File

@ -1,9 +1,14 @@
<script>
import {createEventDispatcher} from "svelte"
import {keyevents} from "../actions"
export let text = ""
export let selected = false
const dispatch = createEventDispatcher()
</script>
<div class="flatbutton" class:selected on:click>{text}</div>
<div class="flatbutton" tabindex="0" use:keyevents={{"Enter": () => dispatch("click")}} class:selected on:click>{text}</div>
<style>
.flatbutton {
@ -19,11 +24,14 @@
justify-content: center;
align-items: center;
background: #f1f3f4;
outline-color: #003cb0;
outline-width: thin;
}
.selected {
color: #ffffff;
background-color: #003cb0;
border: none;
outline: none;
}
</style>

View File

@ -0,0 +1,37 @@
<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,6 @@
<script>
import { onMount, createEventDispatcher } from "svelte"
import dragable from "./drag.js"
import {drag, keyevents} from "../actions"
export let value = 1
export let type = "hue"
@ -9,6 +9,10 @@
let slider
let sliderWidth = 0
let upperLimit = type === "hue" ? 360 : 1
let incrementFactor = type === "hue" ? 1 : 0.01
const isWithinLimit = value => value >= 0 && value <= upperLimit
function onSliderChange(mouseX, isDrag = false) {
const { left, width } = slider.getBoundingClientRect()
@ -17,12 +21,29 @@
let percentageClick = (clickPosition / sliderWidth).toFixed(2)
if (percentageClick >= 0 && percentageClick <= 1) {
let value = type === "hue" ? 360 * percentageClick : percentageClick
dispatch("change", { color: value, isDrag })
}
}
function handleLeftKey() {
let v = value - incrementFactor
if(isWithinLimit(v)) {
value = v
dispatch("change", { color: value })
}
}
function handleRightKey() {
let v = value + incrementFactor
if(isWithinLimit(v)) {
value = v
dispatch("change", { color: value })
}
}
$: thumbPosition =
type === "hue" ? sliderWidth * (value / 360) : sliderWidth * value
@ -30,14 +51,16 @@
</script>
<div
tabindex="0"
bind:this={slider}
use:keyevents={{37: handleLeftKey, 39: handleRightKey}}
bind:clientWidth={sliderWidth}
on:click={event => onSliderChange(event.clientX)}
class="color-format-slider"
class:hue={type === 'hue'}
class:alpha={type === 'alpha'}>
<div
use:dragable
use:drag
on:drag={e => onSliderChange(e.detail, true)}
on:dragend
class="slider-thumb"
@ -54,6 +77,8 @@
margin: 10px 0px;
border: 1px solid #e8e8ef;
cursor: pointer;
outline-color: #003cb0;
outline-width: thin;
}
.hue {

View File

@ -2,6 +2,7 @@
import { createEventDispatcher } from "svelte"
import { fade } from "svelte/transition"
import CheckedBackground from "./CheckedBackground.svelte"
import {keyevents} from "../actions"
export let hovered = false
export let color = "#fff"
@ -12,6 +13,8 @@
<div class="space">
<CheckedBackground borderRadius="6px">
<div
tabindex="0"
use:keyevents={{"Enter": () => dispatch("click")}}
in:fade
class="swatch"
style={`background: ${color};`}
@ -38,6 +41,8 @@
border: 1px solid #dedada;
height: 20px;
width: 20px;
outline-color: #003cb0;
outline-width: thin;
}
.space {

View File

@ -1,4 +1,4 @@
export const buildStyle = styles => {
export function buildStyle(styles) {
let str = ""
for (let s in styles) {
if (styles[s]) {
@ -12,3 +12,9 @@ export const buildStyle = styles => {
export const convertCamel = str => {
return str.replace(/[A-Z]/g, match => `-${match.toLowerCase()}`)
}
export const debounce = (fn, milliseconds) => {
return () => {
setTimeout(fn, milliseconds)
}
}

View File

@ -1,2 +1,2 @@
import Colorpreview from "./Colorpreview.svelte"
import Colorpreview from "./components/Colorpreview.svelte"
export default Colorpreview

View File

@ -144,13 +144,15 @@ export const hslaToHSVA = ([h, s, l, a = 1]) => {
export const hsvaToHexa = (hsva, asString = false) => {
const [r, g, b, a] = hsvaToRgba(hsva)
const padSingle = hex => (hex.length === 1 ? `0${hex}` : hex)
const hexa = [r, g, b]
.map(v => {
let hex = Math.round(v).toString(16)
return hex.length === 1 ? `0${hex}` : hex
})
.concat(Math.round(a * 255).toString(16))
let hexa = [r, g, b].map(v => {
let hex = Math.round(v).toString(16)
return padSingle(hex)
})
let alpha = padSingle(Math.round(a * 255).toString(16))
hexa = [...hexa, alpha]
return asString ? `#${hexa.join("")}` : hexa
}

View File

@ -24,7 +24,7 @@
const changeScreen = screen => {
store.setCurrentScreen(screen.props._instanceName)
$goto(`./:page/${screen.title}`)
$goto(`./:page/${screen.props._instanceName}`)
}
</script>

View File

@ -1,16 +1,28 @@
<script>
import {onMount} from "svelte"
import PropertyGroup from "./PropertyGroup.svelte"
import FlatButtonGroup from "./FlatButtonGroup.svelte"
export let panelDefinition = {}
export let componentInstance = {}
export let componentDefinition = {}
export let onStyleChanged = () => {}
let selectedCategory = "normal"
let propGroup = null
let currentGroup
const getProperties = name => panelDefinition[name]
onMount(() => {
// if(propGroup) {
// propGroup.addEventListener("scroll", function(e){
// console.log("I SCROLLED", e.target.scrollTop)
// })
// }
})
function onChange(category) {
selectedCategory = category
}
@ -22,6 +34,7 @@
]
$: propertyGroupNames = Object.keys(panelDefinition)
</script>
<div class="design-view-container">
@ -31,7 +44,7 @@
</div>
<div class="positioned-wrapper">
<div class="design-view-property-groups">
<div bind:this={propGroup} class="design-view-property-groups">
{#if propertyGroupNames.length > 0}
{#each propertyGroupNames as groupName}
<PropertyGroup
@ -40,7 +53,9 @@
styleCategory={selectedCategory}
{onStyleChanged}
{componentDefinition}
{componentInstance} />
{componentInstance}
open={currentGroup === groupName}
on:open={() => currentGroup = groupName} />
{/each}
{:else}
<div class="no-design">

View File

@ -1,5 +1,6 @@
<script>
import { onMount, beforeUpdate } from "svelte"
import { onMount, beforeUpdate, afterUpdate } from "svelte"
import Portal from "svelte-portal"
import { buildStyle } from "../../helpers.js"
export let options = []
export let value = ""
@ -12,69 +13,80 @@
let selectMenu
let icon
let selectYPosition = null
let availableSpace = 0
let selectAnchor = null;
let dimensions = {top: 0, bottom: 0, left: 0}
let positionSide = "top"
let maxHeight = null
let menuHeight
let maxHeight = 0
let scrollTop = 0;
let containerEl = null;
const handleStyleBind = value =>
!!styleBindingProperty ? { style: `${styleBindingProperty}: ${value}` } : {}
onMount(() => {
if (select) {
select.addEventListener("keydown", addSelectKeyEvents)
select.addEventListener("keydown", handleEnter)
}
return () => {
select.removeEventListener("keydown", addSelectKeyEvents)
select.removeEventListener("keydown", handleEnter)
}
})
function checkPosition() {
const { bottom, top: spaceAbove } = select.getBoundingClientRect()
function handleEscape(e) {
if(open && e.key === "Escape") {
toggleSelect(false)
}
}
function getDimensions() {
const { bottom, top: spaceAbove, left } = selectAnchor.getBoundingClientRect()
const spaceBelow = window.innerHeight - bottom
let y;
if (spaceAbove > spaceBelow) {
positionSide = "bottom"
maxHeight = `${spaceAbove.toFixed(0) - 20}px`
maxHeight = spaceAbove - 20
y = (window.innerHeight - spaceAbove)
} else {
positionSide = "top"
maxHeight = `${spaceBelow.toFixed(0) - 20}px`
y = bottom
maxHeight = spaceBelow - 20
}
dimensions = {[positionSide]: y, left}
}
function addSelectKeyEvents(e) {
if (e.key === "Enter") {
if (!open) {
function handleEnter(e) {
if (!open && e.key === "Enter") {
toggleSelect(true)
}
} else if (e.key === "Escape") {
if (open) {
toggleSelect(false)
}
}
}
}
function toggleSelect(isOpen) {
checkPosition()
getDimensions()
if (isOpen) {
icon.style.transform = "rotate(180deg)"
icon.style.transform = "rotate(180deg)"
} else {
icon.style.transform = "rotate(0deg)"
}
open = isOpen
}
function handleClick(val) {
value = val
onChange(value)
}
$: menuStyle = buildStyle({
"max-height": maxHeight,
"max-height": `${maxHeight.toFixed(0)}px`,
"transform-origin": `center ${positionSide}`,
[positionSide]: "32px",
[positionSide]: `${dimensions[positionSide]}px`,
"left": `${dimensions.left.toFixed(0)}px`,
})
$: isOptionsObject = options.every(o => typeof o === "object")
@ -83,6 +95,10 @@
? options.find(o => o.value === value)
: {}
$: if(open && selectMenu) {
selectMenu.focus()
}
$: displayLabel =
selectedOption && selectedOption.label ? selectedOption.label : value || ""
</script>
@ -92,45 +108,49 @@
bind:this={select}
class="bb-select-container"
on:click={() => toggleSelect(!open)}>
<div class="bb-select-anchor selected">
<div bind:this={selectAnchor} class="bb-select-anchor selected">
<span>{displayLabel}</span>
<i bind:this={icon} class="ri-arrow-down-s-fill" />
</div>
<div
bind:this={selectMenu}
style={menuStyle}
class="bb-select-menu"
class:open>
<ul>
{#if isOptionsObject}
{#each options as { value: v, label }}
<li
{...handleStyleBind(v)}
on:click|self={handleClick(v)}
class:selected={value === v}>
{label}
</li>
{/each}
{:else}
{#each options as v}
<li
{...handleStyleBind(v)}
on:click|self={handleClick(v)}
class:selected={value === v}>
{v}
</li>
{/each}
{/if}
</ul>
</div>
{#if open}
<Portal>
<div
tabindex="0"
class:open
bind:this={selectMenu}
style={menuStyle}
on:keydown={handleEscape}
class="bb-select-menu">
<ul>
{#if isOptionsObject}
{#each options as { value: v, label }}
<li
{...handleStyleBind(v)}
on:click|self={handleClick(v)}
class:selected={value === v}>
{label}
</li>
{/each}
{:else}
{#each options as v}
<li
{...handleStyleBind(v)}
on:click|self={handleClick(v)}
class:selected={value === v}>
{v}
</li>
{/each}
{/if}
</ul>
</div>
<div on:click|self={() => toggleSelect(false)} class="overlay" />
</Portal>
{/if}
</div>
{#if open}
<div on:click|self={() => toggleSelect(false)} class="overlay" />
{/if}
<style>
.overlay {
position: absolute;
position: fixed;
top: 0;
bottom: 0;
right: 0;
@ -139,7 +159,6 @@
}
.bb-select-container {
position: relative;
outline: none;
width: 160px;
height: 36px;
@ -180,6 +199,7 @@
.bb-select-menu {
position: absolute;
display: flex;
outline: none;
box-sizing: border-box;
flex-direction: column;
opacity: 0;
@ -190,9 +210,6 @@
height: fit-content !important;
border-bottom-left-radius: 2px;
border-bottom-right-radius: 2px;
border-right: 1px solid var(--grey-4);
border-left: 1px solid var(--grey-4);
border-bottom: 1px solid var(--grey-4);
background-color: var(--grey-2);
transform: scale(0);
transition: opacity 0.13s linear, transform 0.12s cubic-bezier(0, 0, 0.2, 1);

View File

@ -8,11 +8,12 @@
export let properties = []
export let componentInstance = {}
export let onStyleChanged = () => {}
export let open = false
$: style = componentInstance["_styles"][styleCategory] || {}
</script>
<DetailSummary {name}>
<DetailSummary {name} on:open show={open} >
{#each properties as props}
<PropertyControl
label={props.label}

View File

@ -123,7 +123,7 @@ export const margin = [
},
{
label: "Bottom",
key: "padding-bottom",
key: "margin-bottom",
control: OptionSelect,
options: [
{ label: "None", value: "0px" },
@ -352,7 +352,6 @@ export const typography = [
"Inter",
"Lucida Sans Unicode",
"Open Sans",
"Playfair",
"Roboto",
"Roboto Mono",
"Times New Roman",

View File

@ -321,17 +321,44 @@ export default {
name: "Form",
description: "A component that generates a form from your data.",
icon: "ri-file-edit-fill",
properties: {
design: { ...all },
settings: [{ label: "Model", key: "model", control: ModelSelect }],
},
_component: "@budibase/standard-components/dataform",
template: {
component: "@budibase/materialdesign-components/Form",
description: "Form for saving a record",
name: "@budibase/materialdesign-components/recordForm",
},
children: [],
commonProps: {},
children: [
{
_component: "@budibase/standard-components/dataform",
name: "Form Basic",
icon: "ri-file-edit-fill",
properties: {
design: { ...all },
settings: [
{
label: "Model",
key: "model",
control: ModelSelect,
},
],
},
template: {
component: "@budibase/materialdesign-components/Form",
description: "Form for saving a record",
name: "@budibase/materialdesign-components/recordForm",
},
},
{
_component: "@budibase/standard-components/dataformwide",
name: "Form Wide",
icon: "ri-file-edit-fill",
properties: {
design: { ...all },
settings: [
{
label: "Model",
key: "model",
control: ModelSelect,
},
],
},
},
],
},
{
name: "Chart",
@ -379,7 +406,7 @@ export default {
{
name: "List",
_component: "@budibase/standard-components/list",
description: "Shiny list",
description: "Renders all children once per record, of a given model",
icon: "ri-file-list-fill",
properties: {
design: { ...all },
@ -387,6 +414,18 @@ export default {
},
children: [],
},
{
name: "Record Detail",
_component: "@budibase/standard-components/recorddetail",
description:
"Loads a record, using an id from the URL, which can be used with {{ context }}, in children",
icon: "ri-profile-line",
properties: {
design: { ...all },
settings: [{ label: "Model", key: "model", control: ModelSelect }],
},
children: [],
},
{
name: "Map",
_component: "@budibase/standard-components/datamap",

View File

@ -5,6 +5,7 @@
import { notifier } from "builderStore/store/notifications"
import WorkflowBlockSetup from "./WorkflowBlockSetup.svelte"
import DeleteWorkflowModal from "./DeleteWorkflowModal.svelte"
import { Button } from "@budibase/bbui"
const { open, close } = getContext("simple-modal")
@ -90,26 +91,21 @@
{testResult}
</button>
{/if}
<button class="workflow-button hoverable" on:click={testWorkflow}>
Test
</button>
<Button secondary wide on:click={testWorkflow}>Test Workflow</Button>
</div>
{/if}
{#if selectedTab === 'SETUP'}
{#if workflowBlock}
<WorkflowBlockSetup {workflowBlock} />
<div class="buttons">
<button
<Button
green
wide
data-cy="save-workflow-setup"
class="workflow-button hoverable"
on:click={saveWorkflow}>
Save Workflow
</button>
<button
class="delete-workflow-button hoverable"
on:click={deleteWorkflowBlock}>
Delete Block
</button>
</Button>
<Button red wide on:click={deleteWorkflowBlock}>Delete Block</Button>
</div>
{:else if $workflowStore.currentWorkflow}
<div class="panel">
@ -141,15 +137,14 @@
</div>
</div>
<div class="buttons">
<button
<Button
green
wide
data-cy="save-workflow-setup"
class="workflow-button hoverable"
on:click={saveWorkflow}>
Save Workflow
</button>
<button class="delete-workflow-button" on:click={deleteWorkflow}>
Delete Workflow
</button>
</Button>
<Button red wide on:click={deleteWorkflow}>Delete Workflow</Button>
</div>
</div>
{/if}
@ -175,11 +170,12 @@
}
header {
font-size: 20px;
font-size: 18px;
font-weight: 600;
font-family: inter;
display: flex;
align-items: center;
margin-bottom: 18px;
margin-bottom: 20px;
color: var(--ink);
}
@ -190,13 +186,12 @@
.block-label {
font-weight: 500;
font-size: 14px;
color: var(--ink);
margin: 0px 0px 16px 0px;
color: var(--grey-7);
margin-bottom: 20px;
}
.config-item {
margin: 0px 0px 4px 0px;
padding: 12px 0px;
margin-bottom: 20px;
}
.budibase_input {
@ -214,6 +209,7 @@
header > span {
color: var(--grey-5);
margin-right: 20px;
cursor: pointer;
}
.form {
@ -228,52 +224,10 @@
.buttons {
position: absolute;
bottom: 10px;
}
.delete-workflow-button {
cursor: pointer;
border: 1px solid var(--red);
border-radius: 3px;
width: 260px;
padding: 8px 16px;
display: flex;
justify-content: center;
align-items: center;
background: var(--white);
color: var(--red);
font-size: 14px;
font-weight: 500;
transition: all 2ms;
align-self: flex-end;
margin-bottom: 10px;
}
.delete-workflow-button:hover {
background: var(--red);
border: 1px solid var(--red);
color: var(--white);
}
.workflow-button {
cursor: pointer;
border: 1px solid var(--grey-4);
border-radius: 3px;
bottom: 20px;
display: grid;
width: 100%;
padding: 8px 16px;
display: flex;
justify-content: center;
align-items: center;
background: white;
color: var(--ink);
font-size: 14px;
font-weight: 500;
transition: all 2ms;
margin-bottom: 10px;
}
.workflow-button:hover {
background: var(--grey-1);
gap: 12px;
}
.access-level {
@ -301,7 +255,7 @@
}
.passed {
background: #84c991;
background: var(--green);
}
.failed {

View File

@ -13,85 +13,84 @@
: []
</script>
<label class="uk-form-label">{workflowBlock.type}: {workflowBlock.name}</label>
<label class="selected-label">{workflowBlock.type}: {workflowBlock.name}</label>
{#each workflowParams as [parameter, type]}
<div class="block-field">
<label class="uk-form-label">{parameter}</label>
<div class="uk-form-controls">
{#if Array.isArray(type)}
<select
class="budibase_input"
bind:value={workflowBlock.args[parameter]}>
{#each type as option}
<option value={option}>{option}</option>
{/each}
</select>
{:else if type === 'component'}
<ComponentSelector bind:value={workflowBlock.args[parameter]} />
{:else if type === 'accessLevel'}
<select
class="budibase__input"
bind:value={workflowBlock.args[parameter]}>
<option value="ADMIN">Admin</option>
<option value="POWER_USER">Power User</option>
</select>
{:else if type === 'password'}
<input
type="password"
class="budibase__input"
bind:value={workflowBlock.args[parameter]} />
{:else if type === 'number'}
<input
type="number"
class="budibase__input"
bind:value={workflowBlock.args[parameter]} />
{:else if type === 'longText'}
<textarea
type="text"
class="budibase__input"
bind:value={workflowBlock.args[parameter]} />
{:else if type === 'model'}
<ModelSelector bind:value={workflowBlock.args[parameter]} />
{:else if type === 'record'}
<RecordSelector bind:value={workflowBlock.args[parameter]} />
{:else if type === 'string'}
<input
type="text"
class="budibase__input"
bind:value={workflowBlock.args[parameter]} />
{/if}
</div>
<label class="label">{parameter}</label>
{#if Array.isArray(type)}
<select class="budibase_input" bind:value={workflowBlock.args[parameter]}>
{#each type as option}
<option value={option}>{option}</option>
{/each}
</select>
{:else if type === 'component'}
<ComponentSelector bind:value={workflowBlock.args[parameter]} />
{:else if type === 'accessLevel'}
<select class="budibase_input" bind:value={workflowBlock.args[parameter]}>
<option value="ADMIN">Admin</option>
<option value="POWER_USER">Power User</option>
</select>
{:else if type === 'password'}
<input
type="password"
class="budibase_input"
bind:value={workflowBlock.args[parameter]} />
{:else if type === 'number'}
<input
type="number"
class="budibase_input"
bind:value={workflowBlock.args[parameter]} />
{:else if type === 'longText'}
<textarea
type="text"
class="budibase_input"
bind:value={workflowBlock.args[parameter]} />
{:else if type === 'model'}
<ModelSelector bind:value={workflowBlock.args[parameter]} />
{:else if type === 'record'}
<RecordSelector bind:value={workflowBlock.args[parameter]} />
{:else if type === 'string'}
<input
type="text"
class="budibase_input"
bind:value={workflowBlock.args[parameter]} />
{/if}
</div>
{/each}
<style>
.block-field {
border-radius: 3px;
background: var(--grey-1);
padding: 12px;
margin: 0px 0px 4px 0px;
display: grid;
}
.budibase_input {
height: 36px;
width: 220px;
border-radius: 3px;
border: 1px solid var(--grey-4);
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 {
text-transform: capitalize;
font-size: 14px;
font-weight: 500;
margin-top: 20px;
}
.selected-label {
text-transform: capitalize;
font-size: 14px;
font-weight: 500;
}
textarea {
min-height: 150px;
font-family: inherit;
padding: 5px;
padding: 12px;
margin-top: 8px;
}
</style>

View File

@ -81,7 +81,7 @@
}
.play-button.highlighted {
background: var(--primary);
background: var(--purple);
}
.stop-button.highlighted {

View File

@ -67,6 +67,10 @@
color: var(--ink);
}
p {
color: inherit;
}
div:hover {
transform: scale(1.05);
}

View File

@ -4,6 +4,7 @@
import { onMount, getContext } from "svelte"
import { backendUiStore, workflowStore } from "builderStore"
import CreateWorkflowModal from "./CreateWorkflowModal.svelte"
import { Button } from "@budibase/bbui"
const { open, close } = getContext("simple-modal")
@ -27,10 +28,7 @@
</script>
<section>
<button class="new-workflow-button hoverable" on:click={newWorkflow}>
<i class="icon ri-add-circle-fill" />
Create New Workflow
</button>
<Button purple wide on:click{newWorkflow}>Create New Workflow</Button>
<ul>
{#each $workflowStore.workflows as workflow}
<li
@ -74,10 +72,10 @@
.workflow-item {
display: flex;
border-radius: 3px;
border-radius: 5px;
padding-left: 12px;
align-items: center;
height: 40px;
height: 36px;
margin-bottom: 4px;
color: var(--ink);
}

View File

@ -1,10 +1,11 @@
<script>
import Modal from "svelte-simple-modal"
import { store, workflowStore } from "builderStore"
import SettingsLink from "components/settings/Link.svelte"
import { get } from "builderStore/api"
import { fade } from "svelte/transition"
import { isActive, goto, layout } from "@sveltech/routify"
import { isActive, goto, layout, url } from "@sveltech/routify"
import { SettingsIcon, PreviewIcon } from "components/common/Icons/"
import IconButton from "components/common/IconButton.svelte"
@ -26,6 +27,22 @@
throw new Error(pkg)
}
}
// handles navigation between frontend, backend, workflow.
// this remembers your last place on each of the sections
// e.g. if one of your screens is selected on front end, then
// you browse to backend, when you click fronend, you will be
// brought back to the same screen
const topItemNavigate = path => () => {
const activeTopNav = $layout.children.find(c => $isActive(c.path))
if (!activeTopNav) return
store.update(state => {
if (!state.previousTopNavPath) state.previousTopNavPath = {}
state.previousTopNavPath[activeTopNav.path] = window.location.pathname.replace("/_builder", "")
$goto(state.previousTopNavPath[path] || path)
return state
})
}
</script>
<Modal>
@ -45,7 +62,7 @@
<span
class:active={$isActive(path)}
class="topnavitem"
on:click={() => $goto(path)}>
on:click={topItemNavigate(path)}>
{title}
</span>
{/each}
@ -54,12 +71,7 @@
hoverColor="var(--secondary75)"/> -->
</div>
<div class="toprightnav">
<span
class:active={$isActive(`/settings`)}
class="topnavitemright"
on:click={() => $goto(`/settings`)}>
<SettingsIcon />
</span>
<SettingsLink />
<span
class:active={false}
class="topnavitemright"

View File

@ -1,36 +0,0 @@
<script>
import { store, backendUiStore } from "builderStore"
import { goto } from "@sveltech/routify"
import { onMount } from "svelte"
$: instances = $store.appInstances
async function selectDatabase(database) {
backendUiStore.actions.database.select(database)
}
onMount(async () => {
if ($store.appInstances.length > 0) {
await selectDatabase($store.appInstances[0])
$goto(`./${$backendUiStore.selectedDatabase._id}`)
}
})
</script>
<div class="root">
<div class="node-view">
<slot />
</div>
</div>
<style>
.root {
height: 100%;
position: relative;
}
.node-view {
overflow-y: auto;
flex: 1 1 auto;
}
</style>

View File

@ -1,20 +0,0 @@
<script>
import { store, backendUiStore } from "builderStore"
import { goto } from "@sveltech/routify"
import { onMount } from "svelte"
$: instances = $store.appInstances
async function selectDatabase(database) {
backendUiStore.actions.database.select(database)
}
onMount(async () => {
if ($store.appInstances.length > 0) {
await selectDatabase($store.appInstances[0])
$goto(`../${$backendUiStore.selectedDatabase._id}`)
}
})
</script>
Please select a database

View File

@ -1,6 +1,6 @@
<script>
import { goto } from "@sveltech/routify"
$goto("../database")
$goto("../model")
</script>
<!-- routify:options index=false -->

View File

@ -0,0 +1,14 @@
<script>
import { params } from "@sveltech/routify"
import { backendUiStore } from "builderStore"
if ($params.selectedModel) {
const model = $backendUiStore.models.find(m => m._id === $params.selectedModel)
if (model) {
backendUiStore.actions.models.select(model)
}
}
</script>
<slot />

View File

@ -3,7 +3,7 @@
import { Button } from "@budibase/bbui"
import EmptyModel from "components/nav/ModelNavigator/EmptyModel.svelte"
import ModelDataTable from "components/database/ModelDataTable"
import { store, backendUiStore } from "builderStore"
import { backendUiStore } from "builderStore"
import ActionButton from "components/common/ActionButton.svelte"
import * as api from "components/database/ModelDataTable/api"
import { CreateEditRecordModal } from "components/database/ModelDataTable/modals"

View File

@ -0,0 +1,35 @@
<script>
import { backendUiStore } from "builderStore"
import { goto, leftover } from "@sveltech/routify"
import { onMount } from "svelte"
async function selectModel(model) {
backendUiStore.actions.models.select(model)
}
onMount(async () => {
// navigate to first model in list, if not already selected
// and this is the final url (i.e. no selectedModel)
if (!$leftover && $backendUiStore.models.length > 0 && (!$backendUiStore.selectedModel || !$backendUiStore.selectedModel._id)) {
$goto(`./${$backendUiStore.models[0]._id}`)
}
})
</script>
<div class="root">
<div class="node-view">
<slot />
</div>
</div>
<style>
.root {
height: 100%;
position: relative;
}
.node-view {
overflow-y: auto;
flex: 1 1 auto;
}
</style>

View File

@ -0,0 +1,24 @@
<script>
import { store, backendUiStore } from "builderStore"
import { goto, leftover } from "@sveltech/routify"
import { onMount } from "svelte"
async function selectModel(model) {
backendUiStore.actions.models.select(model)
}
onMount(async () => {
// navigate to first model in list, if not already selected
// and this is the final url (i.e. no selectedModel)
if (!$leftover && $backendUiStore.models.length > 0 && (!$backendUiStore.selectedModel || !$backendUiStore.selectedModel._id)) {
// this file routes as .../models/index, so, go up one.
$goto(`../${$backendUiStore.models[0]._id}`)
}
})
</script>
{#if $backendUiStore.models.length === 0}
Please create a model
{:else}
Please select a model
{/if}

View File

@ -14,7 +14,7 @@
if ($leftover) {
// Get the correct screen children.
const screenChildren = $store.pages[$params.page]._screens.find(
screen => screen.name === $params.screen
screen => screen.props._instanceName === $params.screen
).props._children
findComponent(componentIds, screenChildren)
}

View File

@ -88,7 +88,6 @@
padding: 0;
display: flex;
flex-direction: column;
z-index: 5;
}
.preview-pane {

View File

@ -56,13 +56,13 @@ export const screenRouter = ({ screens, onScreenSelected, window }) => {
const screenIndex = current !== -1 ? current : fallback
onScreenSelected(screens[screenIndex], _url)
try {
!url.state && history.pushState(_url, null, _url)
} catch (_) {
// ignoring an exception here as the builder runs an iframe, which does not like this
}
onScreenSelected(screens[screenIndex], _url)
}
function click(e) {

View File

@ -59,7 +59,7 @@ const _setup = ({ handlerTypes, getCurrentState, bb, store }) => node => {
const propValue = props[propName]
// A little bit of a hack - won't bind if the string doesn't start with {{
const isBound = typeof propValue === "string" && propValue.startsWith("{{")
const isBound = typeof propValue === "string" && propValue.includes("{{")
if (isBound) {
initialProps[propName] = mustache.render(propValue, {

View File

@ -63,7 +63,19 @@ exports.create = async function(ctx) {
}
}
exports.update = async function() {}
exports.update = async function(ctx) {
const db = new CouchDB(ctx.user.instanceId)
const user = ctx.request.body
const dbUser = db.get(ctx.request.body._id)
const newData = { ...dbUser, ...user }
const response = await db.put(newData)
user._rev = response.rev
ctx.status = 200
ctx.message = `User ${ctx.request.body.username} updated successfully.`
ctx.body = response
}
exports.destroy = async function(ctx) {
const database = new CouchDB(ctx.user.instanceId)

View File

@ -8,6 +8,7 @@ const router = Router()
router
.get("/api/users", authorized(LIST_USERS), controller.fetch)
.get("/api/users/:username", authorized(USER_MANAGEMENT), controller.find)
.put("/api/users/", authorized(USER_MANAGEMENT), controller.update)
.post("/api/users", authorized(USER_MANAGEMENT), controller.create)
.delete(
"/api/users/:username",

View File

@ -4,6 +4,7 @@
"stylesheets": [],
"componentLibraries": ["@budibase/standard-components", "@budibase/materialdesign-components"],
"props" : {
"_id": "private-master-root",
"_component": "@budibase/standard-components/container",
"_children": [
{
@ -19,7 +20,6 @@
"_children": []
}
],
"_id": 0,
"type": "div",
"_styles": {
"active": {},

View File

@ -7,6 +7,7 @@
"favicon": "./_shared/favicon.png",
"stylesheets": [],
"props": {
"_id": "public-master-root",
"_component": "@budibase/standard-components/container",
"_children": [
{
@ -31,7 +32,6 @@
"logo": ""
}
],
"_id": 1,
"type": "div",
"_styles": {
"layout": {},

View File

@ -17,6 +17,9 @@
margin: 0px;
padding: 0px;
}
*, *:before, *:after {
box-sizing: border-box;
}
</style>
{{ each(options.stylesheets) }}
@ -31,6 +34,9 @@
<link rel='stylesheet' href='/assets{{ pageStyle }}'>
{{ /if }}
<link rel="stylesheet" href="https://rsms.me/inter/inter.css">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Roboto+Mono">
<script src='/assets/clientFrontendDefinition.js'></script>
<script src='/assets/budibase-client.js'></script>

View File

@ -209,6 +209,13 @@
"model": "models"
}
},
"dataformwide": {
"description": "an HTML table that fetches data from a model or view and displays it.",
"data": true,
"props": {
"model": "models"
}
},
"datalist": {
"description": "A configurable data list that attaches to your backend models.",
"data": true,
@ -228,15 +235,14 @@
"description": "A configurable data list that attaches to your backend models.",
"data": true,
"props": {
"model": "models",
"layout": {
"type": "options",
"default": "list",
"options": [
"list",
"grid"
]
}
"model": "models"
}
},
"recorddetail": {
"description": "Loads a record, using an ID in the url",
"data": true,
"props": {
"model": "models"
}
},
"datamap": {

View File

@ -4,6 +4,12 @@
export let _bb
export let model
const TYPE_MAP = {
string: "text",
boolean: "checkbox",
number: "number",
}
let username
let password
let newModel = {
@ -59,15 +65,23 @@
<form class="form" on:submit|preventDefault>
<h1>{modelDef.name} Form</h1>
<hr />
<div class="form-content">
{#each fields as field}
<div class="form-item">
<label class="form-label" for="form-stacked-text">{field}</label>
<input
class="input"
placeholder={field}
type={schema[field].type === 'string' ? 'text' : schema[field].type}
on:change={handleInput(field)} />
{#if schema[field].type === 'string' && schema[field].constraints.inclusion}
<select on:blur={handleInput(field)}>
{#each schema[field].constraints.inclusion as opt}
<option>{opt}</option>
{/each}
</select>
{:else}
<input
class="input"
type={TYPE_MAP[schema[field].type]}
on:change={handleInput(field)} />
{/if}
</div>
<hr />
{/each}
@ -143,8 +157,42 @@
}
button:hover {
background-color: white;
border-color: #393c44;
color: #393c44;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1),
0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
input[type="checkbox"] {
width: 32px;
height: 32px;
padding: 0;
margin: 0;
vertical-align: bottom;
position: relative;
top: -1px;
*overflow: hidden;
}
select::-ms-expand {
display: none;
}
select {
display: inline-block;
cursor: pointer;
align-items: baseline;
box-sizing: border-box;
padding: 1em 1em;
border: 1px solid #eaeaea;
border-radius: 5px;
font: inherit;
line-height: inherit;
-webkit-appearance: none;
-moz-appearance: none;
-ms-appearance: none;
appearance: none;
background-repeat: no-repeat;
background-image: linear-gradient(45deg, transparent 50%, currentColor 50%),
linear-gradient(135deg, currentColor 50%, transparent 50%);
background-position: right 17px top 1.5em, right 10px top 1.5em;
background-size: 7px 7px, 7px 7px;
}
</style>

View File

@ -0,0 +1,173 @@
<script>
import { onMount } from "svelte"
export let _bb
export let model
const TYPE_MAP = {
string: "text",
boolean: "checkbox",
number: "number",
}
let username
let password
let newModel = {
modelId: model,
}
let store = _bb.store
let schema = {}
let modelDef = {}
$: if (model && model.length !== 0) {
fetchModel()
}
$: fields = Object.keys(schema)
async function fetchModel() {
const FETCH_MODEL_URL = `/api/models/${model}`
const response = await _bb.api.get(FETCH_MODEL_URL)
modelDef = await response.json()
schema = modelDef.schema
}
async function save() {
const SAVE_RECORD_URL = `/api/${model}/records`
const response = await _bb.api.post(SAVE_RECORD_URL, newModel)
const json = await response.json()
store.update(state => {
state[model] = state[model] ? [...state[model], json] : [json]
return state
})
}
const handleInput = field => event => {
let value
if (event.target.type === "checkbox") {
value = event.target.checked
newModel[field] = value
return
}
if (event.target.type === "number") {
value = parseInt(event.target.value)
newModel[field] = value
return
}
value = event.target.value
newModel[field] = value
}
</script>
<form class="form" on:submit|preventDefault>
<h1>{modelDef.name} Form</h1>
<hr />
<div class="form-content">
{#each fields as field}
<div class="form-item">
<label class="form-label" for="form-stacked-text">{field}</label>
{#if schema[field].type === 'string' && schema[field].constraints.inclusion}
<select on:blur={handleInput(field)}>
{#each schema[field].constraints.inclusion as opt}
<option>{opt}</option>
{/each}
</select>
{:else}
<input
class="input"
type={TYPE_MAP[schema[field].type]}
on:change={handleInput(field)} />
{/if}
</div>
<hr />
{/each}
<div class="button-block">
<button on:click={save}>Submit Form</button>
</div>
</div>
</form>
<style>
.form {
display: flex;
flex-direction: column;
justify-content: center;
margin: auto;
padding: 40px;
}
.form-content {
margin-bottom: 20px;
}
.input {
border-radius: 5px;
border: 1px solid #e6e6e6;
padding: 1em;
font-size: 16px;
}
.form-item {
display: grid;
grid-template-columns: 30% 1fr;
margin-bottom: 16px;
align-items: center;
gap: 1em;
}
hr {
border: 1px solid #f5f5f5;
margin: 40px 0px;
}
hr:nth-last-child(2) {
border: 1px solid #f5f5f5;
margin: 40px 0px;
}
.button-block {
display: flex;
justify-content: flex-end;
}
button {
font-size: 16px;
padding: 8px 16px;
box-sizing: border-box;
border-radius: 4px;
color: white;
background-color: black;
outline: none;
height: 40px;
cursor: pointer;
transition: all 0.2s ease 0s;
overflow: hidden;
outline: none;
user-select: none;
white-space: nowrap;
text-align: center;
}
button:hover {
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1),
0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
input[type="checkbox"] {
transform: scale(2);
cursor: pointer;
}
select::-ms-expand {
display: none;
}
select {
cursor: pointer;
display: inline-block;
align-items: baseline;
box-sizing: border-box;
padding: 1em 1em;
border: 1px solid #eaeaea;
border-radius: 5px;
font: inherit;
line-height: inherit;
-webkit-appearance: none;
-moz-appearance: none;
-ms-appearance: none;
appearance: none;
background-repeat: no-repeat;
background-image: linear-gradient(45deg, transparent 50%, currentColor 50%),
linear-gradient(135deg, currentColor 50%, transparent 50%);
background-position: right 17px top 1.5em, right 10px top 1.5em;
background-size: 7px 7px, 7px 7px;
}
</style>

View File

@ -3,7 +3,6 @@
export let _bb
export let model
export let layout = "list"
let headers = []
let store = _bb.store
@ -33,39 +32,5 @@
})
</script>
<section class:grid={layout === 'grid'} class:list={layout === 'list'}>
<div class="data-card" bind:this={target} />
<section bind:this={target}>
</section>
<style>
.list {
display: flex;
flex-direction: column;
font-family: Inter;
}
.grid {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
}
ul {
list-style-type: none;
}
li {
margin: 5px 0 5px 0;
}
.data-card {
border: 1px solid #ccc;
border-radius: 5px;
padding: 10px;
}
.data-key {
font-weight: bold;
font-size: 20px;
text-transform: capitalize;
}
</style>

View File

@ -0,0 +1,52 @@
<script>
import { onMount } from "svelte"
export let _bb
export let model
let headers = []
let store = _bb.store
let target
async function fetchFirstRecord() {
const FETCH_RECORDS_URL = `/api/views/all_${model}`
const response = await _bb.api.get(FETCH_RECORDS_URL)
if (response.status === 200) {
const allRecords = await response.json()
if (allRecords.length > 0) return allRecords[0]
}
}
async function fetchData() {
const pathParts = window.location.pathname.split("/")
let record
// if srcdoc, then we assume this is the builder preview
if(pathParts.length === 0 || pathParts[0] === "srcdoc") {
record = await fetchFirstRecord()
} else {
const id = pathParts[pathParts.length - 1]
const GET_RECORD_URL = `/api/${model}/records/${id}`
const response = await _bb.api.get(GET_RECORD_URL)
if (response.status === 200) {
record = await response.json()
}
}
if (record) {
_bb.attachChildren(target, {
hydrate: false,
context: record,
})
} else {
throw new Error("Failed to fetch record.", response)
}
}
onMount(async () => {
await fetchData()
})
</script>
<section bind:this={target}>
</section>

View File

@ -16,9 +16,11 @@ export { default as icon } from "./Icon.svelte"
export { default as Navigation } from "./Navigation.svelte"
export { default as datatable } from "./DataTable.svelte"
export { default as dataform } from "./DataForm.svelte"
export { default as dataformwide } from "./DataFormWide.svelte"
export { default as datachart } from "./DataChart.svelte"
export { default as datalist } from "./DataList.svelte"
export { default as list } from "./List.svelte"
export { default as datasearch } from "./DataSearch.svelte"
export { default as datamap } from "./DataMap.svelte"
export { default as embed } from "./Embed.svelte"
export { default as recorddetail } from "./RecordDetail.svelte"