forked from tsai/budibase
35 changed files with 724 additions and 216 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,8 @@ |
|||||
|
<script> |
||||
|
import TagsTableRenderer from "./TagsTableRenderer.svelte" |
||||
|
export let value |
||||
|
|
||||
|
$: roles = value?.filter(role => role != null).map(role => role.name) ?? [] |
||||
|
</script> |
||||
|
|
||||
|
<TagsTableRenderer value={roles} /> |
||||
@ -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