diff --git a/packages/builder/cypress/integration/addMultiOptionDatatype.spec.js b/packages/builder/cypress/integration/addMultiOptionDatatype.spec.js
index bc2619c53d..b3e0f413f8 100644
--- a/packages/builder/cypress/integration/addMultiOptionDatatype.spec.js
+++ b/packages/builder/cypress/integration/addMultiOptionDatatype.spec.js
@@ -1,41 +1,44 @@
context("Add Multi-Option Datatype", () => {
- before(() => {
- cy.login()
- cy.createTestApp()
- })
+ before(() => {
+ cy.login()
+ cy.createTestApp()
+ })
- it("should create a new table, with data", () => {
- cy.createTable("Multi Data")
- cy.addColumn("Multi Data", "Test Data", "Multi-select", "1\n2\n3\n4\n5")
- cy.addRowMultiValue(["1", "2", "3", "4", "5"])
- })
+ it("should create a new table, with data", () => {
+ cy.createTable("Multi Data")
+ cy.addColumn("Multi Data", "Test Data", "Multi-select", "1\n2\n3\n4\n5")
+ cy.addRowMultiValue(["1", "2", "3", "4", "5"])
+ })
- it ("should add form with multi select picker, containing 5 options", () => {
- cy.navigateToFrontend()
- cy.wait(500)
- // Add data provider
- cy.get(`[data-cy="category-Data Provider"]`).click()
- cy.get('[data-cy="dataSource-prop-control"]').click()
- cy.get(".dropdown").contains("Multi Data").click()
- cy.wait(500)
- // Add Form with schema to match table
- cy.addComponent("Form", "Form")
- cy.get('[data-cy="dataSource-prop-control"').click()
- cy.get(".dropdown").contains("Multi Data").click()
- cy.wait(500)
- // Add multi-select picker to form
- cy.addComponent("Form", "Multi-select Picker").then((componentId) => {
- cy.get('[data-cy="field-prop-control"]').type("Test Data").type('{enter}')
- cy.wait(1000)
- cy.getComponent(componentId).contains("Choose some options").click()
- // Check picker has 5 items
- cy.getComponent(componentId).find('li').should('have.length', 5)
- // Select all items
- for (let i = 1; i < 6; i++) {
- cy.getComponent(componentId).find('li').contains(i).click()
- }
- // Check items have been selected
- cy.getComponent(componentId).find('.spectrum-Picker-label').contains("(5)")
- })
+ it("should add form with multi select picker, containing 5 options", () => {
+ cy.navigateToFrontend()
+ cy.wait(500)
+ // Add data provider
+ cy.get(`[data-cy="category-Data"]`).click()
+ cy.get(`[data-cy="component-Data Provider"]`).click()
+ cy.get('[data-cy="dataSource-prop-control"]').click()
+ cy.get(".dropdown").contains("Multi Data").click()
+ cy.wait(500)
+ // Add Form with schema to match table
+ cy.addComponent("Form", "Form")
+ cy.get('[data-cy="dataSource-prop-control"').click()
+ cy.get(".dropdown").contains("Multi Data").click()
+ cy.wait(500)
+ // Add multi-select picker to form
+ cy.addComponent("Form", "Multi-select Picker").then(componentId => {
+ cy.get('[data-cy="field-prop-control"]').type("Test Data").type("{enter}")
+ cy.wait(1000)
+ cy.getComponent(componentId).contains("Choose some options").click()
+ // Check picker has 5 items
+ cy.getComponent(componentId).find("li").should("have.length", 5)
+ // Select all items
+ for (let i = 1; i < 6; i++) {
+ cy.getComponent(componentId).find("li").contains(i).click()
+ }
+ // Check items have been selected
+ cy.getComponent(componentId)
+ .find(".spectrum-Picker-label")
+ .contains("(5)")
})
+ })
})
diff --git a/packages/builder/src/components/design/AppPreview/componentStructure.json b/packages/builder/src/components/design/AppPreview/componentStructure.json
index 357ea5a7be..240de3ea6c 100644
--- a/packages/builder/src/components/design/AppPreview/componentStructure.json
+++ b/packages/builder/src/components/design/AppPreview/componentStructure.json
@@ -8,12 +8,24 @@
"repeaterblock"
]
},
- "section",
- "container",
- "dataprovider",
- "table",
- "repeater",
- "button",
+ {
+ "name": "Layout",
+ "icon": "ClassicGridView",
+ "children": [
+ "container",
+ "section"
+ ]
+ },
+ {
+ "name": "Data",
+ "icon": "Data",
+ "children": [
+ "dataprovider",
+ "repeater",
+ "table",
+ "dynamicfilter"
+ ]
+ },
{
"name": "Form",
"icon": "Form",
@@ -60,6 +72,7 @@
"children": [
"heading",
"text",
+ "button",
"divider",
"image",
"backgroundimage",
diff --git a/packages/client/manifest.json b/packages/client/manifest.json
index 375aea3a02..d16b117215 100644
--- a/packages/client/manifest.json
+++ b/packages/client/manifest.json
@@ -247,7 +247,6 @@
"description": "A basic html button that is ready for styling",
"icon": "Button",
"editable": true,
- "illegalChildren": ["section"],
"showSettingsBar": true,
"settings": [
{
@@ -2647,6 +2646,49 @@
}
]
},
+ "dynamicfilter": {
+ "name": "Dynamic Filter",
+ "icon": "FilterEdit",
+ "showSettingsBar": true,
+ "settings": [
+ {
+ "type": "dataProvider",
+ "label": "Provider",
+ "key": "dataProvider"
+ },
+ {
+ "type": "multifield",
+ "label": "Allowed filter fields",
+ "key": "allowedFields",
+ "placeholder": "All fields"
+ },
+ {
+ "type": "select",
+ "label": "Button size",
+ "showInBar": true,
+ "key": "size",
+ "options": [
+ {
+ "label": "Small",
+ "value": "S"
+ },
+ {
+ "label": "Medium",
+ "value": "M"
+ },
+ {
+ "label": "Large",
+ "value": "L"
+ },
+ {
+ "label": "Extra large",
+ "value": "XL"
+ }
+ ],
+ "defaultValue": "M"
+ }
+ ]
+ },
"tableblock": {
"block": true,
"name": "Table block",
diff --git a/packages/client/src/components/ClientApp.svelte b/packages/client/src/components/ClientApp.svelte
index 119a26800f..98dec9667b 100644
--- a/packages/client/src/components/ClientApp.svelte
+++ b/packages/client/src/components/ClientApp.svelte
@@ -121,6 +121,9 @@
-->
+
+
+
diff --git a/packages/client/src/components/Component.svelte b/packages/client/src/components/Component.svelte
index a4ee290028..d8cdbd3f53 100644
--- a/packages/client/src/components/Component.svelte
+++ b/packages/client/src/components/Component.svelte
@@ -72,13 +72,18 @@
$: inSelectedPath = $builderStore.selectedComponentPath?.includes(id)
$: inDragPath = inSelectedPath && $builderStore.editMode
+ // Derive definition properties which can all be optional, so need to be
+ // coerced to booleans
+ $: editable = !!definition?.editable
+ $: hasChildren = !!definition?.hasChildren
+ $: showEmptyState = definition?.showEmptyState !== false
+
// Interactive components can be selected, dragged and highlighted inside
// the builder preview
$: interactive =
$builderStore.inBuilder &&
($builderStore.previewType === "layout" || insideScreenslot) &&
!isBlock
- $: editable = definition?.editable
$: editing = editable && selected && $builderStore.editMode
$: draggable = !inDragPath && interactive && !isLayout && !isScreen
$: droppable = interactive && !isLayout && !isScreen
@@ -86,8 +91,8 @@
// Empty components are those which accept children but do not have any.
// Empty states can be shown for these components, but can be disabled
// in the component manifest.
- $: empty = interactive && !children.length && definition?.hasChildren
- $: emptyState = empty && definition?.showEmptyState !== false
+ $: empty = interactive && !children.length && hasChildren
+ $: emptyState = empty && showEmptyState
// Raw settings are all settings excluding internal props and children
$: rawSettings = getRawSettings(instance)
@@ -103,6 +108,9 @@
// Build up the final settings object to be passed to the component
$: cacheSettings(enrichedSettings, nestedSettings, conditionalSettings)
+ // Render key is used to determine when components need to fully remount
+ $: renderKey = getRenderKey(id, editing)
+
// Update component context
$: componentStore.set({
id,
@@ -268,35 +276,43 @@
})
}
}
+
+ // Generates a key used to determine when components need to fully remount.
+ // Currently only toggling editing requires remounting.
+ const getRenderKey = (id, editing) => {
+ return hashString(`${id}-${editing}`)
+ }
-{#if constructor && cachedSettings && (visible || inSelectedPath)}
-
-
-
-
- {#if children.length}
- {#each children as child (child._id)}
-
- {/each}
- {:else if emptyState}
-
- {:else if isBlock}
-
- {/if}
-
-
-{/if}
+{#key renderKey}
+ {#if constructor && cachedSettings && (visible || inSelectedPath)}
+
+
+
+
+ {#if children.length}
+ {#each children as child (child._id)}
+
+ {/each}
+ {:else if emptyState}
+
+ {:else if isBlock}
+
+ {/if}
+
+
+ {/if}
+{/key}
diff --git a/packages/client/src/components/app/DataProvider.svelte b/packages/client/src/components/app/DataProvider.svelte
index 08f2fb06cd..e9d306cc3b 100644
--- a/packages/client/src/components/app/DataProvider.svelte
+++ b/packages/client/src/components/app/DataProvider.svelte
@@ -275,11 +275,10 @@
allRows = res.rows
}
- const addQueryExtension = (key, operator, field, value) => {
- if (!key || !operator || !field) {
+ const addQueryExtension = (key, extension) => {
+ if (!key || !extension) {
return
}
- const extension = { operator, field, value }
queryExtensions = { ...queryExtensions, [key]: extension }
}
@@ -295,11 +294,13 @@
const extendQuery = (defaultQuery, extensions) => {
const extensionValues = Object.values(extensions || {})
let extendedQuery = { ...defaultQuery }
- extensionValues.forEach(({ operator, field, value }) => {
- extendedQuery[operator] = {
- ...extendedQuery[operator],
- [field]: value,
- }
+ extensionValues.forEach(extension => {
+ Object.entries(extension || {}).forEach(([operator, fields]) => {
+ extendedQuery[operator] = {
+ ...extendedQuery[operator],
+ ...fields,
+ }
+ })
})
if (JSON.stringify(query) !== JSON.stringify(extendedQuery)) {
diff --git a/packages/client/src/components/app/DateRangePicker.svelte b/packages/client/src/components/app/DateRangePicker.svelte
index 651a19abc4..0246b68198 100644
--- a/packages/client/src/components/app/DateRangePicker.svelte
+++ b/packages/client/src/components/app/DateRangePicker.svelte
@@ -13,15 +13,6 @@
const component = getContext("component")
const { styleable, ActionTypes, getAction } = getContext("sdk")
-
- $: addExtension = getAction(
- dataProvider?.id,
- ActionTypes.AddDataProviderQueryExtension
- )
- $: removeExtension = getAction(
- dataProvider?.id,
- ActionTypes.RemoveDataProviderQueryExtension
- )
const options = [
"Last 1 day",
"Last 7 days",
@@ -32,10 +23,23 @@
]
let value = options.includes(defaultValue) ? defaultValue : "Last 30 days"
- $: queryExtension = getQueryExtension(value)
- $: addExtension?.($component.id, "range", field, queryExtension)
+ $: dataProviderId = dataProvider?.id
+ $: addExtension = getAction(
+ dataProviderId,
+ ActionTypes.AddDataProviderQueryExtension
+ )
+ $: removeExtension = getAction(
+ dataProviderId,
+ ActionTypes.RemoveDataProviderQueryExtension
+ )
+ $: queryExtension = getQueryExtension(field, value)
+ $: addExtension?.($component.id, queryExtension)
+
+ const getQueryExtension = (field, value) => {
+ if (!field || !value) {
+ return null
+ }
- const getQueryExtension = value => {
let low = dayjs.utc().subtract(1, "year")
let high = dayjs.utc().add(1, "day")
@@ -51,7 +55,14 @@
low = dayjs.utc().subtract(6, "months")
}
- return { low: low.format(), high: high.format() }
+ return {
+ range: {
+ [field]: {
+ low: low.format(),
+ high: high.format(),
+ },
+ },
+ }
}
onDestroy(() => {
diff --git a/packages/client/src/components/app/Heading.svelte b/packages/client/src/components/app/Heading.svelte
index 0450d2d11d..b27862f164 100644
--- a/packages/client/src/components/app/Heading.svelte
+++ b/packages/client/src/components/app/Heading.svelte
@@ -47,7 +47,7 @@
// Convert contenteditable HTML to text and save
const updateText = e => {
- const sanitized = e.target.innerHTML.replace(/
/gi, "\n")
+ const sanitized = e.target.innerHTML.replace(/
/gi, "\n").trim()
builderStore.actions.updateProp("text", sanitized)
}
diff --git a/packages/client/src/components/app/Link.svelte b/packages/client/src/components/app/Link.svelte
index 9587b34f11..851b2f0b66 100644
--- a/packages/client/src/components/app/Link.svelte
+++ b/packages/client/src/components/app/Link.svelte
@@ -47,7 +47,7 @@
}
const updateText = e => {
- builderStore.actions.updateProp("text", e.target.textContent)
+ builderStore.actions.updateProp("text", e.target.textContent.trim())
}
diff --git a/packages/client/src/components/app/Text.svelte b/packages/client/src/components/app/Text.svelte
index 14681ebbaf..679434edeb 100644
--- a/packages/client/src/components/app/Text.svelte
+++ b/packages/client/src/components/app/Text.svelte
@@ -46,7 +46,7 @@
// Convert contenteditable HTML to text and save
const updateText = e => {
- const sanitized = e.target.innerHTML.replace(/
/gi, "\n")
+ const sanitized = e.target.innerHTML.replace(/
/gi, "\n").trim()
builderStore.actions.updateProp("text", sanitized)
}
diff --git a/packages/client/src/components/app/dynamic-filter/DynamicFilter.svelte b/packages/client/src/components/app/dynamic-filter/DynamicFilter.svelte
new file mode 100644
index 0000000000..20909d011c
--- /dev/null
+++ b/packages/client/src/components/app/dynamic-filter/DynamicFilter.svelte
@@ -0,0 +1,85 @@
+
+
+