Add row conditions

This commit is contained in:
Andrew Kingston 2024-06-28 11:25:00 +01:00
parent cf12c8246b
commit 06e7517529
No known key found for this signature in database
15 changed files with 404 additions and 86 deletions

View File

@ -5,6 +5,7 @@
export let bindings = []
export let value = ""
export let allowHBS = true
export let allowJS = false
export let allowHelpers = true
export let autofocusEditor = false
@ -31,6 +32,7 @@
context={{ ...$previewStore.selectedComponentContext, ...context }}
snippets={$snippets}
{value}
{allowHBS}
{allowJS}
{allowHelpers}
{autofocusEditor}

View File

@ -16,6 +16,7 @@
export let placeholder
export let label
export let disabled = false
export let allowHBS = true
export let allowJS = true
export let allowHelpers = true
export let updateOnChange = true
@ -98,6 +99,7 @@
value={readableValue}
on:change={event => (tempValue = event.detail)}
{bindings}
{allowHBS}
{allowJS}
{allowHelpers}
{context}

View File

@ -31,6 +31,7 @@ import FormStepConfiguration from "./controls/FormStepConfiguration.svelte"
import FormStepControls from "./controls/FormStepControls.svelte"
import PaywalledSetting from "./controls/PaywalledSetting.svelte"
import CellConditionEditor from "./controls/CellConditionEditor.svelte"
import RowConditionEditor from "./controls/RowConditionEditor.svelte"
const componentMap = {
text: DrawerBindableInput,
@ -63,6 +64,7 @@ const componentMap = {
"columns/basic": BasicColumnEditor,
"columns/grid": GridColumnEditor,
cellConditions: CellConditionEditor,
rowConditions: RowConditionEditor,
"field/sortable": SortableFieldSelect,
"field/string": FormFieldSelect,
"field/number": FormFieldSelect,

View File

@ -113,11 +113,17 @@
<ActionButton on:click={openDrawer}>{conditionText}</ActionButton>
<!-- svelte-ignore a11y-no-static-element-interactions -->
<Drawer bind:this={drawer} title="Conditions" on:drawerShow on:drawerHide>
<Drawer
bind:this={drawer}
title="{componentInstance.field} conditions"
on:drawerShow
on:drawerHide
>
<Button cta slot="buttons" on:click={save}>Save</Button>
<DrawerContent slot="body">
<div class="container">
<Layout noPadding>
Update the appearance of cells based on their value.
{#if tempValue.length}
<div
class="conditions"

View File

@ -0,0 +1,200 @@
<script>
import {
ActionButton,
Drawer,
Button,
DrawerContent,
Layout,
Select,
Icon,
} from "@budibase/bbui"
import { createEventDispatcher } from "svelte"
import { cloneDeep } from "lodash"
import ColorPicker from "./ColorPicker.svelte"
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
import { Constants } from "@budibase/frontend-core"
import { generate } from "shortid"
import { dndzone } from "svelte-dnd-action"
import { flip } from "svelte/animate"
import { getDatasourceForProvider, getSchemaForDatasource } from "dataBinding"
import { selectedScreen, selectedComponent } from "stores/builder"
import { makePropSafe } from "@budibase/string-templates"
export let value
const dispatch = createEventDispatcher()
const flipDuration = 130
const conditionOptions = [
{
label: "Update background color",
value: "backgroundColor",
},
{
label: "Update text color",
value: "textColor",
},
]
let tempValue = []
let drawer
let dragDisabled = true
$: count = value?.length
$: conditionText = `${count || "No"} condition${count !== 1 ? "s" : ""} set`
$: datasource = getDatasourceForProvider($selectedScreen, $selectedComponent)
$: schema = getSchemaForDatasource($selectedScreen, datasource)?.schema
$: rowBindings = generateRowBindings(schema)
const generateRowBindings = schema => {
let bindings = []
for (let key of Object.keys(schema || {})) {
bindings.push({
type: "context",
runtimeBinding: `${makePropSafe("row")}.${makePropSafe(key)}`,
readableBinding: `Row.${key}`,
category: "Row",
icon: "RailTop",
display: { name: key },
})
}
return bindings
}
const openDrawer = () => {
tempValue = cloneDeep(value || [])
drawer.show()
}
const save = async () => {
dispatch("change", tempValue)
drawer.hide()
}
const addCondition = () => {
const condition = {
id: generate(),
metadataKey: conditionOptions[0].value,
operator: Constants.OperatorOptions.Equals.value,
referenceValue: true,
}
tempValue = [...tempValue, condition]
}
const duplicateCondition = condition => {
const dupe = { ...condition, id: generate() }
tempValue = [...tempValue, dupe]
}
const removeCondition = condition => {
tempValue = tempValue.filter(c => c.id !== condition.id)
}
const updateConditions = e => {
tempValue = e.detail.items
}
const handleFinalize = e => {
updateConditions(e)
dragDisabled = true
}
</script>
<ActionButton on:click={openDrawer}>{conditionText}</ActionButton>
<!-- svelte-ignore a11y-no-static-element-interactions -->
<Drawer bind:this={drawer} title="Row conditions" on:drawerShow on:drawerHide>
<Button cta slot="buttons" on:click={save}>Save</Button>
<DrawerContent slot="body">
<div class="container">
<Layout noPadding>
Update the appearance of rows based on the entire row value.
{#if tempValue.length}
<div
class="conditions"
use:dndzone={{
items: tempValue,
flipDurationMs: flipDuration,
dropTargetStyle: { outline: "none" },
dragDisabled,
}}
on:consider={updateConditions}
on:finalize={handleFinalize}
>
{#each tempValue as condition (condition.id)}
<div
class="condition"
class:update={condition.action === "update"}
animate:flip={{ duration: flipDuration }}
>
<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={conditionOptions}
bind:value={condition.metadataKey}
/>
<span>to</span>
<ColorPicker
value={condition.metadataValue}
on:change={e => (condition.metadataValue = e.detail)}
/>
<span>if</span>
<DrawerBindableInput
bindings={rowBindings}
allowHBS={false}
placeholder="Expression"
value={condition.value}
on:change={e => (condition.value = e.detail)}
/>
<span>returns true</span>
<Icon
name="Duplicate"
hoverable
size="S"
on:click={() => duplicateCondition(condition)}
/>
<Icon
name="Close"
hoverable
size="S"
on:click={() => removeCondition(condition)}
/>
</div>
{/each}
</div>
{/if}
<div>
<Button secondary icon="Add" on:click={addCondition}>
Add condition
</Button>
</div>
</Layout>
</div>
</DrawerContent>
</Drawer>
<style>
.container {
width: 100%;
max-width: 800px;
margin: 0 auto;
}
.conditions {
display: flex;
flex-direction: column;
gap: var(--spacing-l);
}
.condition {
display: grid;
grid-template-columns: auto 1fr auto auto auto 1fr auto auto auto;
align-items: center;
grid-column-gap: var(--spacing-l);
}
</style>

View File

@ -8,8 +8,11 @@
} from "@budibase/bbui"
import { componentStore } from "stores/builder"
import ConditionalUIDrawer from "./ConditionalUIDrawer.svelte"
import ComponentSettingsSection from "./ComponentSettingsSection.svelte"
export let componentInstance
export let componentDefinition
export let componentBindings
export let bindings
let tempValue
@ -35,6 +38,19 @@
} set`
</script>
<!--
Load any general settings or sections tagged as "condition"
-->
<ComponentSettingsSection
{componentInstance}
{componentDefinition}
isScreen={false}
showInstanceName={false}
{bindings}
{componentBindings}
tag="condition"
/>
<DetailSummary name={"Conditions"} collapsible={false}>
<ActionButton on:click={openDrawer}>{conditionText}</ActionButton>
</DetailSummary>

View File

@ -7329,6 +7329,18 @@
]
}
]
},
{
"section": true,
"tag": "condition",
"name": "Row conditions",
"settings": [
{
"type": "rowConditions",
"key": "rowConditions",
"nested": true
}
]
}
],
"context": [

View File

@ -19,6 +19,7 @@
export let columns = null
export let onRowClick = null
export let buttons = null
export let rowConditions = null
const context = getContext("context")
const component = getContext("component")
@ -62,7 +63,13 @@
const goldenRow = generateGoldenSample(rows)
const id = get(component).id
return {
// Not sure what this one is for...
[id]: goldenRow,
// For row conditions context
row: goldenRow,
// For button action context
eventContext: {
row: goldenRow,
},
@ -166,6 +173,7 @@
{fixedRowHeight}
{columnWhitelist}
{schemaOverrides}
{rowConditions}
canAddRows={allowAddRows}
canEditRows={allowEditRows}
canDeleteRows={allowDeleteRows}

View File

@ -130,7 +130,10 @@
on:mouseup={stopSelectionCallback}
on:click={handleClick}
width={column.width}
metadata={row.__metadata?.[column.name]}
metadata={{
...row.__metadata,
...row.__cellMetadata?.[column.name],
}}
>
<svelte:component
this={getCellRenderer(column)}

View File

@ -54,6 +54,7 @@
selected={rowSelected}
{defaultHeight}
rowIdx={row?.__idx}
metadata={row?.__metadata}
>
<div class="gutter">
{#if $$slots.default}

View File

@ -70,6 +70,7 @@
rowIdx={row.__idx}
selected={rowSelected}
highlighted={rowHovered || rowFocused}
metadata={row.__metadata}
>
<div class="buttons" class:offset={$showVScrollbar}>
{#each buttons as button}

View File

@ -59,6 +59,7 @@
export let darkMode
export let isCloud = null
export let allowViewReadonlyColumns = false
export let rowConditions = null
// Unique identifier for DOM nodes inside this instance
const gridID = `grid-${Math.random().toString().slice(2)}`
@ -114,6 +115,8 @@
buttons,
darkMode,
isCloud,
allowViewReadonlyColumns,
rowConditions,
})
// Derive min height and make available in context

View File

@ -1,19 +1,24 @@
import { writable, get } from "svelte/store"
import { derivedMemo, QueryUtils } from "../../../utils"
import { OperatorOptions } from "@budibase/shared-core"
import { FieldType } from "@budibase/types"
import { processString, processStringSync } from "@budibase/string-templates"
export const createStores = () => {
const conditionMetadata = writable({})
const cellMetadata = writable({})
const rowMetadata = writable({})
return {
conditionMetadata,
cellMetadata,
rowMetadata,
}
}
export const deriveStores = context => {
const { columns } = context
// Derive and memoize the conditions present in our columns so that we only
// recompute condition metdata when absolutely necessary
const conditions = derivedMemo(columns, $columns => {
// Derive and memoize the cell conditions present in our columns so that we
// only recompute condition metadata when absolutely necessary
const cellConditions = derivedMemo(columns, $columns => {
let newConditions = []
for (let column of $columns) {
for (let condition of column.conditions || []) {
@ -28,96 +33,147 @@ export const deriveStores = context => {
})
return {
conditions,
cellConditions,
}
}
export const initialise = context => {
const { conditionMetadata, conditions, rows } = context
const { cellMetadata, cellConditions, rowConditions, rowMetadata, rows } =
context
// Evaluates an array of conditions against a certain row and returns the
// resultant metadata
const evaluateConditions = (row, conditions) => {
let metadata = { version: row._rev }
for (let condition of conditions) {
try {
let {
column,
type,
referenceValue,
operator,
metadataKey,
metadataValue,
} = condition
let value = row[column]
// Coerce values into correct types for primitives
if (type === "number") {
referenceValue = parseFloat(referenceValue)
value = parseFloat(value)
} else if (type === "datetime") {
if (referenceValue) {
referenceValue = new Date(referenceValue).toISOString()
}
if (value) {
value = new Date(value).toISOString()
}
} else if (type === "boolean") {
referenceValue = `${referenceValue}`.toLowerCase() === "true"
value = `${value}`.toLowerCase() === "true"
}
// Build lucene compatible condition expression
const luceneFilter = {
operator,
type,
field: "value",
value: referenceValue,
}
const query = QueryUtils.buildQuery([luceneFilter])
const result = QueryUtils.runQuery([{ value }], query)
if (result.length > 0) {
if (!metadata[column]) {
metadata[column] = {}
}
metadata[column][metadataKey] = metadataValue
}
} catch {
// Swallow
}
}
return metadata
}
// Recompute all metadata if conditions change
conditions.subscribe($conditions => {
// Recompute all cell metadata if cell conditions change
cellConditions.subscribe($conditions => {
let metadata = {}
if ($conditions.length) {
if ($conditions?.length) {
for (let row of get(rows)) {
metadata[row._id] = evaluateConditions(row, $conditions)
metadata[row._id] = evaluateCellConditions(row, $conditions)
}
}
conditionMetadata.set(metadata)
cellMetadata.set(metadata)
})
// Recompute specific rows when they change
rows.subscribe($rows => {
const $conditions = get(conditions)
if (!$conditions.length) {
return
}
const metadata = get(conditionMetadata)
let updates = {}
for (let row of $rows) {
if (!row._rev || metadata[row._id]?.version !== row._rev) {
updates[row._id] = evaluateConditions(row, $conditions)
// Recompute all row metadata if row conditions change
rowConditions.subscribe($conditions => {
let metadata = {}
if ($conditions?.length) {
for (let row of get(rows)) {
metadata[row._id] = evaluateRowConditions(row, $conditions)
}
}
if (Object.keys(updates).length) {
conditionMetadata.update(state => ({
rowMetadata.set(metadata)
})
// Recompute metadata for specific rows when they change
rows.subscribe($rows => {
const $cellConditions = get(cellConditions)
const $rowConditions = get(rowConditions)
const processCells = $cellConditions?.length > 0
const processRows = $rowConditions?.length > 0
if (!processCells && !processRows) {
return
}
const $cellMetadata = get(cellMetadata)
const $rowMetadata = get(rowMetadata)
let cellUpdates = {}
let rowUpdates = {}
for (let row of $rows) {
// Process cell metadata
if (processCells) {
if (!row._rev || $cellMetadata[row._id]?.version !== row._rev) {
cellUpdates[row._id] = evaluateCellConditions(row, $cellConditions)
}
}
// Process row metadata
if (processRows) {
if (!row._rev || $rowMetadata[row._id]?.version !== row._rev) {
rowUpdates[row._id] = evaluateRowConditions(row, $rowConditions)
}
}
}
if (Object.keys(cellUpdates).length) {
cellMetadata.update(state => ({
...state,
...updates,
...cellUpdates,
}))
}
if (Object.keys(rowUpdates).length) {
rowMetadata.update(state => ({
...state,
...rowUpdates,
}))
}
})
}
// Evaluates an array of cell conditions against a certain row and returns the
// resultant metadata
const evaluateCellConditions = (row, conditions) => {
let metadata = { version: row._rev }
for (let condition of conditions) {
try {
let {
column,
type,
referenceValue,
operator,
metadataKey,
metadataValue,
} = condition
let value = row[column]
// Coerce values into correct types for primitives
if (type === "number") {
referenceValue = parseFloat(referenceValue)
value = parseFloat(value)
} else if (type === "datetime") {
if (referenceValue) {
referenceValue = new Date(referenceValue).toISOString()
}
if (value) {
value = new Date(value).toISOString()
}
} else if (type === "boolean") {
referenceValue = `${referenceValue}`.toLowerCase() === "true"
value = `${value}`.toLowerCase() === "true"
}
// Build lucene compatible condition expression
const luceneFilter = {
operator,
type,
field: "value",
value: referenceValue,
}
const query = QueryUtils.buildQuery([luceneFilter])
const result = QueryUtils.runQuery([{ value }], query)
if (result.length > 0) {
if (!metadata[column]) {
metadata[column] = {}
}
metadata[column][metadataKey] = metadataValue
}
} catch {
// Swallow
}
}
return metadata
}
// Evaluates an array of row conditions against a certain row and returns the
// resultant metadata
const evaluateRowConditions = (row, conditions) => {
let metadata = { version: row._rev }
for (let condition of conditions) {
try {
const { metadataKey, metadataValue, value } = condition
console.log("JS")
const result = processStringSync(value, { row })
if (result === true) {
metadata[metadataKey] = metadataValue
}
} catch {
// Swallow
}
}
return metadata
}

View File

@ -15,6 +15,7 @@ export const createStores = context => {
const columnWhitelist = getProp("columnWhitelist")
const notifySuccess = getProp("notifySuccess")
const notifyError = getProp("notifyError")
const rowConditions = getProp("rowConditions")
return {
datasource,
@ -26,6 +27,7 @@ export const createStores = context => {
columnWhitelist,
notifySuccess,
notifyError,
rowConditions,
}
}

View File

@ -11,7 +11,8 @@ export const deriveStores = context => {
width,
height,
rowChangeCache,
conditionMetadata,
cellMetadata,
rowMetadata,
} = context
// Derive visible rows
@ -35,21 +36,24 @@ export const deriveStores = context => {
scrolledRowCount,
visualRowCapacity,
rowChangeCache,
conditionMetadata,
cellMetadata,
rowMetadata,
],
([
$rows,
$scrolledRowCount,
$visualRowCapacity,
$rowChangeCache,
$conditionMetadata,
$cellMetadata,
$rowMetadata,
]) => {
return $rows
.slice($scrolledRowCount, $scrolledRowCount + $visualRowCapacity)
.map(row => ({
...row,
...$rowChangeCache[row._id],
__metadata: $conditionMetadata[row._id],
__metadata: $rowMetadata[row._id],
__cellMetadata: $cellMetadata[row._id],
}))
}
)