Merge branch 'master' of github.com:Budibase/budibase into external-tables

This commit is contained in:
Martin McKeaveney 2021-01-07 13:39:48 +00:00
commit 963d8cb679
25 changed files with 395 additions and 255 deletions

View File

@ -366,20 +366,23 @@ export const getFrontendStore = () => {
await Promise.all(promises) await Promise.all(promises)
}, },
updateStyle: async (type, name, value) => { updateStyle: async (type, name, value) => {
let promises = []
const selected = get(selectedComponent) const selected = get(selectedComponent)
if (value == null || value === "") {
store.update(state => { delete selected._styles[type][name]
if (!selected._styles) { } else {
selected._styles = {}
}
selected._styles[type][name] = value selected._styles[type][name] = value
}
// save without messing with the store await store.actions.preview.saveSelected()
promises.push(store.actions.preview.saveSelected()) },
return state updateCustomStyle: async style => {
}) const selected = get(selectedComponent)
await Promise.all(promises) selected._styles.custom = style
await store.actions.preview.saveSelected()
},
resetStyles: async () => {
const selected = get(selectedComponent)
selected._styles = { normal: {}, hover: {}, active: {} }
await store.actions.preview.saveSelected()
}, },
updateProp: (name, value) => { updateProp: (name, value) => {
store.update(state => { store.update(state => {

View File

@ -1,6 +1,6 @@
<script> <script>
import { onMount } from "svelte" import { onMount } from "svelte"
import { store, currentAsset } from "builderStore" import { store, currentAsset, selectedComponent } from "builderStore"
import iframeTemplate from "./iframeTemplate" import iframeTemplate from "./iframeTemplate"
import { Screen } from "builderStore/store/screenTemplates/utils/Screen" import { Screen } from "builderStore/store/screenTemplates/utils/Screen"
import { FrontendTypes } from "../../../constants" import { FrontendTypes } from "../../../constants"
@ -33,6 +33,7 @@
layout, layout,
screen, screen,
selectedComponentId, selectedComponentId,
previewType: $store.currentFrontEndType,
} }
// Saving pages and screens to the DB causes them to have _revs. // Saving pages and screens to the DB causes them to have _revs.
@ -54,17 +55,18 @@
// Refresh the preview when required // Refresh the preview when required
$: refreshContent(strippedJson) $: refreshContent(strippedJson)
// Initialise the app when mounted
onMount(() => { onMount(() => {
// Initialise the app when mounted
iframe.contentWindow.addEventListener( iframe.contentWindow.addEventListener(
"bb-ready", "bb-ready",
() => { () => refreshContent(strippedJson),
refreshContent(strippedJson) { once: true }
},
{
once: true,
}
) )
// Add listener to select components
iframe.contentWindow.addEventListener("bb-select-component", data => {
store.actions.components.select({ _id: data.detail })
})
}) })
</script> </script>

View File

@ -10,6 +10,9 @@
*, *:before, *:after { *, *:before, *:after {
box-sizing: border-box; box-sizing: border-box;
} }
* {
pointer-events: none;
}
</style> </style>
<script src='/assets/budibase-client.js'></script> <script src='/assets/budibase-client.js'></script>
<script> <script>
@ -19,7 +22,7 @@
} }
// Extract data from message // Extract data from message
const { selectedComponentId, layout, screen } = JSON.parse(event.data) const { selectedComponentId, layout, screen, previewType } = JSON.parse(event.data)
// Set some flags so the app knows we're in the builder // Set some flags so the app knows we're in the builder
window["##BUDIBASE_IN_BUILDER##"] = true window["##BUDIBASE_IN_BUILDER##"] = true
@ -27,6 +30,7 @@
window["##BUDIBASE_PREVIEW_SCREEN##"] = screen window["##BUDIBASE_PREVIEW_SCREEN##"] = screen
window["##BUDIBASE_SELECTED_COMPONENT_ID##"] = selectedComponentId window["##BUDIBASE_SELECTED_COMPONENT_ID##"] = selectedComponentId
window["##BUDIBASE_PREVIEW_ID##"] = Math.random() window["##BUDIBASE_PREVIEW_ID##"] = Math.random()
window["##BUDIBASE_PREVIEW_TYPE##"] = previewType
// Initialise app // Initialise app
if (window.loadBudibase) { if (window.loadBudibase) {
@ -34,15 +38,6 @@
} }
} }
// Ignore clicks
["click", "mousedown"].forEach(type => {
document.addEventListener(type, function(e) {
e.preventDefault()
e.stopPropagation()
return false
}, true)
})
window.addEventListener("message", receiveMessage) window.addEventListener("message", receiveMessage)
window.dispatchEvent(new Event("bb-ready")) window.dispatchEvent(new Event("bb-ready"))
</script> </script>

View File

@ -62,7 +62,7 @@
<TextArea <TextArea
thin thin
bind:value bind:value
placeholder="Add text, or lick the objects on the left to add them to the placeholder="Add text, or click the objects on the left to add them to the
textbox." /> textbox." />
<div class="controls"> <div class="controls">
<a href="https://docs.budibase.com/design/binding"> <a href="https://docs.budibase.com/design/binding">

View File

