mirror of https://github.com/Budibase/budibase.git
committed by
GitHub
16 changed files with 638 additions and 177 deletions
@ -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> |
|||
@ -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> |
|||
@ -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 [] |
|||
} |
|||
@ -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 } |
|||
} |
|||
Loading…
Reference in new issue