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 7f40e89e67
70 changed files with 1540 additions and 537 deletions

View File

@ -55,7 +55,7 @@
] ]
}, },
"dependencies": { "dependencies": {
"@budibase/bbui": "^1.13.0", "@budibase/bbui": "^1.15.0",
"@budibase/client": "^0.0.32", "@budibase/client": "^0.0.32",
"@nx-js/compiler-util": "^2.0.0", "@nx-js/compiler-util": "^2.0.0",
"codemirror": "^5.51.0", "codemirror": "^5.51.0",
@ -70,6 +70,7 @@
"safe-buffer": "^5.1.2", "safe-buffer": "^5.1.2",
"shortid": "^2.2.8", "shortid": "^2.2.8",
"string_decoder": "^1.2.0", "string_decoder": "^1.2.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"
}, },

View File

@ -185,7 +185,11 @@ export default {
svelte({ svelte({
// enable run-time checks when not in production // enable run-time checks when not in production
dev: !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 // we'll extract any component CSS out into
// a separate file — better for performance // a separate file — better for performance
css: css => { css: css => {

View File

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

View File

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

View File

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

View File

@ -25,7 +25,7 @@
function selectModel(model, fieldId) { function selectModel(model, fieldId) {
backendUiStore.actions.models.select(model) backendUiStore.actions.models.select(model)
$goto(`./model/${model._id}`)
if (fieldId) { if (fieldId) {
backendUiStore.update(state => { backendUiStore.update(state => {
state.selectedField = fieldId 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", name: "Screen Placeholder",
route: "*", route: "*",
props: { props: {
_component: "@budibase/standard-components/container", "_id": "49c3d0a2-7028-46f0-b004-7eddf62ad01c",
type: "div", "_component": "@budibase/standard-components/container",
_children: [ "_styles": {
{ "normal": {
_component: "@budibase/standard-components/container", "padding": "0px",
_styles: { normal: {}, hover: {}, active: {}, selected: {} }, "font-family": "Roboto",
_id: "__screenslot__text", "border-width": "0",
_code: "", "border-style": "None",
className: "", "text-align": "center"
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",
},
],
}, },
"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> export default `<html>
<head> <head>
<link rel="stylesheet" href="https://rsms.me/inter/inter.css">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Roboto+Mono">
<style> <style>
body, html { body, html {
height: 100%!important; height: 100%!important;
font-family: Roboto !important; font-family: Roboto !important;
} }
*, *:before, *:after {
box-sizing: border-box;
}
.lay-__screenslot__text { .lay-__screenslot__text {
width: 100%; width: 100%;
height: 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> <script>
import { buildStyle } from "./helpers.js" import {buildStyle} from "../helpers.js"
import { fade } from "svelte/transition" import { fade } from "svelte/transition"
export let backgroundSize = "10px" export let backgroundSize = "10px"

View File

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

View File

@ -1,11 +1,11 @@
<script> <script>
import Colorpicker from "./Colorpicker.svelte" import Colorpicker from "./Colorpicker.svelte"
import CheckedBackground from "./CheckedBackground.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 { fade } from "svelte/transition"
import { getColorFormat } from "./utils.js" import { getColorFormat } from "../utils.js"
export let value = "#3ec1d3ff" export let value = "#3ec1d3ff"
export let swatches = [] export let swatches = []
@ -15,7 +15,8 @@
export let height = "25px" export let height = "25px"
let format = "hexa" 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 colorPreview = null
let previewHeight = null let previewHeight = null
@ -23,17 +24,8 @@
let pickerWidth = 0 let pickerWidth = 0
let pickerHeight = 0 let pickerHeight = 0
let anchorEl = null
let parentNodes = []
let errorMsg = null 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() const dispatch = createEventDispatcher()
beforeUpdate(() => { 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) { function openColorpicker(event) {
if (colorPreview) { 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) { function onColorChange(color) {
value = color.detail value = color.detail
dispatch("change", 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> </script>
<svelte:window on:resize={debounce(calculateDimensions, 200)} />
<div class="color-preview-container"> <div class="color-preview-container">
{#if !errorMsg} {#if !errorMsg}
<CheckedBackground borderRadius="3px" backgroundSize="8px"> <CheckedBackground borderRadius="3px" backgroundSize="8px">
@ -124,19 +105,19 @@
</CheckedBackground> </CheckedBackground>
{#if open} {#if open}
<div transition:fade class="picker-container" style={pickerStyle}> <Colorpicker
<Colorpicker style={pickerStyle}
on:change={onColorChange} on:change={onColorChange}
on:addswatch on:addswatch
on:removeswatch on:removeswatch
bind:format bind:format
bind:value bind:value
bind:pickerHeight bind:pickerHeight
bind:pickerWidth bind:pickerWidth
{swatches} bind:open
{disableSwatches} {swatches}
{open} /> {disableSwatches}
</div> />
<div on:click|self={() => (open = false)} class="overlay" /> <div on:click|self={() => (open = false)} class="overlay" />
{/if} {/if}
{:else} {:else}
@ -154,6 +135,7 @@
} }
.color-preview { .color-preview {
cursor: pointer;
border-radius: 3px; border-radius: 3px;
border: 1px solid #dedada; border: 1px solid #dedada;
} }
@ -166,12 +148,12 @@
cursor: not-allowed; cursor: not-allowed;
} }
.picker-container { /* .picker-container {
position: absolute; position: absolute;
z-index: 3; z-index: 3;
width: fit-content; width: fit-content;
height: fit-content; height: fit-content;
} } */
.overlay { .overlay {
position: fixed; position: fixed;

View File

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

View File

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

View File

@ -1,4 +1,4 @@
export const buildStyle = styles => { export function buildStyle(styles) {
let str = "" let str = ""
for (let s in styles) { for (let s in styles) {
if (styles[s]) { if (styles[s]) {
@ -12,3 +12,9 @@ export const buildStyle = styles => {
export const convertCamel = str => { export const convertCamel = str => {
return str.replace(/[A-Z]/g, match => `-${match.toLowerCase()}`) 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 export default Colorpreview

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -321,17 +321,44 @@ export default {
name: "Form", name: "Form",
description: "A component that generates a form from your data.", description: "A component that generates a form from your data.",
icon: "ri-file-edit-fill", icon: "ri-file-edit-fill",
properties: { commonProps: {},
design: { ...all }, children: [
settings: [{ label: "Model", key: "model", control: ModelSelect }], {
}, _component: "@budibase/standard-components/dataform",
_component: "@budibase/standard-components/dataform", name: "Form Basic",
template: { icon: "ri-file-edit-fill",
component: "@budibase/materialdesign-components/Form", properties: {
description: "Form for saving a record", design: { ...all },
name: "@budibase/materialdesign-components/recordForm", settings: [
}, {
children: [], 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", name: "Chart",
@ -379,7 +406,7 @@ export default {
{ {
name: "List", name: "List",
_component: "@budibase/standard-components/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", icon: "ri-file-list-fill",
properties: { properties: {
design: { ...all }, design: { ...all },
@ -387,6 +414,18 @@ export default {
}, },
children: [], 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", name: "Map",
_component: "@budibase/standard-components/datamap", _component: "@budibase/standard-components/datamap",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,10 +1,11 @@
<script> <script>
import Modal from "svelte-simple-modal" import Modal from "svelte-simple-modal"
import { store, workflowStore } from "builderStore" import { store, workflowStore } from "builderStore"
import SettingsLink from "components/settings/Link.svelte"
import { get } from "builderStore/api" import { get } from "builderStore/api"
import { fade } from "svelte/transition" 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 { SettingsIcon, PreviewIcon } from "components/common/Icons/"
import IconButton from "components/common/IconButton.svelte" import IconButton from "components/common/IconButton.svelte"
@ -26,6 +27,22 @@
throw new Error(pkg) 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> </script>
<Modal> <Modal>
@ -45,7 +62,7 @@
<span <span
class:active={$isActive(path)} class:active={$isActive(path)}
class="topnavitem" class="topnavitem"
on:click={() => $goto(path)}> on:click={topItemNavigate(path)}>
{title} {title}
</span> </span>
{/each} {/each}
@ -54,12 +71,7 @@
hoverColor="var(--secondary75)"/> --> hoverColor="var(--secondary75)"/> -->
</div> </div>
<div class="toprightnav"> <div class="toprightnav">
<span <SettingsLink />
class:active={$isActive(`/settings`)}
class="topnavitemright"
on:click={() => $goto(`/settings`)}>
<SettingsIcon />
</span>
<span <span
class:active={false} class:active={false}
class="topnavitemright" 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> <script>
import { goto } from "@sveltech/routify" import { goto } from "@sveltech/routify"
$goto("../database") $goto("../model")
</script> </script>
<!-- routify:options index=false --> <!-- 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 { Button } from "@budibase/bbui"
import EmptyModel from "components/nav/ModelNavigator/EmptyModel.svelte" import EmptyModel from "components/nav/ModelNavigator/EmptyModel.svelte"
import ModelDataTable from "components/database/ModelDataTable" import ModelDataTable from "components/database/ModelDataTable"
import { store, 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/ModelDataTable/api"
import { CreateEditRecordModal } from "components/database/ModelDataTable/modals" 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) { if ($leftover) {
// Get the correct screen children. // Get the correct screen children.
const screenChildren = $store.pages[$params.page]._screens.find( const screenChildren = $store.pages[$params.page]._screens.find(
screen => screen.name === $params.screen screen => screen.props._instanceName === $params.screen
).props._children ).props._children
findComponent(componentIds, screenChildren) findComponent(componentIds, screenChildren)
} }

View File

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

View File

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

View File

@ -59,7 +59,7 @@ const _setup = ({ handlerTypes, getCurrentState, bb, store }) => node => {
const propValue = props[propName] const propValue = props[propName]
// A little bit of a hack - won't bind if the string doesn't start with {{ // 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) { if (isBound) {
initialProps[propName] = mustache.render(propValue, { 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) { exports.destroy = async function(ctx) {
const database = new CouchDB(ctx.user.instanceId) const database = new CouchDB(ctx.user.instanceId)

View File

@ -8,6 +8,7 @@ const router = Router()
router router
.get("/api/users", authorized(LIST_USERS), controller.fetch) .get("/api/users", authorized(LIST_USERS), controller.fetch)
.get("/api/users/:username", authorized(USER_MANAGEMENT), controller.find) .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) .post("/api/users", authorized(USER_MANAGEMENT), controller.create)
.delete( .delete(
"/api/users/:username", "/api/users/:username",

View File

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

View File

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

View File

@ -17,6 +17,9 @@
margin: 0px; margin: 0px;
padding: 0px; padding: 0px;
} }
*, *:before, *:after {
box-sizing: border-box;
}
</style> </style>
{{ each(options.stylesheets) }} {{ each(options.stylesheets) }}
@ -31,6 +34,9 @@
<link rel='stylesheet' href='/assets{{ pageStyle }}'> <link rel='stylesheet' href='/assets{{ pageStyle }}'>
{{ /if }} {{ /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/clientFrontendDefinition.js'></script>
<script src='/assets/budibase-client.js'></script> <script src='/assets/budibase-client.js'></script>

View File

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

View File

@ -4,6 +4,12 @@
export let _bb export let _bb
export let model export let model
const TYPE_MAP = {
string: "text",
boolean: "checkbox",
number: "number",
}
let username let username
let password let password
let newModel = { let newModel = {
@ -59,15 +65,23 @@
<form class="form" on:submit|preventDefault> <form class="form" on:submit|preventDefault>
<h1>{modelDef.name} Form</h1> <h1>{modelDef.name} Form</h1>
<hr />
<div class="form-content"> <div class="form-content">
{#each fields as field} {#each fields as field}
<div class="form-item"> <div class="form-item">
<label class="form-label" for="form-stacked-text">{field}</label> <label class="form-label" for="form-stacked-text">{field}</label>
<input {#if schema[field].type === 'string' && schema[field].constraints.inclusion}
class="input" <select on:blur={handleInput(field)}>
placeholder={field} {#each schema[field].constraints.inclusion as opt}
type={schema[field].type === 'string' ? 'text' : schema[field].type} <option>{opt}</option>
on:change={handleInput(field)} /> {/each}
</select>
{:else}
<input
class="input"
type={TYPE_MAP[schema[field].type]}
on:change={handleInput(field)} />
{/if}
</div> </div>
<hr /> <hr />
{/each} {/each}
@ -143,8 +157,42 @@
} }
button:hover { button:hover {
background-color: white; box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1),
border-color: #393c44; 0 4px 6px -2px rgba(0, 0, 0, 0.05);
color: #393c44; }
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> </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 _bb
export let model export let model
export let layout = "list"
let headers = [] let headers = []
let store = _bb.store let store = _bb.store
@ -33,39 +32,5 @@
}) })
</script> </script>
<section class:grid={layout === 'grid'} class:list={layout === 'list'}> <section bind:this={target}>
<div class="data-card" bind:this={target} />
</section> </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 Navigation } from "./Navigation.svelte"
export { default as datatable } from "./DataTable.svelte" export { default as datatable } from "./DataTable.svelte"
export { default as dataform } from "./DataForm.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 datachart } from "./DataChart.svelte"
export { default as datalist } from "./DataList.svelte" export { default as datalist } from "./DataList.svelte"
export { default as list } from "./List.svelte" export { default as list } from "./List.svelte"
export { default as datasearch } from "./DataSearch.svelte" export { default as datasearch } from "./DataSearch.svelte"
export { default as datamap } from "./DataMap.svelte" export { default as datamap } from "./DataMap.svelte"
export { default as embed } from "./Embed.svelte" export { default as embed } from "./Embed.svelte"
export { default as recorddetail } from "./RecordDetail.svelte"