Merge branch 'feature/sql-relationships' of github.com:Budibase/budibase into feature/opinionated-relationships-ui

This commit is contained in:
Martin McKeaveney 2021-06-30 15:06:42 +01:00
commit 3c64f870bd
72 changed files with 2225 additions and 1600 deletions

View File

@ -0,0 +1,239 @@
<script>
import { createEventDispatcher } from "svelte"
import "@spectrum-css/popover/dist/index-vars.css"
import clickOutside from "../Actions/click_outside"
import { fly } from "svelte/transition"
import Icon from "../Icon/Icon.svelte"
import Input from "../Form/Input.svelte"
import { capitalise } from "../utils/helpers"
export let value
export let size = "M"
let open = false
$: color = value || "transparent"
$: customValue = getCustomValue(value)
$: checkColor = getCheckColor(value)
const dispatch = createEventDispatcher()
const categories = [
{
label: "Grays",
colors: [
"white",
"gray-100",
"gray-200",
"gray-300",
"gray-400",
"gray-500",
"gray-600",
"gray-700",
"gray-800",
"gray-900",
"black",
],
},
{
label: "Colors",
colors: [
"red-400",
"orange-400",
"yellow-400",
"green-400",
"seafoam-400",
"blue-400",
"indigo-400",
"magenta-400",
"red-500",
"orange-500",
"yellow-500",
"green-500",
"seafoam-500",
"blue-500",
"indigo-500",
"magenta-500",
"red-600",
"orange-600",
"yellow-600",
"green-600",
"seafoam-600",
"blue-600",
"indigo-600",
"magenta-600",
"red-700",
"orange-700",
"yellow-700",
"green-700",
"seafoam-700",
"blue-700",
"indigo-700",
"magenta-700",
],
},
]
const onChange = value => {
dispatch("change", value)
open = false
}
const getCustomValue = value => {
if (!value) {
return value
}
let found = false
const comparisonValue = value.substring(35, value.length - 1)
for (let category of categories) {
found = category.colors.includes(comparisonValue)
if (found) {
break
}
}
return found ? null : value
}
const prettyPrint = color => {
return capitalise(color.split("-").join(" "))
}
const getCheckColor = value => {
return /^.*(white|(gray-(50|75|100|200|300|400|500)))\)$/.test(value)
? "black"
: "white"
}
</script>
<div class="container">
<div
class="preview size--{size || 'M'}"
style="background: {color};"
on:click={() => (open = true)}
/>
{#if open}
<div
use:clickOutside={() => (open = false)}
transition:fly={{ y: -20, duration: 200 }}
class="spectrum-Popover spectrum-Popover--bottom spectrum-Picker-popover is-open"
>
{#each categories as category}
<div class="category">
<div class="heading">{category.label}</div>
<div class="colors">
{#each category.colors as color}
<div
on:click={() => {
onChange(`var(--spectrum-global-color-static-${color})`)
}}
class="color"
style="background: var(--spectrum-global-color-static-{color}); color: {checkColor};"
title={prettyPrint(color)}
>
{#if value === `var(--spectrum-global-color-static-${color})`}
<Icon name="Checkmark" size="S" />
{/if}
</div>
{/each}
</div>
</div>
{/each}
<div class="category category--custom">
<div class="heading">Custom</div>
<div class="custom">
<Input
updateOnChange={false}
quiet
placeholder="Hex, RGB, HSL..."
value={customValue}
on:change
/>
<Icon
size="S"
name="Close"
hoverable
on:click={() => onChange(null)}
/>
</div>
</div>
</div>
{/if}
</div>
<style>
.container {
position: relative;
}
.preview {
width: 32px;
height: 32px;
border-radius: 100%;
transition: border-color 130ms ease-in-out;
box-shadow: 0 0 0 1px var(--spectrum-global-color-gray-300);
}
.preview:hover {
cursor: pointer;
box-shadow: 0 0 2px 2px var(--spectrum-global-color-gray-300);
}
.size--S {
width: 20px;
height: 20px;
}
.size--M {
width: 32px;
height: 32px;
}
.size--L {
width: 48px;
height: 48px;
}
.spectrum-Popover {
width: 210px;
z-index: 999;
top: 100%;
padding: var(--spacing-l) var(--spacing-xl);
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
gap: var(--spacing-xl);
}
.colors {
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr;
gap: var(--spacing-xs);
}
.heading {
font-size: var(--font-size-s);
font-weight: 600;
letter-spacing: 0.14px;
flex: 1 1 auto;
text-transform: uppercase;
grid-column: 1 / 5;
margin-bottom: var(--spacing-s);
}
.color {
height: 16px;
width: 16px;
border-radius: 100%;
box-shadow: 0 0 0 1px var(--spectrum-global-color-gray-300);
display: grid;
place-items: center;
}
.color:hover {
cursor: pointer;
box-shadow: 0 0 2px 2px var(--spectrum-global-color-gray-300);
}
.custom {
display: grid;
grid-template-columns: 1fr auto;
align-items: center;
gap: var(--spacing-m);
margin-right: var(--spacing-xs);
}
.category--custom .heading {
margin-bottom: var(--spacing-xs);
}
</style>

View File

@ -2,13 +2,15 @@
import Icon from "../Icon/Icon.svelte" import Icon from "../Icon/Icon.svelte"
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
export let name
export let show = false
export let collapsible = true
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
export let thin = false
export let name,
show = false
const onHeaderClick = () => { const onHeaderClick = () => {
if (!collapsible) {
return
}
show = !show show = !show
if (show) { if (show) {
dispatch("open") dispatch("open")
@ -16,14 +18,14 @@
} }
</script> </script>
<div class="property-group-container" class:thin> <div class="property-group-container">
<div class="property-group-name" on:click={onHeaderClick}> <div class="property-group-name" on:click={onHeaderClick}>
<div class:thin class="name">{name}</div> <div class="name">{name}</div>
<div class="icon"> {#if collapsible}
<Icon size="S" name={show ? "Remove" : "Add"} /> <Icon size="S" name={show ? "Remove" : "Add"} />
</div> {/if}
</div> </div>
<div class="property-panel" class:show> <div class="property-panel" class:show={show || !collapsible}>
<slot /> <slot />
</div> </div>
</div> </div>
@ -32,10 +34,9 @@
.property-group-container { .property-group-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: auto; justify-content: flex-start;
justify-content: center; align-items: stretch;
border-radius: var(--border-radius-m); border-bottom: var(--border-light);
font-family: var(--font-sans);
} }
.property-group-name { .property-group-name {
@ -45,42 +46,38 @@
flex-direction: row; flex-direction: row;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: var(--spacing-m) var(--spacing-xl);
color: var(--spectrum-global-color-gray-600);
transition: color 130ms ease-in-out;
}
.property-group-name:hover {
color: var(--spectrum-global-color-gray-900);
} }
.name { .name {
text-align: left; text-align: left;
font-size: 14px; font-size: var(--font-size-s);
font-weight: 600; font-weight: 600;
letter-spacing: 0.14px; letter-spacing: 0.14px;
color: var(--ink);
flex: 1 1 auto; flex: 1 1 auto;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
text-transform: capitalize; text-transform: uppercase;
white-space: nowrap; white-space: nowrap;
user-select: none; user-select: none;
} }
.name.thin {
font-size: var(--spectrum-global-dimension-font-size-75);
}
.icon {
flex: 0 0 20px;
text-align: center;
}
.property-panel { .property-panel {
/* height: 0px;
overflow: hidden; */
display: none; display: none;
padding: var(--spacing-s) var(--spacing-xl) var(--spacing-xl)
var(--spacing-xl);
} }
.show { .show {
/* overflow: auto;
height: auto; */
display: flex; display: flex;
flex-direction: column; flex-direction: column;
flex: 1; justify-content: flex-start;
margin-top: var(--spacing-m); align-items: stretch;
gap: var(--spacing-l);
} }
</style> </style>

View File

@ -20,6 +20,7 @@
export let open = false export let open = false
export let readonly = false export let readonly = false
export let quiet = false export let quiet = false
export let autoWidth = false
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const onClick = () => { const onClick = () => {
@ -41,7 +42,11 @@
aria-haspopup="listbox" aria-haspopup="listbox"
on:mousedown={onClick} on:mousedown={onClick}
> >
<span class="spectrum-Picker-label" class:is-placeholder={isPlaceholder}> <span
class="spectrum-Picker-label"
class:is-placeholder={isPlaceholder}
class:auto-width={autoWidth}
>
{fieldText} {fieldText}
</span> </span>
{#if error} {#if error}
@ -67,11 +72,12 @@
use:clickOutside={() => (open = false)} use:clickOutside={() => (open = false)}
transition:fly={{ y: -20, duration: 200 }} transition:fly={{ y: -20, duration: 200 }}
class="spectrum-Popover spectrum-Popover--bottom spectrum-Picker-popover is-open" class="spectrum-Popover spectrum-Popover--bottom spectrum-Picker-popover is-open"
class:auto-width={autoWidth}
> >
<ul class="spectrum-Menu" role="listbox"> <ul class="spectrum-Menu" role="listbox">
{#if placeholderOption} {#if placeholderOption}
<li <li
class="spectrum-Menu-item" class="spectrum-Menu-item placeholder"
class:is-selected={isPlaceholder} class:is-selected={isPlaceholder}
role="option" role="option"
aria-selected="true" aria-selected="true"
@ -118,17 +124,28 @@
<style> <style>
.spectrum-Popover { .spectrum-Popover {
max-height: 240px; max-height: 240px;
width: 100%;
z-index: 999; z-index: 999;
top: 100%; top: 100%;
} }
.spectrum-Popover:not(.auto-width) {
width: 100%;
}
.spectrum-Popover.auto-width :global(.spectrum-Menu-itemLabel) {
white-space: nowrap;
}
.spectrum-Picker { .spectrum-Picker {
width: 100%; width: 100%;
} }
.spectrum-Picker-label { .spectrum-Picker-label:not(.auto-width) {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
width: 0; width: 0;
} }
.placeholder {
font-style: italic;
}
.spectrum-Picker-label.auto-width.is-placeholder {
padding-right: 2px;
}
</style> </style>

View File

@ -12,6 +12,7 @@
export let getOptionValue = option => option export let getOptionValue = option => option
export let readonly = false export let readonly = false
export let quiet = false export let quiet = false
export let autoWidth = false
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
let open = false let open = false
@ -51,6 +52,7 @@
{readonly} {readonly}
{fieldText} {fieldText}
{options} {options}
{autoWidth}
{getOptionLabel} {getOptionLabel}
{getOptionValue} {getOptionValue}
isPlaceholder={value == null || value === ""} isPlaceholder={value == null || value === ""}

View File

@ -10,6 +10,7 @@
export let id = null export let id = null
export let readonly = false export let readonly = false
export let updateOnChange = true export let updateOnChange = true
export let quiet = false
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
let focus = false let focus = false
@ -59,6 +60,7 @@
<div <div
class="spectrum-Textfield" class="spectrum-Textfield"
class:spectrum-Textfield--quiet={quiet}
class:is-invalid={!!error} class:is-invalid={!!error}
class:is-disabled={disabled} class:is-disabled={disabled}
class:is-focused={focus} class:is-focused={focus}

View File

@ -12,6 +12,7 @@
export let readonly = false export let readonly = false
export let error = null export let error = null
export let updateOnChange = true export let updateOnChange = true
export let quiet = false
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const onChange = e => { const onChange = e => {
@ -29,6 +30,7 @@
{value} {value}
{placeholder} {placeholder}
{type} {type}
{quiet}
on:change={onChange} on:change={onChange}
on:click on:click
on:input on:input

View File

@ -14,6 +14,7 @@
export let getOptionLabel = option => extractProperty(option, "label") export let getOptionLabel = option => extractProperty(option, "label")
export let getOptionValue = option => extractProperty(option, "value") export let getOptionValue = option => extractProperty(option, "value")
export let quiet = false export let quiet = false
export let autoWidth = false
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const onChange = e => { const onChange = e => {
@ -37,6 +38,7 @@
{value} {value}
{options} {options}
{placeholder} {placeholder}
{autoWidth}
{getOptionLabel} {getOptionLabel}
{getOptionValue} {getOptionValue}
on:change={onChange} on:change={onChange}

View File

@ -5,6 +5,8 @@
export let selected export let selected
export let vertical = false export let vertical = false
export let noPadding = false
let _id = id() let _id = id()
const tab = writable({ title: selected, id: _id }) const tab = writable({ title: selected, id: _id })
setContext("tab", tab) setContext("tab", tab)
@ -63,14 +65,17 @@
{/if} {/if}
</div> </div>
<div class="spectrum-Tabs-content spectrum-Tabs-content-{_id}" /> <div
class="spectrum-Tabs-content spectrum-Tabs-content-{_id}"
class:noPadding
/>
<style> <style>
.spectrum-Tabs { .spectrum-Tabs {
padding-left: var(--spacing-xl); padding-left: var(--spacing-xl);
padding-right: var(--spacing-xl); padding-right: var(--spacing-xl);
position: relative; position: relative;
border-width: 1px !important; border-bottom: var(--border-light);
} }
.spectrum-Tabs-content { .spectrum-Tabs-content {
margin-top: var(--spectrum-global-dimension-static-size-150); margin-top: var(--spectrum-global-dimension-static-size-150);
@ -81,4 +86,7 @@
.spectrum-Tabs--horizontal .spectrum-Tabs-selectionIndicator { .spectrum-Tabs--horizontal .spectrum-Tabs-selectionIndicator {
bottom: 0 !important; bottom: 0 !important;
} }
.noPadding {
margin: 0;
}
</style> </style>

View File

@ -55,6 +55,7 @@ export { default as Search } from "./Form/Search.svelte"
export { default as Pagination } from "./Pagination/Pagination.svelte" export { default as Pagination } from "./Pagination/Pagination.svelte"
export { default as Badge } from "./Badge/Badge.svelte" export { default as Badge } from "./Badge/Badge.svelte"
export { default as StatusLight } from "./StatusLight/StatusLight.svelte" export { default as StatusLight } from "./StatusLight/StatusLight.svelte"
export { default as ColorPicker } from "./ColorPicker/ColorPicker.svelte"
// Typography // Typography
export { default as Body } from "./Typography/Body.svelte" export { default as Body } from "./Typography/Body.svelte"

View File

@ -4,3 +4,5 @@ export const generateID = () => {
// Starts with a letter so that its a valid DOM ID // Starts with a letter so that its a valid DOM ID
return `A${rand}` return `A${rand}`
} }
export const capitalise = s => s.substring(0, 1).toUpperCase() + s.substring(1)

View File

@ -488,12 +488,12 @@ export const getFrontendStore = () => {
}) })
await Promise.all(promises) await Promise.all(promises)
}, },
updateStyle: async (type, name, value) => { updateStyle: async (name, value) => {
const selected = get(selectedComponent) const selected = get(selectedComponent)
if (value == null || value === "") { if (value == null || value === "") {
delete selected._styles[type][name] delete selected._styles.normal[name]
} else { } else {
selected._styles[type][name] = value selected._styles.normal[name] = value
} }
await store.actions.preview.saveSelected() await store.actions.preview.saveSelected()
}, },

View File

@ -54,6 +54,10 @@ function generateTitleContainer(table) {
.type("h2") .type("h2")
.instanceName("Title") .instanceName("Title")
.text(table.name) .text(table.name)
.customProps({
size: "M",
align: "left",
})
return new Component("@budibase/standard-components/container") return new Component("@budibase/standard-components/container")
.normalStyle({ .normalStyle({

View File

@ -21,6 +21,7 @@ export class Screen extends BaseStructure {
hAlign: "stretch", hAlign: "stretch",
vAlign: "top", vAlign: "top",
size: "grow", size: "grow",
gap: "M",
}, },
routing: { routing: {
route: "", route: "",

View File

@ -25,11 +25,8 @@ export function makeLinkComponent(tableName) {
.customProps({ .customProps({
url: `/${tableName.toLowerCase()}`, url: `/${tableName.toLowerCase()}`,
openInNewTab: false, openInNewTab: false,
color: "", size: "S",
hoverColor: "", align: "left",
underline: false,
fontSize: "",
fontFamily: "initial",
}) })
} }
@ -62,6 +59,10 @@ export function makeBreadcrumbContainer(tableName, text, capitalise = false) {
.customStyle(spectrumColor(700)) .customStyle(spectrumColor(700))
.text(">") .text(">")
.instanceName("Arrow") .instanceName("Arrow")
.customProps({
size: "S",
align: "left",
})
const textStyling = { const textStyling = {
color: "#000000", color: "#000000",
@ -77,6 +78,10 @@ export function makeBreadcrumbContainer(tableName, text, capitalise = false) {
.customStyle(spectrumColor(700)) .customStyle(spectrumColor(700))
.text(text) .text(text)
.instanceName("Identifier") .instanceName("Identifier")
.customProps({
size: "S",
align: "left",
})
return new Component("@budibase/standard-components/container") return new Component("@budibase/standard-components/container")
.normalStyle({ .normalStyle({
@ -148,6 +153,10 @@ export function makeTitleContainer(title) {
.type("h2") .type("h2")
.instanceName("Title") .instanceName("Title")
.text(title) .text(title)
.customProps({
size: "M",
align: "left",
})
return new Component("@budibase/standard-components/container") return new Component("@budibase/standard-components/container")
.normalStyle({ .normalStyle({

View File

@ -53,7 +53,7 @@
let deletion let deletion
$: tableOptions = $tables.list.filter( $: tableOptions = $tables.list.filter(
table => table._id !== $tables.draft._id table => table._id !== $tables.draft._id && table.type !== "external"
) )
$: required = !!field?.constraints?.presence || primaryDisplay $: required = !!field?.constraints?.presence || primaryDisplay
$: uneditable = $: uneditable =

View File

@ -45,7 +45,7 @@
size="L" size="L"
confirmText="Create" confirmText="Create"
onConfirm={saveDatasource} onConfirm={saveDatasource}
disabled={error || !name} disabled={error || !name || !integration?.type}
> >
<Input <Input
data-cy="datasource-name-input" data-cy="datasource-name-input"

View File

@ -4,10 +4,13 @@
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"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
let iframe let iframe
let layout let layout
let screen let screen
let confirmDeleteDialog
let idToDelete
// Create screen slot placeholder for use when a page is selected rather // Create screen slot placeholder for use when a page is selected rather
// than a screen // than a screen
@ -73,15 +76,26 @@
// Add listener for events sent by cliebt library in preview // Add listener for events sent by cliebt library in preview
iframe.contentWindow.addEventListener("bb-event", event => { iframe.contentWindow.addEventListener("bb-event", event => {
const { type, data } = event.detail const { type, data } = event.detail
if (type === "select-component") { if (type === "select-component" && data.id) {
store.actions.components.select({ _id: data.id }) store.actions.components.select({ _id: data.id })
} else if (type === "update-prop") { } else if (type === "update-prop") {
store.actions.components.updateProp(data.prop, data.value) store.actions.components.updateProp(data.prop, data.value)
} else if (type === "delete-component" && data.id) {
idToDelete = data.id
confirmDeleteDialog.show()
} else { } else {
console.log(data) console.log(data)
} }
}) })
}) })
const deleteComponent = () => {
store.actions.components.delete({ _id: idToDelete })
idToDelete = null
}
const cancelDeleteComponent = () => {
idToDelete = null
}
</script> </script>
<div class="component-container"> <div class="component-container">
@ -92,6 +106,14 @@
srcdoc={template} srcdoc={template}
/> />
</div> </div>
<ConfirmDialog
bind:this={confirmDeleteDialog}
title="Confirm Deletion"
body={`Are you sure you want to delete this component?`}
okText="Delete component"
onOk={deleteComponent}
onCancel={cancelDeleteComponent}
/>
<style> <style>
.component-container { .component-container {

View File

@ -1,40 +0,0 @@
<script>
export let categories = []
export let selectedCategory = {}
export let onClick = () => {}
</script>
<div class="tabs">
{#each categories as category}
<li
data-cy={category.name}
on:click={() => onClick(category)}
class:active={selectedCategory === category}
>
{category.name}
</li>
{/each}
</div>
<style>
.tabs {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
list-style: none;
font-size: var(--font-size-m);
font-weight: 600;
height: 24px;
}
li {
color: var(--grey-5);
cursor: pointer;
margin-right: 20px;
}
.active {
color: var(--ink);
}
</style>

View File

@ -1,11 +1,13 @@
<script> <script>
import { get } from "lodash"
import { isEmpty } from "lodash/fp" import { isEmpty } from "lodash/fp"
import { Button, Checkbox, Input, Select } from "@budibase/bbui" import {
import ConfirmDialog from "components/common/ConfirmDialog.svelte" Checkbox,
import { currentAsset } from "builderStore" Input,
import { findClosestMatchingComponent } from "builderStore/storeUtils" Select,
import { makeDatasourceFormComponents } from "builderStore/store/screenTemplates/utils/commonComponents" DetailSummary,
ColorPicker,
} from "@budibase/bbui"
import { store } from "builderStore"
import PropertyControl from "./PropertyControls/PropertyControl.svelte" import PropertyControl from "./PropertyControls/PropertyControl.svelte"
import LayoutSelect from "./PropertyControls/LayoutSelect.svelte" import LayoutSelect from "./PropertyControls/LayoutSelect.svelte"
import RoleSelect from "./PropertyControls/RoleSelect.svelte" import RoleSelect from "./PropertyControls/RoleSelect.svelte"
@ -20,7 +22,6 @@
import EventsEditor from "./PropertyControls/EventsEditor" import EventsEditor from "./PropertyControls/EventsEditor"
import FilterEditor from "./PropertyControls/FilterEditor/FilterEditor.svelte" import FilterEditor from "./PropertyControls/FilterEditor/FilterEditor.svelte"
import { IconSelect } from "./PropertyControls/IconSelect" import { IconSelect } from "./PropertyControls/IconSelect"
import ColorPicker from "./PropertyControls/ColorPicker.svelte"
import StringFieldSelect from "./PropertyControls/StringFieldSelect.svelte" import StringFieldSelect from "./PropertyControls/StringFieldSelect.svelte"
import NumberFieldSelect from "./PropertyControls/NumberFieldSelect.svelte" import NumberFieldSelect from "./PropertyControls/NumberFieldSelect.svelte"
import OptionsFieldSelect from "./PropertyControls/OptionsFieldSelect.svelte" import OptionsFieldSelect from "./PropertyControls/OptionsFieldSelect.svelte"
@ -29,13 +30,11 @@
import DateTimeFieldSelect from "./PropertyControls/DateTimeFieldSelect.svelte" import DateTimeFieldSelect from "./PropertyControls/DateTimeFieldSelect.svelte"
import AttachmentFieldSelect from "./PropertyControls/AttachmentFieldSelect.svelte" import AttachmentFieldSelect from "./PropertyControls/AttachmentFieldSelect.svelte"
import RelationshipFieldSelect from "./PropertyControls/RelationshipFieldSelect.svelte" import RelationshipFieldSelect from "./PropertyControls/RelationshipFieldSelect.svelte"
import ResetFieldsButton from "./PropertyControls/ResetFieldsButton.svelte"
export let componentDefinition = {} export let componentDefinition
export let componentInstance = {} export let componentInstance
export let assetInstance export let assetInstance
export let onChange = () => {}
export let onScreenPropChange = () => {}
export let showDisplayName = false
const layoutDefinition = [] const layoutDefinition = []
const screenDefinition = [ const screenDefinition = [
@ -44,12 +43,12 @@
{ key: "routing.roleId", label: "Access", control: RoleSelect }, { key: "routing.roleId", label: "Access", control: RoleSelect },
{ key: "layoutId", label: "Layout", control: LayoutSelect }, { key: "layoutId", label: "Layout", control: LayoutSelect },
] ]
let confirmResetFieldsDialog
$: settings = componentDefinition?.settings ?? [] $: settings = componentDefinition?.settings ?? []
$: isLayout = assetInstance && assetInstance.favicon $: isLayout = assetInstance && assetInstance.favicon
$: assetDefinition = isLayout ? layoutDefinition : screenDefinition $: assetDefinition = isLayout ? layoutDefinition : screenDefinition
const updateProp = store.actions.components.updateProp
const controlMap = { const controlMap = {
text: Input, text: Input,
select: Select, select: Select,
@ -91,51 +90,19 @@
} }
return true return true
} }
const onInstanceNameChange = name => {
onChange("_instanceName", name)
}
const resetFormFields = () => {
const form = findClosestMatchingComponent(
$currentAsset.props,
componentInstance._id,
component => component._component.endsWith("/form")
)
const dataSource = form?.dataSource
const fields = makeDatasourceFormComponents(dataSource)
onChange(
"_children",
fields.map(field => field.json())
)
}
</script> </script>
<div class="settings-view-container"> <DetailSummary name="General" collapsible={false}>
{#if assetInstance} {#if !componentInstance._component.endsWith("/layout")}
{#each assetDefinition as def (`${componentInstance._id}-${def.key}`)}
<PropertyControl
bindable={false}
control={def.control}
label={def.label}
key={def.key}
value={get(assetInstance, def.key)}
onChange={val => onScreenPropChange(def.key, val)}
/>
{/each}
{/if}
{#if showDisplayName}
<PropertyControl <PropertyControl
bindable={false} bindable={false}
control={Input} control={Input}
label="Name" label="Name"
key="_instanceName" key="_instanceName"
value={componentInstance._instanceName} value={componentInstance._instanceName}
onChange={onInstanceNameChange} onChange={val => updateProp("_instanceName", val)}
/> />
{/if} {/if}
{#if settings && settings.length > 0} {#if settings && settings.length > 0}
{#each settings as setting (`${componentInstance._id}-${setting.key}`)} {#each settings as setting (`${componentInstance._id}-${setting.key}`)}
{#if canRenderControl(setting)} {#if canRenderControl(setting)}
@ -147,52 +114,28 @@
value={componentInstance[setting.key] ?? value={componentInstance[setting.key] ??
componentInstance[setting.key]?.defaultValue} componentInstance[setting.key]?.defaultValue}
{componentInstance} {componentInstance}
onChange={val => onChange(setting.key, val)} onChange={val => updateProp(setting.key, val)}
props={{ options: setting.options, placeholder: setting.placeholder }} props={{
options: setting.options,
placeholder: setting.placeholder,
}}
/> />
{/if} {/if}
{/each} {/each}
{:else} {/if}
<div class="text">This component doesn't have any additional settings.</div> {#if componentDefinition?.component?.endsWith("/fieldgroup")}
<ResetFieldsButton {componentInstance} />
{/if} {/if}
{#if componentDefinition?.info} {#if componentDefinition?.info}
<div class="text"> <div class="text">
{@html componentDefinition?.info} {@html componentDefinition?.info}
</div> </div>
{/if} {/if}
</DetailSummary>
{#if componentDefinition?.component?.endsWith("/fieldgroup")}
<div class="buttonWrapper">
<Button secondary wide on:click={() => confirmResetFieldsDialog?.show()}>
Update Form Fields
</Button>
</div>
{/if}
</div>
<ConfirmDialog
bind:this={confirmResetFieldsDialog}
body={`All components inside this group will be deleted and replaced with fields to match the schema. Are you sure you want to update this Field Group?`}
okText="Update"
onOk={resetFormFields}
title="Confirm Form Field Update"
/>
<style> <style>
.settings-view-container {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
gap: var(--spacing-s);
}
.text { .text {
font-size: var(--spectrum-global-dimension-font-size-75); font-size: var(--spectrum-global-dimension-font-size-75);
margin-top: var(--spacing-m);
color: var(--grey-6); color: var(--grey-6);
} }
.buttonWrapper {
margin-top: 10px;
display: flex;
flex-direction: column;
}
</style> </style>

View File

@ -0,0 +1,60 @@
<script>
import {
TextArea,
DetailSummary,
ActionButton,
Drawer,
DrawerContent,
Layout,
Body,
Button,
} from "@budibase/bbui"
import { store } from "builderStore"
export let componentInstance
let tempValue
let drawer
const openDrawer = () => {
tempValue = componentInstance?._styles?.custom
drawer.show()
}
const save = () => {
store.actions.components.updateCustomStyle(tempValue)
drawer.hide()
}
</script>
<DetailSummary
name={`Custom CSS${componentInstance?._styles?.custom ? " *" : ""}`}
collapsible={false}
>
<div>
<ActionButton on:click={openDrawer}>Edit custom CSS</ActionButton>
</div>
</DetailSummary>
<Drawer bind:this={drawer} title="Custom CSS">
<Button cta slot="buttons" on:click={save}>Save</Button>
<DrawerContent slot="body">
<div class="content">
<Layout gap="S">
<Body size="S">Custom CSS overrides all other component styles.</Body>
<TextArea bind:value={tempValue} placeholder="Enter some CSS..." />
</Layout>
</div>
</DrawerContent>
</Drawer>
<style>
.content {
max-width: 800px;
margin: 0 auto;
}
.content :global(textarea) {
font-family: monospace;
min-height: 240px !important;
font-size: var(--font-size-s);
}
</style>

View File

@ -0,0 +1,34 @@
<script>
import StyleSection from "./StyleSection.svelte"
import * as ComponentStyles from "./componentStyles"
export let componentDefinition
export let componentInstance
const getStyles = def => {
if (!def?.styles?.length) {
return [...ComponentStyles.all]
}
let styles = [...ComponentStyles.all]
def.styles.forEach(style => {
if (ComponentStyles[style]) {
styles.push(ComponentStyles[style])
}
})
return styles
}
$: styles = getStyles(componentDefinition)
</script>
{#if styles?.length > 0}
{#each styles as style}
<StyleSection
{style}
name={style.label}
columns={style.columns}
properties={style.settings}
{componentInstance}
/>
{/each}
{/if}

View File

@ -1,111 +0,0 @@
<script>
import { TextArea, DetailSummary, Button } from "@budibase/bbui"
import PropertyGroup from "./PropertyControls/PropertyGroup.svelte"
import FlatButtonGroup from "./PropertyControls/FlatButtonGroup"
import { allStyles } from "./componentStyles"
export let componentDefinition = {}
export let componentInstance = {}
export let onStyleChanged = () => {}
export let onCustomStyleChanged = () => {}
export let onResetStyles = () => {}
let selectedCategory = "normal"
let currentGroup
function onChange(category) {
selectedCategory = category
}
const buttonProps = [
{ value: "normal", text: "Normal" },
{ value: "hover", text: "Hover" },
{ value: "active", text: "Active" },
]
$: groups = componentDefinition?.styleable ? Object.keys(allStyles) : []
</script>
<div class="container">
<div class="state-categories">
<FlatButtonGroup value={selectedCategory} {buttonProps} {onChange} />
</div>
<div class="positioned-wrapper">
<div class="property-groups">
{#if groups.length > 0}
{#each groups as groupName}
<PropertyGroup
name={groupName}
properties={allStyles[groupName]}
styleCategory={selectedCategory}
{onStyleChanged}
{componentInstance}
open={currentGroup === groupName}
on:open={() => (currentGroup = groupName)}
/>
{/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.detail)}
placeholder="Enter some CSS..."
/>
</div>
</DetailSummary>
<Button secondary wide on:click={onResetStyles}>Reset Styles</Button>
{:else}
<div class="no-design">
This component doesn't have any design properties.
</div>
{/if}
</div>
</div>
</div>
<style>
.container {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
gap: var(--spacing-l);
}
.positioned-wrapper {
position: relative;
display: flex;
min-height: 0;
flex: 1 1 auto;
}
.property-groups {
flex: 1;
overflow-y: auto;
min-height: 0;
margin: 0 -20px;
padding: 0 20px;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
gap: var(--spacing-m);
}
.no-design {
font-size: var(--font-size-xs);
color: var(--grey-5);
}
.custom-styles :global(textarea) {
font-family: monospace;
min-height: 120px;
font-size: var(--font-size-xs);
}
</style>

View File

@ -1,84 +1,33 @@
<script> <script>
import { get } from "svelte/store" import { store, selectedComponent } from "builderStore"
import { store, selectedComponent, currentAsset } from "builderStore"
import { Tabs, Tab } from "@budibase/bbui" import { Tabs, Tab } from "@budibase/bbui"
import { FrontendTypes } from "constants" import ScreenSettingsSection from "./ScreenSettingsSection.svelte"
import DesignView from "./DesignView.svelte" import ComponentSettingsSection from "./ComponentSettingsSection.svelte"
import SettingsView from "./SettingsView.svelte" import DesignSection from "./DesignSection.svelte"
import { setWith } from "lodash" import CustomStylesSection from "./CustomStylesSection.svelte"
$: definition = store.actions.components.getDefinition( $: componentInstance = $selectedComponent
$: componentDefinition = store.actions.components.getDefinition(
$selectedComponent?._component $selectedComponent?._component
) )
$: isComponentOrScreen =
$store.currentView === "component" ||
$store.currentFrontEndType === FrontendTypes.SCREEN
$: isNotScreenslot = !$selectedComponent._component.endsWith("screenslot")
$: showDisplayName = isComponentOrScreen && isNotScreenslot
const onStyleChanged = store.actions.components.updateStyle
const onCustomStyleChanged = store.actions.components.updateCustomStyle
const onResetStyles = store.actions.components.resetStyles
function setAssetProps(name, value) {
const selectedAsset = get(currentAsset)
store.update(state => {
if (
name === "_instanceName" &&
state.currentFrontEndType === FrontendTypes.SCREEN
) {
selectedAsset.props._instanceName = value
} else {
setWith(selectedAsset, name.split("."), value, Object)
}
return state
})
store.actions.preview.saveSelected()
}
</script> </script>
<Tabs selected="Settings"> <Tabs selected="Settings" noPadding>
<Tab title="Settings"> <Tab title="Settings">
<div class="tab-content-padding"> <div class="container">
{#if definition && definition.name} <ScreenSettingsSection {componentInstance} {componentDefinition} />
<div class="instance-name">{definition.name}</div> <ComponentSettingsSection {componentInstance} {componentDefinition} />
{/if} <DesignSection {componentInstance} {componentDefinition} />
<SettingsView <CustomStylesSection {componentInstance} {componentDefinition} />
componentInstance={$selectedComponent}
componentDefinition={definition}
{showDisplayName}
onChange={store.actions.components.updateProp}
onScreenPropChange={setAssetProps}
assetInstance={$store.currentView !== "component" && $currentAsset}
/>
</div>
</Tab>
<Tab title="Design">
<div class="tab-content-padding">
{#if definition && definition.name}
<div class="instance-name">{definition.name}</div>
{/if}
<DesignView
componentInstance={$selectedComponent}
componentDefinition={definition}
{onStyleChanged}
{onCustomStyleChanged}
{onResetStyles}
/>
</div> </div>
</Tab> </Tab>
</Tabs> </Tabs>
<style> <style>
.tab-content-padding { .container {
padding: 0 var(--spacing-xl); display: flex;
} flex-direction: column;
justify-content: flex-start;
.instance-name { align-items: stretch;
font-size: var(--spectrum-global-dimension-font-size-75);
margin-bottom: var(--spacing-m);
margin-top: var(--spacing-xs);
font-weight: 600;
color: var(--grey-7);
} }
</style> </style>

View File

@ -1,5 +1,5 @@
<script> <script>
import { Button, Icon, Drawer } from "@budibase/bbui" import { Button, Icon, Drawer, Label } from "@budibase/bbui"
import { store, currentAsset } from "builderStore" import { store, currentAsset } from "builderStore"
import { import {
getBindableProperties, getBindableProperties,
@ -70,7 +70,11 @@
</script> </script>
<div class="property-control" bind:this={anchor} data-cy={`setting-${key}`}> <div class="property-control" bind:this={anchor} data-cy={`setting-${key}`}>
<div class="label">{label}</div> {#if type !== "boolean"}
<div class="label">
<Label>{label}</Label>
</div>
{/if}
<div data-cy={`${key}-prop-control`} class="control"> <div data-cy={`${key}-prop-control`} class="control">
<svelte:component <svelte:component
this={control} this={control}
@ -79,63 +83,55 @@
updateOnChange={false} updateOnChange={false}
on:change={handleChange} on:change={handleChange}
onChange={handleChange} onChange={handleChange}
name={key}
text={label}
{type} {type}
{...props} {...props}
name={key}
/> />
</div> {#if bindable && !key.startsWith("_") && type === "text"}
{#if bindable && !key.startsWith("_") && type === "text"} <div
<div class="icon"
class="icon" data-cy={`${key}-binding-button`}
data-cy={`${key}-binding-button`} on:click={bindingDrawer.show}
on:click={bindingDrawer.show}
>
<Icon size="S" name="FlashOn" />
</div>
<Drawer bind:this={bindingDrawer} title={capitalise(key)}>
<svelte:fragment slot="description">
Add the objects on the left to enrich your text.
</svelte:fragment>
<Button cta slot="buttons" disabled={!valid} on:click={handleClose}
>Save</Button
> >
<BindingPanel <Icon size="S" name="FlashOn" />
slot="body" </div>
bind:valid <Drawer bind:this={bindingDrawer} title={capitalise(key)}>
value={safeValue} <svelte:fragment slot="description">
close={handleClose} Add the objects on the left to enrich your text.
on:update={e => (temporaryBindableValue = e.detail)} </svelte:fragment>
{bindableProperties} <Button cta slot="buttons" disabled={!valid} on:click={handleClose}>
/> Save
</Drawer> </Button>
{/if} <BindingPanel
slot="body"
bind:valid
value={safeValue}
close={handleClose}
on:update={e => (temporaryBindableValue = e.detail)}
{bindableProperties}
/>
</Drawer>
{/if}
</div>
</div> </div>
<style> <style>
.property-control { .property-control {
position: relative; position: relative;
display: flex; display: flex;
flex-flow: row; flex-direction: column;
align-items: center; justify-content: flex-start;
align-items: stretch;
} }
.label { .label {
display: flex;
align-items: center;
font-size: 12px;
font-weight: 400;
flex: 0 0 80px;
text-align: left;
color: var(--ink);
margin-right: auto;
text-transform: capitalize; text-transform: capitalize;
padding-bottom: var(--spectrum-global-dimension-size-65);
} }
.control { .control {
flex: 1; position: relative;
display: inline-block;
padding-left: 2px;
width: 0;
} }
.icon { .icon {

View File

@ -1,54 +0,0 @@
<script>
import PropertyControl from "./PropertyControl.svelte"
import { DetailSummary } from "@budibase/bbui"
export let name = ""
export let styleCategory = "normal"
export let properties = []
export let componentInstance = {}
export let onStyleChanged = () => {}
export let open = false
$: style = componentInstance["_styles"][styleCategory] || {}
$: changed = properties.some(prop => hasPropChanged(style, prop))
const hasPropChanged = (style, prop) => {
return style[prop.key] != null && style[prop.key] !== ""
}
const getControlProps = props => {
let controlProps = { ...(props || {}) }
delete controlProps.label
delete controlProps.key
delete controlProps.control
return controlProps
}
</script>
<DetailSummary name={`${name}${changed ? " *" : ""}`} on:open show={open} thin>
{#if open}
<div>
{#each properties as prop (`${componentInstance._id}-${prop.key}-${prop.label}`)}
<PropertyControl
bindable={false}
label={`${prop.label}${hasPropChanged(style, prop) ? " *" : ""}`}
control={prop.control}
key={prop.key}
value={style[prop.key]}
onChange={value => onStyleChanged(styleCategory, prop.key, value)}
props={getControlProps(prop)}
/>
{/each}
</div>
{/if}
</DetailSummary>
<style>
div {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
gap: var(--spacing-s);
}
</style>

View File

@ -0,0 +1,43 @@
<script>
import { ActionButton } from "@budibase/bbui"
import { currentAsset, store } from "builderStore"
import { findClosestMatchingComponent } from "builderStore/storeUtils"
import { makeDatasourceFormComponents } from "builderStore/store/screenTemplates/utils/commonComponents"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
export let componentInstance
let confirmResetFieldsDialog
const resetFormFields = () => {
const form = findClosestMatchingComponent(
$currentAsset.props,
componentInstance._id,
component => component._component.endsWith("/form")
)
const dataSource = form?.dataSource
const fields = makeDatasourceFormComponents(dataSource)
store.actions.components.updateProp(
"_children",
fields.map(field => field.json())
)
}
</script>
<div>
<ActionButton
secondary
wide
on:click={() => confirmResetFieldsDialog?.show()}
>
Update form fields
</ActionButton>
</div>
<ConfirmDialog
bind:this={confirmResetFieldsDialog}
body={`All components inside this group will be deleted and replaced with fields to match the schema. Are you sure you want to update this Field Group?`}
okText="Update"
onOk={resetFormFields}
title="Confirm Form Field Update"
/>

View File

@ -0,0 +1,50 @@
<script>
import { get } from "svelte/store"
import { get as deepGet, setWith } from "lodash"
import { Input, DetailSummary } from "@budibase/bbui"
import PropertyControl from "./PropertyControls/PropertyControl.svelte"
import LayoutSelect from "./PropertyControls/LayoutSelect.svelte"
import RoleSelect from "./PropertyControls/RoleSelect.svelte"
import { currentAsset, store } from "builderStore"
import { FrontendTypes } from "constants"
export let componentInstance
function setAssetProps(name, value) {
const selectedAsset = get(currentAsset)
store.update(state => {
if (
name === "_instanceName" &&
state.currentFrontEndType === FrontendTypes.SCREEN
) {
selectedAsset.props._instanceName = value
} else {
setWith(selectedAsset, name.split("."), value, Object)
}
return state
})
store.actions.preview.saveSelected()
}
const screenSettings = [
// { key: "description", label: "Description", control: Input },
{ key: "routing.route", label: "Route", control: Input },
{ key: "routing.roleId", label: "Access", control: RoleSelect },
{ key: "layoutId", label: "Layout", control: LayoutSelect },
]
</script>
{#if $store.currentView !== "component" && $currentAsset && $store.currentFrontEndType === FrontendTypes.SCREEN}
<DetailSummary name="Screen" collapsible={false}>
{#each screenSettings as def (`${componentInstance._id}-${def.key}`)}
<PropertyControl
bindable={false}
control={def.control}
label={def.label}
key={def.key}
value={deepGet($currentAsset, def.key)}
onChange={val => setAssetProps(def.key, val)}
/>
{/each}
</DetailSummary>
{/if}

View File

@ -0,0 +1,51 @@
<script>
import PropertyControl from "./PropertyControls/PropertyControl.svelte"
import { DetailSummary } from "@budibase/bbui"
import { store } from "builderStore"
export let name
export let columns
export let properties
export let componentInstance
$: style = componentInstance._styles.normal || {}
$: changed = properties?.some(prop => hasPropChanged(style, prop)) ?? false
const hasPropChanged = (style, prop) => {
return style[prop.key] != null && style[prop.key] !== ""
}
const getControlProps = props => {
let controlProps = { ...(props || {}) }
delete controlProps.label
delete controlProps.key
delete controlProps.control
return controlProps
}
</script>
<DetailSummary collapsible={false} name={`${name}${changed ? " *" : ""}`}>
<div class="group-content" style="grid-template-columns: {columns || '1fr'}">
{#each properties as prop (`${componentInstance._id}-${prop.key}-${prop.label}`)}
<div style="grid-column: {prop.column || 'auto'}">
<PropertyControl
bindable={false}
label={`${prop.label}${hasPropChanged(style, prop) ? " *" : ""}`}
control={prop.control}
key={prop.key}
value={style[prop.key]}
onChange={val => store.actions.components.updateStyle(prop.key, val)}
props={getControlProps(prop)}
/>
</div>
{/each}
</div>
</DetailSummary>
<style>
.group-content {
display: grid;
align-items: stretch;
gap: var(--spacing-l);
}
</style>

View File

@ -86,15 +86,17 @@
{#if datasource && integration} {#if datasource && integration}
<section> <section>
<Layout> <Layout>
<header> <Layout gap="XS" noPadding>
<svelte:component <header>
this={ICONS[datasource.source]} <svelte:component
height="26" this={ICONS[datasource.source]}
width="26" height="26"
/> width="26"
<Heading size="M">{datasource.name}</Heading> />
</header> <Heading size="M">{datasource.name}</Heading>
<Body size="S" grey lh>{integration.description}</Body> </header>
<Body size="M">{integration.description}</Body>
</Layout>
<Divider /> <Divider />
<div class="container"> <div class="container">
<div class="config-header"> <div class="config-header">
@ -188,7 +190,6 @@
} }
header { header {
margin: 0 0 var(--spacing-xs) 0;
display: flex; display: flex;
gap: var(--spacing-l); gap: var(--spacing-l);
align-items: center; align-items: center;

View File

@ -14,14 +14,16 @@
<section> <section>
<Layout> <Layout>
<header> <Layout gap="XS" noPadding>
<svelte:component this={ICONS.BUDIBASE} height="26" width="26" /> <header>
<Heading size="M">Budibase Internal</Heading> <svelte:component this={ICONS.BUDIBASE} height="26" width="26" />
</header> <Heading size="M">Budibase Internal</Heading>
<Body size="S" grey lh </header>
>Budibase internal tables are part of your app, the data will be stored in <Body size="M">
your apps context.</Body Budibase internal tables are part of your app, so the data will be
> stored in your apps context.
</Body>
</Layout>
<Divider /> <Divider />
<Heading size="S">Tables</Heading> <Heading size="S">Tables</Heading>
<div class="table-list"> <div class="table-list">
@ -32,7 +34,7 @@
> >
<Body size="S">{table.name}</Body> <Body size="S">{table.name}</Body>
{#if table.primaryDisplay} {#if table.primaryDisplay}
<Body size="S">display column: {table.primaryDisplay}</Body> <Body size="S">Display column: {table.primaryDisplay}</Body>
{/if} {/if}
</div> </div>
{/each} {/each}
@ -50,7 +52,6 @@
} }
header { header {
margin: 0 0 var(--spacing-xs) 0;
display: flex; display: flex;
gap: var(--spacing-l); gap: var(--spacing-l);
align-items: center; align-items: center;

View File

@ -91,14 +91,15 @@
{/if} {/if}
<style> <style>
#spectrum-root { #spectrum-root,
height: 100%;
width: 100%;
overflow: hidden;
}
#app-root { #app-root {
height: 100%; height: 100%;
width: 100%; width: 100%;
padding: 0;
margin: 0;
overflow: hidden;
}
#app-root {
position: relative; position: relative;
} }
</style> </style>

View File

@ -23,6 +23,11 @@
// props with old ones, depending on how long enrichment takes. // props with old ones, depending on how long enrichment takes.
let latestUpdateTime let latestUpdateTime
// Keep track of stringified representations of context and instance
// to avoid enriching bindings as much as possible
let lastContextKey
let lastInstanceKey
// Get contexts // Get contexts
const context = getContext("context") const context = getContext("context")
const insideScreenslot = !!getContext("screenslot") const insideScreenslot = !!getContext("screenslot")
@ -42,7 +47,9 @@
definition?.hasChildren && definition?.hasChildren &&
definition?.showEmptyState !== false && definition?.showEmptyState !== false &&
$builderStore.inBuilder $builderStore.inBuilder
$: updateComponentProps(instance, $context) $: rawProps = getRawProps(instance)
$: instanceKey = JSON.stringify(rawProps)
$: updateComponentProps(rawProps, instanceKey, $context)
$: selected = $: selected =
$builderStore.inBuilder && $builderStore.inBuilder &&
$builderStore.selectedComponentId === instance._id $builderStore.selectedComponentId === instance._id
@ -59,6 +66,16 @@
name, name,
}) })
const getRawProps = instance => {
let validProps = {}
Object.entries(instance)
.filter(([name]) => !name.startsWith("_"))
.forEach(([key, value]) => {
validProps[key] = value
})
return validProps
}
// Gets the component constructor for the specified component // Gets the component constructor for the specified component
const getComponentConstructor = component => { const getComponentConstructor = component => {
const split = component?.split("/") const split = component?.split("/")
@ -76,13 +93,23 @@
} }
// Enriches any string component props using handlebars // Enriches any string component props using handlebars
const updateComponentProps = (instance, context) => { const updateComponentProps = (rawProps, instanceKey, context) => {
const instanceSame = instanceKey === lastInstanceKey
const contextSame = context.key === lastContextKey
if (instanceSame && contextSame) {
return
} else {
lastInstanceKey = instanceKey
lastContextKey = context.key
}
// Record the timestamp so we can reference it after enrichment // Record the timestamp so we can reference it after enrichment
latestUpdateTime = Date.now() latestUpdateTime = Date.now()
const enrichmentTime = latestUpdateTime const enrichmentTime = latestUpdateTime
// Enrich props with context // Enrich props with context
const enrichedProps = enrichProps(instance, context) const enrichedProps = enrichProps(rawProps, context)
// Abandon this update if a newer update has started // Abandon this update if a newer update has started
if (enrichmentTime !== latestUpdateTime) { if (enrichmentTime !== latestUpdateTime) {

View File

@ -14,18 +14,32 @@
const newContext = createContextStore(context) const newContext = createContextStore(context)
setContext("context", newContext) setContext("context", newContext)
$: providerKey = key || $component.id const providerKey = key || $component.id
// Add data context // Generate a permanent unique ID for this component and use it to register
$: newContext.actions.provideData(providerKey, data) // any datasource actions
const instanceId = generate()
// Instance ID is unique to each instance of a provider // Keep previous state around so we can avoid updating unless necessary
let instanceId let lastDataKey
let lastActionsKey
// Add actions context $: provideData(data)
$: { $: provideActions(actions, instanceId)
if (instanceId) {
actions?.forEach(({ type, callback, metadata }) => { const provideData = newData => {
const dataKey = JSON.stringify(newData)
if (dataKey !== lastDataKey) {
newContext.actions.provideData(providerKey, newData)
lastDataKey = dataKey
}
}
const provideActions = newActions => {
const actionsKey = JSON.stringify(newActions)
if (actionsKey !== lastActionsKey) {
lastActionsKey = actionsKey
newActions?.forEach(({ type, callback, metadata }) => {
newContext.actions.provideAction(providerKey, type, callback) newContext.actions.provideAction(providerKey, type, callback)
// Register any "refresh datasource" actions with a singleton store // Register any "refresh datasource" actions with a singleton store
@ -43,10 +57,6 @@
} }
onMount(() => { onMount(() => {
// Generate a permanent unique ID for this component and use it to register
// any datasource actions
instanceId = generate()
// Unregister all datasource instances when unmounting this provider // Unregister all datasource instances when unmounting this provider
return () => dataSourceStore.actions.unregisterInstance(instanceId) return () => dataSourceStore.actions.unregisterInstance(instanceId)
}) })

View File

@ -1,6 +1,8 @@
<script> <script>
import { onMount, onDestroy } from "svelte" import { onMount, onDestroy } from "svelte"
import SettingsButton from "./SettingsButton.svelte" import SettingsButton from "./SettingsButton.svelte"
import SettingsColorPicker from "./SettingsColorPicker.svelte"
import SettingsPicker from "./SettingsPicker.svelte"
import { builderStore } from "../../store" import { builderStore } from "../../store"
import { domDebounce } from "../../utils/domDebounce" import { domDebounce } from "../../utils/domDebounce"
@ -87,19 +89,44 @@
> >
{#each settings as setting, idx} {#each settings as setting, idx}
{#if setting.type === "select"} {#if setting.type === "select"}
{#each setting.options as option} {#if setting.barStyle === "buttons"}
<SettingsButton {#each setting.options as option}
<SettingsButton
prop={setting.key}
value={option.value}
icon={option.barIcon}
title={option.barTitle}
/>
{/each}
{:else}
<SettingsPicker
prop={setting.key} prop={setting.key}
value={option.value} options={setting.options}
icon={option.barIcon} label={setting.label}
title={option.barTitle}
/> />
{/each} {/if}
{:else if setting.type === "boolean"}
<SettingsButton
prop={setting.key}
icon={setting.barIcon}
title={setting.barTitle}
bool
/>
{:else if setting.type === "color"}
<SettingsColorPicker prop={setting.key} />
{/if} {/if}
{#if idx < settings.length - 1} {#if setting.barSeparator !== false}
<div class="divider" /> <div class="divider" />
{/if} {/if}
{/each} {/each}
<SettingsButton
icon="Delete"
on:click={() => {
builderStore.actions.deleteComponent(
$builderStore.selectedComponent._id
)
}}
/>
</div> </div>
{/if} {/if}

View File

@ -1,6 +1,7 @@
<script> <script>
import { Icon } from "@budibase/bbui" import { Icon } from "@budibase/bbui"
import { builderStore } from "../../store" import { builderStore } from "../../store"
import { createEventDispatcher } from "svelte"
export let prop export let prop
export let value export let value
@ -9,6 +10,7 @@
export let rotate = false export let rotate = false
export let bool = false export let bool = false
const dispatch = createEventDispatcher()
$: currentValue = $builderStore.selectedComponent?.[prop] $: currentValue = $builderStore.selectedComponent?.[prop]
$: active = prop && (bool ? !!currentValue : currentValue === value) $: active = prop && (bool ? !!currentValue : currentValue === value)
</script> </script>
@ -22,6 +24,7 @@
const newValue = bool ? !currentValue : value const newValue = bool ? !currentValue : value
builderStore.actions.updateProp(prop, newValue) builderStore.actions.updateProp(prop, newValue)
} }
dispatch("click")
}} }}
> >
<Icon name={icon} size="S" /> <Icon name={icon} size="S" />

View File

@ -0,0 +1,26 @@
<script>
import { ColorPicker } from "@budibase/bbui"
import { builderStore } from "../../store"
export let prop
$: currentValue = $builderStore.selectedComponent?.[prop]
</script>
<div>
<ColorPicker
size="S"
value={currentValue}
on:change={e => {
if (prop) {
builderStore.actions.updateProp(prop, e.detail)
}
}}
/>
</div>
<style>
div {
padding: 0 4px;
}
</style>

View File

@ -0,0 +1,31 @@
<script>
import { Select } from "@budibase/bbui"
import { builderStore } from "../../store"
export let prop
export let options
export let label
$: currentValue = $builderStore.selectedComponent?.[prop]
</script>
<div>
<Select
quiet
autoWidth
placeholder={label}
{options}
value={currentValue}
on:change={e => {
if (prop) {
builderStore.actions.updateProp(prop, e.detail)
}
}}
/>
</div>
<style>
div {
padding: 0 4px;
}
</style>

View File

@ -56,13 +56,14 @@ const createBuilderStore = () => {
const actions = { const actions = {
selectComponent: id => { selectComponent: id => {
if (id) { dispatchEvent("select-component", { id })
dispatchEvent("select-component", { id })
}
}, },
updateProp: (prop, value) => { updateProp: (prop, value) => {
dispatchEvent("update-prop", { prop, value }) dispatchEvent("update-prop", { prop, value })
}, },
deleteComponent: id => {
dispatchEvent("delete-component", { id })
},
} }
return { return {
...writableStore, ...writableStore,

View File

@ -4,7 +4,21 @@ export const createContextStore = oldContext => {
const newContext = writable({}) const newContext = writable({})
const contexts = oldContext ? [oldContext, newContext] : [newContext] const contexts = oldContext ? [oldContext, newContext] : [newContext]
const totalContext = derived(contexts, $contexts => { const totalContext = derived(contexts, $contexts => {
return $contexts.reduce((total, context) => ({ ...total, ...context }), {}) // The key is the serialized representation of context
let key = ""
for (let i = 0; i < $contexts.length - 1; i++) {
key += $contexts[i].key
}
key += JSON.stringify($contexts[$contexts.length - 1])
// Reduce global state
const reducer = (total, context) => ({ ...total, ...context })
const context = $contexts.reduce(reducer, {})
return {
...context,
key,
}
}) })
// Adds a data context layer to the tree // Adds a data context layer to the tree

View File

@ -31,8 +31,14 @@ const triggerAutomationHandler = async action => {
} }
const navigationHandler = action => { const navigationHandler = action => {
if (action.parameters.url) { const { url } = action.parameters
routeStore.actions.navigate(action.parameters.url) if (url) {
const external = !url.startsWith("/")
if (external) {
window.location.href = url
} else {
routeStore.actions.navigate(action.parameters.url)
}
} }
} }

View File

@ -22,14 +22,6 @@ export const propsAreSame = (a, b) => {
* Data bindings are enriched, and button actions are enriched. * Data bindings are enriched, and button actions are enriched.
*/ */
export const enrichProps = (props, context) => { export const enrichProps = (props, context) => {
// Exclude all private props that start with an underscore
let validProps = {}
Object.entries(props)
.filter(([name]) => !name.startsWith("_"))
.forEach(([key, value]) => {
validProps[key] = value
})
// Create context of all bindings and data contexts // Create context of all bindings and data contexts
// Duplicate the closest context as "data" which the builder requires // Duplicate the closest context as "data" which the builder requires
const totalContext = { const totalContext = {
@ -41,7 +33,7 @@ export const enrichProps = (props, context) => {
} }
// Enrich all data bindings in top level props // Enrich all data bindings in top level props
let enrichedProps = enrichDataBindings(validProps, totalContext) let enrichedProps = enrichDataBindings(props, totalContext)
// Enrich click actions if they exist // Enrich click actions if they exist
if (enrichedProps.onClick) { if (enrichedProps.onClick) {

View File

@ -31,6 +31,12 @@ export const styleable = (node, styles = {}) => {
if (newStyles.empty) { if (newStyles.empty) {
baseStyles.border = "2px dashed var(--grey-5)" baseStyles.border = "2px dashed var(--grey-5)"
baseStyles.padding = "var(--spacing-l)" baseStyles.padding = "var(--spacing-l)"
baseStyles.overflow = "hidden"
}
// Append border-style css if border-width is specified
if (newStyles.normal?.["border-width"]) {
baseStyles["border-style"] = "solid"
} }
const componentId = newStyles.id const componentId = newStyles.id

View File

@ -32,9 +32,11 @@ CREATE TABLE Products_Tasks (
); );
INSERT INTO Persons (PersonID, FirstName, LastName, Address, City) VALUES (1, 'Mike', 'Hughes', '123 Fake Street', 'Belfast'); INSERT INTO Persons (PersonID, FirstName, LastName, Address, City) VALUES (1, 'Mike', 'Hughes', '123 Fake Street', 'Belfast');
INSERT INTO Tasks (TaskID, PersonID, TaskName) VALUES (1, 1, 'assembling'); INSERT INTO Tasks (TaskID, PersonID, TaskName) VALUES (1, 1, 'assembling');
INSERT INTO Tasks (TaskID, PersonID, TaskName) VALUES (2, 1, 'processing');
INSERT INTO Products (ProductID, ProductName) VALUES (1, 'Computers'); INSERT INTO Products (ProductID, ProductName) VALUES (1, 'Computers');
INSERT INTO Products (ProductID, ProductName) VALUES (2, 'Laptops'); INSERT INTO Products (ProductID, ProductName) VALUES (2, 'Laptops');
INSERT INTO Products (ProductID, ProductName) VALUES (3, 'Chairs'); INSERT INTO Products (ProductID, ProductName) VALUES (3, 'Chairs');
INSERT INTO Products_Tasks (ProductID, TaskID) VALUES (1, 1); INSERT INTO Products_Tasks (ProductID, TaskID) VALUES (1, 1);
INSERT INTO Products_Tasks (ProductID, TaskID) VALUES (2, 1); INSERT INTO Products_Tasks (ProductID, TaskID) VALUES (2, 1);
INSERT INTO Products_Tasks (ProductID, TaskID) VALUES (3, 1); INSERT INTO Products_Tasks (ProductID, TaskID) VALUES (3, 1);
INSERT INTO Products_Tasks (ProductID, TaskID) VALUES (1, 2);

View File

@ -14,6 +14,7 @@ exports.save = async function (ctx) {
...EMPTY_LAYOUT, ...EMPTY_LAYOUT,
...layout, ...layout,
} }
layout.props._instanceName = layout.name
} }
layout._id = layout._id || generateLayoutID() layout._id = layout._id || generateLayoutID()

View File

@ -1,161 +1,19 @@
const { makeExternalQuery } = require("./utils") const { makeExternalQuery } = require("./utils")
const { const { DataSourceOperation, SortDirection } = require("../../../constants")
DataSourceOperation,
SortDirection,
FieldTypes,
RelationshipTypes,
} = require("../../../constants")
const { getAllExternalTables } = require("../table/utils") const { getAllExternalTables } = require("../table/utils")
const { const {
breakExternalTableId, breakExternalTableId,
generateRowIdField,
breakRowIdField, breakRowIdField,
} = require("../../../integrations/utils") } = require("../../../integrations/utils")
const { cloneDeep } = require("lodash/fp") const {
buildRelationships,
function inputProcessing(row, table) { buildFilters,
if (!row) { inputProcessing,
return row outputProcessing,
} generateIdForRow,
let newRow = {} buildFields,
for (let key of Object.keys(table.schema)) { } = require("./externalUtils")
// currently excludes empty strings const { processObjectSync } = require("@budibase/string-templates")
if (row[key]) {
newRow[key] = row[key]
}
}
return newRow
}
function generateIdForRow(row, table) {
if (!row) {
return
}
const primary = table.primary
// build id array
let idParts = []
for (let field of primary) {
idParts.push(row[field])
}
return generateRowIdField(idParts)
}
function updateRelationshipColumns(rows, row, relationships, allTables) {
const columns = {}
for (let relationship of relationships) {
const linkedTable = allTables[relationship.tableName]
if (!linkedTable) {
continue
}
const display = linkedTable.primaryDisplay
const related = {}
if (display && row[display]) {
related.primaryDisplay = row[display]
}
related._id = row[relationship.to]
columns[relationship.from] = related
}
for (let [column, related] of Object.entries(columns)) {
if (!Array.isArray(rows[row._id][column])) {
rows[row._id][column] = []
}
rows[row._id][column].push(related)
}
return rows
}
function outputProcessing(rows, table, relationships, allTables) {
// if no rows this is what is returned? Might be PG only
if (rows[0].read === true) {
return []
}
let finalRows = {}
for (let row of rows) {
row._id = generateIdForRow(row, table)
// this is a relationship of some sort
if (finalRows[row._id]) {
finalRows = updateRelationshipColumns(
finalRows,
row,
relationships,
allTables
)
continue
}
const thisRow = {}
// filter the row down to what is actually the row (not joined)
for (let fieldName of Object.keys(table.schema)) {
thisRow[fieldName] = row[fieldName]
}
thisRow._id = row._id
thisRow.tableId = table._id
thisRow._rev = "rev"
finalRows[thisRow._id] = thisRow
// do this at end once its been added to the final rows
finalRows = updateRelationshipColumns(
finalRows,
row,
relationships,
allTables
)
}
return Object.values(finalRows)
}
function buildFilters(id, filters, table) {
const primary = table.primary
// if passed in array need to copy for shifting etc
let idCopy = cloneDeep(id)
if (filters) {
// need to map over the filters and make sure the _id field isn't present
for (let filter of Object.values(filters)) {
if (filter._id) {
const parts = breakRowIdField(filter._id)
for (let field of primary) {
filter[field] = parts.shift()
}
}
// make sure this field doesn't exist on any filter
delete filter._id
}
}
// there is no id, just use the user provided filters
if (!idCopy || !table) {
return filters
}
// if used as URL parameter it will have been joined
if (typeof idCopy === "string") {
idCopy = breakRowIdField(idCopy)
}
const equal = {}
for (let field of primary) {
// work through the ID and get the parts
equal[field] = idCopy.shift()
}
return {
equal,
}
}
function buildRelationships(table) {
const relationships = []
for (let [fieldName, field] of Object.entries(table.schema)) {
if (field.type !== FieldTypes.LINK) {
continue
}
// TODO: through field
if (field.relationshipType === RelationshipTypes.MANY_TO_MANY) {
continue
}
const broken = breakExternalTableId(field.tableId)
relationships.push({
from: fieldName,
to: field.fieldName,
tableName: broken.tableName,
})
}
return relationships
}
async function handleRequest( async function handleRequest(
appId, appId,
@ -171,8 +29,9 @@ async function handleRequest(
} }
// clean up row on ingress using schema // clean up row on ingress using schema
filters = buildFilters(id, filters, table) filters = buildFilters(id, filters, table)
const relationships = buildRelationships(table) const relationships = buildRelationships(table, tables)
row = inputProcessing(row, table) const processed = inputProcessing(row, table, tables)
row = processed.row
if ( if (
operation === DataSourceOperation.DELETE && operation === DataSourceOperation.DELETE &&
(filters == null || Object.keys(filters).length === 0) (filters == null || Object.keys(filters).length === 0)
@ -186,8 +45,8 @@ async function handleRequest(
operation, operation,
}, },
resource: { resource: {
// not specifying any fields means "*" // have to specify the fields to avoid column overlap
fields: [], fields: buildFields(table, tables),
}, },
filters, filters,
sort, sort,
@ -201,6 +60,25 @@ async function handleRequest(
} }
// can't really use response right now // can't really use response right now
const response = await makeExternalQuery(appId, json) const response = await makeExternalQuery(appId, json)
// handle many to many relationships now if we know the ID (could be auto increment)
if (processed.manyRelationships) {
const promises = []
for (let toInsert of processed.manyRelationships) {
const { tableName } = breakExternalTableId(toInsert.tableId)
delete toInsert.tableId
promises.push(
makeExternalQuery(appId, {
endpoint: {
...json.endpoint,
entityId: tableName,
},
// if we're doing many relationships then we're writing, only one response
body: processObjectSync(toInsert, response[0]),
})
)
}
await Promise.all(promises)
}
// we searched for rows in someway // we searched for rows in someway
if (operation === DataSourceOperation.READ && Array.isArray(response)) { if (operation === DataSourceOperation.READ && Array.isArray(response)) {
return outputProcessing(response, table, relationships, tables) return outputProcessing(response, table, relationships, tables)

View File

@ -0,0 +1,236 @@
const {
breakExternalTableId,
generateRowIdField,
breakRowIdField,
} = require("../../../integrations/utils")
const { FieldTypes } = require("../../../constants")
const { cloneDeep } = require("lodash/fp")
exports.inputProcessing = (row, table, allTables) => {
if (!row) {
return { row, manyRelationships: [] }
}
let newRow = {},
manyRelationships = []
for (let [key, field] of Object.entries(table.schema)) {
// if set already, or not set just skip it
if (!row[key] || newRow[key]) {
continue
}
// if its not a link then just copy it over
if (field.type !== FieldTypes.LINK) {
newRow[key] = row[key]
continue
}
const { tableName: linkTableName } = breakExternalTableId(field.tableId)
// table has to exist for many to many
if (!allTables[linkTableName]) {
continue
}
const linkTable = allTables[linkTableName]
if (!field.through) {
// we don't really support composite keys for relationships, this is why [0] is used
newRow[field.foreignKey || linkTable.primary] = breakRowIdField(
row[key][0]
)[0]
} else {
row[key].map(relationship => {
// we don't really support composite keys for relationships, this is why [0] is used
manyRelationships.push({
tableId: field.through,
[linkTable.primary]: breakRowIdField(relationship)[0],
// leave the ID for enrichment later
[table.primary]: `{{ ${table.primary} }}`,
})
})
}
}
// we return the relationships that may need to be created in the through table
// we do this so that if the ID is generated by the DB it can be inserted
// after the fact
return { row: newRow, manyRelationships }
}
exports.generateIdForRow = (row, table) => {
if (!row) {
return
}
const primary = table.primary
// build id array
let idParts = []
for (let field of primary) {
idParts.push(row[field])
}
return generateRowIdField(idParts)
}
exports.updateRelationshipColumns = (rows, row, relationships, allTables) => {
const columns = {}
for (let relationship of relationships) {
const linkedTable = allTables[relationship.tableName]
if (!linkedTable) {
continue
}
const display = linkedTable.primaryDisplay
const related = {}
if (display && row[display]) {
related.primaryDisplay = row[display]
}
related._id = row[relationship.to]
columns[relationship.column] = related
}
for (let [column, related] of Object.entries(columns)) {
if (!Array.isArray(rows[row._id][column])) {
rows[row._id][column] = []
}
// make sure relationship hasn't been found already
if (!rows[row._id][column].find(relation => relation._id === related._id)) {
rows[row._id][column].push(related)
}
}
return rows
}
exports.outputProcessing = (rows, table, relationships, allTables) => {
// if no rows this is what is returned? Might be PG only
if (rows[0].read === true) {
return []
}
let finalRows = {}
for (let row of rows) {
row._id = exports.generateIdForRow(row, table)
// this is a relationship of some sort
if (finalRows[row._id]) {
finalRows = exports.updateRelationshipColumns(
finalRows,
row,
relationships,
allTables
)
continue
}
const thisRow = {}
// filter the row down to what is actually the row (not joined)
for (let fieldName of Object.keys(table.schema)) {
thisRow[fieldName] = row[fieldName]
}
thisRow._id = row._id
thisRow.tableId = table._id
thisRow._rev = "rev"
finalRows[thisRow._id] = thisRow
// do this at end once its been added to the final rows
finalRows = exports.updateRelationshipColumns(
finalRows,
row,
relationships,
allTables
)
}
return Object.values(finalRows)
}
/**
* This function is a bit crazy, but the exact purpose of it is to protect against the scenario in which
* you have column overlap in relationships, e.g. we join a few different tables and they all have the
* concept of an ID, but for some of them it will be null (if they say don't have a relationship).
* Creating the specific list of fields that we desire, and excluding the ones that are no use to us
* is more performant and has the added benefit of protecting against this scenario.
* @param {Object} table The table we are retrieving fields for.
* @param {Object[]} allTables All of the tables that exist in the external data source, this is
* needed to work out what is needed from other tables based on relationships.
* @return {string[]} A list of fields like ["products.productid"] which can be used for an SQL select.
*/
exports.buildFields = (table, allTables) => {
function extractNonLinkFieldNames(table, existing = []) {
return Object.entries(table.schema)
.filter(
column =>
column[1].type !== FieldTypes.LINK &&
!existing.find(field => field.includes(column[0]))
)
.map(column => `${table.name}.${column[0]}`)
}
let fields = extractNonLinkFieldNames(table)
for (let field of Object.values(table.schema)) {
if (field.type !== FieldTypes.LINK) {
continue
}
const { tableName: linkTableName } = breakExternalTableId(field.tableId)
const linkTable = allTables[linkTableName]
if (linkTable) {
const linkedFields = extractNonLinkFieldNames(linkTable, fields)
fields = fields.concat(linkedFields)
}
}
return fields
}
exports.buildFilters = (id, filters, table) => {
const primary = table.primary
// if passed in array need to copy for shifting etc
let idCopy = cloneDeep(id)
if (filters) {
// need to map over the filters and make sure the _id field isn't present
for (let filter of Object.values(filters)) {
if (filter._id) {
const parts = breakRowIdField(filter._id)
for (let field of primary) {
filter[field] = parts.shift()
}
}
// make sure this field doesn't exist on any filter
delete filter._id
}
}
// there is no id, just use the user provided filters
if (!idCopy || !table) {
return filters
}
// if used as URL parameter it will have been joined
if (typeof idCopy === "string") {
idCopy = breakRowIdField(idCopy)
}
const equal = {}
for (let field of primary) {
// work through the ID and get the parts
equal[field] = idCopy.shift()
}
return {
equal,
}
}
exports.buildRelationships = (table, allTables) => {
const relationships = []
for (let [fieldName, field] of Object.entries(table.schema)) {
if (field.type !== FieldTypes.LINK) {
continue
}
const { tableName: linkTableName } = breakExternalTableId(field.tableId)
// no table to link to, this is not a valid relationships
if (!allTables[linkTableName]) {
continue
}
const linkTable = allTables[linkTableName]
const definition = {
// if no foreign key specified then use the name of the field in other table
from: field.foreignKey || table.primary[0],
to: field.fieldName,
tableName: linkTableName,
through: undefined,
// need to specify where to put this back into
column: fieldName,
}
if (field.through) {
const { tableName: throughTableName } = breakExternalTableId(
field.through
)
definition.through = throughTableName
// don't support composite keys for relationships
definition.from = table.primary[0]
definition.to = linkTable.primary[0]
}
relationships.push(definition)
}
return relationships
}

View File

@ -57,6 +57,7 @@ const BASE_LAYOUTS = [
name: "Navigation Layout", name: "Navigation Layout",
props: { props: {
_id: "4f569166-a4f3-47ea-a09e-6d218c75586f", _id: "4f569166-a4f3-47ea-a09e-6d218c75586f",
_instanceName: "Navigation Layout",
_component: "@budibase/standard-components/layout", _component: "@budibase/standard-components/layout",
_children: [ _children: [
{ {
@ -102,6 +103,7 @@ const BASE_LAYOUTS = [
name: "Empty Layout", name: "Empty Layout",
props: { props: {
_id: "3723ffa1-f9e0-4c05-8013-98195c788ed6", _id: "3723ffa1-f9e0-4c05-8013-98195c788ed6",
_instanceName: "Empty Layout",
_component: "@budibase/standard-components/layout", _component: "@budibase/standard-components/layout",
_children: [ _children: [
{ {

View File

@ -20,15 +20,13 @@ exports.createHomeScreen = () => ({
_id: "ef60083f-4a02-4df3-80f3-a0d3d16847e7", _id: "ef60083f-4a02-4df3-80f3-a0d3d16847e7",
_component: "@budibase/standard-components/heading", _component: "@budibase/standard-components/heading",
_styles: { _styles: {
normal: {
"text-align": "left",
},
hover: {}, hover: {},
active: {}, active: {},
selected: {}, selected: {},
}, },
text: "Welcome to your Budibase App 👋", text: "Welcome to your Budibase App 👋",
type: "h2", size: "M",
align: "left",
_instanceName: "Heading", _instanceName: "Heading",
_children: [], _children: [],
}, },
@ -38,6 +36,7 @@ exports.createHomeScreen = () => ({
hAlign: "stretch", hAlign: "stretch",
vAlign: "top", vAlign: "top",
size: "grow", size: "grow",
gap: "M",
}, },
routing: { routing: {
route: "/", route: "/",

View File

@ -9,6 +9,10 @@ export interface TableSchema {
type: string type: string
fieldName?: string fieldName?: string
name: string name: string
tableId?: string
relationshipType?: string
through?: string
foreignKey?: string
constraints?: { constraints?: {
type?: string type?: string
email?: boolean email?: boolean

View File

@ -79,17 +79,26 @@ function addRelationships(
return query return query
} }
for (let relationship of relationships) { for (let relationship of relationships) {
const from = `${fromTable}.${relationship.from}` const from = relationship.from,
const to = `${relationship.tableName}.${relationship.to}` to = relationship.to,
toTable = relationship.tableName
if (!relationship.through) { if (!relationship.through) {
// @ts-ignore // @ts-ignore
query = query.innerJoin(relationship.tableName, from, to) query = query.leftJoin(
toTable,
`${fromTable}.${from}`,
`${relationship.tableName}.${to}`
)
} else { } else {
const through = relationship const throughTable = relationship.through
query = query query = query
// @ts-ignore // @ts-ignore
.innerJoin(through.tableName, from, through.from) .leftJoin(
.innerJoin(relationship.tableName, to, through.to) throughTable,
`${fromTable}.${from}`,
`${throughTable}.${from}`
)
.leftJoin(toTable, `${toTable}.${to}`, `${throughTable}.${to}`)
} }
} }
return query return query

View File

@ -175,17 +175,51 @@ module PostgresModule {
type, type,
} }
// // TODO: hack for testing // TODO: hack for testing
// if (tableName === "persons") { // if (tableName === "persons") {
// tables[tableName].primaryDisplay = "firstname" // tables[tableName].primaryDisplay = "firstname"
// } // }
// if (columnName.toLowerCase() === "personid" && tableName === "tasks") { // if (tableName === "products") {
// tables[tableName].schema[columnName] = { // tables[tableName].primaryDisplay = "productname"
// name: columnName, // }
// if (tableName === "tasks") {
// tables[tableName].primaryDisplay = "taskname"
// }
// if (tableName === "products") {
// tables[tableName].schema["tasks"] = {
// name: "tasks",
// type: "link",
// tableId: buildExternalTableId(datasourceId, "tasks"),
// relationshipType: "many-to-many",
// through: buildExternalTableId(datasourceId, "products_tasks"),
// fieldName: "taskid",
// }
// }
// if (tableName === "persons") {
// tables[tableName].schema["tasks"] = {
// name: "tasks",
// type: "link",
// tableId: buildExternalTableId(datasourceId, "tasks"),
// relationshipType: "many-to-one",
// fieldName: "personid",
// }
// }
// if (tableName === "tasks") {
// tables[tableName].schema["products"] = {
// name: "products",
// type: "link",
// tableId: buildExternalTableId(datasourceId, "products"),
// relationshipType: "many-to-many",
// through: buildExternalTableId(datasourceId, "products_tasks"),
// fieldName: "productid",
// }
// tables[tableName].schema["people"] = {
// name: "people",
// type: "link", // type: "link",
// tableId: buildExternalTableId(datasourceId, "persons"), // tableId: buildExternalTableId(datasourceId, "persons"),
// relationshipType: "one-to-many", // relationshipType: "one-to-many",
// fieldName: "personid", // fieldName: "personid",
// foreignKey: "personid",
// } // }
// } // }
} }

View File

@ -1,6 +1,6 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "es5", "target": "es6",
"module": "commonjs", "module": "commonjs",
"lib": ["es6"], "lib": ["es6"],
"allowJs": true, "allowJs": true,

View File

@ -4,8 +4,7 @@
"description": "This component is specific only to layouts", "description": "This component is specific only to layouts",
"icon": "Sandbox", "icon": "Sandbox",
"hasChildren": true, "hasChildren": true,
"styleable": true, "styles": ["padding", "background"],
"illegalChildren": [],
"settings": [ "settings": [
{ {
"type": "text", "type": "text",
@ -61,14 +60,15 @@
"description": "This component contains things within itself", "description": "This component contains things within itself",
"icon": "Sandbox", "icon": "Sandbox",
"hasChildren": true, "hasChildren": true,
"styleable": true,
"showSettingsBar": true, "showSettingsBar": true,
"styles": ["padding", "size", "background", "border", "shadow"],
"settings": [ "settings": [
{ {
"type": "select", "type": "select",
"label": "Direction", "label": "Direction",
"key": "direction", "key": "direction",
"showInBar": true, "showInBar": true,
"barStyle": "buttons",
"options": [ "options": [
{ {
"label": "Column", "label": "Column",
@ -90,6 +90,7 @@
"label": "Horiz. Align", "label": "Horiz. Align",
"key": "hAlign", "key": "hAlign",
"showInBar": true, "showInBar": true,
"barStyle": "buttons",
"options": [ "options": [
{ {
"label": "Left", "label": "Left",
@ -122,7 +123,8 @@
"type": "select", "type": "select",
"label": "Vert. Align", "label": "Vert. Align",
"key": "vAlign", "key": "vAlign",
"showInBar": "true", "showInBar": true,
"barStyle": "buttons",
"options": [ "options": [
{ {
"label": "Top", "label": "Top",
@ -156,6 +158,7 @@
"label": "Size", "label": "Size",
"key": "size", "key": "size",
"showInBar": true, "showInBar": true,
"barStyle": "buttons",
"options": [ "options": [
{ {
"label": "Shrink", "label": "Shrink",
@ -171,6 +174,40 @@
} }
], ],
"defaultValue": "shrink" "defaultValue": "shrink"
},
{
"type": "select",
"label": "Gap",
"key": "gap",
"showInBar": true,
"barStyle": "picker",
"options": [
{
"label": "None",
"value": "N"
},
{
"label": "Small",
"value": "S"
},
{
"label": "Medium",
"value": "M"
},
{
"label": "Large",
"value": "L"
}
],
"defaultValue": "M"
},
{
"type": "boolean",
"label": "Wrap",
"key": "wrap",
"showInBar": true,
"barIcon": "ModernGridView",
"barTitle": "Wrap"
} }
] ]
}, },
@ -179,7 +216,6 @@
"description": "Add a section to your application", "description": "Add a section to your application",
"icon": "ColumnTwoB", "icon": "ColumnTwoB",
"hasChildren": true, "hasChildren": true,
"styleable": true,
"illegalChildren": ["section"], "illegalChildren": ["section"],
"showEmptyState": false, "showEmptyState": false,
"settings": [ "settings": [
@ -194,15 +230,13 @@
"screenslot": { "screenslot": {
"name": "Screenslot", "name": "Screenslot",
"icon": "WebPage", "icon": "WebPage",
"description": "Contains your app screens", "description": "Contains your app screens"
"styleable": true
}, },
"button": { "button": {
"name": "Button", "name": "Button",
"description": "A basic html button that is ready for styling", "description": "A basic html button that is ready for styling",
"icon": "Button", "icon": "Button",
"illegalChildren": ["section"], "illegalChildren": ["section"],
"styleable": true,
"settings": [ "settings": [
{ {
"type": "text", "type": "text",
@ -211,16 +245,49 @@
}, },
{ {
"type": "select", "type": "select",
"label": "Button Type", "label": "Variant",
"key": "type", "key": "type",
"options": ["primary", "secondary", "cta", "warning"], "options": [
{
"label": "Primary",
"value": "primary"
}, {
"label": "Secondary",
"value": "secondary"
},
{
"label": "Action",
"value": "cta"
},
{
"label": "Warning",
"value": "warning"
}
],
"defaultValue": "primary" "defaultValue": "primary"
}, },
{ {
"type": "select", "type": "select",
"label": "Size", "label": "Size",
"key": "size", "key": "size",
"options": ["S", "M", "L", "XL"], "options": [
{
"label": "Small",
"value": "S"
},
{
"label": "Medium",
"value": "M"
},
{
"label": "Large",
"value": "L"
},
{
"label": "Extra large",
"value": "XL"
}
],
"defaultValue": "M" "defaultValue": "M"
}, },
{ {
@ -239,7 +306,6 @@
"name": "Repeater", "name": "Repeater",
"description": "A configurable data list that attaches to your backend tables.", "description": "A configurable data list that attaches to your backend tables.",
"icon": "ViewList", "icon": "ViewList",
"styleable": true,
"illegalChildren": ["section"], "illegalChildren": ["section"],
"hasChildren": true, "hasChildren": true,
"showSettingsBar": true, "showSettingsBar": true,
@ -265,6 +331,7 @@
"label": "Direction", "label": "Direction",
"key": "direction", "key": "direction",
"showInBar": true, "showInBar": true,
"barStyle": "buttons",
"options": [ "options": [
{ {
"label": "Column", "label": "Column",
@ -286,6 +353,7 @@
"label": "Horiz. Align", "label": "Horiz. Align",
"key": "hAlign", "key": "hAlign",
"showInBar": true, "showInBar": true,
"barStyle": "buttons",
"options": [ "options": [
{ {
"label": "Left", "label": "Left",
@ -318,7 +386,8 @@
"type": "select", "type": "select",
"label": "Vert. Align", "label": "Vert. Align",
"key": "vAlign", "key": "vAlign",
"showInBar": "true", "showInBar": true,
"barStyle": "buttons",
"options": [ "options": [
{ {
"label": "Top", "label": "Top",
@ -346,6 +415,32 @@
} }
], ],
"defaultValue": "top" "defaultValue": "top"
},
{
"type": "select",
"label": "Gap",
"key": "gap",
"showInBar": true,
"barStyle": "picker",
"options": [
{
"label": "None",
"value": "N"
},
{
"label": "Small",
"value": "S"
},
{
"label": "Medium",
"value": "M"
},
{
"label": "Large",
"value": "L"
}
],
"defaultValue": "M"
} }
], ],
"context": { "context": {
@ -356,7 +451,6 @@
"name": "Stacked List", "name": "Stacked List",
"icon": "TaskList", "icon": "TaskList",
"description": "A basic card component that can contain content and actions.", "description": "A basic card component that can contain content and actions.",
"styleable": true,
"illegalChildren": ["section"], "illegalChildren": ["section"],
"settings": [ "settings": [
{ {
@ -386,7 +480,6 @@
"name": "Vertical Card", "name": "Vertical Card",
"description": "A basic card component that can contain content and actions.", "description": "A basic card component that can contain content and actions.",
"icon": "ViewColumn", "icon": "ViewColumn",
"styleable": true,
"illegalChildren": ["section"], "illegalChildren": ["section"],
"settings": [ "settings": [
{ {
@ -447,13 +540,93 @@
"name": "Paragraph", "name": "Paragraph",
"description": "A component for displaying paragraph text.", "description": "A component for displaying paragraph text.",
"icon": "TextParagraph", "icon": "TextParagraph",
"styleable": true,
"illegalChildren": ["section"], "illegalChildren": ["section"],
"showSettingsBar": true,
"settings": [ "settings": [
{ {
"type": "text", "type": "text",
"label": "Text", "label": "Text",
"key": "text" "key": "text"
},
{
"type": "select",
"label": "Size",
"key": "size",
"defaultValue": "M",
"showInBar": true,
"barStyle": "picker",
"options": [{
"label": "Small",
"value": "S"
}, {
"label": "Medium",
"value": "M"
}, {
"label": "Large",
"value": "L"
}]
},
{
"type": "color",
"label": "Color",
"key": "color",
"showInBar": true,
"barSeparator": false
},
{
"type": "boolean",
"label": "Bold",
"key": "bold",
"showInBar": true,
"barIcon": "TagBold",
"barTitle": "Bold text",
"barSeparator": false
},
{
"type": "boolean",
"label": "Italic",
"key": "italic",
"showInBar": true,
"barIcon": "TagItalic",
"barTitle": "Italic text",
"barSeparator": false
},
{
"type": "boolean",
"label": "Underline",
"key": "underline",
"showInBar": true,
"barIcon": "TagUnderline",
"barTitle": "Underline text"
},
{
"type": "select",
"label": "Alignment",
"key": "align",
"defaultValue": "left",
"showInBar": true,
"barStyle": "buttons",
"options": [{
"label": "Left",
"value": "left",
"barIcon": "TextAlignLeft",
"barTitle": "Align left"
}, {
"label": "Center",
"value": "center",
"barIcon": "TextAlignCenter",
"barTitle": "Align center"
}, {
"label": "Right",
"value": "right",
"barIcon": "TextAlignRight",
"barTitle": "Align right"
}, {
"label": "Justify",
"value": "justify",
"barIcon": "TextAlignJustify",
"barTitle": "Justify text"
}]
} }
] ]
}, },
@ -461,8 +634,8 @@
"name": "Headline", "name": "Headline",
"icon": "TextBold", "icon": "TextBold",
"description": "A component for displaying heading text", "description": "A component for displaying heading text",
"styleable": true,
"illegalChildren": ["section"], "illegalChildren": ["section"],
"showSettingsBar": true,
"settings": [ "settings": [
{ {
"type": "text", "type": "text",
@ -471,10 +644,83 @@
}, },
{ {
"type": "select", "type": "select",
"key": "type", "label": "Size",
"label": "Type", "key": "size",
"options": ["h1", "h2", "h3", "h4", "h5", "h6"], "defaultValue": "M",
"defaultValue": "h1" "showInBar": true,
"barStyle": "picker",
"options": [{
"label": "Small",
"value": "S"
}, {
"label": "Medium",
"value": "M"
}, {
"label": "Large",
"value": "L"
}]
},
{
"type": "color",
"label": "Color",
"key": "color",
"showInBar": true,
"barSeparator": false
},
{
"type": "boolean",
"label": "Bold",
"key": "bold",
"showInBar": true,
"barIcon": "TagBold",
"barTitle": "Bold text",
"barSeparator": false
},
{
"type": "boolean",
"label": "Italic",
"key": "italic",
"showInBar": true,
"barIcon": "TagItalic",
"barTitle": "Italic text",
"barSeparator": false
},
{
"type": "boolean",
"label": "Underline",
"key": "underline",
"showInBar": true,
"barIcon": "TagUnderline",
"barTitle": "Underline text"
},
{
"type": "select",
"label": "Alignment",
"key": "align",
"defaultValue": "left",
"showInBar": true,
"barStyle": "buttons",
"options": [{
"label": "Left",
"value": "left",
"barIcon": "TextAlignLeft",
"barTitle": "Align left"
}, {
"label": "Center",
"value": "center",
"barIcon": "TextAlignCenter",
"barTitle": "Align center"
}, {
"label": "Right",
"value": "right",
"barIcon": "TextAlignRight",
"barTitle": "Align right"
}, {
"label": "Justify",
"value": "justify",
"barIcon": "TextAlignJustify",
"barTitle": "Justify text"
}]
} }
] ]
}, },
@ -482,8 +728,8 @@
"name": "Image", "name": "Image",
"description": "A basic component for displaying images", "description": "A basic component for displaying images",
"icon": "Image", "icon": "Image",
"styleable": true,
"illegalChildren": ["section"], "illegalChildren": ["section"],
"styles": ["size"],
"settings": [ "settings": [
{ {
"type": "text", "type": "text",
@ -496,7 +742,7 @@
"name": "Background Image", "name": "Background Image",
"description": "A background image", "description": "A background image",
"icon": "Images", "icon": "Images",
"styleable": true, "styles": ["size"],
"illegalChildren": ["section"], "illegalChildren": ["section"],
"settings": [ "settings": [
{ {
@ -554,7 +800,6 @@
"name": "Icon", "name": "Icon",
"description": "A basic component for displaying icons", "description": "A basic component for displaying icons",
"icon": "Bell", "icon": "Bell",
"styleable": true,
"illegalChildren": ["section"], "illegalChildren": ["section"],
"settings": [ "settings": [
{ {
@ -602,7 +847,6 @@
"name": "Nav Bar", "name": "Nav Bar",
"description": "A component for handling the navigation within your app.", "description": "A component for handling the navigation within your app.",
"icon": "BreadcrumbNavigation", "icon": "BreadcrumbNavigation",
"styleable": true,
"illegalChildren": ["section"], "illegalChildren": ["section"],
"hasChildren": true, "hasChildren": true,
"settings": [ "settings": [
@ -623,7 +867,7 @@
"name": "Link", "name": "Link",
"description": "A basic link component for internal and external links", "description": "A basic link component for internal and external links",
"icon": "Link", "icon": "Link",
"styleable": true, "showSettingsBar": true,
"illegalChildren": ["section"], "illegalChildren": ["section"],
"settings": [ "settings": [
{ {
@ -642,10 +886,85 @@
"label": "New Tab", "label": "New Tab",
"key": "openInNewTab" "key": "openInNewTab"
}, },
{
"type": "select",
"label": "Size",
"key": "size",
"defaultValue": "M",
"showInBar": true,
"barStyle": "picker",
"options": [{
"label": "Small",
"value": "S"
}, {
"label": "Medium",
"value": "M"
}, {
"label": "Large",
"value": "L"
}]
},
{
"type": "color",
"label": "Color",
"key": "color",
"showInBar": true,
"barSeparator": false
},
{ {
"type": "boolean", "type": "boolean",
"label": "External", "label": "Bold",
"key": "external" "key": "bold",
"showInBar": true,
"barIcon": "TagBold",
"barTitle": "Bold text",
"barSeparator": false
},
{
"type": "boolean",
"label": "Italic",
"key": "italic",
"showInBar": true,
"barIcon": "TagItalic",
"barTitle": "Italic text",
"barSeparator": false
},
{
"type": "boolean",
"label": "Underline",
"key": "underline",
"showInBar": true,
"barIcon": "TagUnderline",
"barTitle": "Underline text"
},
{
"type": "select",
"label": "Alignment",
"key": "align",
"defaultValue": "left",
"showInBar": true,
"barStyle": "buttons",
"options": [{
"label": "Left",
"value": "left",
"barIcon": "TextAlignLeft",
"barTitle": "Align left"
}, {
"label": "Center",
"value": "center",
"barIcon": "TextAlignCenter",
"barTitle": "Align center"
}, {
"label": "Right",
"value": "right",
"barIcon": "TextAlignRight",
"barTitle": "Align right"
}, {
"label": "Justify",
"value": "justify",
"barIcon": "TextAlignJustify",
"barTitle": "Justify text"
}]
} }
] ]
}, },
@ -653,7 +972,6 @@
"name": "Horizontal Card", "name": "Horizontal Card",
"description": "A basic card component that can contain content and actions.", "description": "A basic card component that can contain content and actions.",
"icon": "ViewRow", "icon": "ViewRow",
"styleable": true,
"illegalChildren": ["section"], "illegalChildren": ["section"],
"settings": [ "settings": [
{ {
@ -726,7 +1044,6 @@
"name": "Stat Card", "name": "Stat Card",
"description": "A card component for displaying numbers.", "description": "A card component for displaying numbers.",
"icon": "Card", "icon": "Card",
"styleable": true,
"illegalChildren": ["section"], "illegalChildren": ["section"],
"settings": [ "settings": [
{ {
@ -753,8 +1070,8 @@
"name": "Embed", "name": "Embed",
"icon": "Code", "icon": "Code",
"description": "Embed content from 3rd party sources", "description": "Embed content from 3rd party sources",
"styleable": true,
"illegalChildren": ["section"], "illegalChildren": ["section"],
"styles": ["size"],
"settings": [ "settings": [
{ {
"type": "text", "type": "text",
@ -767,7 +1084,6 @@
"name": "Bar Chart", "name": "Bar Chart",
"description": "Bar chart", "description": "Bar chart",
"icon": "GraphBarVertical", "icon": "GraphBarVertical",
"styleable": true,
"illegalChildren": ["section"], "illegalChildren": ["section"],
"settings": [ "settings": [
{ {
@ -868,7 +1184,6 @@
"name": "Line Chart", "name": "Line Chart",
"description": "Line chart", "description": "Line chart",
"icon": "GraphTrend", "icon": "GraphTrend",
"styleable": true,
"illegalChildren": ["section"], "illegalChildren": ["section"],
"settings": [ "settings": [
{ {
@ -970,7 +1285,6 @@
"name": "Area Chart", "name": "Area Chart",
"description": "Line chart", "description": "Line chart",
"icon": "GraphAreaStacked", "icon": "GraphAreaStacked",
"styleable": true,
"illegalChildren": ["section"], "illegalChildren": ["section"],
"settings": [ "settings": [
{ {
@ -1084,7 +1398,6 @@
"name": "Pie Chart", "name": "Pie Chart",
"description": "Pie chart", "description": "Pie chart",
"icon": "GraphPie", "icon": "GraphPie",
"styleable": true,
"illegalChildren": ["section"], "illegalChildren": ["section"],
"settings": [ "settings": [
{ {
@ -1162,7 +1475,6 @@
"name": "Donut Chart", "name": "Donut Chart",
"description": "Donut chart", "description": "Donut chart",
"icon": "GraphDonut", "icon": "GraphDonut",
"styleable": true,
"illegalChildren": ["section"], "illegalChildren": ["section"],
"settings": [ "settings": [
{ {
@ -1240,7 +1552,6 @@
"name": "Candlestick Chart", "name": "Candlestick Chart",
"description": "Candlestick chart", "description": "Candlestick chart",
"icon": "GraphBarVerticalStacked", "icon": "GraphBarVerticalStacked",
"styleable": true,
"illegalChildren": ["section"], "illegalChildren": ["section"],
"settings": [ "settings": [
{ {
@ -1322,7 +1633,6 @@
"form": { "form": {
"name": "Form", "name": "Form",
"icon": "Form", "icon": "Form",
"styleable": true,
"hasChildren": true, "hasChildren": true,
"illegalChildren": ["section"], "illegalChildren": ["section"],
"actions": [ "actions": [
@ -1395,7 +1705,6 @@
"fieldgroup": { "fieldgroup": {
"name": "Field Group", "name": "Field Group",
"icon": "Group", "icon": "Group",
"styleable": true,
"illegalChildren": ["section"], "illegalChildren": ["section"],
"hasChildren": true, "hasChildren": true,
"settings": [ "settings": [
@ -1424,7 +1733,6 @@
"stringfield": { "stringfield": {
"name": "Text Field", "name": "Text Field",
"icon": "Text", "icon": "Text",
"styleable": true,
"illegalChildren": ["section"], "illegalChildren": ["section"],
"settings": [ "settings": [
{ {
@ -1453,7 +1761,6 @@
"numberfield": { "numberfield": {
"name": "Number Field", "name": "Number Field",
"icon": "123", "icon": "123",
"styleable": true,
"illegalChildren": ["section"], "illegalChildren": ["section"],
"settings": [ "settings": [
{ {
@ -1482,7 +1789,6 @@
"passwordfield": { "passwordfield": {
"name": "Password Field", "name": "Password Field",
"icon": "LockClosed", "icon": "LockClosed",
"styleable": true,
"illegalChildren": ["section"], "illegalChildren": ["section"],
"settings": [ "settings": [
{ {
@ -1511,7 +1817,6 @@
"optionsfield": { "optionsfield": {
"name": "Options Picker", "name": "Options Picker",
"icon": "ViewList", "icon": "ViewList",
"styleable": true,
"illegalChildren": ["section"], "illegalChildren": ["section"],
"settings": [ "settings": [
{ {
@ -1541,7 +1846,6 @@
"booleanfield": { "booleanfield": {
"name": "Checkbox", "name": "Checkbox",
"icon": "Checkmark", "icon": "Checkmark",
"styleable": true,
"illegalChildren": ["section"], "illegalChildren": ["section"],
"settings": [ "settings": [
{ {
@ -1570,7 +1874,6 @@
"longformfield": { "longformfield": {
"name": "Rich Text", "name": "Rich Text",
"icon": "TextParagraph", "icon": "TextParagraph",
"styleable": true,
"illegalChildren": ["section"], "illegalChildren": ["section"],
"settings": [ "settings": [
{ {
@ -1600,7 +1903,6 @@
"datetimefield": { "datetimefield": {
"name": "Date Picker", "name": "Date Picker",
"icon": "DateInput", "icon": "DateInput",
"styleable": true,
"illegalChildren": ["section"], "illegalChildren": ["section"],
"settings": [ "settings": [
{ {
@ -1635,7 +1937,6 @@
"attachmentfield": { "attachmentfield": {
"name": "Attachment", "name": "Attachment",
"icon": "Attach", "icon": "Attach",
"styleable": true,
"illegalChildren": ["section"], "illegalChildren": ["section"],
"settings": [ "settings": [
{ {
@ -1659,7 +1960,6 @@
"relationshipfield": { "relationshipfield": {
"name": "Relationship Picker", "name": "Relationship Picker",
"icon": "TaskList", "icon": "TaskList",
"styleable": true,
"illegalChildren": ["section"], "illegalChildren": ["section"],
"settings": [ "settings": [
{ {
@ -1689,7 +1989,6 @@
"name": "Data Provider", "name": "Data Provider",
"info": "Pagination is only available for data stored in internal tables.", "info": "Pagination is only available for data stored in internal tables.",
"icon": "Data", "icon": "Data",
"styleable": false,
"illegalChildren": ["section"], "illegalChildren": ["section"],
"hasChildren": true, "hasChildren": true,
"settings": [ "settings": [
@ -1753,7 +2052,6 @@
"table": { "table": {
"name": "Table", "name": "Table",
"icon": "Table", "icon": "Table",
"styleable": true,
"illegalChildren": ["section"], "illegalChildren": ["section"],
"hasChildren": true, "hasChildren": true,
"showEmptyState": false, "showEmptyState": false,
@ -1809,11 +2107,6 @@
} }
] ]
}, },
{
"type": "boolean",
"label": "Quiet",
"key": "quiet"
},
{ {
"type": "multifield", "type": "multifield",
"label": "Columns", "label": "Columns",
@ -1823,7 +2116,12 @@
}, },
{ {
"type": "boolean", "type": "boolean",
"label": "Auto Cols.", "label": "Quiet",
"key": "quiet"
},
{
"type": "boolean",
"label": "Auto Columns",
"key": "showAutoColumns", "key": "showAutoColumns",
"defaultValue": false "defaultValue": false
} }
@ -1835,7 +2133,6 @@
"daterangepicker": { "daterangepicker": {
"name": "Date Range", "name": "Date Range",
"icon": "Date", "icon": "Date",
"styleable": true,
"hasChildren": false, "hasChildren": false,
"info": "Your data provider will be automatically filtered to the given date range.", "info": "Your data provider will be automatically filtered to the given date range.",
"settings": [ "settings": [

View File

@ -1,7 +1,8 @@
<script> <script>
import { getContext } from "svelte" import { getContext } from "svelte"
import Placeholder from "./Placeholder.svelte"
const { styleable } = getContext("sdk") const { styleable, builderStore } = getContext("sdk")
const component = getContext("component") const component = getContext("component")
export let url export let url
@ -18,13 +19,24 @@
} }
</script> </script>
<div class="outer" use:styleable={$component.styles}> {#if url}
<div class="inner" {style} /> <div class="outer" use:styleable={$component.styles}>
</div> <div class="inner" {style} />
</div>
{:else if $builderStore.inBuilder}
<div
class="placeholder"
use:styleable={{ ...$component.styles, empty: true }}
>
<Placeholder />
</div>
{/if}
<style> <style>
.outer { .outer {
position: relative; position: relative;
width: 100%;
height: 400px;
} }
.inner { .inner {
@ -35,4 +47,9 @@
background-size: cover; background-size: cover;
background-position: center center; background-position: center center;
} }
.placeholder {
display: grid;
place-items: center;
}
</style> </style>

View File

@ -19,3 +19,10 @@
> >
{text || ""} {text || ""}
</button> </button>
<style>
button {
width: fit-content;
width: -moz-fit-content;
}
</style>

View File

@ -8,17 +8,24 @@
export let hAlign export let hAlign
export let vAlign export let vAlign
export let size export let size
export let gap
export let wrap
$: directionClass = direction ? `valid-container direction-${direction}` : "" $: directionClass = direction ? `valid-container direction-${direction}` : ""
$: hAlignClass = hAlign ? `hAlign-${hAlign}` : "" $: hAlignClass = hAlign ? `hAlign-${hAlign}` : ""
$: vAlignClass = vAlign ? `vAlign-${vAlign}` : "" $: vAlignClass = vAlign ? `vAlign-${vAlign}` : ""
$: sizeClass = size ? `size-${size}` : "" $: sizeClass = size ? `size-${size}` : ""
$: gapClass = gap ? `gap-${gap}` : ""
$: classNames = [
directionClass,
hAlignClass,
vAlignClass,
sizeClass,
gapClass,
].join(" ")
</script> </script>
<div <div class={classNames} use:styleable={$component.styles} class:wrap>
class={[directionClass, hAlignClass, vAlignClass, sizeClass].join(" ")}
use:styleable={$component.styles}
>
<slot /> <slot />
</div> </div>
@ -83,4 +90,18 @@
.direction-column.hAlign-stretch { .direction-column.hAlign-stretch {
align-items: stretch; align-items: stretch;
} }
.gap-S {
gap: 8px;
}
.gap-M {
gap: 16px;
}
.gap-L {
gap: 32px;
}
.wrap {
flex-wrap: wrap;
}
</style> </style>

View File

@ -1,23 +1,29 @@
<script> <script>
import { getContext } from "svelte" import { getContext } from "svelte"
import Placeholder from "./Placeholder.svelte"
const { styleable } = getContext("sdk") const { styleable, builderStore } = getContext("sdk")
const component = getContext("component") const component = getContext("component")
export let embed export let embed
</script> </script>
<div use:styleable={$component.styles}> {#if embed}
{@html embed} <div class="embed" use:styleable={$component.styles}>
</div> {@html embed}
</div>
{:else if $builderStore.inBuilder}
<div use:styleable={{ ...$component.styles, empty: true }}>
<Placeholder />
</div>
{/if}
<style> <style>
div { .embed {
position: relative; position: relative;
} }
div :global(> *) { .embed :global(> *) {
width: 100%; width: 100%;
height: 100%; height: 100%;
position: absolute;
} }
</style> </style>

View File

@ -4,41 +4,84 @@
const { styleable, builderStore } = getContext("sdk") const { styleable, builderStore } = getContext("sdk")
const component = getContext("component") const component = getContext("component")
export let type
export let text export let text
export let color
export let align
export let bold
export let italic
export let underline
export let size
$: placeholder = $builderStore.inBuilder && !text $: placeholder = $builderStore.inBuilder && !text
$: componentText = $builderStore.inBuilder $: componentText = $builderStore.inBuilder
? text || "Placeholder text" ? text || "Placeholder text"
: text || "" : text || ""
// Add color styles to main styles object, otherwise the styleable helper
// overrides the color when it's passed as inline style.
$: styles = {
...$component.styles,
normal: {
...$component.styles?.normal,
color,
},
}
</script> </script>
{#if type === "h1"} <h1
<h1 class:placeholder use:styleable={$component.styles}>{componentText}</h1> use:styleable={styles}
{:else if type === "h2"} class:placeholder
<h2 class:placeholder use:styleable={$component.styles}>{componentText}</h2> class:bold
{:else if type === "h3"} class:italic
<h3 class:placeholder use:styleable={$component.styles}>{componentText}</h3> class:underline
{:else if type === "h4"} class="align--{align || 'left'} size--{size || 'M'}"
<h4 class:placeholder use:styleable={$component.styles}>{componentText}</h4> >
{:else if type === "h5"} {#if bold}
<h5 class:placeholder use:styleable={$component.styles}>{componentText}</h5> <strong>{componentText}</strong>
{:else if type === "h6"} {:else}
<h6 class:placeholder use:styleable={$component.styles}>{componentText}</h6> {componentText}
{/if} {/if}
</h1>
<style> <style>
h1, h1 {
h2, display: inline-block;
h3,
h4,
h5,
h6 {
white-space: pre-wrap; white-space: pre-wrap;
font-weight: 600;
margin: 0;
} }
.placeholder { .placeholder {
font-style: italic; font-style: italic;
color: var(--grey-6); color: var(--grey-6);
} }
.bold {
font-weight: 700;
}
.italic {
font-style: italic;
}
.underline {
text-decoration: underline;
}
.size--S {
font-size: 18px;
}
.size--M {
font-size: 22px;
}
.size--L {
font-size: 28px;
}
.align--left {
text-align: left;
}
.align--center {
text-align: center;
}
.align--right {
text-align: right;
}
.align-justify {
text-align: justify;
}
</style> </style>

View File

@ -1,21 +1,27 @@
<script> <script>
import { getContext } from "svelte" import { getContext } from "svelte"
import Placeholder from "./Placeholder.svelte"
const { styleable } = getContext("sdk") const { styleable, builderStore } = getContext("sdk")
const component = getContext("component") const component = getContext("component")
export let className = "" export let url
export let url = ""
export let description = ""
export let height
export let width
</script> </script>
<img {#if url}
{height} <img src={url} alt={$component.name} use:styleable={$component.styles} />
{width} {:else if $builderStore.inBuilder}
class={className} <div
src={url} class="placeholder"
alt={description} use:styleable={{ ...$component.styles, empty: true }}
use:styleable={$component.styles} >
/> <Placeholder />
</div>
{/if}
<style>
.placeholder {
display: grid;
place-items: center;
}
</style>

View File

@ -123,6 +123,8 @@
align-items: stretch; align-items: stretch;
height: 100%; height: 100%;
overflow: auto; overflow: auto;
overflow-x: hidden;
position: relative;
} }
.nav-wrapper { .nav-wrapper {
@ -131,7 +133,7 @@
justify-content: center; justify-content: center;
align-items: stretch; align-items: stretch;
background: white; background: white;
z-index: 1; z-index: 2;
box-shadow: 0 0 8px -1px rgba(0, 0, 0, 0.075); box-shadow: 0 0 8px -1px rgba(0, 0, 0, 0.075);
} }
.layout--top .nav-wrapper.sticky { .layout--top .nav-wrapper.sticky {
@ -163,6 +165,7 @@
justify-content: center; justify-content: center;
align-items: stretch; align-items: stretch;
flex: 1 1 auto; flex: 1 1 auto;
z-index: 1;
} }
.main { .main {
display: flex; display: flex;

View File

@ -1,31 +1,105 @@
<script> <script>
import { getContext } from "svelte" import { getContext } from "svelte"
const { linkable, styleable } = getContext("sdk") const { linkable, styleable, builderStore } = getContext("sdk")
const component = getContext("component") const component = getContext("component")
export let url = "" export let url
export let text = "" export let text
export let openInNewTab = false export let openInNewTab
export let external = false export let color
export let align
export let bold
export let italic
export let underline
export let size
$: external = url && !url.startsWith("/")
$: target = openInNewTab ? "_blank" : "_self" $: target = openInNewTab ? "_blank" : "_self"
$: placeholder = $builderStore.inBuilder && !text
$: componentText = $builderStore.inBuilder
? text || "Placeholder link"
: text || ""
// Add color styles to main styles object, otherwise the styleable helper
// overrides the color when it's passed as inline style.
$: styles = {
...$component.styles,
normal: {
...$component.styles?.normal,
color,
},
}
</script> </script>
{#if external} {#if $builderStore.inBuilder || componentText}
<a href={url || "/"} {target} use:styleable={$component.styles}> {#if external}
{text} <a
<slot /> {target}
</a> href={url || "/"}
{:else} use:styleable={styles}
<a href={url || "/"} use:linkable {target} use:styleable={$component.styles}> class:placeholder
{text} class:bold
<slot /> class:italic
</a> class:underline
class="align--{align || 'left'} size--{size || 'M'}"
>
{componentText}
</a>
{:else}
<a
use:linkable
href={url || "/"}
use:styleable={styles}
class:placeholder
class:bold
class:italic
class:underline
class="align--{align || 'left'} size--{size || 'M'}"
>
{componentText}
</a>
{/if}
{/if} {/if}
<style> <style>
a { a {
color: var(--spectrum-alias-text-color); color: var(--spectrum-alias-text-color);
display: inline-block;
white-space: pre-wrap;
}
.placeholder {
font-style: italic;
color: var(--grey-6);
}
.bold {
font-weight: 600;
}
.italic {
font-style: italic;
}
.underline {
text-decoration: underline;
}
.size--S {
font-size: 14px;
}
.size--M {
font-size: 16px;
}
.size--L {
font-size: 18px;
}
.align--left {
text-align: left;
}
.align--center {
text-align: center;
}
.align--right {
text-align: right;
}
.align-justify {
text-align: justify;
} }
</style> </style>

View File

@ -4,12 +4,12 @@
const { builderStore } = getContext("sdk") const { builderStore } = getContext("sdk")
const component = getContext("component") const component = getContext("component")
export let text = $component.name || "Placeholder" export let text
</script> </script>
{#if $builderStore.inBuilder} {#if $builderStore.inBuilder}
<div> <div>
{text} {text || $component.name || "Placeholder"}
</div> </div>
{/if} {/if}

View File

@ -8,6 +8,7 @@
export let direction export let direction
export let hAlign export let hAlign
export let vAlign export let vAlign
export let gap
const { Provider } = getContext("sdk") const { Provider } = getContext("sdk")
const component = getContext("component") const component = getContext("component")
@ -16,7 +17,7 @@
$: loaded = dataProvider?.loaded ?? true $: loaded = dataProvider?.loaded ?? true
</script> </script>
<Container {direction} {hAlign} {vAlign}> <Container {direction} {hAlign} {vAlign} {gap} wrap>
{#if $component.empty} {#if $component.empty}
<Placeholder /> <Placeholder />
{:else if rows.length > 0} {:else if rows.length > 0}

View File

@ -5,14 +5,37 @@
const component = getContext("component") const component = getContext("component")
export let text export let text
export let color
export let align
export let bold
export let italic
export let underline
export let size
$: placeholder = $builderStore.inBuilder && !text $: placeholder = $builderStore.inBuilder && !text
$: componentText = $builderStore.inBuilder $: componentText = $builderStore.inBuilder
? text || "Placeholder text" ? text || "Placeholder text"
: text || "" : text || ""
// Add color styles to main styles object, otherwise the styleable helper
// overrides the color when it's passed as inline style.
$: styles = {
...$component.styles,
normal: {
...$component.styles?.normal,
color,
},
}
</script> </script>
<p use:styleable={$component.styles} class:placeholder> <p
use:styleable={styles}
class:placeholder
class:bold
class:italic
class:underline
class="align--{align || 'left'} size--{size || 'M'}"
>
{componentText} {componentText}
</p> </p>
@ -20,10 +43,40 @@
p { p {
display: inline-block; display: inline-block;
white-space: pre-wrap; white-space: pre-wrap;
margin: 0;
} }
.placeholder { .placeholder {
font-style: italic; font-style: italic;
color: var(--grey-6); color: var(--grey-6);
} }
.bold {
font-weight: 600;
}
.italic {
font-style: italic;
}
.underline {
text-decoration: underline;
}
.size--S {
font-size: 14px;
}
.size--M {
font-size: 16px;
}
.size--L {
font-size: 18px;
}
.align--left {
text-align: left;
}
.align--center {
text-align: center;
}
.align--right {
text-align: right;
}
.align-justify {
text-align: justify;
}
</style> </style>

View File

@ -21,7 +21,7 @@
const setUpChart = provider => { const setUpChart = provider => {
const allCols = [labelColumn, ...(valueColumns || [null])] const allCols = [labelColumn, ...(valueColumns || [null])]
if (!provider || allCols.find(x => x == null)) { if (!provider || !provider.rows?.length || allCols.find(x => x == null)) {
return null return null
} }

View File

@ -21,7 +21,7 @@
// Fetch data on mount // Fetch data on mount
const setUpChart = provider => { const setUpChart = provider => {
const allCols = [dateColumn, openColumn, highColumn, lowColumn, closeColumn] const allCols = [dateColumn, openColumn, highColumn, lowColumn, closeColumn]
if (!provider || allCols.find(x => x == null)) { if (!provider || !provider.rows?.length || allCols.find(x => x == null)) {
return null return null
} }

View File

@ -28,7 +28,7 @@
// Fetch data on mount // Fetch data on mount
const setUpChart = provider => { const setUpChart = provider => {
const allCols = [labelColumn, ...(valueColumns || [null])] const allCols = [labelColumn, ...(valueColumns || [null])]
if (!provider || allCols.find(x => x == null)) { if (!provider || !provider.rows?.length || allCols.find(x => x == null)) {
return null return null
} }

View File

@ -18,7 +18,7 @@
// Fetch data on mount // Fetch data on mount
const setUpChart = provider => { const setUpChart = provider => {
if (!provider || !labelColumn || !valueColumn) { if (!provider || !provider.rows?.length || !labelColumn || !valueColumn) {
return null return null
} }

View File

@ -1,12 +1,7 @@
const handlebars = require("handlebars") const handlebars = require("handlebars")
const { registerAll } = require("./helpers/index") const { registerAll } = require("./helpers/index")
const processors = require("./processors") const processors = require("./processors")
const { cloneDeep } = require("lodash/fp") const { removeHandlebarsStatements } = require("./utilities")
const {
removeNull,
updateContext,
removeHandlebarsStatements,
} = require("./utilities")
const manifest = require("../manifest.json") const manifest = require("../manifest.json")
const hbsInstance = handlebars.create() const hbsInstance = handlebars.create()
@ -92,8 +87,6 @@ module.exports.processStringSync = (string, context) => {
} }
// take a copy of input incase error // take a copy of input incase error
const input = string const input = string
const clonedContext = removeNull(updateContext(cloneDeep(context)))
// remove any null/undefined properties
if (typeof string !== "string") { if (typeof string !== "string") {
throw "Cannot process non-string types." throw "Cannot process non-string types."
} }
@ -103,7 +96,10 @@ module.exports.processStringSync = (string, context) => {
const template = hbsInstance.compile(string, { const template = hbsInstance.compile(string, {
strict: false, strict: false,
}) })
return processors.postprocess(template(clonedContext)) return processors.postprocess(template({
now: new Date().toISOString(),
...context,
}))
} catch (err) { } catch (err) {
return removeHandlebarsStatements(input) return removeHandlebarsStatements(input)
} }

View File

@ -1,4 +1,3 @@
const _ = require("lodash")
const ALPHA_NUMERIC_REGEX = /^[A-Za-z0-9]+$/g const ALPHA_NUMERIC_REGEX = /^[A-Za-z0-9]+$/g
module.exports.FIND_HBS_REGEX = /{{([^{].*?)}}/g module.exports.FIND_HBS_REGEX = /{{([^{].*?)}}/g
@ -11,38 +10,6 @@ module.exports.swapStrings = (string, start, length, swap) => {
return string.slice(0, start) + swap + string.slice(start + length) return string.slice(0, start) + swap + string.slice(start + length)
} }
// removes null and undefined
module.exports.removeNull = obj => {
obj = _(obj).omitBy(_.isUndefined).omitBy(_.isNull).value()
for (let [key, value] of Object.entries(obj)) {
// only objects
if (typeof value === "object" && !Array.isArray(value)) {
obj[key] = module.exports.removeNull(value)
}
}
return obj
}
module.exports.updateContext = obj => {
if (obj.now == null) {
obj.now = new Date().toISOString()
}
function recurse(obj) {
for (let key of Object.keys(obj)) {
if (!obj[key]) {
continue
}
if (obj[key] instanceof Date) {
obj[key] = obj[key].toISOString()
} else if (typeof obj[key] === "object") {
obj[key] = recurse(obj[key])
}
}
return obj
}
return recurse(obj)
}
module.exports.removeHandlebarsStatements = string => { module.exports.removeHandlebarsStatements = string => {
let regexp = new RegExp(exports.FIND_HBS_REGEX) let regexp = new RegExp(exports.FIND_HBS_REGEX)
let matches = string.match(regexp) let matches = string.match(regexp)

View File

@ -111,7 +111,7 @@ describe("check the utility functions", () => {
it("should be able to handle an input date object", async () => { it("should be able to handle an input date object", async () => {
const date = new Date() const date = new Date()
const output = await processString("{{ dateObj }}", { dateObj: date }) const output = await processString("{{ dateObj }}", { dateObj: date })
expect(date.toISOString()).toEqual(output) expect(date.toString()).toEqual(output)
}) })
}) })