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

View File

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

View File

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

View File

@ -113,11 +113,17 @@
<ActionButton on:click={openDrawer}>{conditionText}</ActionButton> <ActionButton on:click={openDrawer}>{conditionText}</ActionButton>
<!-- svelte-ignore a11y-no-static-element-interactions --> <!-- 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> <Button cta slot="buttons" on:click={save}>Save</Button>
<DrawerContent slot="body"> <DrawerContent slot="body">
<div class="container"> <div class="container">
<Layout noPadding> <Layout noPadding>
Update the appearance of cells based on their value.
{#if tempValue.length} {#if tempValue.length}
<div <div
class="conditions" 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" } from "@budibase/bbui"
import { componentStore } from "stores/builder" import { componentStore } from "stores/builder"
import ConditionalUIDrawer from "./ConditionalUIDrawer.svelte" import ConditionalUIDrawer from "./ConditionalUIDrawer.svelte"
import ComponentSettingsSection from "./ComponentSettingsSection.svelte"
export let componentInstance export let componentInstance
export let componentDefinition
export let componentBindings
export let bindings export let bindings
let tempValue let tempValue
@ -35,6 +38,19 @@
} set` } set`
</script> </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}> <DetailSummary name={"Conditions"} collapsible={false}>
<ActionButton on:click={openDrawer}>{conditionText}</ActionButton> <ActionButton on:click={openDrawer}>{conditionText}</ActionButton>
</DetailSummary> </DetailSummary>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,19 +1,24 @@
import { writable, get } from "svelte/store" import { writable, get } from "svelte/store"
import { derivedMemo, QueryUtils } from "../../../utils" 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 = () => { export const createStores = () => {
const conditionMetadata = writable({}) const cellMetadata = writable({})
const rowMetadata = writable({})
return { return {
conditionMetadata, cellMetadata,
rowMetadata,
} }
} }
export const deriveStores = context => { export const deriveStores = context => {
const { columns } = context const { columns } = context
// Derive and memoize the conditions present in our columns so that we only // Derive and memoize the cell conditions present in our columns so that we
// recompute condition metdata when absolutely necessary // only recompute condition metadata when absolutely necessary
const conditions = derivedMemo(columns, $columns => { const cellConditions = derivedMemo(columns, $columns => {
let newConditions = [] let newConditions = []
for (let column of $columns) { for (let column of $columns) {
for (let condition of column.conditions || []) { for (let condition of column.conditions || []) {
@ -28,96 +33,147 @@ export const deriveStores = context => {
}) })
return { return {
conditions, cellConditions,
} }
} }
export const initialise = context => { 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 // Recompute all cell metadata if cell conditions change
// resultant metadata cellConditions.subscribe($conditions => {
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 => {
let metadata = {} let metadata = {}
if ($conditions.length) { if ($conditions?.length) {
for (let row of get(rows)) { 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 // Recompute all row metadata if row conditions change
rows.subscribe($rows => { rowConditions.subscribe($conditions => {
const $conditions = get(conditions) let metadata = {}
if (!$conditions.length) { if ($conditions?.length) {
return for (let row of get(rows)) {
} metadata[row._id] = evaluateRowConditions(row, $conditions)
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)
} }
} }
if (Object.keys(updates).length) { rowMetadata.set(metadata)
conditionMetadata.update(state => ({ })
// 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, ...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 columnWhitelist = getProp("columnWhitelist")
const notifySuccess = getProp("notifySuccess") const notifySuccess = getProp("notifySuccess")
const notifyError = getProp("notifyError") const notifyError = getProp("notifyError")
const rowConditions = getProp("rowConditions")
return { return {
datasource, datasource,
@ -26,6 +27,7 @@ export const createStores = context => {
columnWhitelist, columnWhitelist,
notifySuccess, notifySuccess,
notifyError, notifyError,
rowConditions,
} }
} }

View File

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