commit
919368db05
|
@ -115,7 +115,6 @@
|
|||
}
|
||||
|
||||
.spectrum-Modal {
|
||||
background: var(--background);
|
||||
overflow: visible;
|
||||
max-height: none;
|
||||
margin: 40px 0;
|
||||
|
|
|
@ -88,7 +88,6 @@
|
|||
display: grid;
|
||||
position: relative;
|
||||
gap: var(--spacing-xl);
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.spectrum-Dialog-content {
|
||||
|
@ -106,13 +105,8 @@
|
|||
position: absolute;
|
||||
top: 15px;
|
||||
right: 15px;
|
||||
color: var(--ink);
|
||||
font-size: var(--font-size-m);
|
||||
}
|
||||
.close-icon:hover {
|
||||
color: var(--grey-6);
|
||||
cursor: pointer;
|
||||
}
|
||||
.close-icon :global(svg) {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
|
|
@ -530,6 +530,11 @@ export const getFrontendStore = () => {
|
|||
selected._styles = { normal: {}, hover: {}, active: {} }
|
||||
await store.actions.preview.saveSelected()
|
||||
},
|
||||
updateConditions: async conditions => {
|
||||
const selected = get(selectedComponent)
|
||||
selected._conditions = conditions
|
||||
await store.actions.preview.saveSelected()
|
||||
},
|
||||
updateProp: async (name, value) => {
|
||||
let component = get(selectedComponent)
|
||||
if (!name || !component) {
|
||||
|
|
|
@ -1,32 +1,12 @@
|
|||
<script>
|
||||
import { isEmpty } from "lodash/fp"
|
||||
import { Checkbox, Input, Select, DetailSummary } from "@budibase/bbui"
|
||||
import { Input, DetailSummary } 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"
|
||||
import TableSelect from "./PropertyControls/TableSelect.svelte"
|
||||
import DataSourceSelect from "./PropertyControls/DataSourceSelect.svelte"
|
||||
import DataProviderSelect from "./PropertyControls/DataProviderSelect.svelte"
|
||||
import FieldSelect from "./PropertyControls/FieldSelect.svelte"
|
||||
import MultiFieldSelect from "./PropertyControls/MultiFieldSelect.svelte"
|
||||
import SchemaSelect from "./PropertyControls/SchemaSelect.svelte"
|
||||
import SectionSelect from "./PropertyControls/SectionSelect.svelte"
|
||||
import NavigationEditor from "./PropertyControls/NavigationEditor/NavigationEditor.svelte"
|
||||
import EventsEditor from "./PropertyControls/EventsEditor"
|
||||
import FilterEditor from "./PropertyControls/FilterEditor/FilterEditor.svelte"
|
||||
import { IconSelect } from "./PropertyControls/IconSelect"
|
||||
import StringFieldSelect from "./PropertyControls/StringFieldSelect.svelte"
|
||||
import NumberFieldSelect from "./PropertyControls/NumberFieldSelect.svelte"
|
||||
import OptionsFieldSelect from "./PropertyControls/OptionsFieldSelect.svelte"
|
||||
import BooleanFieldSelect from "./PropertyControls/BooleanFieldSelect.svelte"
|
||||
import LongFormFieldSelect from "./PropertyControls/LongFormFieldSelect.svelte"
|
||||
import DateTimeFieldSelect from "./PropertyControls/DateTimeFieldSelect.svelte"
|
||||
import AttachmentFieldSelect from "./PropertyControls/AttachmentFieldSelect.svelte"
|
||||
import RelationshipFieldSelect from "./PropertyControls/RelationshipFieldSelect.svelte"
|
||||
import ResetFieldsButton from "./PropertyControls/ResetFieldsButton.svelte"
|
||||
import ColorPicker from "./PropertyControls/ColorPicker.svelte"
|
||||
import URLSelect from "./PropertyControls/URLSelect.svelte"
|
||||
import { getComponentForSettingType } from "./PropertyControls/componentSettings"
|
||||
|
||||
export let componentDefinition
|
||||
export let componentInstance
|
||||
|
@ -45,40 +25,9 @@
|
|||
$: assetDefinition = isLayout ? layoutDefinition : screenDefinition
|
||||
|
||||
const updateProp = store.actions.components.updateProp
|
||||
const controlMap = {
|
||||
text: Input,
|
||||
select: Select,
|
||||
dataSource: DataSourceSelect,
|
||||
dataProvider: DataProviderSelect,
|
||||
boolean: Checkbox,
|
||||
number: Input,
|
||||
event: EventsEditor,
|
||||
table: TableSelect,
|
||||
color: ColorPicker,
|
||||
icon: IconSelect,
|
||||
field: FieldSelect,
|
||||
multifield: MultiFieldSelect,
|
||||
schema: SchemaSelect,
|
||||
section: SectionSelect,
|
||||
navigation: NavigationEditor,
|
||||
filter: FilterEditor,
|
||||
url: URLSelect,
|
||||
"field/string": StringFieldSelect,
|
||||
"field/number": NumberFieldSelect,
|
||||
"field/options": OptionsFieldSelect,
|
||||
"field/boolean": BooleanFieldSelect,
|
||||
"field/longform": LongFormFieldSelect,
|
||||
"field/datetime": DateTimeFieldSelect,
|
||||
"field/attachment": AttachmentFieldSelect,
|
||||
"field/link": RelationshipFieldSelect,
|
||||
}
|
||||
|
||||
const getControl = type => {
|
||||
return controlMap[type]
|
||||
}
|
||||
|
||||
const canRenderControl = setting => {
|
||||
const control = getControl(setting?.type)
|
||||
const control = getComponentForSettingType(setting?.type)
|
||||
if (!control) {
|
||||
return false
|
||||
}
|
||||
|
@ -101,11 +50,11 @@
|
|||
/>
|
||||
{/if}
|
||||
{#if settings && settings.length > 0}
|
||||
{#each settings as setting (`${componentInstance._id}-${setting.key}`)}
|
||||
{#each settings as setting}
|
||||
{#if canRenderControl(setting)}
|
||||
<PropertyControl
|
||||
type={setting.type}
|
||||
control={getControl(setting.type)}
|
||||
control={getComponentForSettingType(setting.type)}
|
||||
label={setting.label}
|
||||
key={setting.key}
|
||||
value={componentInstance[setting.key] ??
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
<script>
|
||||
import { DetailSummary, ActionButton, Drawer, Button } from "@budibase/bbui"
|
||||
import { store } from "builderStore"
|
||||
import ConditionalUIDrawer from "./PropertyControls/ConditionalUIDrawer.svelte"
|
||||
|
||||
export let componentInstance
|
||||
|
||||
let tempValue
|
||||
let drawer
|
||||
|
||||
const openDrawer = () => {
|
||||
tempValue = JSON.parse(JSON.stringify(componentInstance?._conditions ?? []))
|
||||
drawer.show()
|
||||
}
|
||||
|
||||
const save = () => {
|
||||
store.actions.components.updateConditions(tempValue)
|
||||
drawer.hide()
|
||||
}
|
||||
</script>
|
||||
|
||||
<DetailSummary
|
||||
name={`Conditions${componentInstance?._conditions ? " *" : ""}`}
|
||||
collapsible={false}
|
||||
>
|
||||
<div>
|
||||
<ActionButton on:click={openDrawer}>Configure conditions</ActionButton>
|
||||
</div>
|
||||
</DetailSummary>
|
||||
<Drawer bind:this={drawer} title="Conditions">
|
||||
<svelte:fragment slot="description">
|
||||
Show, hide and update components in response to conditions being met.
|
||||
</svelte:fragment>
|
||||
<Button cta slot="buttons" on:click={() => save()}>Save</Button>
|
||||
<ConditionalUIDrawer slot="body" bind:conditions={tempValue} />
|
||||
</Drawer>
|
|
@ -5,6 +5,7 @@
|
|||
import ComponentSettingsSection from "./ComponentSettingsSection.svelte"
|
||||
import DesignSection from "./DesignSection.svelte"
|
||||
import CustomStylesSection from "./CustomStylesSection.svelte"
|
||||
import ConditionalUISection from "./ConditionalUISection.svelte"
|
||||
|
||||
$: componentInstance = $selectedComponent
|
||||
$: componentDefinition = store.actions.components.getDefinition(
|
||||
|
@ -15,10 +16,13 @@
|
|||
<Tabs selected="Settings" noPadding>
|
||||
<Tab title="Settings">
|
||||
<div class="container">
|
||||
{#key componentInstance?._id}
|
||||
<ScreenSettingsSection {componentInstance} {componentDefinition} />
|
||||
<ComponentSettingsSection {componentInstance} {componentDefinition} />
|
||||
<DesignSection {componentInstance} {componentDefinition} />
|
||||
<CustomStylesSection {componentInstance} {componentDefinition} />
|
||||
<ConditionalUISection {componentInstance} {componentDefinition} />
|
||||
{/key}
|
||||
</div>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
|
|
@ -0,0 +1,290 @@
|
|||
<script>
|
||||
import {
|
||||
Button,
|
||||
Body,
|
||||
Icon,
|
||||
DrawerContent,
|
||||
Layout,
|
||||
Select,
|
||||
DatePicker,
|
||||
} from "@budibase/bbui"
|
||||
import { flip } from "svelte/animate"
|
||||
import { dndzone } from "svelte-dnd-action"
|
||||
import { generate } from "shortid"
|
||||
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
|
||||
import { OperatorOptions, getValidOperatorsForType } from "helpers/lucene"
|
||||
import { getBindableProperties } from "builderStore/dataBinding"
|
||||
import { currentAsset, selectedComponent, store } from "builderStore"
|
||||
import { getComponentForSettingType } from "./componentSettings"
|
||||
import PropertyControl from "./PropertyControl.svelte"
|
||||
|
||||
export let conditions = []
|
||||
|
||||
const flipDurationMs = 150
|
||||
const actionOptions = [
|
||||
{
|
||||
label: "Hide component",
|
||||
value: "hide",
|
||||
},
|
||||
{
|
||||
label: "Show component",
|
||||
value: "show",
|
||||
},
|
||||
{
|
||||
label: "Update setting",
|
||||
value: "update",
|
||||
},
|
||||
]
|
||||
const valueTypeOptions = [
|
||||
{
|
||||
value: "string",
|
||||
label: "Binding",
|
||||
},
|
||||
{
|
||||
value: "number",
|
||||
label: "Number",
|
||||
},
|
||||
{
|
||||
value: "datetime",
|
||||
label: "Date",
|
||||
},
|
||||
{
|
||||
value: "boolean",
|
||||
label: "Boolean",
|
||||
},
|
||||
]
|
||||
|
||||
let dragDisabled = true
|
||||
$: definition = store.actions.components.getDefinition(
|
||||
$selectedComponent?._component
|
||||
)
|
||||
$: settings = (definition?.settings ?? []).map(setting => {
|
||||
return {
|
||||
label: setting.label,
|
||||
value: setting.key,
|
||||
}
|
||||
})
|
||||
$: bindableProperties = getBindableProperties(
|
||||
$currentAsset,
|
||||
$store.selectedComponentId
|
||||
)
|
||||
$: conditions.forEach(link => {
|
||||
if (!link.id) {
|
||||
link.id = generate()
|
||||
}
|
||||
})
|
||||
|
||||
const getSettingDefinition = key => {
|
||||
return definition?.settings?.find(setting => {
|
||||
return setting.key === key
|
||||
})
|
||||
}
|
||||
|
||||
const getComponentForSetting = key => {
|
||||
const settingDefinition = getSettingDefinition(key)
|
||||
return getComponentForSettingType(settingDefinition?.type || "text")
|
||||
}
|
||||
|
||||
const addCondition = () => {
|
||||
conditions = [
|
||||
...conditions,
|
||||
{
|
||||
valueType: "string",
|
||||
id: generate(),
|
||||
action: "hide",
|
||||
operator: OperatorOptions.Equals.value,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
const removeCondition = id => {
|
||||
conditions = conditions.filter(link => link.id !== id)
|
||||
}
|
||||
|
||||
const handleFinalize = e => {
|
||||
updateConditions(e)
|
||||
dragDisabled = true
|
||||
}
|
||||
|
||||
const updateConditions = e => {
|
||||
conditions = e.detail.items
|
||||
}
|
||||
|
||||
const getOperatorOptions = condition => {
|
||||
return getValidOperatorsForType(condition.valueType)
|
||||
}
|
||||
|
||||
const onOperatorChange = (condition, newOperator) => {
|
||||
const noValueOptions = [
|
||||
OperatorOptions.Empty.value,
|
||||
OperatorOptions.NotEmpty.value,
|
||||
]
|
||||
condition.noValue = noValueOptions.includes(newOperator)
|
||||
if (condition.noValue) {
|
||||
condition.referenceValue = null
|
||||
condition.valueType = "string"
|
||||
}
|
||||
}
|
||||
|
||||
const onValueTypeChange = (condition, newType) => {
|
||||
condition.referenceValue = null
|
||||
|
||||
// Ensure a valid operator is set
|
||||
const validOperators = getValidOperatorsForType(newType).map(x => x.value)
|
||||
if (!validOperators.includes(condition.operator)) {
|
||||
condition.operator = validOperators[0] ?? OperatorOptions.Equals.value
|
||||
onOperatorChange(condition, condition.operator)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<DrawerContent>
|
||||
<div class="container">
|
||||
<Layout noPadding>
|
||||
{#if conditions?.length}
|
||||
<div
|
||||
class="conditions"
|
||||
use:dndzone={{
|
||||
items: conditions,
|
||||
flipDurationMs,
|
||||
dropTargetStyle: { outline: "none" },
|
||||
dragDisabled,
|
||||
}}
|
||||
on:finalize={handleFinalize}
|
||||
on:consider={updateConditions}
|
||||
>
|
||||
{#each conditions as condition (condition.id)}
|
||||
<div
|
||||
class="condition"
|
||||
class:update={condition.action === "update"}
|
||||
animate:flip={{ duration: flipDurationMs }}
|
||||
>
|
||||
<div
|
||||
class="handle"
|
||||
aria-label="drag-handle"
|
||||
style={dragDisabled ? "cursor: grab" : "cursor: grabbing"}
|
||||
on:mousedown={() => (dragDisabled = false)}
|
||||
>
|
||||
<Icon name="DragHandle" size="XL" />
|
||||
</div>
|
||||
<Select
|
||||
placeholder={null}
|
||||
options={actionOptions}
|
||||
bind:value={condition.action}
|
||||
/>
|
||||
{#if condition.action === "update"}
|
||||
<Select options={settings} bind:value={condition.setting} />
|
||||
<div>TO</div>
|
||||
{#if getSettingDefinition(condition.setting)}
|
||||
<PropertyControl
|
||||
type={getSettingDefinition(condition.setting).type}
|
||||
control={getComponentForSetting(condition.setting)}
|
||||
key={getSettingDefinition(condition.setting).key}
|
||||
value={condition.settingValue}
|
||||
componentInstance={$selectedComponent}
|
||||
onChange={val => (condition.settingValue = val)}
|
||||
props={{
|
||||
options: getSettingDefinition(condition.setting).options,
|
||||
placeholder: getSettingDefinition(condition.setting)
|
||||
.placeholder,
|
||||
}}
|
||||
/>
|
||||
{:else}
|
||||
<Select disabled placeholder=" " />
|
||||
{/if}
|
||||
{/if}
|
||||
<div>IF</div>
|
||||
<DrawerBindableInput
|
||||
bindings={bindableProperties}
|
||||
placeholder="Value"
|
||||
value={condition.newValue}
|
||||
on:change={e => (condition.newValue = e.detail)}
|
||||
/>
|
||||
<Select
|
||||
placeholder={null}
|
||||
options={getOperatorOptions(condition)}
|
||||
bind:value={condition.operator}
|
||||
on:change={e => onOperatorChange(condition, e.detail)}
|
||||
/>
|
||||
<Select
|
||||
disabled={condition.noValue}
|
||||
options={valueTypeOptions}
|
||||
bind:value={condition.valueType}
|
||||
placeholder={null}
|
||||
on:change={e => onValueTypeChange(condition, e.detail)}
|
||||
/>
|
||||
{#if ["string", "number"].includes(condition.valueType)}
|
||||
<DrawerBindableInput
|
||||
disabled={condition.noValue}
|
||||
bindings={bindableProperties}
|
||||
placeholder="Value"
|
||||
value={condition.referenceValue}
|
||||
on:change={e => (condition.referenceValue = e.detail)}
|
||||
/>
|
||||
{:else if condition.valueType === "datetime"}
|
||||
<DatePicker
|
||||
placeholder="Value"
|
||||
disabled={condition.noValue}
|
||||
bind:value={condition.referenceValue}
|
||||
/>
|
||||
{:else if condition.valueType === "boolean"}
|
||||
<Select
|
||||
placeholder="Value"
|
||||
disabled={condition.noValue}
|
||||
options={["True", "False"]}
|
||||
bind:value={condition.referenceValue}
|
||||
/>
|
||||
{/if}
|
||||
<Icon
|
||||
name="Close"
|
||||
hoverable
|
||||
size="S"
|
||||
on:click={() => removeCondition(condition.id)}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<Body size="S">Add your first condition to get started.</Body>
|
||||
{/if}
|
||||
<div>
|
||||
<Button secondary icon="Add" on:click={addCondition}>
|
||||
Add condition
|
||||
</Button>
|
||||
</div>
|
||||
</Layout>
|
||||
</div>
|
||||
</DrawerContent>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
width: 100%;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.conditions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
gap: var(--spacing-m);
|
||||
}
|
||||
.condition {
|
||||
gap: var(--spacing-l);
|
||||
display: grid;
|
||||
align-items: center;
|
||||
grid-template-columns: auto 1fr auto 1fr 1fr 1fr 1fr auto;
|
||||
border-radius: var(--border-radius-s);
|
||||
transition: background-color ease-in-out 130ms;
|
||||
}
|
||||
.condition.update {
|
||||
grid-template-columns: auto 1fr 1fr auto 1fr auto 1fr 1fr 1fr 1fr auto;
|
||||
}
|
||||
.condition:hover {
|
||||
background-color: var(--spectrum-global-color-gray-100);
|
||||
}
|
||||
.handle {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
</style>
|
|
@ -11,44 +11,11 @@
|
|||
import { getBindableProperties } from "builderStore/dataBinding"
|
||||
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
|
||||
import { generate } from "shortid"
|
||||
import { OperatorOptions, getValidOperatorsForType } from "helpers/lucene"
|
||||
|
||||
export let schemaFields
|
||||
export let value
|
||||
|
||||
const OperatorOptions = {
|
||||
Equals: {
|
||||
value: "equal",
|
||||
label: "Equals",
|
||||
},
|
||||
NotEquals: {
|
||||
value: "notEqual",
|
||||
label: "Not equals",
|
||||
},
|
||||
Empty: {
|
||||
value: "empty",
|
||||
label: "Is empty",
|
||||
},
|
||||
NotEmpty: {
|
||||
value: "notEmpty",
|
||||
label: "Is not empty",
|
||||
},
|
||||
StartsWith: {
|
||||
value: "string",
|
||||
label: "Starts with",
|
||||
},
|
||||
Like: {
|
||||
value: "fuzzy",
|
||||
label: "Like",
|
||||
},
|
||||
MoreThan: {
|
||||
value: "rangeLow",
|
||||
label: "More than",
|
||||
},
|
||||
LessThan: {
|
||||
value: "rangeHigh",
|
||||
label: "Less than",
|
||||
},
|
||||
}
|
||||
const BannedTypes = ["link", "attachment"]
|
||||
$: bindableProperties = getBindableProperties(
|
||||
$currentAsset,
|
||||
|
@ -75,61 +42,16 @@
|
|||
value = value.filter(field => field.id !== id)
|
||||
}
|
||||
|
||||
const getValidOperatorsForType = type => {
|
||||
const Op = OperatorOptions
|
||||
if (type === "string") {
|
||||
return [
|
||||
Op.Equals,
|
||||
Op.NotEquals,
|
||||
Op.StartsWith,
|
||||
Op.Like,
|
||||
Op.Empty,
|
||||
Op.NotEmpty,
|
||||
]
|
||||
} else if (type === "number") {
|
||||
return [
|
||||
Op.Equals,
|
||||
Op.NotEquals,
|
||||
Op.MoreThan,
|
||||
Op.LessThan,
|
||||
Op.Empty,
|
||||
Op.NotEmpty,
|
||||
]
|
||||
} else if (type === "options") {
|
||||
return [Op.Equals, Op.NotEquals, Op.Empty, Op.NotEmpty]
|
||||
} else if (type === "boolean") {
|
||||
return [Op.Equals, Op.NotEquals, Op.Empty, Op.NotEmpty]
|
||||
} else if (type === "longform") {
|
||||
return [
|
||||
Op.Equals,
|
||||
Op.NotEquals,
|
||||
Op.StartsWith,
|
||||
Op.Like,
|
||||
Op.Empty,
|
||||
Op.NotEmpty,
|
||||
]
|
||||
} else if (type === "datetime") {
|
||||
return [
|
||||
Op.Equals,
|
||||
Op.NotEquals,
|
||||
Op.MoreThan,
|
||||
Op.LessThan,
|
||||
Op.Empty,
|
||||
Op.NotEmpty,
|
||||
]
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
const onFieldChange = (expression, field) => {
|
||||
// Update the field type
|
||||
expression.type = schemaFields.find(x => x.name === field)?.type
|
||||
|
||||
// Ensure a valid operator is set
|
||||
const validOperators = getValidOperatorsForType(expression.type)
|
||||
const validOperators = getValidOperatorsForType(expression.type).map(
|
||||
x => x.value
|
||||
)
|
||||
if (!validOperators.includes(expression.operator)) {
|
||||
expression.operator =
|
||||
validOperators[0]?.value ?? OperatorOptions.Equals.value
|
||||
expression.operator = validOperators[0] ?? OperatorOptions.Equals.value
|
||||
onOperatorChange(expression, expression.operator)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,13 +15,13 @@
|
|||
export let links = []
|
||||
|
||||
const flipDurationMs = 150
|
||||
let dragDisabled = true
|
||||
|
||||
$: links.forEach(link => {
|
||||
if (!link.id) {
|
||||
link.id = generate()
|
||||
}
|
||||
})
|
||||
|
||||
$: urlOptions = $store.screens
|
||||
.map(screen => screen.routing?.route)
|
||||
.filter(x => x != null)
|
||||
|
@ -37,6 +37,11 @@
|
|||
const updateLinks = e => {
|
||||
links = e.detail.items
|
||||
}
|
||||
|
||||
const handleFinalize = e => {
|
||||
updateLinks(e)
|
||||
dragDisabled = true
|
||||
}
|
||||
</script>
|
||||
|
||||
<DrawerContent>
|
||||
|
@ -49,13 +54,21 @@
|
|||
items: links,
|
||||
flipDurationMs,
|
||||
dropTargetStyle: { outline: "none" },
|
||||
dragDisabled,
|
||||
}}
|
||||
on:finalize={updateLinks}
|
||||
on:finalize={handleFinalize}
|
||||
on:consider={updateLinks}
|
||||
>
|
||||
{#each links as link (link.id)}
|
||||
<div class="link" animate:flip={{ duration: flipDurationMs }}>
|
||||
<div
|
||||
class="handle"
|
||||
aria-label="drag-handle"
|
||||
style={dragDisabled ? "cursor: grab" : "cursor: grabbing"}
|
||||
on:mousedown={() => (dragDisabled = false)}
|
||||
>
|
||||
<Icon name="DragHandle" size="XL" />
|
||||
</div>
|
||||
<Input bind:value={link.text} placeholder="Text" />
|
||||
<Combobox
|
||||
bind:value={link.url}
|
||||
|
@ -90,7 +103,7 @@
|
|||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
gap: var(--spacing-s);
|
||||
gap: var(--spacing-m);
|
||||
}
|
||||
.link {
|
||||
gap: var(--spacing-l);
|
||||
|
@ -108,4 +121,8 @@
|
|||
flex: 1 1 auto;
|
||||
width: 0;
|
||||
}
|
||||
.handle {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -70,7 +70,7 @@
|
|||
</script>
|
||||
|
||||
<div class="property-control" bind:this={anchor} data-cy={`setting-${key}`}>
|
||||
{#if type !== "boolean"}
|
||||
{#if type !== "boolean" && label}
|
||||
<div class="label">
|
||||
<Label>{label}</Label>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
import { Checkbox, Input, Select } from "@budibase/bbui"
|
||||
import DataSourceSelect from "./DataSourceSelect.svelte"
|
||||
import DataProviderSelect from "./DataProviderSelect.svelte"
|
||||
import EventsEditor from "./EventsEditor"
|
||||
import TableSelect from "./TableSelect.svelte"
|
||||
import ColorPicker from "./ColorPicker.svelte"
|
||||
import { IconSelect } from "./IconSelect"
|
||||
import FieldSelect from "./FieldSelect.svelte"
|
||||
import MultiFieldSelect from "./MultiFieldSelect.svelte"
|
||||
import SchemaSelect from "./SchemaSelect.svelte"
|
||||
import SectionSelect from "./SectionSelect.svelte"
|
||||
import NavigationEditor from "./NavigationEditor/NavigationEditor.svelte"
|
||||
import FilterEditor from "./FilterEditor/FilterEditor.svelte"
|
||||
import URLSelect from "./URLSelect.svelte"
|
||||
import StringFieldSelect from "./StringFieldSelect.svelte"
|
||||
import NumberFieldSelect from "./NumberFieldSelect.svelte"
|
||||
import OptionsFieldSelect from "./OptionsFieldSelect.svelte"
|
||||
import BooleanFieldSelect from "./BooleanFieldSelect.svelte"
|
||||
import LongFormFieldSelect from "./LongFormFieldSelect.svelte"
|
||||
import DateTimeFieldSelect from "./DateTimeFieldSelect.svelte"
|
||||
import AttachmentFieldSelect from "./AttachmentFieldSelect.svelte"
|
||||
import RelationshipFieldSelect from "./RelationshipFieldSelect.svelte"
|
||||
|
||||
const componentMap = {
|
||||
text: Input,
|
||||
select: Select,
|
||||
dataSource: DataSourceSelect,
|
||||
dataProvider: DataProviderSelect,
|
||||
boolean: Checkbox,
|
||||
number: Input,
|
||||
event: EventsEditor,
|
||||
table: TableSelect,
|
||||
color: ColorPicker,
|
||||
icon: IconSelect,
|
||||
field: FieldSelect,
|
||||
multifield: MultiFieldSelect,
|
||||
schema: SchemaSelect,
|
||||
section: SectionSelect,
|
||||
navigation: NavigationEditor,
|
||||
filter: FilterEditor,
|
||||
url: URLSelect,
|
||||
"field/string": StringFieldSelect,
|
||||
"field/number": NumberFieldSelect,
|
||||
"field/options": OptionsFieldSelect,
|
||||
"field/boolean": BooleanFieldSelect,
|
||||
"field/longform": LongFormFieldSelect,
|
||||
"field/datetime": DateTimeFieldSelect,
|
||||
"field/attachment": AttachmentFieldSelect,
|
||||
"field/link": RelationshipFieldSelect,
|
||||
}
|
||||
|
||||
export const getComponentForSettingType = type => {
|
||||
return componentMap[type]
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
export const OperatorOptions = {
|
||||
Equals: {
|
||||
value: "equal",
|
||||
label: "Equals",
|
||||
},
|
||||
NotEquals: {
|
||||
value: "notEqual",
|
||||
label: "Not equals",
|
||||
},
|
||||
Empty: {
|
||||
value: "empty",
|
||||
label: "Is empty",
|
||||
},
|
||||
NotEmpty: {
|
||||
value: "notEmpty",
|
||||
label: "Is not empty",
|
||||
},
|
||||
StartsWith: {
|
||||
value: "string",
|
||||
label: "Starts with",
|
||||
},
|
||||
Like: {
|
||||
value: "fuzzy",
|
||||
label: "Like",
|
||||
},
|
||||
MoreThan: {
|
||||
value: "rangeLow",
|
||||
label: "More than",
|
||||
},
|
||||
LessThan: {
|
||||
value: "rangeHigh",
|
||||
label: "Less than",
|
||||
},
|
||||
}
|
||||
|
||||
export const getValidOperatorsForType = type => {
|
||||
const Op = OperatorOptions
|
||||
if (type === "string") {
|
||||
return [
|
||||
Op.Equals,
|
||||
Op.NotEquals,
|
||||
Op.StartsWith,
|
||||
Op.Like,
|
||||
Op.Empty,
|
||||
Op.NotEmpty,
|
||||
]
|
||||
} else if (type === "number") {
|
||||
return [
|
||||
Op.Equals,
|
||||
Op.NotEquals,
|
||||
Op.MoreThan,
|
||||
Op.LessThan,
|
||||
Op.Empty,
|
||||
Op.NotEmpty,
|
||||
]
|
||||
} else if (type === "options") {
|
||||
return [Op.Equals, Op.NotEquals, Op.Empty, Op.NotEmpty]
|
||||
} else if (type === "boolean") {
|
||||
return [Op.Equals, Op.NotEquals, Op.Empty, Op.NotEmpty]
|
||||
} else if (type === "longform") {
|
||||
return [
|
||||
Op.Equals,
|
||||
Op.NotEquals,
|
||||
Op.StartsWith,
|
||||
Op.Like,
|
||||
Op.Empty,
|
||||
Op.NotEmpty,
|
||||
]
|
||||
} else if (type === "datetime") {
|
||||
return [
|
||||
Op.Equals,
|
||||
Op.NotEquals,
|
||||
Op.MoreThan,
|
||||
Op.LessThan,
|
||||
Op.Empty,
|
||||
Op.NotEmpty,
|
||||
]
|
||||
}
|
||||
return []
|
||||
}
|
|
@ -8,11 +8,18 @@
|
|||
import { hashString } from "../utils/hash"
|
||||
import Manifest from "@budibase/standard-components/manifest.json"
|
||||
import { Placeholder } from "@budibase/standard-components"
|
||||
import {
|
||||
getActiveConditions,
|
||||
reduceConditionActions,
|
||||
} from "../utils/conditions"
|
||||
|
||||
export let instance = {}
|
||||
|
||||
// Props that will be passed to the component instance
|
||||
let componentProps
|
||||
// The enriched component settings
|
||||
let enrichedSettings
|
||||
|
||||
// Any prop overrides that need to be applied due to conditional UI
|
||||
let conditionalSettings
|
||||
|
||||
// Props are hashed when inside the builder preview and used as a key, so that
|
||||
// components fully remount whenever any props change
|
||||
|
@ -28,6 +35,9 @@
|
|||
let lastContextKey
|
||||
let lastInstanceKey
|
||||
|
||||
// Visibility flag used by conditional UI
|
||||
let visible = true
|
||||
|
||||
// Get contexts
|
||||
const context = getContext("context")
|
||||
const insideScreenslot = !!getContext("screenslot")
|
||||
|
@ -54,6 +64,8 @@
|
|||
$builderStore.inBuilder &&
|
||||
$builderStore.selectedComponentId === instance._id
|
||||
$: interactive = $builderStore.previewType === "layout" || insideScreenslot
|
||||
$: evaluateConditions(enrichedSettings?._conditions)
|
||||
$: componentSettings = { ...enrichedSettings, ...conditionalSettings }
|
||||
|
||||
// Update component context
|
||||
$: componentStore.set({
|
||||
|
@ -62,14 +74,14 @@
|
|||
styles: { ...instance._styles, id, empty, interactive },
|
||||
empty,
|
||||
selected,
|
||||
props: componentProps,
|
||||
props: componentSettings,
|
||||
name,
|
||||
})
|
||||
|
||||
const getRawProps = instance => {
|
||||
let validProps = {}
|
||||
Object.entries(instance)
|
||||
.filter(([name]) => !name.startsWith("_"))
|
||||
.filter(([name]) => name === "_conditions" || !name.startsWith("_"))
|
||||
.forEach(([key, value]) => {
|
||||
validProps[key] = value
|
||||
})
|
||||
|
@ -123,34 +135,55 @@
|
|||
return
|
||||
}
|
||||
let propsChanged = false
|
||||
if (!componentProps) {
|
||||
componentProps = {}
|
||||
if (!enrichedSettings) {
|
||||
enrichedSettings = {}
|
||||
propsChanged = true
|
||||
}
|
||||
Object.keys(enrichedProps).forEach(key => {
|
||||
if (!propsAreSame(enrichedProps[key], componentProps[key])) {
|
||||
if (!propsAreSame(enrichedProps[key], enrichedSettings[key])) {
|
||||
propsChanged = true
|
||||
componentProps[key] = enrichedProps[key]
|
||||
enrichedSettings[key] = enrichedProps[key]
|
||||
}
|
||||
})
|
||||
|
||||
// Update the hash if we're in the builder so we can fully remount this
|
||||
// component
|
||||
if (get(builderStore).inBuilder && propsChanged) {
|
||||
propsHash = hashString(JSON.stringify(componentProps))
|
||||
propsHash = hashString(JSON.stringify(enrichedSettings))
|
||||
}
|
||||
}
|
||||
|
||||
const evaluateConditions = conditions => {
|
||||
if (!conditions?.length) {
|
||||
return
|
||||
}
|
||||
|
||||
// Default visible to false if there is a show condition
|
||||
let nextVisible = !conditions.find(condition => condition.action === "show")
|
||||
|
||||
// Execute conditions and determine settings and visibility changes
|
||||
const activeConditions = getActiveConditions(conditions)
|
||||
const result = reduceConditionActions(activeConditions)
|
||||
if (result.visible != null) {
|
||||
nextVisible = result.visible
|
||||
}
|
||||
|
||||
// Update state from condition results
|
||||
conditionalSettings = result.settingUpdates
|
||||
visible = nextVisible
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
{#key propsHash}
|
||||
{#if constructor && componentSettings && visible}
|
||||
<div
|
||||
class={`component ${id}`}
|
||||
data-type={interactive ? "component" : ""}
|
||||
data-id={id}
|
||||
data-name={name}
|
||||
>
|
||||
{#key propsHash}
|
||||
{#if constructor && componentProps}
|
||||
<svelte:component this={constructor} {...componentProps}>
|
||||
class:hidden={!visible}
|
||||
>
|
||||
<svelte:component this={constructor} {...componentSettings}>
|
||||
{#if children.length}
|
||||
{#each children as child (child._id)}
|
||||
<svelte:self instance={child} />
|
||||
|
@ -159,9 +192,9 @@
|
|||
<Placeholder />
|
||||
{/if}
|
||||
</svelte:component>
|
||||
</div>
|
||||
{/if}
|
||||
{/key}
|
||||
</div>
|
||||
{/key}
|
||||
|
||||
<style>
|
||||
.component {
|
||||
|
|
|
@ -43,5 +43,17 @@ export const enrichProps = (props, context) => {
|
|||
)
|
||||
}
|
||||
|
||||
// Enrich any click actions in conditions
|
||||
if (enrichedProps._conditions) {
|
||||
enrichedProps._conditions.forEach(condition => {
|
||||
if (condition.setting === "onClick") {
|
||||
condition.settingValue = enrichButtonActions(
|
||||
condition.settingValue,
|
||||
totalContext
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return enrichedProps
|
||||
}
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
import {
|
||||
buildLuceneQuery,
|
||||
luceneQuery,
|
||||
} from "../../../standard-components/src/lucene"
|
||||
|
||||
export const getActiveConditions = conditions => {
|
||||
if (!conditions?.length) {
|
||||
return []
|
||||
}
|
||||
|
||||
return conditions.filter(condition => {
|
||||
// Parse values into correct types
|
||||
if (condition.valueType === "number") {
|
||||
condition.referenceValue = parseFloat(condition.referenceValue)
|
||||
condition.newValue = parseFloat(condition.newValue)
|
||||
} else if (condition.valueType === "datetime") {
|
||||
if (condition.referenceValue) {
|
||||
condition.referenceValue = new Date(
|
||||
condition.referenceValue
|
||||
).toISOString()
|
||||
}
|
||||
if (condition.newValue) {
|
||||
condition.newValue = new Date(condition.newValue).toISOString()
|
||||
}
|
||||
} else if (condition.valueType === "boolean") {
|
||||
condition.referenceValue =
|
||||
`${condition.referenceValue}`.toLowerCase() === "true"
|
||||
condition.newValue = `${condition.newValue}`.toLowerCase() === "true"
|
||||
}
|
||||
|
||||
// Build lucene compatible condition expression
|
||||
const luceneCondition = {
|
||||
...condition,
|
||||
type: condition.valueType,
|
||||
field: "newValue",
|
||||
value: condition.referenceValue,
|
||||
}
|
||||
|
||||
const query = buildLuceneQuery([luceneCondition])
|
||||
const result = luceneQuery([luceneCondition], query)
|
||||
return result.length > 0
|
||||
})
|
||||
}
|
||||
|
||||
export const reduceConditionActions = conditions => {
|
||||
let settingUpdates = {}
|
||||
let visible = null
|
||||
|
||||
conditions?.forEach(condition => {
|
||||
if (condition.action === "show") {
|
||||
visible = true
|
||||
} else if (condition.action === "hide") {
|
||||
visible = false
|
||||
} else if (condition.setting) {
|
||||
settingUpdates[condition.setting] = condition.settingValue
|
||||
}
|
||||
})
|
||||
|
||||
return { settingUpdates, visible }
|
||||
}
|
|
@ -23,7 +23,7 @@ export const buildLuceneQuery = filter => {
|
|||
value = parseFloat(value)
|
||||
}
|
||||
if (type === "boolean") {
|
||||
value = value?.toLowerCase() === "true"
|
||||
value = `${value}`?.toLowerCase() === "true"
|
||||
}
|
||||
if (operator.startsWith("range")) {
|
||||
if (!query.range[field]) {
|
||||
|
@ -91,6 +91,11 @@ export const luceneQuery = (docs, query) => {
|
|||
return !doc[key] || !doc[key].startsWith(value)
|
||||
})
|
||||
|
||||
// Process a fuzzy match (treat the same as starts with when running locally)
|
||||
const fuzzyMatch = match("fuzzy", (key, value, doc) => {
|
||||
return !doc[key] || !doc[key].startsWith(value)
|
||||
})
|
||||
|
||||
// Process a range match
|
||||
const rangeMatch = match("range", (key, value, doc) => {
|
||||
return !doc[key] || doc[key] < value.low || doc[key] > value.high
|
||||
|
@ -120,6 +125,7 @@ export const luceneQuery = (docs, query) => {
|
|||
const docMatch = doc => {
|
||||
return (
|
||||
stringMatch(doc) &&
|
||||
fuzzyMatch(doc) &&
|
||||
rangeMatch(doc) &&
|
||||
equalMatch(doc) &&
|
||||
notEqualMatch(doc) &&
|
||||
|
|
Loading…
Reference in New Issue