Merge branch 'feature/sql-relationships' of github.com:Budibase/budibase into feature/opinionated-relationships-ui
This commit is contained in:
commit
3c64f870bd
|
@ -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>
|
|
@ -2,13 +2,15 @@
|
|||
import Icon from "../Icon/Icon.svelte"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
|
||||
export let name
|
||||
export let show = false
|
||||
export let collapsible = true
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
export let thin = false
|
||||
export let name,
|
||||
show = false
|
||||
|
||||
const onHeaderClick = () => {
|
||||
if (!collapsible) {
|
||||
return
|
||||
}
|
||||
show = !show
|
||||
if (show) {
|
||||
dispatch("open")
|
||||
|
@ -16,14 +18,14 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<div class="property-group-container" class:thin>
|
||||
<div class="property-group-container">
|
||||
<div class="property-group-name" on:click={onHeaderClick}>
|
||||
<div class:thin class="name">{name}</div>
|
||||
<div class="icon">
|
||||
<div class="name">{name}</div>
|
||||
{#if collapsible}
|
||||
<Icon size="S" name={show ? "Remove" : "Add"} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="property-panel" class:show>
|
||||
<div class="property-panel" class:show={show || !collapsible}>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -32,10 +34,9 @@
|
|||
.property-group-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: auto;
|
||||
justify-content: center;
|
||||
border-radius: var(--border-radius-m);
|
||||
font-family: var(--font-sans);
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
border-bottom: var(--border-light);
|
||||
}
|
||||
|
||||
.property-group-name {
|
||||
|
@ -45,42 +46,38 @@
|
|||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
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 {
|
||||
text-align: left;
|
||||
font-size: 14px;
|
||||
font-size: var(--font-size-s);
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.14px;
|
||||
color: var(--ink);
|
||||
flex: 1 1 auto;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-transform: capitalize;
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
user-select: none;
|
||||
}
|
||||
.name.thin {
|
||||
font-size: var(--spectrum-global-dimension-font-size-75);
|
||||
}
|
||||
|
||||
.icon {
|
||||
flex: 0 0 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.property-panel {
|
||||
/* height: 0px;
|
||||
overflow: hidden; */
|
||||
display: none;
|
||||
padding: var(--spacing-s) var(--spacing-xl) var(--spacing-xl)
|
||||
var(--spacing-xl);
|
||||
}
|
||||
|
||||
.show {
|
||||
/* overflow: auto;
|
||||
height: auto; */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
margin-top: var(--spacing-m);
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
gap: var(--spacing-l);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
export let open = false
|
||||
export let readonly = false
|
||||
export let quiet = false
|
||||
export let autoWidth = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const onClick = () => {
|
||||
|
@ -41,7 +42,11 @@
|
|||
aria-haspopup="listbox"
|
||||
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}
|
||||
</span>
|
||||
{#if error}
|
||||
|
@ -67,11 +72,12 @@
|
|||
use:clickOutside={() => (open = false)}
|
||||
transition:fly={{ y: -20, duration: 200 }}
|
||||
class="spectrum-Popover spectrum-Popover--bottom spectrum-Picker-popover is-open"
|
||||
class:auto-width={autoWidth}
|
||||
>
|
||||
<ul class="spectrum-Menu" role="listbox">
|
||||
{#if placeholderOption}
|
||||
<li
|
||||
class="spectrum-Menu-item"
|
||||
class="spectrum-Menu-item placeholder"
|
||||
class:is-selected={isPlaceholder}
|
||||
role="option"
|
||||
aria-selected="true"
|
||||
|
@ -118,17 +124,28 @@
|
|||
<style>
|
||||
.spectrum-Popover {
|
||||
max-height: 240px;
|
||||
width: 100%;
|
||||
z-index: 999;
|
||||
top: 100%;
|
||||
}
|
||||
.spectrum-Popover:not(.auto-width) {
|
||||
width: 100%;
|
||||
}
|
||||
.spectrum-Popover.auto-width :global(.spectrum-Menu-itemLabel) {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.spectrum-Picker {
|
||||
width: 100%;
|
||||
}
|
||||
.spectrum-Picker-label {
|
||||
.spectrum-Picker-label:not(.auto-width) {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
width: 0;
|
||||
}
|
||||
.placeholder {
|
||||
font-style: italic;
|
||||
}
|
||||
.spectrum-Picker-label.auto-width.is-placeholder {
|
||||
padding-right: 2px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
export let getOptionValue = option => option
|
||||
export let readonly = false
|
||||
export let quiet = false
|
||||
export let autoWidth = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
let open = false
|
||||
|
@ -51,6 +52,7 @@
|
|||
{readonly}
|
||||
{fieldText}
|
||||
{options}
|
||||
{autoWidth}
|
||||
{getOptionLabel}
|
||||
{getOptionValue}
|
||||
isPlaceholder={value == null || value === ""}
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
export let id = null
|
||||
export let readonly = false
|
||||
export let updateOnChange = true
|
||||
export let quiet = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
let focus = false
|
||||
|
@ -59,6 +60,7 @@
|
|||
|
||||
<div
|
||||
class="spectrum-Textfield"
|
||||
class:spectrum-Textfield--quiet={quiet}
|
||||
class:is-invalid={!!error}
|
||||
class:is-disabled={disabled}
|
||||
class:is-focused={focus}
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
export let readonly = false
|
||||
export let error = null
|
||||
export let updateOnChange = true
|
||||
export let quiet = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const onChange = e => {
|
||||
|
@ -29,6 +30,7 @@
|
|||
{value}
|
||||
{placeholder}
|
||||
{type}
|
||||
{quiet}
|
||||
on:change={onChange}
|
||||
on:click
|
||||
on:input
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
export let getOptionLabel = option => extractProperty(option, "label")
|
||||
export let getOptionValue = option => extractProperty(option, "value")
|
||||
export let quiet = false
|
||||
export let autoWidth = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const onChange = e => {
|
||||
|
@ -37,6 +38,7 @@
|
|||
{value}
|
||||
{options}
|
||||
{placeholder}
|
||||
{autoWidth}
|
||||
{getOptionLabel}
|
||||
{getOptionValue}
|
||||
on:change={onChange}
|
||||
|
|
|
@ -5,6 +5,8 @@
|
|||
|
||||
export let selected
|
||||
export let vertical = false
|
||||
export let noPadding = false
|
||||
|
||||
let _id = id()
|
||||
const tab = writable({ title: selected, id: _id })
|
||||
setContext("tab", tab)
|
||||
|
@ -63,14 +65,17 @@
|
|||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="spectrum-Tabs-content spectrum-Tabs-content-{_id}" />
|
||||
<div
|
||||
class="spectrum-Tabs-content spectrum-Tabs-content-{_id}"
|
||||
class:noPadding
|
||||
/>
|
||||
|
||||
<style>
|
||||
.spectrum-Tabs {
|
||||
padding-left: var(--spacing-xl);
|
||||
padding-right: var(--spacing-xl);
|
||||
position: relative;
|
||||
border-width: 1px !important;
|
||||
border-bottom: var(--border-light);
|
||||
}
|
||||
.spectrum-Tabs-content {
|
||||
margin-top: var(--spectrum-global-dimension-static-size-150);
|
||||
|
@ -81,4 +86,7 @@
|
|||
.spectrum-Tabs--horizontal .spectrum-Tabs-selectionIndicator {
|
||||
bottom: 0 !important;
|
||||
}
|
||||
.noPadding {
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -55,6 +55,7 @@ export { default as Search } from "./Form/Search.svelte"
|
|||
export { default as Pagination } from "./Pagination/Pagination.svelte"
|
||||
export { default as Badge } from "./Badge/Badge.svelte"
|
||||
export { default as StatusLight } from "./StatusLight/StatusLight.svelte"
|
||||
export { default as ColorPicker } from "./ColorPicker/ColorPicker.svelte"
|
||||
|
||||
// Typography
|
||||
export { default as Body } from "./Typography/Body.svelte"
|
||||
|
|
|
@ -4,3 +4,5 @@ export const generateID = () => {
|
|||
// Starts with a letter so that its a valid DOM ID
|
||||
return `A${rand}`
|
||||
}
|
||||
|
||||
export const capitalise = s => s.substring(0, 1).toUpperCase() + s.substring(1)
|
||||
|
|
|
@ -488,12 +488,12 @@ export const getFrontendStore = () => {
|
|||
})
|
||||
await Promise.all(promises)
|
||||
},
|
||||
updateStyle: async (type, name, value) => {
|
||||
updateStyle: async (name, value) => {
|
||||
const selected = get(selectedComponent)
|
||||
if (value == null || value === "") {
|
||||
delete selected._styles[type][name]
|
||||
delete selected._styles.normal[name]
|
||||
} else {
|
||||
selected._styles[type][name] = value
|
||||
selected._styles.normal[name] = value
|
||||
}
|
||||
await store.actions.preview.saveSelected()
|
||||
},
|
||||
|
|
|
@ -54,6 +54,10 @@ function generateTitleContainer(table) {
|
|||
.type("h2")
|
||||
.instanceName("Title")
|
||||
.text(table.name)
|
||||
.customProps({
|
||||
size: "M",
|
||||
align: "left",
|
||||
})
|
||||
|
||||
return new Component("@budibase/standard-components/container")
|
||||
.normalStyle({
|
||||
|
|
|
@ -21,6 +21,7 @@ export class Screen extends BaseStructure {
|
|||
hAlign: "stretch",
|
||||
vAlign: "top",
|
||||
size: "grow",
|
||||
gap: "M",
|
||||
},
|
||||
routing: {
|
||||
route: "",
|
||||
|
|
|
@ -25,11 +25,8 @@ export function makeLinkComponent(tableName) {
|
|||
.customProps({
|
||||
url: `/${tableName.toLowerCase()}`,
|
||||
openInNewTab: false,
|
||||
color: "",
|
||||
hoverColor: "",
|
||||
underline: false,
|
||||
fontSize: "",
|
||||
fontFamily: "initial",
|
||||
size: "S",
|
||||
align: "left",
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -62,6 +59,10 @@ export function makeBreadcrumbContainer(tableName, text, capitalise = false) {
|
|||
.customStyle(spectrumColor(700))
|
||||
.text(">")
|
||||
.instanceName("Arrow")
|
||||
.customProps({
|
||||
size: "S",
|
||||
align: "left",
|
||||
})
|
||||
|
||||
const textStyling = {
|
||||
color: "#000000",
|
||||
|
@ -77,6 +78,10 @@ export function makeBreadcrumbContainer(tableName, text, capitalise = false) {
|
|||
.customStyle(spectrumColor(700))
|
||||
.text(text)
|
||||
.instanceName("Identifier")
|
||||
.customProps({
|
||||
size: "S",
|
||||
align: "left",
|
||||
})
|
||||
|
||||
return new Component("@budibase/standard-components/container")
|
||||
.normalStyle({
|
||||
|
@ -148,6 +153,10 @@ export function makeTitleContainer(title) {
|
|||
.type("h2")
|
||||
.instanceName("Title")
|
||||
.text(title)
|
||||
.customProps({
|
||||
size: "M",
|
||||
align: "left",
|
||||
})
|
||||
|
||||
return new Component("@budibase/standard-components/container")
|
||||
.normalStyle({
|
||||
|
|
|
@ -53,7 +53,7 @@
|
|||
let deletion
|
||||
|
||||
$: tableOptions = $tables.list.filter(
|
||||
table => table._id !== $tables.draft._id
|
||||
table => table._id !== $tables.draft._id && table.type !== "external"
|
||||
)
|
||||
$: required = !!field?.constraints?.presence || primaryDisplay
|
||||
$: uneditable =
|
||||
|
|
|
@ -45,7 +45,7 @@
|
|||
size="L"
|
||||
confirmText="Create"
|
||||
onConfirm={saveDatasource}
|
||||
disabled={error || !name}
|
||||
disabled={error || !name || !integration?.type}
|
||||
>
|
||||
<Input
|
||||
data-cy="datasource-name-input"
|
||||
|
|
|
@ -4,10 +4,13 @@
|
|||
import iframeTemplate from "./iframeTemplate"
|
||||
import { Screen } from "builderStore/store/screenTemplates/utils/Screen"
|
||||
import { FrontendTypes } from "constants"
|
||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||
|
||||
let iframe
|
||||
let layout
|
||||
let screen
|
||||
let confirmDeleteDialog
|
||||
let idToDelete
|
||||
|
||||
// Create screen slot placeholder for use when a page is selected rather
|
||||
// than a screen
|
||||
|
@ -73,15 +76,26 @@
|
|||
// Add listener for events sent by cliebt library in preview
|
||||
iframe.contentWindow.addEventListener("bb-event", event => {
|
||||
const { type, data } = event.detail
|
||||
if (type === "select-component") {
|
||||
if (type === "select-component" && data.id) {
|
||||
store.actions.components.select({ _id: data.id })
|
||||
} else if (type === "update-prop") {
|
||||
store.actions.components.updateProp(data.prop, data.value)
|
||||
} else if (type === "delete-component" && data.id) {
|
||||
idToDelete = data.id
|
||||
confirmDeleteDialog.show()
|
||||
} else {
|
||||
console.log(data)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const deleteComponent = () => {
|
||||
store.actions.components.delete({ _id: idToDelete })
|
||||
idToDelete = null
|
||||
}
|
||||
const cancelDeleteComponent = () => {
|
||||
idToDelete = null
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="component-container">
|
||||
|
@ -92,6 +106,14 @@
|
|||
srcdoc={template}
|
||||
/>
|
||||
</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>
|
||||
.component-container {
|
||||
|
|
|
@ -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>
|
|
@ -1,11 +1,13 @@
|
|||
<script>
|
||||
import { get } from "lodash"
|
||||
import { isEmpty } from "lodash/fp"
|
||||
import { Button, Checkbox, Input, Select } from "@budibase/bbui"
|
||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||
import { currentAsset } from "builderStore"
|
||||
import { findClosestMatchingComponent } from "builderStore/storeUtils"
|
||||
import { makeDatasourceFormComponents } from "builderStore/store/screenTemplates/utils/commonComponents"
|
||||
import {
|
||||
Checkbox,
|
||||
Input,
|
||||
Select,
|
||||
DetailSummary,
|
||||
ColorPicker,
|
||||
} from "@budibase/bbui"
|
||||
import { store } from "builderStore"
|
||||
import PropertyControl from "./PropertyControls/PropertyControl.svelte"
|
||||
import LayoutSelect from "./PropertyControls/LayoutSelect.svelte"
|
||||
import RoleSelect from "./PropertyControls/RoleSelect.svelte"
|
||||
|
@ -20,7 +22,6 @@
|
|||
import EventsEditor from "./PropertyControls/EventsEditor"
|
||||
import FilterEditor from "./PropertyControls/FilterEditor/FilterEditor.svelte"
|
||||
import { IconSelect } from "./PropertyControls/IconSelect"
|
||||
import ColorPicker from "./PropertyControls/ColorPicker.svelte"
|
||||
import StringFieldSelect from "./PropertyControls/StringFieldSelect.svelte"
|
||||
import NumberFieldSelect from "./PropertyControls/NumberFieldSelect.svelte"
|
||||
import OptionsFieldSelect from "./PropertyControls/OptionsFieldSelect.svelte"
|
||||
|
@ -29,13 +30,11 @@
|
|||
import DateTimeFieldSelect from "./PropertyControls/DateTimeFieldSelect.svelte"
|
||||
import AttachmentFieldSelect from "./PropertyControls/AttachmentFieldSelect.svelte"
|
||||
import RelationshipFieldSelect from "./PropertyControls/RelationshipFieldSelect.svelte"
|
||||
import ResetFieldsButton from "./PropertyControls/ResetFieldsButton.svelte"
|
||||
|
||||
export let componentDefinition = {}
|
||||
export let componentInstance = {}
|
||||
export let componentDefinition
|
||||
export let componentInstance
|
||||
export let assetInstance
|
||||
export let onChange = () => {}
|
||||
export let onScreenPropChange = () => {}
|
||||
export let showDisplayName = false
|
||||
|
||||
const layoutDefinition = []
|
||||
const screenDefinition = [
|
||||
|
@ -44,12 +43,12 @@
|
|||
{ key: "routing.roleId", label: "Access", control: RoleSelect },
|
||||
{ key: "layoutId", label: "Layout", control: LayoutSelect },
|
||||
]
|
||||
let confirmResetFieldsDialog
|
||||
|
||||
$: settings = componentDefinition?.settings ?? []
|
||||
$: isLayout = assetInstance && assetInstance.favicon
|
||||
$: assetDefinition = isLayout ? layoutDefinition : screenDefinition
|
||||
|
||||
const updateProp = store.actions.components.updateProp
|
||||
const controlMap = {
|
||||
text: Input,
|
||||
select: Select,
|
||||
|
@ -91,51 +90,19 @@
|
|||
}
|
||||
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>
|
||||
|
||||
<div class="settings-view-container">
|
||||
{#if assetInstance}
|
||||
{#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}
|
||||
<DetailSummary name="General" collapsible={false}>
|
||||
{#if !componentInstance._component.endsWith("/layout")}
|
||||
<PropertyControl
|
||||
bindable={false}
|
||||
control={Input}
|
||||
label="Name"
|
||||
key="_instanceName"
|
||||
value={componentInstance._instanceName}
|
||||
onChange={onInstanceNameChange}
|
||||
onChange={val => updateProp("_instanceName", val)}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if settings && settings.length > 0}
|
||||
{#each settings as setting (`${componentInstance._id}-${setting.key}`)}
|
||||
{#if canRenderControl(setting)}
|
||||
|
@ -147,52 +114,28 @@
|
|||
value={componentInstance[setting.key] ??
|
||||
componentInstance[setting.key]?.defaultValue}
|
||||
{componentInstance}
|
||||
onChange={val => onChange(setting.key, val)}
|
||||
props={{ options: setting.options, placeholder: setting.placeholder }}
|
||||
onChange={val => updateProp(setting.key, val)}
|
||||
props={{
|
||||
options: setting.options,
|
||||
placeholder: setting.placeholder,
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
{/each}
|
||||
{:else}
|
||||
<div class="text">This component doesn't have any additional settings.</div>
|
||||
{/if}
|
||||
{#if componentDefinition?.component?.endsWith("/fieldgroup")}
|
||||
<ResetFieldsButton {componentInstance} />
|
||||
{/if}
|
||||
{#if componentDefinition?.info}
|
||||
<div class="text">
|
||||
{@html componentDefinition?.info}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#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"
|
||||
/>
|
||||
</DetailSummary>
|
||||
|
||||
<style>
|
||||
.settings-view-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
gap: var(--spacing-s);
|
||||
}
|
||||
.text {
|
||||
font-size: var(--spectrum-global-dimension-font-size-75);
|
||||
margin-top: var(--spacing-m);
|
||||
color: var(--grey-6);
|
||||
}
|
||||
.buttonWrapper {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
|
@ -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>
|
|
@ -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}
|
|
@ -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>
|
|
@ -1,84 +1,33 @@
|
|||
<script>
|
||||
import { get } from "svelte/store"
|
||||
import { store, selectedComponent, currentAsset } from "builderStore"
|
||||
import { store, selectedComponent } from "builderStore"
|
||||
import { Tabs, Tab } from "@budibase/bbui"
|
||||
import { FrontendTypes } from "constants"
|
||||
import DesignView from "./DesignView.svelte"
|
||||
import SettingsView from "./SettingsView.svelte"
|
||||
import { setWith } from "lodash"
|
||||
import ScreenSettingsSection from "./ScreenSettingsSection.svelte"
|
||||
import ComponentSettingsSection from "./ComponentSettingsSection.svelte"
|
||||
import DesignSection from "./DesignSection.svelte"
|
||||
import CustomStylesSection from "./CustomStylesSection.svelte"
|
||||
|
||||
$: definition = store.actions.components.getDefinition(
|
||||
$: componentInstance = $selectedComponent
|
||||
$: componentDefinition = store.actions.components.getDefinition(
|
||||
$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>
|
||||
|
||||
<Tabs selected="Settings">
|
||||
<Tabs selected="Settings" noPadding>
|
||||
<Tab title="Settings">
|
||||
<div class="tab-content-padding">
|
||||
{#if definition && definition.name}
|
||||
<div class="instance-name">{definition.name}</div>
|
||||
{/if}
|
||||
<SettingsView
|
||||
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 class="container">
|
||||
<ScreenSettingsSection {componentInstance} {componentDefinition} />
|
||||
<ComponentSettingsSection {componentInstance} {componentDefinition} />
|
||||
<DesignSection {componentInstance} {componentDefinition} />
|
||||
<CustomStylesSection {componentInstance} {componentDefinition} />
|
||||
</div>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
<style>
|
||||
.tab-content-padding {
|
||||
padding: 0 var(--spacing-xl);
|
||||
}
|
||||
|
||||
.instance-name {
|
||||
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);
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { Button, Icon, Drawer } from "@budibase/bbui"
|
||||
import { Button, Icon, Drawer, Label } from "@budibase/bbui"
|
||||
import { store, currentAsset } from "builderStore"
|
||||
import {
|
||||
getBindableProperties,
|
||||
|
@ -70,7 +70,11 @@
|
|||
</script>
|
||||
|
||||
<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">
|
||||
<svelte:component
|
||||
this={control}
|
||||
|
@ -79,11 +83,11 @@
|
|||
updateOnChange={false}
|
||||
on:change={handleChange}
|
||||
onChange={handleChange}
|
||||
name={key}
|
||||
text={label}
|
||||
{type}
|
||||
{...props}
|
||||
name={key}
|
||||
/>
|
||||
</div>
|
||||
{#if bindable && !key.startsWith("_") && type === "text"}
|
||||
<div
|
||||
class="icon"
|
||||
|
@ -96,9 +100,9 @@
|
|||
<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
|
||||
>
|
||||
<Button cta slot="buttons" disabled={!valid} on:click={handleClose}>
|
||||
Save
|
||||
</Button>
|
||||
<BindingPanel
|
||||
slot="body"
|
||||
bind:valid
|
||||
|
@ -109,33 +113,25 @@
|
|||
/>
|
||||
</Drawer>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.property-control {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.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;
|
||||
padding-bottom: var(--spectrum-global-dimension-size-65);
|
||||
}
|
||||
|
||||
.control {
|
||||
flex: 1;
|
||||
display: inline-block;
|
||||
padding-left: 2px;
|
||||
width: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.icon {
|
||||
|
|
|
@ -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>
|
|
@ -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"
|
||||
/>
|
|
@ -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}
|
|
@ -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>
|
|
@ -1,112 +1,16 @@
|
|||
import { Input, Select } from "@budibase/bbui"
|
||||
import FlatButtonGroup from "./PropertyControls/FlatButtonGroup"
|
||||
import Colorpicker from "./PropertyControls/ColorPicker.svelte"
|
||||
import { Input, Select, ColorPicker } from "@budibase/bbui"
|
||||
|
||||
export const layout = [
|
||||
{
|
||||
label: "Display",
|
||||
key: "display",
|
||||
control: Select,
|
||||
options: [
|
||||
{ label: "Block", value: "block" },
|
||||
{ label: "Inline Block", value: "inline-block" },
|
||||
{ label: "Flex", value: "flex" },
|
||||
{ label: "Inline Flex", value: "inline-flex" },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Direction",
|
||||
key: "flex-direction",
|
||||
control: FlatButtonGroup,
|
||||
buttonProps: [
|
||||
{ icon: "ri-arrow-right-line", padding: "0px 5px", value: "row" },
|
||||
{ icon: "ri-arrow-left-line", padding: "0px 5px", value: "rowReverse" },
|
||||
{ icon: "ri-arrow-down-line", padding: "0px 5px", value: "column" },
|
||||
{
|
||||
icon: "ri-arrow-up-line",
|
||||
padding: "0px 5px",
|
||||
value: "columnReverse",
|
||||
},
|
||||
{ icon: "ri-close-line", value: "" },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Justify",
|
||||
key: "justify-content",
|
||||
control: Select,
|
||||
options: [
|
||||
{ label: "Flex Start", value: "flex-start" },
|
||||
{ label: "Flex End", value: "flex-end" },
|
||||
{ label: "Center", value: "center" },
|
||||
{ label: "Space Between", value: "space-between" },
|
||||
{ label: "Space Around", value: "space-around" },
|
||||
{ label: "Space Evenly", value: "space-evenly" },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Align",
|
||||
key: "align-items",
|
||||
control: Select,
|
||||
options: [
|
||||
{ label: "Flex Start", value: "flex-start" },
|
||||
{ label: "Flex End", value: "flex-end" },
|
||||
{ label: "Center", value: "center" },
|
||||
{ label: "Baseline", value: "baseline" },
|
||||
{ label: "Stretch", value: "stretch" },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Wrap",
|
||||
key: "flex-wrap",
|
||||
control: Select,
|
||||
options: [
|
||||
{ label: "Wrap", value: "wrap" },
|
||||
{ label: "No wrap", value: "nowrap" },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Gap",
|
||||
key: "gap",
|
||||
control: Select,
|
||||
options: [
|
||||
{ label: "None", value: "0px" },
|
||||
{ label: "4px", value: "4px" },
|
||||
{ label: "8px", value: "8px" },
|
||||
{ label: "16px", value: "16px" },
|
||||
{ label: "20px", value: "20px" },
|
||||
{ label: "32px", value: "32px" },
|
||||
{ label: "48px", value: "48px" },
|
||||
{ label: "64px", value: "64px" },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
export const margin = [
|
||||
{
|
||||
label: "All sides",
|
||||
key: "margin",
|
||||
control: Select,
|
||||
options: [
|
||||
{ label: "None", value: "0px" },
|
||||
{ label: "4px", value: "4px" },
|
||||
{ label: "8px", value: "8px" },
|
||||
{ label: "16px", value: "16px" },
|
||||
{ label: "20px", value: "20px" },
|
||||
{ label: "32px", value: "32px" },
|
||||
{ label: "48px", value: "48px" },
|
||||
{ label: "64px", value: "64px" },
|
||||
{ label: "128px", value: "128px" },
|
||||
{ label: "256px", value: "256px" },
|
||||
{ label: "Auto", value: "auto" },
|
||||
{ label: "100%", value: "100%" },
|
||||
],
|
||||
},
|
||||
export const margin = {
|
||||
label: "Margin",
|
||||
columns: "1fr 1fr",
|
||||
settings: [
|
||||
{
|
||||
label: "Top",
|
||||
key: "margin-top",
|
||||
control: Select,
|
||||
bindable: false,
|
||||
placeholder: "None",
|
||||
options: [
|
||||
{ label: "None", value: "0px" },
|
||||
{ label: "4px", value: "4px" },
|
||||
{ label: "8px", value: "8px" },
|
||||
{ label: "16px", value: "16px" },
|
||||
|
@ -124,8 +28,9 @@ export const margin = [
|
|||
label: "Right",
|
||||
key: "margin-right",
|
||||
control: Select,
|
||||
bindable: false,
|
||||
placeholder: "None",
|
||||
options: [
|
||||
{ label: "None", value: "0px" },
|
||||
{ label: "4px", value: "4px" },
|
||||
{ label: "8px", value: "8px" },
|
||||
{ label: "16px", value: "16px" },
|
||||
|
@ -143,8 +48,9 @@ export const margin = [
|
|||
label: "Bottom",
|
||||
key: "margin-bottom",
|
||||
control: Select,
|
||||
bindable: false,
|
||||
placeholder: "None",
|
||||
options: [
|
||||
{ label: "None", value: "0px" },
|
||||
{ label: "4px", value: "4px" },
|
||||
{ label: "8px", value: "8px" },
|
||||
{ label: "16px", value: "16px" },
|
||||
|
@ -162,8 +68,9 @@ export const margin = [
|
|||
label: "Left",
|
||||
key: "margin-left",
|
||||
control: Select,
|
||||
bindable: false,
|
||||
placeholder: "None",
|
||||
options: [
|
||||
{ label: "None", value: "0px" },
|
||||
{ label: "4px", value: "4px" },
|
||||
{ label: "8px", value: "8px" },
|
||||
{ label: "16px", value: "16px" },
|
||||
|
@ -177,32 +84,20 @@ export const margin = [
|
|||
{ label: "100%", value: "100%" },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
export const padding = [
|
||||
{
|
||||
label: "All sides",
|
||||
key: "padding",
|
||||
control: Select,
|
||||
options: [
|
||||
{ label: "None", value: "0px" },
|
||||
{ label: "4px", value: "4px" },
|
||||
{ label: "8px", value: "8px" },
|
||||
{ label: "16px", value: "16px" },
|
||||
{ label: "20px", value: "20px" },
|
||||
{ label: "32px", value: "32px" },
|
||||
{ label: "48px", value: "48px" },
|
||||
{ label: "64px", value: "64px" },
|
||||
{ label: "Auto", value: "auto" },
|
||||
{ label: "100%", value: "100%" },
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
export const padding = {
|
||||
label: "Padding",
|
||||
columns: "1fr 1fr",
|
||||
settings: [
|
||||
{
|
||||
label: "Top",
|
||||
key: "padding-top",
|
||||
control: Select,
|
||||
bindable: false,
|
||||
placeholder: "None",
|
||||
options: [
|
||||
{ label: "None", value: "0px" },
|
||||
{ label: "4px", value: "4px" },
|
||||
{ label: "8px", value: "8px" },
|
||||
{ label: "16px", value: "16px" },
|
||||
|
@ -210,6 +105,8 @@ export const padding = [
|
|||
{ label: "32px", value: "32px" },
|
||||
{ label: "48px", value: "48px" },
|
||||
{ label: "64px", value: "64px" },
|
||||
{ label: "128px", value: "128px" },
|
||||
{ label: "256px", value: "256px" },
|
||||
{ label: "Auto", value: "auto" },
|
||||
{ label: "100%", value: "100%" },
|
||||
],
|
||||
|
@ -218,8 +115,9 @@ export const padding = [
|
|||
label: "Right",
|
||||
key: "padding-right",
|
||||
control: Select,
|
||||
bindable: false,
|
||||
placeholder: "None",
|
||||
options: [
|
||||
{ label: "None", value: "0px" },
|
||||
{ label: "4px", value: "4px" },
|
||||
{ label: "8px", value: "8px" },
|
||||
{ label: "16px", value: "16px" },
|
||||
|
@ -227,6 +125,8 @@ export const padding = [
|
|||
{ label: "32px", value: "32px" },
|
||||
{ label: "48px", value: "48px" },
|
||||
{ label: "64px", value: "64px" },
|
||||
{ label: "128px", value: "128px" },
|
||||
{ label: "256px", value: "256px" },
|
||||
{ label: "Auto", value: "auto" },
|
||||
{ label: "100%", value: "100%" },
|
||||
],
|
||||
|
@ -235,8 +135,9 @@ export const padding = [
|
|||
label: "Bottom",
|
||||
key: "padding-bottom",
|
||||
control: Select,
|
||||
bindable: false,
|
||||
placeholder: "None",
|
||||
options: [
|
||||
{ label: "None", value: "0px" },
|
||||
{ label: "4px", value: "4px" },
|
||||
{ label: "8px", value: "8px" },
|
||||
{ label: "16px", value: "16px" },
|
||||
|
@ -244,6 +145,8 @@ export const padding = [
|
|||
{ label: "32px", value: "32px" },
|
||||
{ label: "48px", value: "48px" },
|
||||
{ label: "64px", value: "64px" },
|
||||
{ label: "128px", value: "128px" },
|
||||
{ label: "256px", value: "256px" },
|
||||
{ label: "Auto", value: "auto" },
|
||||
{ label: "100%", value: "100%" },
|
||||
],
|
||||
|
@ -252,8 +155,9 @@ export const padding = [
|
|||
label: "Left",
|
||||
key: "padding-left",
|
||||
control: Select,
|
||||
bindable: false,
|
||||
placeholder: "None",
|
||||
options: [
|
||||
{ label: "None", value: "0px" },
|
||||
{ label: "4px", value: "4px" },
|
||||
{ label: "8px", value: "8px" },
|
||||
{ label: "16px", value: "16px" },
|
||||
|
@ -261,240 +165,52 @@ export const padding = [
|
|||
{ label: "32px", value: "32px" },
|
||||
{ label: "48px", value: "48px" },
|
||||
{ label: "64px", value: "64px" },
|
||||
{ label: "128px", value: "128px" },
|
||||
{ label: "256px", value: "256px" },
|
||||
{ label: "Auto", value: "auto" },
|
||||
{ label: "100%", value: "100%" },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
export const size = [
|
||||
{
|
||||
label: "Flex",
|
||||
key: "flex",
|
||||
control: Select,
|
||||
options: [
|
||||
{ label: "Shrink", value: "0 1 auto" },
|
||||
{ label: "Grow", value: "1 1 auto" },
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
export const size = {
|
||||
label: "Size",
|
||||
columns: "1fr 1fr",
|
||||
settings: [
|
||||
{
|
||||
label: "Width",
|
||||
key: "width",
|
||||
control: Input,
|
||||
placeholder: "px",
|
||||
placeholder: "Auto",
|
||||
},
|
||||
{
|
||||
label: "Height",
|
||||
key: "height",
|
||||
control: Input,
|
||||
placeholder: "px",
|
||||
placeholder: "Auto",
|
||||
},
|
||||
{
|
||||
label: "Min Width",
|
||||
key: "min-width",
|
||||
control: Input,
|
||||
placeholder: "px",
|
||||
},
|
||||
{
|
||||
label: "Max Width",
|
||||
key: "max-width",
|
||||
control: Input,
|
||||
placeholder: "px",
|
||||
},
|
||||
{
|
||||
label: "Min Height",
|
||||
key: "min-height",
|
||||
control: Input,
|
||||
placeholder: "px",
|
||||
},
|
||||
{
|
||||
label: "Max Height",
|
||||
key: "max-height",
|
||||
control: Input,
|
||||
placeholder: "px",
|
||||
},
|
||||
]
|
||||
],
|
||||
}
|
||||
|
||||
export const position = [
|
||||
{
|
||||
label: "Position",
|
||||
key: "position",
|
||||
control: Select,
|
||||
options: [
|
||||
{ label: "Static", value: "static" },
|
||||
{ label: "Relative", value: "relative" },
|
||||
{ label: "Fixed", value: "fixed" },
|
||||
{ label: "Absolute", value: "absolute" },
|
||||
{ label: "Sticky", value: "sticky" },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Top",
|
||||
key: "top",
|
||||
control: Input,
|
||||
placeholder: "px",
|
||||
},
|
||||
{
|
||||
label: "Right",
|
||||
key: "right",
|
||||
control: Input,
|
||||
placeholder: "px",
|
||||
},
|
||||
{
|
||||
label: "Bottom",
|
||||
key: "bottom",
|
||||
control: Input,
|
||||
placeholder: "px",
|
||||
},
|
||||
{
|
||||
label: "Left",
|
||||
key: "left",
|
||||
control: Input,
|
||||
placeholder: "px",
|
||||
},
|
||||
{
|
||||
label: "Z-index",
|
||||
key: "z-index",
|
||||
control: Select,
|
||||
options: [
|
||||
{ label: "-9999", value: "-9999" },
|
||||
{ label: "-3", value: "-3" },
|
||||
{ label: "-2", value: "-2" },
|
||||
{ label: "-1", value: "-1" },
|
||||
{ label: "0", value: "0" },
|
||||
{ label: "1", value: "1" },
|
||||
{ label: "2", value: "2" },
|
||||
{ label: "3", value: "3" },
|
||||
{ label: "9999", value: "9999" },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
export const typography = [
|
||||
{
|
||||
label: "Font",
|
||||
key: "font-family",
|
||||
control: Select,
|
||||
options: [
|
||||
{ label: "Arial", value: "Arial" },
|
||||
{ label: "Arial Black", value: "Arial Black" },
|
||||
{ label: "Cursive", value: "Cursive" },
|
||||
{ label: "Courier", value: "Courier" },
|
||||
{ label: "Comic Sans MS", value: "Comic Sans MS" },
|
||||
{ label: "Helvetica", value: "Helvetica" },
|
||||
{ label: "Helvetica Neue", value: "Helvetica Neue" },
|
||||
{ label: "Impact", value: "Impact" },
|
||||
{ label: "Inter", value: "Inter" },
|
||||
{ label: "Lucida Sans Unicode", value: "Lucida Sans Unicode" },
|
||||
{ label: "Source Sans Pro", value: "Source Sans Pro" },
|
||||
{ label: "Times New Roman", value: "Times New Roman" },
|
||||
{ label: "Verdana", value: "Verdana" },
|
||||
],
|
||||
styleBindingProperty: "font-family",
|
||||
},
|
||||
{
|
||||
label: "Weight",
|
||||
key: "font-weight",
|
||||
control: Select,
|
||||
options: [
|
||||
{ label: "200", value: "200" },
|
||||
{ label: "300", value: "300" },
|
||||
{ label: "400", value: "400" },
|
||||
{ label: "500", value: "500" },
|
||||
{ label: "600", value: "600" },
|
||||
{ label: "700", value: "700" },
|
||||
{ label: "800", value: "800" },
|
||||
{ label: "900", value: "900" },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "size",
|
||||
key: "font-size",
|
||||
control: Select,
|
||||
options: [
|
||||
{ label: "8px", value: "8px" },
|
||||
{ label: "10px", value: "10px" },
|
||||
{ label: "12px", value: "12px" },
|
||||
{ label: "14px", value: "14px" },
|
||||
{ label: "16px", value: "16px" },
|
||||
{ label: "18px", value: "18px" },
|
||||
{ label: "20px", value: "20px" },
|
||||
{ label: "24px", value: "24px" },
|
||||
{ label: "32px", value: "32px" },
|
||||
{ label: "48px", value: "48px" },
|
||||
{ label: "60px", value: "60px" },
|
||||
{ label: "72px", value: "72px" },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Line H",
|
||||
key: "line-height",
|
||||
control: Select,
|
||||
options: [
|
||||
{ label: "1", value: "1" },
|
||||
{ label: "1.25", value: "1.25" },
|
||||
{ label: "1.5", value: "1.5" },
|
||||
{ label: "1.75", value: "1.75" },
|
||||
{ label: "2", value: "2" },
|
||||
{ label: "4", value: "4" },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Color",
|
||||
key: "color",
|
||||
control: Colorpicker,
|
||||
},
|
||||
{
|
||||
label: "align",
|
||||
key: "text-align",
|
||||
control: FlatButtonGroup,
|
||||
buttonProps: [
|
||||
{ icon: "ri-align-left", padding: "0px 5px", value: "left" },
|
||||
{ icon: "ri-align-center", padding: "0px 5px", value: "center" },
|
||||
{ icon: "ri-align-right", padding: "0px 5px", value: "right" },
|
||||
{ icon: "ri-align-justify", padding: "0px 5px", value: "justify" },
|
||||
{ icon: "ri-close-line", value: "" },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "transform",
|
||||
key: "text-transform",
|
||||
control: FlatButtonGroup,
|
||||
buttonProps: [
|
||||
{ text: "BB", value: "uppercase" },
|
||||
{ text: "Bb", value: "capitalize" },
|
||||
{ text: "bb", value: "lowercase" },
|
||||
{ icon: "ri-close-line", value: "" },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Decoration",
|
||||
key: "text-decoration-line",
|
||||
control: Select,
|
||||
options: [
|
||||
{ label: "Underline", value: "underline" },
|
||||
{ label: "Overline", value: "overline" },
|
||||
{ label: "Line-through", value: "line-through" },
|
||||
{ label: "Under Over", value: "underline overline" },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
export const background = [
|
||||
export const background = {
|
||||
label: "Background",
|
||||
columns: "auto 1fr",
|
||||
settings: [
|
||||
{
|
||||
label: "Color",
|
||||
key: "background",
|
||||
control: Colorpicker,
|
||||
control: ColorPicker,
|
||||
},
|
||||
{
|
||||
label: "Gradient",
|
||||
key: "background-image",
|
||||
control: Select,
|
||||
options: [
|
||||
{ label: "None", value: "none" },
|
||||
{
|
||||
label: "Warm Flame",
|
||||
value: "linear-gradient(45deg, #ff9a9e 0%, #fad0c4 99%, #fad0c4 100%);",
|
||||
value:
|
||||
"linear-gradient(45deg, #ff9a9e 0%, #fad0c4 99%, #fad0c4 100%);",
|
||||
},
|
||||
{
|
||||
label: "Night Fade",
|
||||
|
@ -563,104 +279,46 @@ export const background = [
|
|||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Image",
|
||||
key: "background",
|
||||
control: Input,
|
||||
placeholder: "URL",
|
||||
},
|
||||
]
|
||||
|
||||
export const border = [
|
||||
{
|
||||
label: "Radius",
|
||||
key: "border-radius",
|
||||
control: Select,
|
||||
options: [
|
||||
{ label: "None", value: "0" },
|
||||
{ label: "X Small", value: "0.125rem" },
|
||||
{ label: "Small", value: "0.25rem" },
|
||||
{ label: "Medium", value: "0.5rem" },
|
||||
{ label: "Large", value: "1rem" },
|
||||
{ label: "X Large", value: "2rem" },
|
||||
{ label: "XX Large", value: "4rem" },
|
||||
{ label: "Round", value: "5678px" },
|
||||
],
|
||||
}
|
||||
|
||||
export const border = {
|
||||
label: "Border",
|
||||
columns: "auto 1fr",
|
||||
settings: [
|
||||
{
|
||||
label: "Color",
|
||||
key: "border-color",
|
||||
control: ColorPicker,
|
||||
},
|
||||
{
|
||||
label: "Width",
|
||||
key: "border-width",
|
||||
control: Select,
|
||||
options: [
|
||||
{ label: "None", value: "0" },
|
||||
{ label: "X Small", value: "0.5px" },
|
||||
{ label: "Small", value: "1px" },
|
||||
{ label: "Medium", value: "2px" },
|
||||
{ label: "Large", value: "4px" },
|
||||
{ label: "X large", value: "8px" },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Color",
|
||||
key: "border-color",
|
||||
control: Colorpicker,
|
||||
},
|
||||
{
|
||||
label: "Style",
|
||||
key: "border-style",
|
||||
label: "Radius",
|
||||
key: "border-radius",
|
||||
control: Select,
|
||||
column: "1 / 3",
|
||||
options: [
|
||||
{ label: "None", value: "none" },
|
||||
{ label: "Hidden", value: "hidden" },
|
||||
{ label: "Dotted", value: "dotted" },
|
||||
{ label: "Dashed", value: "dashed" },
|
||||
{ label: "Solid", value: "solid" },
|
||||
{ label: "Double", value: "double" },
|
||||
{ label: "Groove", value: "groove" },
|
||||
{ label: "Ridge", value: "ridge" },
|
||||
{ label: "Inset", value: "inset" },
|
||||
{ label: "Outset", value: "outset" },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
export const effects = [
|
||||
{
|
||||
label: "Opacity",
|
||||
key: "opacity",
|
||||
control: Select,
|
||||
options: [
|
||||
{ label: "0", value: "0" },
|
||||
{ label: "0.2", value: "0.2" },
|
||||
{ label: "0.4", value: "0.4" },
|
||||
{ label: "0.6", value: "0.6" },
|
||||
{ label: "0.8", value: "0.8" },
|
||||
{ label: "1", value: "1" },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Rotate",
|
||||
key: "transform",
|
||||
control: Select,
|
||||
options: [
|
||||
{ label: "None", value: "0" },
|
||||
{ label: "45 deg", value: "rotate(45deg)" },
|
||||
{ label: "90 deg", value: "rotate(90deg)" },
|
||||
{ label: "135 deg", value: "rotate(135deg)" },
|
||||
{ label: "180 deg", value: "rotate(180deg)" },
|
||||
{ label: "225 deg", value: "rotate(225deg)" },
|
||||
{ label: "270 deg", value: "rotate(270deg)" },
|
||||
{ label: "315 deg", value: "rotate(315deg)" },
|
||||
{ label: "360 deg", value: "rotate(360deg)" },
|
||||
{ label: "Small", value: "0.25rem" },
|
||||
{ label: "Medium", value: "0.5rem" },
|
||||
{ label: "Large", value: "1rem" },
|
||||
{ label: "Round", value: "100%" },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Shadow",
|
||||
key: "box-shadow",
|
||||
control: Select,
|
||||
column: "1 / 3",
|
||||
options: [
|
||||
{ label: "None", value: "none" },
|
||||
{ label: "X Small", value: "0 1px 2px 0 rgba(0, 0, 0, 0.05)" },
|
||||
{
|
||||
label: "Small",
|
||||
value:
|
||||
|
@ -674,75 +332,11 @@ export const effects = [
|
|||
{
|
||||
label: "Large",
|
||||
value:
|
||||
"0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)",
|
||||
},
|
||||
{
|
||||
label: "X Large",
|
||||
value:
|
||||
"0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)",
|
||||
"0 8px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
export const transitions = [
|
||||
{
|
||||
label: "Property",
|
||||
key: "transition-property",
|
||||
control: Select,
|
||||
options: [
|
||||
{ label: "None", value: "none" },
|
||||
{ label: "All", value: "all" },
|
||||
{ label: "Background Color", value: "background color" },
|
||||
{ label: "Color", value: "color" },
|
||||
{ label: "Font Size", value: "font size" },
|
||||
{ label: "Font Weight", value: "font weight" },
|
||||
{ label: "Height", value: "height" },
|
||||
{ label: "Margin", value: "margin" },
|
||||
{ label: "Opacity", value: "opacity" },
|
||||
{ label: "Padding", value: "padding" },
|
||||
{ label: "Rotate", value: "rotate" },
|
||||
{ label: "Shadow", value: "shadow" },
|
||||
{ label: "Width", value: "width" },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Duration",
|
||||
key: "transition-duration",
|
||||
control: Select,
|
||||
placeholder: "sec",
|
||||
options: [
|
||||
{ label: "0.4s", value: "0.4s" },
|
||||
{ label: "0.6s", value: "0.6s" },
|
||||
{ label: "0.8s", value: "0.8s" },
|
||||
{ label: "1s", value: "1s" },
|
||||
{ label: "2s", value: "2s" },
|
||||
{ label: "4s", value: "4s" },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Ease",
|
||||
key: "transition-timing-function",
|
||||
control: Select,
|
||||
options: [
|
||||
{ label: "Linear", value: "linear" },
|
||||
{ label: "Ease", value: "ease" },
|
||||
{ label: "Ease in", value: "ease-in" },
|
||||
{ label: "Ease out", value: "ease-out" },
|
||||
{ label: "Ease in out", value: "ease-in-out" },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
export const allStyles = {
|
||||
layout,
|
||||
margin,
|
||||
padding,
|
||||
size,
|
||||
position,
|
||||
typography,
|
||||
background,
|
||||
border,
|
||||
effects,
|
||||
transitions,
|
||||
}
|
||||
|
||||
export const all = [margin]
|
||||
|
|
|
@ -86,6 +86,7 @@
|
|||
{#if datasource && integration}
|
||||
<section>
|
||||
<Layout>
|
||||
<Layout gap="XS" noPadding>
|
||||
<header>
|
||||
<svelte:component
|
||||
this={ICONS[datasource.source]}
|
||||
|
@ -94,7 +95,8 @@
|
|||
/>
|
||||
<Heading size="M">{datasource.name}</Heading>
|
||||
</header>
|
||||
<Body size="S" grey lh>{integration.description}</Body>
|
||||
<Body size="M">{integration.description}</Body>
|
||||
</Layout>
|
||||
<Divider />
|
||||
<div class="container">
|
||||
<div class="config-header">
|
||||
|
@ -188,7 +190,6 @@
|
|||
}
|
||||
|
||||
header {
|
||||
margin: 0 0 var(--spacing-xs) 0;
|
||||
display: flex;
|
||||
gap: var(--spacing-l);
|
||||
align-items: center;
|
||||
|
|
|
@ -14,14 +14,16 @@
|
|||
|
||||
<section>
|
||||
<Layout>
|
||||
<Layout gap="XS" noPadding>
|
||||
<header>
|
||||
<svelte:component this={ICONS.BUDIBASE} height="26" width="26" />
|
||||
<Heading size="M">Budibase Internal</Heading>
|
||||
</header>
|
||||
<Body size="S" grey lh
|
||||
>Budibase internal tables are part of your app, the data will be stored in
|
||||
your apps context.</Body
|
||||
>
|
||||
<Body size="M">
|
||||
Budibase internal tables are part of your app, so the data will be
|
||||
stored in your apps context.
|
||||
</Body>
|
||||
</Layout>
|
||||
<Divider />
|
||||
<Heading size="S">Tables</Heading>
|
||||
<div class="table-list">
|
||||
|
@ -32,7 +34,7 @@
|
|||
>
|
||||
<Body size="S">{table.name}</Body>
|
||||
{#if table.primaryDisplay}
|
||||
<Body size="S">display column: {table.primaryDisplay}</Body>
|
||||
<Body size="S">Display column: {table.primaryDisplay}</Body>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
|
@ -50,7 +52,6 @@
|
|||
}
|
||||
|
||||
header {
|
||||
margin: 0 0 var(--spacing-xs) 0;
|
||||
display: flex;
|
||||
gap: var(--spacing-l);
|
||||
align-items: center;
|
||||
|
|
|
@ -91,14 +91,15 @@
|
|||
{/if}
|
||||
|
||||
<style>
|
||||
#spectrum-root {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
#spectrum-root,
|
||||
#app-root {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
#app-root {
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -23,6 +23,11 @@
|
|||
// props with old ones, depending on how long enrichment takes.
|
||||
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
|
||||
const context = getContext("context")
|
||||
const insideScreenslot = !!getContext("screenslot")
|
||||
|
@ -42,7 +47,9 @@
|
|||
definition?.hasChildren &&
|
||||
definition?.showEmptyState !== false &&
|
||||
$builderStore.inBuilder
|
||||
$: updateComponentProps(instance, $context)
|
||||
$: rawProps = getRawProps(instance)
|
||||
$: instanceKey = JSON.stringify(rawProps)
|
||||
$: updateComponentProps(rawProps, instanceKey, $context)
|
||||
$: selected =
|
||||
$builderStore.inBuilder &&
|
||||
$builderStore.selectedComponentId === instance._id
|
||||
|
@ -59,6 +66,16 @@
|
|||
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
|
||||
const getComponentConstructor = component => {
|
||||
const split = component?.split("/")
|
||||
|
@ -76,13 +93,23 @@
|
|||
}
|
||||
|
||||
// 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
|
||||
latestUpdateTime = Date.now()
|
||||
const enrichmentTime = latestUpdateTime
|
||||
|
||||
// Enrich props with context
|
||||
const enrichedProps = enrichProps(instance, context)
|
||||
const enrichedProps = enrichProps(rawProps, context)
|
||||
|
||||
// Abandon this update if a newer update has started
|
||||
if (enrichmentTime !== latestUpdateTime) {
|
||||
|
|
|
@ -14,18 +14,32 @@
|
|||
const newContext = createContextStore(context)
|
||||
setContext("context", newContext)
|
||||
|
||||
$: providerKey = key || $component.id
|
||||
const providerKey = key || $component.id
|
||||
|
||||
// Add data context
|
||||
$: newContext.actions.provideData(providerKey, data)
|
||||
// Generate a permanent unique ID for this component and use it to register
|
||||
// any datasource actions
|
||||
const instanceId = generate()
|
||||
|
||||
// Instance ID is unique to each instance of a provider
|
||||
let instanceId
|
||||
// Keep previous state around so we can avoid updating unless necessary
|
||||
let lastDataKey
|
||||
let lastActionsKey
|
||||
|
||||
// Add actions context
|
||||
$: {
|
||||
if (instanceId) {
|
||||
actions?.forEach(({ type, callback, metadata }) => {
|
||||
$: provideData(data)
|
||||
$: provideActions(actions, instanceId)
|
||||
|
||||
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)
|
||||
|
||||
// Register any "refresh datasource" actions with a singleton store
|
||||
|
@ -43,10 +57,6 @@
|
|||
}
|
||||
|
||||
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
|
||||
return () => dataSourceStore.actions.unregisterInstance(instanceId)
|
||||
})
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
<script>
|
||||
import { onMount, onDestroy } from "svelte"
|
||||
import SettingsButton from "./SettingsButton.svelte"
|
||||
import SettingsColorPicker from "./SettingsColorPicker.svelte"
|
||||
import SettingsPicker from "./SettingsPicker.svelte"
|
||||
import { builderStore } from "../../store"
|
||||
import { domDebounce } from "../../utils/domDebounce"
|
||||
|
||||
|
@ -87,6 +89,7 @@
|
|||
>
|
||||
{#each settings as setting, idx}
|
||||
{#if setting.type === "select"}
|
||||
{#if setting.barStyle === "buttons"}
|
||||
{#each setting.options as option}
|
||||
<SettingsButton
|
||||
prop={setting.key}
|
||||
|
@ -95,11 +98,35 @@
|
|||
title={option.barTitle}
|
||||
/>
|
||||
{/each}
|
||||
{:else}
|
||||
<SettingsPicker
|
||||
prop={setting.key}
|
||||
options={setting.options}
|
||||
label={setting.label}
|
||||
/>
|
||||
{/if}
|
||||
{#if idx < settings.length - 1}
|
||||
{: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 setting.barSeparator !== false}
|
||||
<div class="divider" />
|
||||
{/if}
|
||||
{/each}
|
||||
<SettingsButton
|
||||
icon="Delete"
|
||||
on:click={() => {
|
||||
builderStore.actions.deleteComponent(
|
||||
$builderStore.selectedComponent._id
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<script>
|
||||
import { Icon } from "@budibase/bbui"
|
||||
import { builderStore } from "../../store"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
|
||||
export let prop
|
||||
export let value
|
||||
|
@ -9,6 +10,7 @@
|
|||
export let rotate = false
|
||||
export let bool = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
$: currentValue = $builderStore.selectedComponent?.[prop]
|
||||
$: active = prop && (bool ? !!currentValue : currentValue === value)
|
||||
</script>
|
||||
|
@ -22,6 +24,7 @@
|
|||
const newValue = bool ? !currentValue : value
|
||||
builderStore.actions.updateProp(prop, newValue)
|
||||
}
|
||||
dispatch("click")
|
||||
}}
|
||||
>
|
||||
<Icon name={icon} size="S" />
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -56,13 +56,14 @@ const createBuilderStore = () => {
|
|||
|
||||
const actions = {
|
||||
selectComponent: id => {
|
||||
if (id) {
|
||||
dispatchEvent("select-component", { id })
|
||||
}
|
||||
},
|
||||
updateProp: (prop, value) => {
|
||||
dispatchEvent("update-prop", { prop, value })
|
||||
},
|
||||
deleteComponent: id => {
|
||||
dispatchEvent("delete-component", { id })
|
||||
},
|
||||
}
|
||||
return {
|
||||
...writableStore,
|
||||
|
|
|
@ -4,7 +4,21 @@ export const createContextStore = oldContext => {
|
|||
const newContext = writable({})
|
||||
const contexts = oldContext ? [oldContext, newContext] : [newContext]
|
||||
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
|
||||
|
|
|
@ -31,9 +31,15 @@ const triggerAutomationHandler = async action => {
|
|||
}
|
||||
|
||||
const navigationHandler = action => {
|
||||
if (action.parameters.url) {
|
||||
const { url } = action.parameters
|
||||
if (url) {
|
||||
const external = !url.startsWith("/")
|
||||
if (external) {
|
||||
window.location.href = url
|
||||
} else {
|
||||
routeStore.actions.navigate(action.parameters.url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const queryExecutionHandler = async action => {
|
||||
|
|
|
@ -22,14 +22,6 @@ export const propsAreSame = (a, b) => {
|
|||
* Data bindings are enriched, and button actions are enriched.
|
||||
*/
|
||||
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
|
||||
// Duplicate the closest context as "data" which the builder requires
|
||||
const totalContext = {
|
||||
|
@ -41,7 +33,7 @@ export const enrichProps = (props, context) => {
|
|||
}
|
||||
|
||||
// Enrich all data bindings in top level props
|
||||
let enrichedProps = enrichDataBindings(validProps, totalContext)
|
||||
let enrichedProps = enrichDataBindings(props, totalContext)
|
||||
|
||||
// Enrich click actions if they exist
|
||||
if (enrichedProps.onClick) {
|
||||
|
|
|
@ -31,6 +31,12 @@ export const styleable = (node, styles = {}) => {
|
|||
if (newStyles.empty) {
|
||||
baseStyles.border = "2px dashed var(--grey-5)"
|
||||
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
|
||||
|
|
|
@ -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 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 (2, 'Laptops');
|
||||
INSERT INTO Products (ProductID, ProductName) VALUES (3, 'Chairs');
|
||||
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 (3, 1);
|
||||
INSERT INTO Products_Tasks (ProductID, TaskID) VALUES (1, 2);
|
||||
|
|
|
@ -14,6 +14,7 @@ exports.save = async function (ctx) {
|
|||
...EMPTY_LAYOUT,
|
||||
...layout,
|
||||
}
|
||||
layout.props._instanceName = layout.name
|
||||
}
|
||||
|
||||
layout._id = layout._id || generateLayoutID()
|
||||
|
|
|
@ -1,161 +1,19 @@
|
|||
const { makeExternalQuery } = require("./utils")
|
||||
const {
|
||||
DataSourceOperation,
|
||||
SortDirection,
|
||||
FieldTypes,
|
||||
RelationshipTypes,
|
||||
} = require("../../../constants")
|
||||
const { DataSourceOperation, SortDirection } = require("../../../constants")
|
||||
const { getAllExternalTables } = require("../table/utils")
|
||||
const {
|
||||
breakExternalTableId,
|
||||
generateRowIdField,
|
||||
breakRowIdField,
|
||||
} = require("../../../integrations/utils")
|
||||
const { cloneDeep } = require("lodash/fp")
|
||||
|
||||
function inputProcessing(row, table) {
|
||||
if (!row) {
|
||||
return row
|
||||
}
|
||||
let newRow = {}
|
||||
for (let key of Object.keys(table.schema)) {
|
||||
// currently excludes empty strings
|
||||
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
|
||||
}
|
||||
const {
|
||||
buildRelationships,
|
||||
buildFilters,
|
||||
inputProcessing,
|
||||
outputProcessing,
|
||||
generateIdForRow,
|
||||
buildFields,
|
||||
} = require("./externalUtils")
|
||||
const { processObjectSync } = require("@budibase/string-templates")
|
||||
|
||||
async function handleRequest(
|
||||
appId,
|
||||
|
@ -171,8 +29,9 @@ async function handleRequest(
|
|||
}
|
||||
// clean up row on ingress using schema
|
||||
filters = buildFilters(id, filters, table)
|
||||
const relationships = buildRelationships(table)
|
||||
row = inputProcessing(row, table)
|
||||
const relationships = buildRelationships(table, tables)
|
||||
const processed = inputProcessing(row, table, tables)
|
||||
row = processed.row
|
||||
if (
|
||||
operation === DataSourceOperation.DELETE &&
|
||||
(filters == null || Object.keys(filters).length === 0)
|
||||
|
@ -186,8 +45,8 @@ async function handleRequest(
|
|||
operation,
|
||||
},
|
||||
resource: {
|
||||
// not specifying any fields means "*"
|
||||
fields: [],
|
||||
// have to specify the fields to avoid column overlap
|
||||
fields: buildFields(table, tables),
|
||||
},
|
||||
filters,
|
||||
sort,
|
||||
|
@ -201,6 +60,25 @@ async function handleRequest(
|
|||
}
|
||||
// can't really use response right now
|
||||
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
|
||||
if (operation === DataSourceOperation.READ && Array.isArray(response)) {
|
||||
return outputProcessing(response, table, relationships, tables)
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -57,6 +57,7 @@ const BASE_LAYOUTS = [
|
|||
name: "Navigation Layout",
|
||||
props: {
|
||||
_id: "4f569166-a4f3-47ea-a09e-6d218c75586f",
|
||||
_instanceName: "Navigation Layout",
|
||||
_component: "@budibase/standard-components/layout",
|
||||
_children: [
|
||||
{
|
||||
|
@ -102,6 +103,7 @@ const BASE_LAYOUTS = [
|
|||
name: "Empty Layout",
|
||||
props: {
|
||||
_id: "3723ffa1-f9e0-4c05-8013-98195c788ed6",
|
||||
_instanceName: "Empty Layout",
|
||||
_component: "@budibase/standard-components/layout",
|
||||
_children: [
|
||||
{
|
||||
|
|
|
@ -20,15 +20,13 @@ exports.createHomeScreen = () => ({
|
|||
_id: "ef60083f-4a02-4df3-80f3-a0d3d16847e7",
|
||||
_component: "@budibase/standard-components/heading",
|
||||
_styles: {
|
||||
normal: {
|
||||
"text-align": "left",
|
||||
},
|
||||
hover: {},
|
||||
active: {},
|
||||
selected: {},
|
||||
},
|
||||
text: "Welcome to your Budibase App 👋",
|
||||
type: "h2",
|
||||
size: "M",
|
||||
align: "left",
|
||||
_instanceName: "Heading",
|
||||
_children: [],
|
||||
},
|
||||
|
@ -38,6 +36,7 @@ exports.createHomeScreen = () => ({
|
|||
hAlign: "stretch",
|
||||
vAlign: "top",
|
||||
size: "grow",
|
||||
gap: "M",
|
||||
},
|
||||
routing: {
|
||||
route: "/",
|
||||
|
|
|
@ -9,6 +9,10 @@ export interface TableSchema {
|
|||
type: string
|
||||
fieldName?: string
|
||||
name: string
|
||||
tableId?: string
|
||||
relationshipType?: string
|
||||
through?: string
|
||||
foreignKey?: string
|
||||
constraints?: {
|
||||
type?: string
|
||||
email?: boolean
|
||||
|
|
|
@ -79,17 +79,26 @@ function addRelationships(
|
|||
return query
|
||||
}
|
||||
for (let relationship of relationships) {
|
||||
const from = `${fromTable}.${relationship.from}`
|
||||
const to = `${relationship.tableName}.${relationship.to}`
|
||||
const from = relationship.from,
|
||||
to = relationship.to,
|
||||
toTable = relationship.tableName
|
||||
if (!relationship.through) {
|
||||
// @ts-ignore
|
||||
query = query.innerJoin(relationship.tableName, from, to)
|
||||
query = query.leftJoin(
|
||||
toTable,
|
||||
`${fromTable}.${from}`,
|
||||
`${relationship.tableName}.${to}`
|
||||
)
|
||||
} else {
|
||||
const through = relationship
|
||||
const throughTable = relationship.through
|
||||
query = query
|
||||
// @ts-ignore
|
||||
.innerJoin(through.tableName, from, through.from)
|
||||
.innerJoin(relationship.tableName, to, through.to)
|
||||
.leftJoin(
|
||||
throughTable,
|
||||
`${fromTable}.${from}`,
|
||||
`${throughTable}.${from}`
|
||||
)
|
||||
.leftJoin(toTable, `${toTable}.${to}`, `${throughTable}.${to}`)
|
||||
}
|
||||
}
|
||||
return query
|
||||
|
|
|
@ -175,17 +175,51 @@ module PostgresModule {
|
|||
type,
|
||||
}
|
||||
|
||||
// // TODO: hack for testing
|
||||
// TODO: hack for testing
|
||||
// if (tableName === "persons") {
|
||||
// tables[tableName].primaryDisplay = "firstname"
|
||||
// }
|
||||
// if (columnName.toLowerCase() === "personid" && tableName === "tasks") {
|
||||
// tables[tableName].schema[columnName] = {
|
||||
// name: columnName,
|
||||
// if (tableName === "products") {
|
||||
// tables[tableName].primaryDisplay = "productname"
|
||||
// }
|
||||
// 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",
|
||||
// tableId: buildExternalTableId(datasourceId, "persons"),
|
||||
// relationshipType: "one-to-many",
|
||||
// fieldName: "personid",
|
||||
// foreignKey: "personid",
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"target": "es6",
|
||||
"module": "commonjs",
|
||||
"lib": ["es6"],
|
||||
"allowJs": true,
|
||||
|
|
|
@ -4,8 +4,7 @@
|
|||
"description": "This component is specific only to layouts",
|
||||
"icon": "Sandbox",
|
||||
"hasChildren": true,
|
||||
"styleable": true,
|
||||
"illegalChildren": [],
|
||||
"styles": ["padding", "background"],
|
||||
"settings": [
|
||||
{
|
||||
"type": "text",
|
||||
|
@ -61,14 +60,15 @@
|
|||
"description": "This component contains things within itself",
|
||||
"icon": "Sandbox",
|
||||
"hasChildren": true,
|
||||
"styleable": true,
|
||||
"showSettingsBar": true,
|
||||
"styles": ["padding", "size", "background", "border", "shadow"],
|
||||
"settings": [
|
||||
{
|
||||
"type": "select",
|
||||
"label": "Direction",
|
||||
"key": "direction",
|
||||
"showInBar": true,
|
||||
"barStyle": "buttons",
|
||||
"options": [
|
||||
{
|
||||
"label": "Column",
|
||||
|
@ -90,6 +90,7 @@
|
|||
"label": "Horiz. Align",
|
||||
"key": "hAlign",
|
||||
"showInBar": true,
|
||||
"barStyle": "buttons",
|
||||
"options": [
|
||||
{
|
||||
"label": "Left",
|
||||
|
@ -122,7 +123,8 @@
|
|||
"type": "select",
|
||||
"label": "Vert. Align",
|
||||
"key": "vAlign",
|
||||
"showInBar": "true",
|
||||
"showInBar": true,
|
||||
"barStyle": "buttons",
|
||||
"options": [
|
||||
{
|
||||
"label": "Top",
|
||||
|
@ -156,6 +158,7 @@
|
|||
"label": "Size",
|
||||
"key": "size",
|
||||
"showInBar": true,
|
||||
"barStyle": "buttons",
|
||||
"options": [
|
||||
{
|
||||
"label": "Shrink",
|
||||
|
@ -171,6 +174,40 @@
|
|||
}
|
||||
],
|
||||
"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",
|
||||
"icon": "ColumnTwoB",
|
||||
"hasChildren": true,
|
||||
"styleable": true,
|
||||
"illegalChildren": ["section"],
|
||||
"showEmptyState": false,
|
||||
"settings": [
|
||||
|
@ -194,15 +230,13 @@
|
|||
"screenslot": {
|
||||
"name": "Screenslot",
|
||||
"icon": "WebPage",
|
||||
"description": "Contains your app screens",
|
||||
"styleable": true
|
||||
"description": "Contains your app screens"
|
||||
},
|
||||
"button": {
|
||||
"name": "Button",
|
||||
"description": "A basic html button that is ready for styling",
|
||||
"icon": "Button",
|
||||
"illegalChildren": ["section"],
|
||||
"styleable": true,
|
||||
"settings": [
|
||||
{
|
||||
"type": "text",
|
||||
|
@ -211,16 +245,49 @@
|
|||
},
|
||||
{
|
||||
"type": "select",
|
||||
"label": "Button Type",
|
||||
"label": "Variant",
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"type": "select",
|
||||
"label": "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"
|
||||
},
|
||||
{
|
||||
|
@ -239,7 +306,6 @@
|
|||
"name": "Repeater",
|
||||
"description": "A configurable data list that attaches to your backend tables.",
|
||||
"icon": "ViewList",
|
||||
"styleable": true,
|
||||
"illegalChildren": ["section"],
|
||||
"hasChildren": true,
|
||||
"showSettingsBar": true,
|
||||
|
@ -265,6 +331,7 @@
|
|||
"label": "Direction",
|
||||
"key": "direction",
|
||||
"showInBar": true,
|
||||
"barStyle": "buttons",
|
||||
"options": [
|
||||
{
|
||||
"label": "Column",
|
||||
|
@ -286,6 +353,7 @@
|
|||
"label": "Horiz. Align",
|
||||
"key": "hAlign",
|
||||
"showInBar": true,
|
||||
"barStyle": "buttons",
|
||||
"options": [
|
||||
{
|
||||
"label": "Left",
|
||||
|
@ -318,7 +386,8 @@
|
|||
"type": "select",
|
||||
"label": "Vert. Align",
|
||||
"key": "vAlign",
|
||||
"showInBar": "true",
|
||||
"showInBar": true,
|
||||
"barStyle": "buttons",
|
||||
"options": [
|
||||
{
|
||||
"label": "Top",
|
||||
|
@ -346,6 +415,32 @@
|
|||
}
|
||||
],
|
||||
"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": {
|
||||
|
@ -356,7 +451,6 @@
|
|||
"name": "Stacked List",
|
||||
"icon": "TaskList",
|
||||
"description": "A basic card component that can contain content and actions.",
|
||||
"styleable": true,
|
||||
"illegalChildren": ["section"],
|
||||
"settings": [
|
||||
{
|
||||
|
@ -386,7 +480,6 @@
|
|||
"name": "Vertical Card",
|
||||
"description": "A basic card component that can contain content and actions.",
|
||||
"icon": "ViewColumn",
|
||||
"styleable": true,
|
||||
"illegalChildren": ["section"],
|
||||
"settings": [
|
||||
{
|
||||
|
@ -447,13 +540,93 @@
|
|||
"name": "Paragraph",
|
||||
"description": "A component for displaying paragraph text.",
|
||||
"icon": "TextParagraph",
|
||||
"styleable": true,
|
||||
"illegalChildren": ["section"],
|
||||
"showSettingsBar": true,
|
||||
"settings": [
|
||||
{
|
||||
"type": "text",
|
||||
"label": "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",
|
||||
"icon": "TextBold",
|
||||
"description": "A component for displaying heading text",
|
||||
"styleable": true,
|
||||
"illegalChildren": ["section"],
|
||||
"showSettingsBar": true,
|
||||
"settings": [
|
||||
{
|
||||
"type": "text",
|
||||
|
@ -471,10 +644,83 @@
|
|||
},
|
||||
{
|
||||
"type": "select",
|
||||
"key": "type",
|
||||
"label": "Type",
|
||||
"options": ["h1", "h2", "h3", "h4", "h5", "h6"],
|
||||
"defaultValue": "h1"
|
||||
"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"
|
||||
}]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -482,8 +728,8 @@
|
|||
"name": "Image",
|
||||
"description": "A basic component for displaying images",
|
||||
"icon": "Image",
|
||||
"styleable": true,
|
||||
"illegalChildren": ["section"],
|
||||
"styles": ["size"],
|
||||
"settings": [
|
||||
{
|
||||
"type": "text",
|
||||
|
@ -496,7 +742,7 @@
|
|||
"name": "Background Image",
|
||||
"description": "A background image",
|
||||
"icon": "Images",
|
||||
"styleable": true,
|
||||
"styles": ["size"],
|
||||
"illegalChildren": ["section"],
|
||||
"settings": [
|
||||
{
|
||||
|
@ -554,7 +800,6 @@
|
|||
"name": "Icon",
|
||||
"description": "A basic component for displaying icons",
|
||||
"icon": "Bell",
|
||||
"styleable": true,
|
||||
"illegalChildren": ["section"],
|
||||
"settings": [
|
||||
{
|
||||
|
@ -602,7 +847,6 @@
|
|||
"name": "Nav Bar",
|
||||
"description": "A component for handling the navigation within your app.",
|
||||
"icon": "BreadcrumbNavigation",
|
||||
"styleable": true,
|
||||
"illegalChildren": ["section"],
|
||||
"hasChildren": true,
|
||||
"settings": [
|
||||
|
@ -623,7 +867,7 @@
|
|||
"name": "Link",
|
||||
"description": "A basic link component for internal and external links",
|
||||
"icon": "Link",
|
||||
"styleable": true,
|
||||
"showSettingsBar": true,
|
||||
"illegalChildren": ["section"],
|
||||
"settings": [
|
||||
{
|
||||
|
@ -642,10 +886,85 @@
|
|||
"label": "New Tab",
|
||||
"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",
|
||||
"label": "External",
|
||||
"key": "external"
|
||||
"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"
|
||||
}]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -653,7 +972,6 @@
|
|||
"name": "Horizontal Card",
|
||||
"description": "A basic card component that can contain content and actions.",
|
||||
"icon": "ViewRow",
|
||||
"styleable": true,
|
||||
"illegalChildren": ["section"],
|
||||
"settings": [
|
||||
{
|
||||
|
@ -726,7 +1044,6 @@
|
|||
"name": "Stat Card",
|
||||
"description": "A card component for displaying numbers.",
|
||||
"icon": "Card",
|
||||
"styleable": true,
|
||||
"illegalChildren": ["section"],
|
||||
"settings": [
|
||||
{
|
||||
|
@ -753,8 +1070,8 @@
|
|||
"name": "Embed",
|
||||
"icon": "Code",
|
||||
"description": "Embed content from 3rd party sources",
|
||||
"styleable": true,
|
||||
"illegalChildren": ["section"],
|
||||
"styles": ["size"],
|
||||
"settings": [
|
||||
{
|
||||
"type": "text",
|
||||
|
@ -767,7 +1084,6 @@
|
|||
"name": "Bar Chart",
|
||||
"description": "Bar chart",
|
||||
"icon": "GraphBarVertical",
|
||||
"styleable": true,
|
||||
"illegalChildren": ["section"],
|
||||
"settings": [
|
||||
{
|
||||
|
@ -868,7 +1184,6 @@
|
|||
"name": "Line Chart",
|
||||
"description": "Line chart",
|
||||
"icon": "GraphTrend",
|
||||
"styleable": true,
|
||||
"illegalChildren": ["section"],
|
||||
"settings": [
|
||||
{
|
||||
|
@ -970,7 +1285,6 @@
|
|||
"name": "Area Chart",
|
||||
"description": "Line chart",
|
||||
"icon": "GraphAreaStacked",
|
||||
"styleable": true,
|
||||
"illegalChildren": ["section"],
|
||||
"settings": [
|
||||
{
|
||||
|
@ -1084,7 +1398,6 @@
|
|||
"name": "Pie Chart",
|
||||
"description": "Pie chart",
|
||||
"icon": "GraphPie",
|
||||
"styleable": true,
|
||||
"illegalChildren": ["section"],
|
||||
"settings": [
|
||||
{
|
||||
|
@ -1162,7 +1475,6 @@
|
|||
"name": "Donut Chart",
|
||||
"description": "Donut chart",
|
||||
"icon": "GraphDonut",
|
||||
"styleable": true,
|
||||
"illegalChildren": ["section"],
|
||||
"settings": [
|
||||
{
|
||||
|
@ -1240,7 +1552,6 @@
|
|||
"name": "Candlestick Chart",
|
||||
"description": "Candlestick chart",
|
||||
"icon": "GraphBarVerticalStacked",
|
||||
"styleable": true,
|
||||
"illegalChildren": ["section"],
|
||||
"settings": [
|
||||
{
|
||||
|
@ -1322,7 +1633,6 @@
|
|||
"form": {
|
||||
"name": "Form",
|
||||
"icon": "Form",
|
||||
"styleable": true,
|
||||
"hasChildren": true,
|
||||
"illegalChildren": ["section"],
|
||||
"actions": [
|
||||
|
@ -1395,7 +1705,6 @@
|
|||
"fieldgroup": {
|
||||
"name": "Field Group",
|
||||
"icon": "Group",
|
||||
"styleable": true,
|
||||
"illegalChildren": ["section"],
|
||||
"hasChildren": true,
|
||||
"settings": [
|
||||
|
@ -1424,7 +1733,6 @@
|
|||
"stringfield": {
|
||||
"name": "Text Field",
|
||||
"icon": "Text",
|
||||
"styleable": true,
|
||||
"illegalChildren": ["section"],
|
||||
"settings": [
|
||||
{
|
||||
|
@ -1453,7 +1761,6 @@
|
|||
"numberfield": {
|
||||
"name": "Number Field",
|
||||
"icon": "123",
|
||||
"styleable": true,
|
||||
"illegalChildren": ["section"],
|
||||
"settings": [
|
||||
{
|
||||
|
@ -1482,7 +1789,6 @@
|
|||
"passwordfield": {
|
||||
"name": "Password Field",
|
||||
"icon": "LockClosed",
|
||||
"styleable": true,
|
||||
"illegalChildren": ["section"],
|
||||
"settings": [
|
||||
{
|
||||
|
@ -1511,7 +1817,6 @@
|
|||
"optionsfield": {
|
||||
"name": "Options Picker",
|
||||
"icon": "ViewList",
|
||||
"styleable": true,
|
||||
"illegalChildren": ["section"],
|
||||
"settings": [
|
||||
{
|
||||
|
@ -1541,7 +1846,6 @@
|
|||
"booleanfield": {
|
||||
"name": "Checkbox",
|
||||
"icon": "Checkmark",
|
||||
"styleable": true,
|
||||
"illegalChildren": ["section"],
|
||||
"settings": [
|
||||
{
|
||||
|
@ -1570,7 +1874,6 @@
|
|||
"longformfield": {
|
||||
"name": "Rich Text",
|
||||
"icon": "TextParagraph",
|
||||
"styleable": true,
|
||||
"illegalChildren": ["section"],
|
||||
"settings": [
|
||||
{
|
||||
|
@ -1600,7 +1903,6 @@
|
|||
"datetimefield": {
|
||||
"name": "Date Picker",
|
||||
"icon": "DateInput",
|
||||
"styleable": true,
|
||||
"illegalChildren": ["section"],
|
||||
"settings": [
|
||||
{
|
||||
|
@ -1635,7 +1937,6 @@
|
|||
"attachmentfield": {
|
||||
"name": "Attachment",
|
||||
"icon": "Attach",
|
||||
"styleable": true,
|
||||
"illegalChildren": ["section"],
|
||||
"settings": [
|
||||
{
|
||||
|
@ -1659,7 +1960,6 @@
|
|||
"relationshipfield": {
|
||||
"name": "Relationship Picker",
|
||||
"icon": "TaskList",
|
||||
"styleable": true,
|
||||
"illegalChildren": ["section"],
|
||||
"settings": [
|
||||
{
|
||||
|
@ -1689,7 +1989,6 @@
|
|||
"name": "Data Provider",
|
||||
"info": "Pagination is only available for data stored in internal tables.",
|
||||
"icon": "Data",
|
||||
"styleable": false,
|
||||
"illegalChildren": ["section"],
|
||||
"hasChildren": true,
|
||||
"settings": [
|
||||
|
@ -1753,7 +2052,6 @@
|
|||
"table": {
|
||||
"name": "Table",
|
||||
"icon": "Table",
|
||||
"styleable": true,
|
||||
"illegalChildren": ["section"],
|
||||
"hasChildren": true,
|
||||
"showEmptyState": false,
|
||||
|
@ -1809,11 +2107,6 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"label": "Quiet",
|
||||
"key": "quiet"
|
||||
},
|
||||
{
|
||||
"type": "multifield",
|
||||
"label": "Columns",
|
||||
|
@ -1823,7 +2116,12 @@
|
|||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"label": "Auto Cols.",
|
||||
"label": "Quiet",
|
||||
"key": "quiet"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"label": "Auto Columns",
|
||||
"key": "showAutoColumns",
|
||||
"defaultValue": false
|
||||
}
|
||||
|
@ -1835,7 +2133,6 @@
|
|||
"daterangepicker": {
|
||||
"name": "Date Range",
|
||||
"icon": "Date",
|
||||
"styleable": true,
|
||||
"hasChildren": false,
|
||||
"info": "Your data provider will be automatically filtered to the given date range.",
|
||||
"settings": [
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
<script>
|
||||
import { getContext } from "svelte"
|
||||
import Placeholder from "./Placeholder.svelte"
|
||||
|
||||
const { styleable } = getContext("sdk")
|
||||
const { styleable, builderStore } = getContext("sdk")
|
||||
const component = getContext("component")
|
||||
|
||||
export let url
|
||||
|
@ -18,13 +19,24 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<div class="outer" use:styleable={$component.styles}>
|
||||
{#if url}
|
||||
<div class="outer" use:styleable={$component.styles}>
|
||||
<div class="inner" {style} />
|
||||
</div>
|
||||
</div>
|
||||
{:else if $builderStore.inBuilder}
|
||||
<div
|
||||
class="placeholder"
|
||||
use:styleable={{ ...$component.styles, empty: true }}
|
||||
>
|
||||
<Placeholder />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.outer {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
.inner {
|
||||
|
@ -35,4 +47,9 @@
|
|||
background-size: cover;
|
||||
background-position: center center;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -19,3 +19,10 @@
|
|||
>
|
||||
{text || ""}
|
||||
</button>
|
||||
|
||||
<style>
|
||||
button {
|
||||
width: fit-content;
|
||||
width: -moz-fit-content;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -8,17 +8,24 @@
|
|||
export let hAlign
|
||||
export let vAlign
|
||||
export let size
|
||||
export let gap
|
||||
export let wrap
|
||||
|
||||
$: directionClass = direction ? `valid-container direction-${direction}` : ""
|
||||
$: hAlignClass = hAlign ? `hAlign-${hAlign}` : ""
|
||||
$: vAlignClass = vAlign ? `vAlign-${vAlign}` : ""
|
||||
$: sizeClass = size ? `size-${size}` : ""
|
||||
$: gapClass = gap ? `gap-${gap}` : ""
|
||||
$: classNames = [
|
||||
directionClass,
|
||||
hAlignClass,
|
||||
vAlignClass,
|
||||
sizeClass,
|
||||
gapClass,
|
||||
].join(" ")
|
||||
</script>
|
||||
|
||||
<div
|
||||
class={[directionClass, hAlignClass, vAlignClass, sizeClass].join(" ")}
|
||||
use:styleable={$component.styles}
|
||||
>
|
||||
<div class={classNames} use:styleable={$component.styles} class:wrap>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
|
@ -83,4 +90,18 @@
|
|||
.direction-column.hAlign-stretch {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.gap-S {
|
||||
gap: 8px;
|
||||
}
|
||||
.gap-M {
|
||||
gap: 16px;
|
||||
}
|
||||
.gap-L {
|
||||
gap: 32px;
|
||||
}
|
||||
|
||||
.wrap {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,23 +1,29 @@
|
|||
<script>
|
||||
import { getContext } from "svelte"
|
||||
import Placeholder from "./Placeholder.svelte"
|
||||
|
||||
const { styleable } = getContext("sdk")
|
||||
const { styleable, builderStore } = getContext("sdk")
|
||||
const component = getContext("component")
|
||||
|
||||
export let embed
|
||||
</script>
|
||||
|
||||
<div use:styleable={$component.styles}>
|
||||
{#if embed}
|
||||
<div class="embed" use:styleable={$component.styles}>
|
||||
{@html embed}
|
||||
</div>
|
||||
</div>
|
||||
{:else if $builderStore.inBuilder}
|
||||
<div use:styleable={{ ...$component.styles, empty: true }}>
|
||||
<Placeholder />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
div {
|
||||
.embed {
|
||||
position: relative;
|
||||
}
|
||||
div :global(> *) {
|
||||
.embed :global(> *) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -4,41 +4,84 @@
|
|||
const { styleable, builderStore } = getContext("sdk")
|
||||
const component = getContext("component")
|
||||
|
||||
export let type
|
||||
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
|
||||
$: componentText = $builderStore.inBuilder
|
||||
? text || "Placeholder 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>
|
||||
|
||||
{#if type === "h1"}
|
||||
<h1 class:placeholder use:styleable={$component.styles}>{componentText}</h1>
|
||||
{:else if type === "h2"}
|
||||
<h2 class:placeholder use:styleable={$component.styles}>{componentText}</h2>
|
||||
{:else if type === "h3"}
|
||||
<h3 class:placeholder use:styleable={$component.styles}>{componentText}</h3>
|
||||
{:else if type === "h4"}
|
||||
<h4 class:placeholder use:styleable={$component.styles}>{componentText}</h4>
|
||||
{:else if type === "h5"}
|
||||
<h5 class:placeholder use:styleable={$component.styles}>{componentText}</h5>
|
||||
{:else if type === "h6"}
|
||||
<h6 class:placeholder use:styleable={$component.styles}>{componentText}</h6>
|
||||
{/if}
|
||||
<h1
|
||||
use:styleable={styles}
|
||||
class:placeholder
|
||||
class:bold
|
||||
class:italic
|
||||
class:underline
|
||||
class="align--{align || 'left'} size--{size || 'M'}"
|
||||
>
|
||||
{#if bold}
|
||||
<strong>{componentText}</strong>
|
||||
{:else}
|
||||
{componentText}
|
||||
{/if}
|
||||
</h1>
|
||||
|
||||
<style>
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
h1 {
|
||||
display: inline-block;
|
||||
white-space: pre-wrap;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
font-style: italic;
|
||||
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>
|
||||
|
|
|
@ -1,21 +1,27 @@
|
|||
<script>
|
||||
import { getContext } from "svelte"
|
||||
import Placeholder from "./Placeholder.svelte"
|
||||
|
||||
const { styleable } = getContext("sdk")
|
||||
const { styleable, builderStore } = getContext("sdk")
|
||||
const component = getContext("component")
|
||||
|
||||
export let className = ""
|
||||
export let url = ""
|
||||
export let description = ""
|
||||
export let height
|
||||
export let width
|
||||
export let url
|
||||
</script>
|
||||
|
||||
<img
|
||||
{height}
|
||||
{width}
|
||||
class={className}
|
||||
src={url}
|
||||
alt={description}
|
||||
use:styleable={$component.styles}
|
||||
/>
|
||||
{#if url}
|
||||
<img src={url} alt={$component.name} use:styleable={$component.styles} />
|
||||
{:else if $builderStore.inBuilder}
|
||||
<div
|
||||
class="placeholder"
|
||||
use:styleable={{ ...$component.styles, empty: true }}
|
||||
>
|
||||
<Placeholder />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.placeholder {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -123,6 +123,8 @@
|
|||
align-items: stretch;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
overflow-x: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.nav-wrapper {
|
||||
|
@ -131,7 +133,7 @@
|
|||
justify-content: center;
|
||||
align-items: stretch;
|
||||
background: white;
|
||||
z-index: 1;
|
||||
z-index: 2;
|
||||
box-shadow: 0 0 8px -1px rgba(0, 0, 0, 0.075);
|
||||
}
|
||||
.layout--top .nav-wrapper.sticky {
|
||||
|
@ -163,6 +165,7 @@
|
|||
justify-content: center;
|
||||
align-items: stretch;
|
||||
flex: 1 1 auto;
|
||||
z-index: 1;
|
||||
}
|
||||
.main {
|
||||
display: flex;
|
||||
|
|
|
@ -1,31 +1,105 @@
|
|||
<script>
|
||||
import { getContext } from "svelte"
|
||||
|
||||
const { linkable, styleable } = getContext("sdk")
|
||||
const { linkable, styleable, builderStore } = getContext("sdk")
|
||||
const component = getContext("component")
|
||||
|
||||
export let url = ""
|
||||
export let text = ""
|
||||
export let openInNewTab = false
|
||||
export let external = false
|
||||
export let url
|
||||
export let text
|
||||
export let openInNewTab
|
||||
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"
|
||||
$: 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>
|
||||
|
||||
{#if external}
|
||||
<a href={url || "/"} {target} use:styleable={$component.styles}>
|
||||
{text}
|
||||
<slot />
|
||||
{#if $builderStore.inBuilder || componentText}
|
||||
{#if external}
|
||||
<a
|
||||
{target}
|
||||
href={url || "/"}
|
||||
use:styleable={styles}
|
||||
class:placeholder
|
||||
class:bold
|
||||
class:italic
|
||||
class:underline
|
||||
class="align--{align || 'left'} size--{size || 'M'}"
|
||||
>
|
||||
{componentText}
|
||||
</a>
|
||||
{:else}
|
||||
<a href={url || "/"} use:linkable {target} use:styleable={$component.styles}>
|
||||
{text}
|
||||
<slot />
|
||||
{: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}
|
||||
|
||||
<style>
|
||||
a {
|
||||
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>
|
||||
|
|
|
@ -4,12 +4,12 @@
|
|||
const { builderStore } = getContext("sdk")
|
||||
const component = getContext("component")
|
||||
|
||||
export let text = $component.name || "Placeholder"
|
||||
export let text
|
||||
</script>
|
||||
|
||||
{#if $builderStore.inBuilder}
|
||||
<div>
|
||||
{text}
|
||||
{text || $component.name || "Placeholder"}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
export let direction
|
||||
export let hAlign
|
||||
export let vAlign
|
||||
export let gap
|
||||
|
||||
const { Provider } = getContext("sdk")
|
||||
const component = getContext("component")
|
||||
|
@ -16,7 +17,7 @@
|
|||
$: loaded = dataProvider?.loaded ?? true
|
||||
</script>
|
||||
|
||||
<Container {direction} {hAlign} {vAlign}>
|
||||
<Container {direction} {hAlign} {vAlign} {gap} wrap>
|
||||
{#if $component.empty}
|
||||
<Placeholder />
|
||||
{:else if rows.length > 0}
|
||||
|
|
|
@ -5,14 +5,37 @@
|
|||
const component = getContext("component")
|
||||
|
||||
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
|
||||
$: componentText = $builderStore.inBuilder
|
||||
? text || "Placeholder 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>
|
||||
|
||||
<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}
|
||||
</p>
|
||||
|
||||
|
@ -20,10 +43,40 @@
|
|||
p {
|
||||
display: inline-block;
|
||||
white-space: pre-wrap;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.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>
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
|
||||
const setUpChart = provider => {
|
||||
const allCols = [labelColumn, ...(valueColumns || [null])]
|
||||
if (!provider || allCols.find(x => x == null)) {
|
||||
if (!provider || !provider.rows?.length || allCols.find(x => x == null)) {
|
||||
return null
|
||||
}
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
// Fetch data on mount
|
||||
const setUpChart = provider => {
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
@ -28,7 +28,7 @@
|
|||
// Fetch data on mount
|
||||
const setUpChart = provider => {
|
||||
const allCols = [labelColumn, ...(valueColumns || [null])]
|
||||
if (!provider || allCols.find(x => x == null)) {
|
||||
if (!provider || !provider.rows?.length || allCols.find(x => x == null)) {
|
||||
return null
|
||||
}
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
|
||||
// Fetch data on mount
|
||||
const setUpChart = provider => {
|
||||
if (!provider || !labelColumn || !valueColumn) {
|
||||
if (!provider || !provider.rows?.length || !labelColumn || !valueColumn) {
|
||||
return null
|
||||
}
|
||||
|
||||
|
|
|
@ -1,12 +1,7 @@
|
|||
const handlebars = require("handlebars")
|
||||
const { registerAll } = require("./helpers/index")
|
||||
const processors = require("./processors")
|
||||
const { cloneDeep } = require("lodash/fp")
|
||||
const {
|
||||
removeNull,
|
||||
updateContext,
|
||||
removeHandlebarsStatements,
|
||||
} = require("./utilities")
|
||||
const { removeHandlebarsStatements } = require("./utilities")
|
||||
const manifest = require("../manifest.json")
|
||||
|
||||
const hbsInstance = handlebars.create()
|
||||
|
@ -92,8 +87,6 @@ module.exports.processStringSync = (string, context) => {
|
|||
}
|
||||
// take a copy of input incase error
|
||||
const input = string
|
||||
const clonedContext = removeNull(updateContext(cloneDeep(context)))
|
||||
// remove any null/undefined properties
|
||||
if (typeof string !== "string") {
|
||||
throw "Cannot process non-string types."
|
||||
}
|
||||
|
@ -103,7 +96,10 @@ module.exports.processStringSync = (string, context) => {
|
|||
const template = hbsInstance.compile(string, {
|
||||
strict: false,
|
||||
})
|
||||
return processors.postprocess(template(clonedContext))
|
||||
return processors.postprocess(template({
|
||||
now: new Date().toISOString(),
|
||||
...context,
|
||||
}))
|
||||
} catch (err) {
|
||||
return removeHandlebarsStatements(input)
|
||||
}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
const _ = require("lodash")
|
||||
const ALPHA_NUMERIC_REGEX = /^[A-Za-z0-9]+$/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)
|
||||
}
|
||||
|
||||
// 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 => {
|
||||
let regexp = new RegExp(exports.FIND_HBS_REGEX)
|
||||
let matches = string.match(regexp)
|
||||
|
|
|
@ -111,7 +111,7 @@ describe("check the utility functions", () => {
|
|||
it("should be able to handle an input date object", async () => {
|
||||
const date = new Date()
|
||||
const output = await processString("{{ dateObj }}", { dateObj: date })
|
||||
expect(date.toISOString()).toEqual(output)
|
||||
expect(date.toString()).toEqual(output)
|
||||
})
|
||||
})
|
||||
|
||||
|
|
Loading…
Reference in New Issue