@ -31,6 +31,8 @@
componentPropDefinition.properties[selectedCategory.value] componentPropDefinition.properties[selectedCategory.value]
const onStyleChanged = store.actions.components.updateStyle const onStyleChanged = store.actions.components.updateStyle
const onCustomStyleChanged = store.actions.components.updateCustomStyle
const onResetStyles = store.actions.components.resetStyles
$: isComponentOrScreen = $: isComponentOrScreen =
$store.currentView === "component" || $store.currentView === "component" ||
@ -93,7 +95,12 @@
<div class="component-props-container"> <div class="component-props-container">
{#if selectedCategory.value === 'design'} {#if selectedCategory.value === 'design'}
<DesignView {panelDefinition} {componentInstance} {onStyleChanged} /> <DesignView
{panelDefinition}
{componentInstance}
{onStyleChanged}
{onCustomStyleChanged}
{onResetStyles} />
{:else if selectedCategory.value === 'settings'} {:else if selectedCategory.value === 'settings'}
<SettingsView <SettingsView
{componentInstance} {componentInstance}

View File

@ -1,82 +0,0 @@
<script>
import { store, allScreens } from "builderStore"
import { FrontendTypes } from "constants"
import ComponentPropertiesPanel from "./ComponentPropertiesPanel.svelte"
import ComponentSelectionList from "./ComponentSelectionList.svelte"
const PROPERTIES_TAB = "properties"
const COMPONENT_SELECTION_TAB = "components"
let selected = PROPERTIES_TAB
const isSelected = tab => selected === tab
const selectTab = tab => (selected = tab)
const toggleTab = () =>
(selected =
selected === PROPERTIES_TAB ? COMPONENT_SELECTION_TAB : PROPERTIES_TAB)
</script>
<div class="root">
{#if $store.currentFrontEndType === FrontendTypes.LAYOUT || $allScreens.length}
<div class="switcher">
<button
class:selected={selected === COMPONENT_SELECTION_TAB}
on:click={() => selectTab(COMPONENT_SELECTION_TAB)}>
Add
</button>
<button
class:selected={selected === PROPERTIES_TAB}
on:click={() => selectTab(PROPERTIES_TAB)}>
Edit
</button>
</div>
<div class="panel">
{#if selected === PROPERTIES_TAB}
<ComponentPropertiesPanel {toggleTab} />
{/if}
{#if selected === COMPONENT_SELECTION_TAB}
<ComponentSelectionList {toggleTab} />
{/if}
</div>
{/if}
</div>
<style>
.root {
height: 100%;
display: flex;
flex-direction: column;
padding: 20px 5px 20px 10px;
border-left: solid 1px var(--grey-2);
}
.switcher {
display: flex;
margin: 0px 20px 20px 0px;
}
.switcher > button {
display: inline-block;
border: none;
margin: 0;
padding: 0;
cursor: pointer;
font-size: 18px;
font-weight: 600;
color: var(--grey-5);
margin-right: 20px;
}
.switcher > .selected {
color: var(--ink);
}
.panel {
height: 100%;
}
</style>

View File

@ -1,12 +1,13 @@
<script> <script>
import { onMount } from "svelte" import { TextArea, DetailSummary, Button } from "@budibase/bbui"
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 onStyleChanged = () => {} export let onStyleChanged = () => {}
export let onCustomStyleChanged = () => {}
export let onResetStyles = () => {}
let selectedCategory = "normal" let selectedCategory = "normal"
let propGroup = null let propGroup = null
@ -39,11 +40,23 @@
properties={panelDefinition[groupName]} properties={panelDefinition[groupName]}
styleCategory={selectedCategory} styleCategory={selectedCategory}
{onStyleChanged} {onStyleChanged}
{componentDefinition}
{componentInstance} {componentInstance}
open={currentGroup === groupName} open={currentGroup === groupName}
on:open={() => (currentGroup = groupName)} /> on:open={() => (currentGroup = groupName)} />
{/each} {/each}
<DetailSummary
name={`Custom Styles${componentInstance._styles.custom ? ' *' : ''}`}
on:open={() => (currentGroup = 'custom')}
show={currentGroup === 'custom'}
thin>
<div class="custom-styles">
<TextArea
value={componentInstance._styles.custom}
on:change={event => onCustomStyleChanged(event.target.value)}
placeholder="Enter some CSS..." />
</div>
</DetailSummary>
<Button secondary wide on:click={onResetStyles}>Reset Styles</Button>
{:else} {:else}
<div class="no-design"> <div class="no-design">
This component doesn't have any design properties. This component doesn't have any design properties.
@ -85,4 +98,10 @@
font-size: var(--font-size-xs); font-size: var(--font-size-xs);
color: var(--grey-5); color: var(--grey-5);
} }
.custom-styles :global(textarea) {
font-family: monospace;
min-height: 120px;
font-size: var(--font-size-xs);
}
</style> </style>

View File

@ -98,7 +98,7 @@
$: isOptionsObject = options.every(o => typeof o === "object") $: isOptionsObject = options.every(o => typeof o === "object")
$: selectedOption = isOptionsObject $: selectedOption = isOptionsObject
? options.find(o => o.value === value) ? options.find(o => o.value === value || (o.value === "" && value == null))
: {} : {}
$: if (open && selectMenu) { $: if (open && selectMenu) {

View File

@ -6,11 +6,9 @@
import { import {
readableToRuntimeBinding, readableToRuntimeBinding,
runtimeToReadableBinding, runtimeToReadableBinding,
CAPTURE_VAR_INSIDE_MUSTACHE,
} from "builderStore/replaceBindings" } from "builderStore/replaceBindings"
import { DropdownMenu } from "@budibase/bbui" import { DropdownMenu } from "@budibase/bbui"
import BindingDropdown from "components/userInterface/BindingDropdown.svelte" import BindingDropdown from "components/userInterface/BindingDropdown.svelte"
import { onMount } from "svelte"
export let label = "" export let label = ""
export let bindable = true export let bindable = true
@ -70,8 +68,8 @@
let temp = runtimeToReadableBinding(bindableProperties, value) let temp = runtimeToReadableBinding(bindableProperties, value)
return value == null && props.defaultValue !== undefined return value == null && props.initialValue !== undefined
? props.defaultValue ? props.initialValue
: temp : temp
} }

View File

@ -10,21 +10,36 @@
export let onStyleChanged = () => {} export let onStyleChanged = () => {}
export let open = false export let open = false
const hasPropChanged = (style, prop) => {
// TODO: replace color picker with one that works better.
// Currently it cannot support null values, so this is a hack which
// prevents the color fields from always being marked as changed
if (!["color", "background", "border-color"].includes(prop.key)) {
if (prop.initialValue !== undefined) {
return style[prop.key] !== prop.initialValue
}
}
return style[prop.key] != null && style[prop.key] !== ""
}
$: style = componentInstance["_styles"][styleCategory] || {} $: style = componentInstance["_styles"][styleCategory] || {}
$: changed = properties.some(prop => hasPropChanged(style, prop))
</script> </script>
<DetailSummary {name} on:open show={open} thin> <DetailSummary name={`${name}${changed ? ' *' : ''}`} on:open show={open} thin>
<div> {#if open}
{#each properties as props} <div>
<PropertyControl {#each properties as prop}
label={props.label} <PropertyControl
control={props.control} label={`${prop.label}${hasPropChanged(style, prop) ? ' *' : ''}`}
key={props.key} control={prop.control}
value={style[props.key]} key={prop.key}
onChange={(key, value) => onStyleChanged(styleCategory, key, value)} value={style[prop.key]}
props={{ ...excludeProps(props, ['control', 'label']) }} /> onChange={(key, value) => onStyleChanged(styleCategory, key, value)}
{/each} props={{ ...excludeProps(prop, ['control', 'label']) }} />
</div> {/each}
</div>
{/if}
</DetailSummary> </DetailSummary>
<style> <style>

View File

@ -41,7 +41,7 @@
{ key: "layoutId", label: "Layout", control: LayoutSelect }, { key: "layoutId", label: "Layout", control: LayoutSelect },
] ]
const layoutDefinition = [{ key: "title", label: "Title", control: Input }] const layoutDefinition = []
const canRenderControl = (key, dependsOn) => { const canRenderControl = (key, dependsOn) => {
let test = !isEmpty(componentInstance[dependsOn]) let test = !isEmpty(componentInstance[dependsOn])
@ -119,7 +119,7 @@
{/if} {/if}
{/if} {/if}
{#if panelDefinition && panelDefinition.length > 0} {#if !isLayout && panelDefinition && panelDefinition.length > 0}
{#each panelDefinition as definition} {#each panelDefinition as definition}
{#if canRenderControl(definition.key, definition.dependsOn)} {#if canRenderControl(definition.key, definition.dependsOn)}
<PropertyControl <PropertyControl

View File

@ -24,7 +24,7 @@ export const createProps = (componentDefinition, derivedFromProps) => {
const props = { const props = {
_id: uuid(), _id: uuid(),
_component: componentDefinition._component, _component: componentDefinition._component,
_styles: { normal: {}, hover: {}, active: {}, selected: {} }, _styles: { normal: {}, hover: {}, active: {} },
} }
const errors = [] const errors = []
@ -75,7 +75,7 @@ export const makePropsSafe = (componentDefinition, props) => {
} }
if (!props._styles) { if (!props._styles) {
props._styles = { normal: {}, hover: {}, active: {}, selected: {} } props._styles = { normal: {}, hover: {}, active: {} }
} }
return props return props

View File

@ -2,18 +2,16 @@ import Input from "./PropertyPanelControls/Input.svelte"
import OptionSelect from "./OptionSelect.svelte" import OptionSelect from "./OptionSelect.svelte"
import FlatButtonGroup from "./FlatButtonGroup.svelte" import FlatButtonGroup from "./FlatButtonGroup.svelte"
import Colorpicker from "@budibase/colorpicker" import Colorpicker from "@budibase/colorpicker"
/*
TODO: Allow for default values for all properties
*/
export const layout = [ export const layout = [
{ {
label: "Display", label: "Display",
key: "display", key: "display",
control: OptionSelect, control: OptionSelect,
initialValue: "",
options: [ options: [
{ label: "N/A ", value: "N/A" }, { label: "Choose option", value: "" },
{ label: "Block", value: "block" },
{ label: "Inline Block", value: "inline-block" },
{ label: "Flex", value: "flex" }, { label: "Flex", value: "flex" },
{ label: "Inline Flex", value: "inline-flex" }, { label: "Inline Flex", value: "inline-flex" },
], ],
@ -31,15 +29,15 @@ export const layout = [
padding: "0px 5px", padding: "0px 5px",
value: "columnReverse", value: "columnReverse",
}, },
{ icon: "ri-close-line", value: "" },
], ],
}, },
{ {
label: "Justify", label: "Justify",
key: "justify-content", key: "justify-content",
control: OptionSelect, control: OptionSelect,
initialValue: "Flex Start",
options: [ options: [
{ label: "", value: "" }, { label: "Choose option", value: "" },
{ label: "Flex Start", value: "flex-start" }, { label: "Flex Start", value: "flex-start" },
{ label: "Flex End", value: "flex-end" }, { label: "Flex End", value: "flex-end" },
{ label: "Center", value: "center" }, { label: "Center", value: "center" },
@ -52,8 +50,8 @@ export const layout = [
label: "Align", label: "Align",
key: "align-items", key: "align-items",
control: OptionSelect, control: OptionSelect,
initialValue: "Flex Start",
options: [ options: [
{ label: "Choose option", value: "" },
{ label: "Flex Start", value: "flex-start" }, { label: "Flex Start", value: "flex-start" },
{ label: "Flex End", value: "flex-end" }, { label: "Flex End", value: "flex-end" },
{ label: "Center", value: "center" }, { label: "Center", value: "center" },
@ -66,8 +64,9 @@ export const layout = [
key: "flex-wrap", key: "flex-wrap",
control: OptionSelect, control: OptionSelect,
options: [ options: [
{ label: "wrap", value: "wrap" }, { label: "Choose option", value: "" },
{ label: "no wrap", value: "noWrap" }, { label: "Wrap", value: "wrap" },
{ label: "No wrap", value: "nowrap" },
], ],
}, },
{ {
@ -75,6 +74,7 @@ export const layout = [
key: "gap", key: "gap",
control: OptionSelect, control: OptionSelect,
options: [ options: [
{ label: "Choose option", value: "" },
{ label: "None", value: "0px" }, { label: "None", value: "0px" },
{ label: "4px", value: "4px" }, { label: "4px", value: "4px" },
{ label: "8px", value: "8px" }, { label: "8px", value: "8px" },
@ -93,6 +93,7 @@ export const margin = [
key: "margin", key: "margin",
control: OptionSelect, control: OptionSelect,
options: [ options: [
{ label: "Choose option", value: "" },
{ label: "None", value: "0px" }, { label: "None", value: "0px" },
{ label: "4px", value: "4px" }, { label: "4px", value: "4px" },
{ label: "8px", value: "8px" }, { label: "8px", value: "8px" },
@ -112,6 +113,7 @@ export const margin = [
key: "margin-top", key: "margin-top",
control: OptionSelect, control: OptionSelect,
options: [ options: [
{ label: "Choose option", value: "" },
{ label: "None", value: "0px" }, { label: "None", value: "0px" },
{ label: "4px", value: "4px" }, { label: "4px", value: "4px" },
{ label: "8px", value: "8px" }, { label: "8px", value: "8px" },
@ -131,6 +133,7 @@ export const margin = [
key: "margin-right", key: "margin-right",
control: OptionSelect, control: OptionSelect,
options: [ options: [
{ label: "Choose option", value: "" },
{ label: "None", value: "0px" }, { label: "None", value: "0px" },
{ label: "4px", value: "4px" }, { label: "4px", value: "4px" },
{ label: "8px", value: "8px" }, { label: "8px", value: "8px" },
@ -150,6 +153,7 @@ export const margin = [
key: "margin-bottom", key: "margin-bottom",
control: OptionSelect, control: OptionSelect,
options: [ options: [
{ label: "Choose option", value: "" },
{ label: "None", value: "0px" }, { label: "None", value: "0px" },
{ label: "4px", value: "4px" }, { label: "4px", value: "4px" },
{ label: "8px", value: "8px" }, { label: "8px", value: "8px" },
@ -169,6 +173,7 @@ export const margin = [
key: "margin-left", key: "margin-left",
control: OptionSelect, control: OptionSelect,
options: [ options: [
{ label: "Choose option", value: "" },
{ label: "None", value: "0px" }, { label: "None", value: "0px" },
{ label: "4px", value: "4px" }, { label: "4px", value: "4px" },
{ label: "8px", value: "8px" }, { label: "8px", value: "8px" },
@ -191,6 +196,7 @@ export const padding = [
key: "padding", key: "padding",
control: OptionSelect, control: OptionSelect,
options: [ options: [
{ label: "Choose option", value: "" },
{ label: "None", value: "0px" }, { label: "None", value: "0px" },
{ label: "4px", value: "4px" }, { label: "4px", value: "4px" },
{ label: "8px", value: "8px" }, { label: "8px", value: "8px" },
@ -208,6 +214,7 @@ export const padding = [
key: "padding-top", key: "padding-top",
control: OptionSelect, control: OptionSelect,
options: [ options: [
{ label: "Choose option", value: "" },
{ label: "None", value: "0px" }, { label: "None", value: "0px" },
{ label: "4px", value: "4px" }, { label: "4px", value: "4px" },
{ label: "8px", value: "8px" }, { label: "8px", value: "8px" },
@ -225,6 +232,7 @@ export const padding = [
key: "padding-right", key: "padding-right",
control: OptionSelect, control: OptionSelect,
options: [ options: [
{ label: "Choose option", value: "" },
{ label: "None", value: "0px" }, { label: "None", value: "0px" },
{ label: "4px", value: "4px" }, { label: "4px", value: "4px" },
{ label: "8px", value: "8px" }, { label: "8px", value: "8px" },
@ -242,6 +250,7 @@ export const padding = [
key: "padding-bottom", key: "padding-bottom",
control: OptionSelect, control: OptionSelect,
options: [ options: [
{ label: "Choose option", value: "" },
{ label: "None", value: "0px" }, { label: "None", value: "0px" },
{ label: "4px", value: "4px" }, { label: "4px", value: "4px" },
{ label: "8px", value: "8px" }, { label: "8px", value: "8px" },
@ -259,6 +268,7 @@ export const padding = [
key: "padding-left", key: "padding-left",
control: OptionSelect, control: OptionSelect,
options: [ options: [
{ label: "Choose option", value: "" },
{ label: "None", value: "0px" }, { label: "None", value: "0px" },
{ label: "4px", value: "4px" }, { label: "4px", value: "4px" },
{ label: "8px", value: "8px" }, { label: "8px", value: "8px" },
@ -278,8 +288,8 @@ export const size = [
label: "Flex", label: "Flex",
key: "flex", key: "flex",
control: OptionSelect, control: OptionSelect,
defaultValue: "0 1 auto",
options: [ options: [
{ label: "Choose option", value: "" },
{ label: "Shrink", value: "0 1 auto" }, { label: "Shrink", value: "0 1 auto" },
{ label: "Grow", value: "1 1 auto" }, { label: "Grow", value: "1 1 auto" },
], ],
@ -333,9 +343,8 @@ export const position = [
label: "Position", label: "Position",
key: "position", key: "position",
control: OptionSelect, control: OptionSelect,
initialValue: "None",
options: [ options: [
{ label: "None", value: "none" }, { label: "Choose option", value: "" },
{ label: "Static", value: "static" }, { label: "Static", value: "static" },
{ label: "Relative", value: "relative" }, { label: "Relative", value: "relative" },
{ label: "Fixed", value: "fixed" }, { label: "Fixed", value: "fixed" },
@ -375,7 +384,18 @@ export const position = [
label: "Z-index", label: "Z-index",
key: "z-index", key: "z-index",
control: OptionSelect, control: OptionSelect,
options: ["-9999", "-3", "-2", "-1", "0", "1", "2", "3", "9999"], options: [
{ label: "Choose option", value: "" },
{ label: "-9999", value: "-9999" },
{ label: "-3", value: "-3" },
{ label: "-2", value: "-2" },
{ label: "-1", value: "-1" },
{ label: "0", value: "0" },
{ label: "1", value: "1" },
{ label: "2", value: "2" },
{ label: "3", value: "3" },
{ label: "9999", value: "9999" },
],
}, },
] ]
@ -384,22 +404,22 @@ export const typography = [
label: "Font", label: "Font",
key: "font-family", key: "font-family",
control: OptionSelect, control: OptionSelect,
defaultValue: "Arial",
options: [ options: [
"Arial", { label: "Choose option", value: "" },
"Arial Black", { label: "Arial", value: "Arial" },
"Cursive", { label: "Arial Black", value: "Arial Black" },
"Courier", { label: "Cursive", value: "Cursive" },
"Comic Sans MS", { label: "Courier", value: "Courier" },
"Helvetica", { label: "Comic Sans MS", value: "Comic Sans MS" },
"Helvetica Neue", { label: "Helvetica", value: "Helvetica" },
"Impact", { label: "Helvetica Neue", value: "Helvetica Neue" },
"Inter", { label: "Impact", value: "Impact" },
"Lucida Sans Unicode", { label: "Inter", value: "Inter" },
"Roboto", { label: "Lucida Sans Unicode", value: "Lucida Sans Unicode" },
"Roboto Mono", { label: "Roboto", value: "Roboto" },
"Times New Roman", { label: "Roboto Mono", value: "Roboto Mono" },
"Verdana", { label: "Times New Roman", value: "Times New Roman" },
{ label: "Verdana", value: "Verdana" },
], ],
styleBindingProperty: "font-family", styleBindingProperty: "font-family",
}, },
@ -407,25 +427,36 @@ export const typography = [
label: "Weight", label: "Weight",
key: "font-weight", key: "font-weight",
control: OptionSelect, control: OptionSelect,
options: ["200", "300", "400", "500", "600", "700", "800", "900"], options: [
{ label: "Choose option", value: "" },
{ label: "200", value: "200" },
{ label: "300", value: "300" },
{ label: "400", value: "400" },
{ label: "500", value: "500" },
{ label: "600", value: "600" },
{ label: "700", value: "700" },
{ label: "800", value: "800" },
{ label: "900", value: "900" },
],
}, },
{ {
label: "size", label: "size",
key: "font-size", key: "font-size",
control: OptionSelect, control: OptionSelect,
options: [ options: [
"8px", { label: "Choose option", value: "" },
"10px", { label: "8px", value: "8px" },
"12px", { label: "10px", value: "10px" },
"14px", { label: "12px", value: "12px" },
"16px", { label: "14px", value: "14px" },
"18px", { label: "16px", value: "16px" },
"20px", { label: "18px", value: "18px" },
"24px", { label: "20px", value: "20px" },
"32px", { label: "24px", value: "24px" },
"48px", { label: "32px", value: "32px" },
"60px", { label: "48px", value: "48px" },
"72px", { label: "60px", value: "60px" },
{ label: "72px", value: "72px" },
], ],
textAlign: "center", textAlign: "center",
}, },
@ -433,13 +464,21 @@ export const typography = [
label: "Line H", label: "Line H",
key: "line-height", key: "line-height",
control: OptionSelect, control: OptionSelect,
options: ["1", "1.25", "1.5", "1.75", "2", "4"], options: [
{ label: "Choose option", value: "" },
{ label: "1", value: "1" },
{ label: "1.25", value: "1.25" },
{ label: "1.5", value: "1.5" },
{ label: "1.75", value: "1.75" },
{ label: "2", value: "2" },
{ label: "4", value: "4" },
],
}, },
{ {
label: "Color", label: "Color",
key: "color", key: "color",
control: Colorpicker, control: Colorpicker,
defaultValue: "#000", initialValue: "#000",
}, },
{ {
label: "align", label: "align",
@ -450,6 +489,7 @@ export const typography = [
{ icon: "ri-align-center", padding: "0px 5px", value: "center" }, { icon: "ri-align-center", padding: "0px 5px", value: "center" },
{ icon: "ri-align-right", padding: "0px 5px", value: "right" }, { icon: "ri-align-right", padding: "0px 5px", value: "right" },
{ icon: "ri-align-justify", padding: "0px 5px", value: "justify" }, { icon: "ri-align-justify", padding: "0px 5px", value: "justify" },
{ icon: "ri-close-line", value: "" },
], ],
}, },
{ {
@ -460,16 +500,15 @@ export const typography = [
{ text: "BB", value: "uppercase" }, { text: "BB", value: "uppercase" },
{ text: "Bb", value: "capitalize" }, { text: "Bb", value: "capitalize" },
{ text: "bb", value: "lowercase" }, { text: "bb", value: "lowercase" },
{ text: "&times;", value: "none" }, { icon: "ri-close-line", value: "" },
], ],
}, },
{ {
label: "Decoration", label: "Decoration",
key: "text-decoration-line", key: "text-decoration-line",
control: OptionSelect, control: OptionSelect,
defaultValue: "None",
options: [ options: [
{ label: "None", value: "none" }, { label: "Choose option", value: "" },
{ label: "Underline", value: "underline" }, { label: "Underline", value: "underline" },
{ label: "Overline", value: "overline" }, { label: "Overline", value: "overline" },
{ label: "Line-through", value: "line-through" }, { label: "Line-through", value: "line-through" },
@ -483,15 +522,15 @@ export const background = [
label: "Color", label: "Color",
key: "background", key: "background",
control: Colorpicker, control: Colorpicker,
defaultValue: "#000", initialValue: "#000",
}, },
{ {
label: "Gradient", label: "Gradient",
key: "background-image", key: "background-image",
control: OptionSelect, control: OptionSelect,
defaultValue: "",
options: [ options: [
{ label: "Select option", value: "" }, { label: "Choose option", value: "" },
{ label: "None", value: "none" },
{ {
label: "Warm Flame", label: "Warm Flame",
value: "linear-gradient(45deg, #ff9a9e 0%, #fad0c4 99%, #fad0c4 100%);", value: "linear-gradient(45deg, #ff9a9e 0%, #fad0c4 99%, #fad0c4 100%);",
@ -567,7 +606,7 @@ export const background = [
label: "Image", label: "Image",
key: "background", key: "background",
control: Input, control: Input,
placeholder: "url", placeholder: "URL",
}, },
] ]
@ -576,8 +615,8 @@ export const border = [
label: "Radius", label: "Radius",
key: "border-radius", key: "border-radius",
control: OptionSelect, control: OptionSelect,
defaultValue: "None",
options: [ options: [
{ label: "Choose option", value: "" },
{ label: "None", value: "0" }, { label: "None", value: "0" },
{ label: "X Small", value: "0.125rem" }, { label: "X Small", value: "0.125rem" },
{ label: "Small", value: "0.25rem" }, { label: "Small", value: "0.25rem" },
@ -592,8 +631,8 @@ export const border = [
label: "Width", label: "Width",
key: "border-width", key: "border-width",
control: OptionSelect, control: OptionSelect,
defaultValue: "None",
options: [ options: [
{ label: "Choose option", value: "" },
{ label: "None", value: "0" }, { label: "None", value: "0" },
{ label: "X Small", value: "0.5px" }, { label: "X Small", value: "0.5px" },
{ label: "Small", value: "1px" }, { label: "Small", value: "1px" },
@ -606,24 +645,24 @@ export const border = [
label: "Color", label: "Color",
key: "border-color", key: "border-color",
control: Colorpicker, control: Colorpicker,
defaultValue: "#000", initialValue: "#000",
}, },
{ {
label: "Style", label: "Style",
key: "border-style", key: "border-style",
control: OptionSelect, control: OptionSelect,
defaultValue: "None",
options: [ options: [
"None", { label: "Choose option", value: "" },
"Hidden", { label: "None", value: "none" },
"Dotted", { label: "Hidden", value: "hidden" },
"Dashed", { label: "Dotted", value: "dotted" },
"Solid", { label: "Dashed", value: "dashed" },
"Double", { label: "Solid", value: "solid" },
"Groove", { label: "Double", value: "double" },
"Ridge", { label: "Groove", value: "groove" },
"Inset", { label: "Ridge", value: "ridge" },
"Outset", { label: "Inset", value: "inset" },
{ label: "Outset", value: "outset" },
], ],
}, },
] ]
@ -634,14 +673,22 @@ export const effects = [
key: "opacity", key: "opacity",
control: OptionSelect, control: OptionSelect,
textAlign: "center", textAlign: "center",
options: ["0", "0.2", "0.4", "0.6", "0.8", "1"], options: [
{ label: "Choose option", value: "" },
{ label: "0", value: "0" },
{ label: "0.2", value: "0.2" },
{ label: "0.4", value: "0.4" },
{ label: "0.6", value: "0.6" },
{ label: "0.8", value: "0.8" },
{ label: "1", value: "1" },
],
}, },
{ {
label: "Rotate", label: "Rotate",
key: "transform", key: "transform",
control: OptionSelect, control: OptionSelect,
defaultValue: "0",
options: [ options: [
{ label: "Choose option", value: "" },
{ label: "None", value: "0" }, { label: "None", value: "0" },
{ label: "45 deg", value: "rotate(45deg)" }, { label: "45 deg", value: "rotate(45deg)" },
{ label: "90 deg", value: "rotate(90deg)" }, { label: "90 deg", value: "rotate(90deg)" },
@ -657,8 +704,8 @@ export const effects = [
label: "Shadow", label: "Shadow",
key: "box-shadow", key: "box-shadow",
control: OptionSelect, control: OptionSelect,
defaultValue: "None",
options: [ options: [
{ label: "Choose option", value: "" },
{ label: "None", value: "none" }, { label: "None", value: "none" },
{ label: "X Small", value: "0 1px 2px 0 rgba(0, 0, 0, 0.05)" }, { label: "X Small", value: "0 1px 2px 0 rgba(0, 0, 0, 0.05)" },
{ {
@ -691,19 +738,20 @@ export const transitions = [
key: "transition-property", key: "transition-property",
control: OptionSelect, control: OptionSelect,
options: [ options: [
"None", { label: "Choose option", value: "" },
"All", { label: "None", value: "none" },
"Background Color", { label: "All", value: "all" },
"Color", { label: "Background Color", value: "background color" },
"Font Size", { label: "Color", value: "color" },
"Font Weight", { label: "Font Size", value: "font size" },
"Height", { label: "Font Weight", value: "font weight" },
"Margin", { label: "Height", value: "height" },
"Opacity", { label: "Margin", value: "margin" },
"Padding", { label: "Opacity", value: "opacity" },
"Rotate", { label: "Padding", value: "padding" },
"Shadow", { label: "Rotate", value: "rotate" },
"Width", { label: "Shadow", value: "shadow" },
{ label: "Width", value: "width" },
], ],
}, },
{ {
@ -712,13 +760,28 @@ export const transitions = [
control: OptionSelect, control: OptionSelect,
textAlign: "center", textAlign: "center",
placeholder: "sec", placeholder: "sec",
options: ["0.4s", "0.6s", "0.8s", "1s", "2s", "4s"], options: [
{ label: "Choose option", value: "" },
{ label: "0.4s", value: "0.4s" },
{ label: "0.6s", value: "0.6s" },
{ label: "0.8s", value: "0.8s" },
{ label: "1s", value: "1s" },
{ label: "2s", value: "2s" },
{ label: "4s", value: "4s" },
],
}, },
{ {
label: "Ease", label: "Ease",
key: "transition-timing-function:", key: "transition-timing-function",
control: OptionSelect, control: OptionSelect,
options: ["linear", "ease", "ease-in", "ease-out", "ease-in-out"], options: [
{ label: "Choose option", value: "" },
{ label: "Linear", value: "linear" },
{ label: "Ease", value: "ease" },
{ label: "Ease in", value: "ease-in" },
{ label: "Ease out", value: "ease-out" },
{ label: "Ease in out", value: "ease-in-out" },
],
}, },
] ]

View File

@ -1,7 +1,11 @@
<script> <script>
import { store, backendUiStore, currentAsset } from "builderStore" import {
store,
backendUiStore,
currentAsset,
selectedComponent,
} from "builderStore"
import { onMount } from "svelte" import { onMount } from "svelte"
import { FrontendTypes } from "constants"
import CurrentItemPreview from "components/userInterface/AppPreview" import CurrentItemPreview from "components/userInterface/AppPreview"
import ComponentPropertiesPanel from "components/userInterface/ComponentPropertiesPanel.svelte" import ComponentPropertiesPanel from "components/userInterface/ComponentPropertiesPanel.svelte"
import ComponentSelectionList from "components/userInterface/ComponentSelectionList.svelte" import ComponentSelectionList from "components/userInterface/ComponentSelectionList.svelte"
@ -43,7 +47,7 @@
{/if} {/if}
</div> </div>
{#if $store.currentFrontEndType === FrontendTypes.SCREEN || $store.currentFrontEndType === FrontendTypes.LAYOUT} {#if $selectedComponent != null}
<div class="components-pane"> <div class="components-pane">
<ComponentPropertiesPanel /> <ComponentPropertiesPanel />
</div> </div>

View File

@ -9,6 +9,7 @@
setContext("sdk", SDK) setContext("sdk", SDK)
setContext("component", writable({})) setContext("component", writable({}))
setContext("data", createDataStore()) setContext("data", createDataStore())
setContext("screenslot", false)
let loaded = false let loaded = false

View File

@ -8,8 +8,9 @@
export let definition = {} export let definition = {}
// Get local data binding context // Get contexts
const dataContext = getContext("data") const dataContext = getContext("data")
const screenslotContext = getContext("screenslot")
// Create component context // Create component context
const componentStore = writable({}) const componentStore = writable({})
@ -20,10 +21,15 @@
$: children = definition._children $: children = definition._children
$: id = definition._id $: id = definition._id
$: enrichedProps = enrichProps(definition, $dataContext, $bindingStore) $: enrichedProps = enrichProps(definition, $dataContext, $bindingStore)
$: selected = id === $builderStore.selectedComponentId $: styles = definition._styles
// Allow component selection in the builder preview if we're previewing a
// layout, or we're preview a screen and we're inside the screenslot
$: allowSelection =
$builderStore.previewType === "layout" || screenslotContext
// Update component context // Update component context
$: componentStore.set({ id, styles: { ...definition._styles, selected } }) $: componentStore.set({ id, styles: { ...styles, id, allowSelection } })
// Gets the component constructor for the specified component // Gets the component constructor for the specified component
const getComponentConstructor = component => { const getComponentConstructor = component => {

View File

@ -1,5 +1,5 @@
<script> <script>
import { getContext } from "svelte" import { getContext, setContext } from "svelte"
import Router from "svelte-spa-router" import Router from "svelte-spa-router"
import { routeStore } from "../store" import { routeStore } from "../store"
import Screen from "./Screen.svelte" import Screen from "./Screen.svelte"
@ -7,6 +7,9 @@
const { styleable } = getContext("sdk") const { styleable } = getContext("sdk")
const component = getContext("component") const component = getContext("component")
// Set context flag so components know that we're now inside the screenslot
setContext("screenslot", true)
// Only wrap this as an array to take advantage of svelte keying, // Only wrap this as an array to take advantage of svelte keying,
// to ensure the svelte-spa-router is fully remounted when route config // to ensure the svelte-spa-router is fully remounted when route config
// changes // changes

View File

@ -11,6 +11,7 @@ const loadBudibase = () => {
screen: window["##BUDIBASE_PREVIEW_SCREEN##"], screen: window["##BUDIBASE_PREVIEW_SCREEN##"],
selectedComponentId: window["##BUDIBASE_SELECTED_COMPONENT_ID##"], selectedComponentId: window["##BUDIBASE_SELECTED_COMPONENT_ID##"],
previewId: window["##BUDIBASE_PREVIEW_ID##"], previewId: window["##BUDIBASE_PREVIEW_ID##"],
previewType: window["##BUDIBASE_PREVIEW_TYPE##"],
}) })
// Create app if one hasn't been created yet // Create app if one hasn't been created yet

View File

@ -1,8 +1,8 @@
import * as API from "./api" import * as API from "./api"
import { authStore, routeStore, screenStore, bindingStore } from "./store" import { authStore, routeStore, screenStore, bindingStore } from "./store"
import { styleable } from "./utils/styleable" import { styleable } from "./utils/styleable"
import { linkable } from "./utils/linkable"
import { getAppId } from "./utils/getAppId" import { getAppId } from "./utils/getAppId"
import { link as linkable } from "svelte-spa-router"
import DataProvider from "./components/DataProvider.svelte" import DataProvider from "./components/DataProvider.svelte"
export default { export default {

View File

@ -7,8 +7,20 @@ const createBuilderStore = () => {
screen: null, screen: null,
selectedComponentId: null, selectedComponentId: null,
previewId: null, previewId: null,
previewType: null,
}
const store = writable(initialState)
const actions = {
selectComponent: id => {
window.dispatchEvent(
new CustomEvent("bb-select-component", { detail: id })
)
},
}
return {
...store,
actions,
} }
return writable(initialState)
} }
export const builderStore = createBuilderStore() export const builderStore = createBuilderStore()

View File

@ -0,0 +1,10 @@
import { get } from "svelte/store"
import { link } from "svelte-spa-router"
import { builderStore } from "../store"
export const linkable = (node, href) => {
if (get(builderStore).inBuilder) {
return
}
link(node, href)
}

View File

@ -1,47 +1,111 @@
import { get } from "svelte/store"
import { builderStore } from "../store"
const selectedComponentWidth = 2
const selectedComponentColor = "#4285f4"
/** /**
* Helper to build a CSS string from a style object * Helper to build a CSS string from a style object.
*/ */
const buildStyleString = (styles, selected) => { const buildStyleString = (styleObject, customStyles) => {
let str = "" let str = ""
if (selected) { Object.entries(styleObject).forEach(([style, value]) => {
styles.border = "2px solid #0055ff !important"
}
Object.entries(styles).forEach(([style, value]) => {
if (style && value != null) { if (style && value != null) {
str += `${style}: ${value}; ` str += `${style}: ${value}; `
} }
}) })
return str + (customStyles || "")
}
/**
* Applies styles to enrich the builder preview.
* Applies styles to highlight the selected component, and allows pointer
* events for any selectable components (overriding the blanket ban on pointer
* events in the iframe HTML).
*/
const addBuilderPreviewStyles = (styleString, componentId, selectable) => {
let str = styleString
// Apply extra styles if we're in the builder preview
const state = get(builderStore)
if (state.inBuilder) {
// Allow pointer events and always enable cursor
if (selectable) {
str += ";pointer-events: all !important; cursor: pointer !important;"
}
// Highlighted selected element
if (componentId === state.selectedComponentId) {
str += `;box-shadow: 0 0 0 ${selectedComponentWidth}px ${selectedComponentColor} inset !important;`
}
}
return str return str
} }
/** /**
* Svelte action to apply correct component styles. * Svelte action to apply correct component styles.
* This also applies handlers for selecting components from the builder preview.
*/ */
export const styleable = (node, styles = {}) => { export const styleable = (node, styles = {}) => {
let applyNormalStyles let applyNormalStyles
let applyHoverStyles let applyHoverStyles
let selectComponent
// Kill JS even bubbling
const blockEvent = event => {
event.preventDefault()
event.stopPropagation()
return false
}
// Creates event listeners and applies initial styles // Creates event listeners and applies initial styles
const setupStyles = newStyles => { const setupStyles = newStyles => {
const selected = newStyles.selected const componentId = newStyles.id
const normalStyles = newStyles.normal || {} const selectable = newStyles.allowSelection
const customStyles = newStyles.custom
const normalStyles = newStyles.normal
const hoverStyles = { const hoverStyles = {
...normalStyles, ...normalStyles,
...newStyles.hover, ...newStyles.hover,
} }
applyNormalStyles = () => { // Applies a style string to a DOM node, enriching it for the builder
node.style = buildStyleString(normalStyles, selected) // preview
const applyStyles = styleString => {
node.style = addBuilderPreviewStyles(styleString, componentId, selectable)
} }
// Applies the "normal" style definition
applyNormalStyles = () => {
applyStyles(buildStyleString(normalStyles, customStyles))
}
// Applies any "hover" styles as well as the base "normal" styles
applyHoverStyles = () => { applyHoverStyles = () => {
node.style = buildStyleString(hoverStyles, selected) applyStyles(buildStyleString(hoverStyles, customStyles))
}
// Handler to select a component in the builder when clicking it in the
// builder preview
selectComponent = event => {
builderStore.actions.selectComponent(newStyles.id)
return blockEvent(event)
} }
// Add listeners to toggle hover styles // Add listeners to toggle hover styles
node.addEventListener("mouseover", applyHoverStyles) node.addEventListener("mouseover", applyHoverStyles)
node.addEventListener("mouseout", applyNormalStyles) node.addEventListener("mouseout", applyNormalStyles)
// Add builder preview click listener
if (get(builderStore).inBuilder) {
node.addEventListener("click", selectComponent, false)
// Kill other interaction events
node.addEventListener("mousedown", blockEvent)
node.addEventListener("mouseup", blockEvent)
}
// Apply initial normal styles // Apply initial normal styles
applyNormalStyles() applyNormalStyles()
} }
@ -50,6 +114,13 @@ export const styleable = (node, styles = {}) => {
const removeListeners = () => { const removeListeners = () => {
node.removeEventListener("mouseover", applyHoverStyles) node.removeEventListener("mouseover", applyHoverStyles)
node.removeEventListener("mouseout", applyNormalStyles) node.removeEventListener("mouseout", applyNormalStyles)
// Remove builder preview click listener
if (get(builderStore).inBuilder) {
node.removeEventListener("click", selectComponent)
node.removeEventListener("mousedown", blockEvent)
node.removeEventListener("mouseup", blockEvent)
}
} }
// Apply initial styles // Apply initial styles

View File

@ -8,8 +8,8 @@ exports.save = async function(ctx) {
if (!layout.props) { if (!layout.props) {
layout = { layout = {
...layout,
...EMPTY_LAYOUT, ...EMPTY_LAYOUT,
...layout,
} }
} }
@ -34,7 +34,7 @@ exports.destroy = async function(ctx) {
) )
).rows.map(element => element.doc.layoutId) ).rows.map(element => element.doc.layoutId)
if (layoutsUsedByScreens.includes(layoutId)) { if (layoutsUsedByScreens.includes(layoutId)) {
ctx.throw(400, "Cannot delete a base layout") ctx.throw(400, "Cannot delete a layout that's being used by a screen")
} }
await db.remove(layoutId, layoutRev) await db.remove(layoutId, layoutRev)

View File

@ -16,7 +16,17 @@ const EMPTY_LAYOUT = {
_id: "7fcf11e4-6f5b-4085-8e0d-9f3d44c98967", _id: "7fcf11e4-6f5b-4085-8e0d-9f3d44c98967",
_component: "##builtin/screenslot", _component: "##builtin/screenslot",
_styles: { _styles: {
normal: {}, normal: {
flex: "1 1 auto",
display: "flex",
"flex-direction": "column",
"justify-content": "flex-start",
"align-items": "stretch",
"max-width": "100%",
"margin-left": "20px",
"margin-right": "20px",
width: "1400px",
},
hover: {}, hover: {},
active: {}, active: {},
selected: {}, selected: {},
@ -28,7 +38,16 @@ const EMPTY_LAYOUT = {
_styles: { _styles: {
active: {}, active: {},
hover: {}, hover: {},
normal: {}, normal: {
display: "flex",
"flex-direction": "column",
"align-items": "center",
"justify-content": "flex-start",
"margin-right": "auto",
"margin-left": "auto",
"min-height": "100%",
"background-image": "#f5f5f5",
},
selected: {}, selected: {},
}, },
className: "", className: "",

View File

@ -1,11 +1,4 @@
<script> <div>
import { getContext } from "svelte"
const component = getContext("component")
const { styleable } = getContext("sdk")
</script>
<div use:styleable={$component.styles}>
<h1>Screen Slot</h1> <h1>Screen Slot</h1>
<span> <span>
The screens that you create will be displayed inside this box. The screens that you create will be displayed inside this box.