From cb3937b5c10c439ffec2c2c4910434a5927d2a6c Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 18 Apr 2024 15:58:40 +0100 Subject: [PATCH 1/8] Auto fill card blocks with bindings and reset when changing datasource --- .../builder/src/stores/builder/components.js | 57 ++++++++++++++++--- packages/client/manifest.json | 9 +-- 2 files changed, 53 insertions(+), 13 deletions(-) diff --git a/packages/builder/src/stores/builder/components.js b/packages/builder/src/stores/builder/components.js index fe5f4e8a05..24789b6b05 100644 --- a/packages/builder/src/stores/builder/components.js +++ b/packages/builder/src/stores/builder/components.js @@ -1,4 +1,4 @@ -import { get, derived } from "svelte/store" +import { get, derived, readable } from "svelte/store" import { cloneDeep } from "lodash/fp" import { API } from "api" import { Helpers } from "@budibase/bbui" @@ -20,7 +20,7 @@ import { previewStore, tables, componentTreeNodesStore, -} from "stores/builder/index" +} from "stores/builder" import { buildFormSchema, getSchemaForDatasource } from "dataBinding" import { BUDIBASE_INTERNAL_DB_ID, @@ -30,6 +30,7 @@ import { } from "constants/backend" import BudiStore from "../BudiStore" import { Utils } from "@budibase/frontend-core" +import { FieldType } from "@budibase/types" export const INITIAL_COMPONENTS_STATE = { components: {}, @@ -295,6 +296,44 @@ export class ComponentStore extends BudiStore { } } }) + + // Add default bindings to card blocks + if (component._component.endsWith("/cardsblock")) { + const { _id, dataSource } = component + if (dataSource) { + const { schema, table } = getSchemaForDatasource(screen, dataSource) + const readableTypes = [ + FieldType.STRING, + FieldType.OPTIONS, + FieldType.DATETIME, + FieldType.NUMBER, + ] + + // Extract good field candidates to prefil our cards with + const fields = Object.entries(schema || {}) + .filter(([name, fieldSchema]) => { + return ( + readableTypes.includes(fieldSchema.type) && + !fieldSchema.autoColumn && + name !== table?.primaryDisplay + ) + }) + .map(([name]) => name) + + // Use the primary display as the best field, if it exists + if (schema?.[table?.primaryDisplay]) { + fields.unshift(table.primaryDisplay) + } + + // Fill our cards with as many bindings as we can + const cardKeys = ["cardTitle", "cardSubtitle", "cardDescription"] + cardKeys.forEach(key => { + if (!fields[0] || component[key]) return + component[key] = `{{ ${safe(`${_id}-repeater`)}.${safe(fields[0])} }}` + fields.shift() + }) + } + } } /** @@ -323,21 +362,21 @@ export class ComponentStore extends BudiStore { ...presetProps, } - // Enrich empty settings + // Standard post processing this.enrichEmptySettings(instance, { parent, screen: get(selectedScreen), useDefaultValues: true, }) - - // Migrate nested component settings this.migrateSettings(instance) - // Add any extra properties the component needs + // Custom post processing for creation only let extras = {} if (definition.hasChildren) { extras._children = [] } + + // Add step name to form steps if (componentName.endsWith("/formstep")) { const parentForm = findClosestMatchingComponent( get(selectedScreen).props, @@ -350,6 +389,7 @@ export class ComponentStore extends BudiStore { extras.step = formSteps.length + 1 extras._instanceName = `Step ${formSteps.length + 1}` } + return { ...cloneDeep(instance), ...extras, @@ -460,7 +500,6 @@ export class ComponentStore extends BudiStore { if (!componentId || !screenId) { const state = get(this.store) componentId = componentId || state.selectedComponentId - const screenState = get(screenStore) screenId = screenId || screenState.selectedScreenId } @@ -468,7 +507,6 @@ export class ComponentStore extends BudiStore { return } const patchScreen = screen => { - // findComponent looks in the tree not comp.settings[0] let component = findComponent(screen.props, componentId) if (!component) { return false @@ -477,7 +515,8 @@ export class ComponentStore extends BudiStore { // Mutates the fetched component with updates const patchResult = patchFn(component, screen) - // Mutates the component with any required settings updates + // Post processing + this.enrichEmptySettings(component, { screen, useDefaultValues: false }) const migrated = this.migrateSettings(component) // Returning an explicit false signifies that we should skip this diff --git a/packages/client/manifest.json b/packages/client/manifest.json index 40abc7a9a0..91fd141704 100644 --- a/packages/client/manifest.json +++ b/packages/client/manifest.json @@ -5990,27 +5990,28 @@ "key": "cardTitle", "label": "Title", "nested": true, - "defaultValue": "Title" + "resetOn": "dataSource" }, { "type": "text", "key": "cardSubtitle", "label": "Subtitle", "nested": true, - "defaultValue": "Subtitle" + "resetOn": "dataSource" }, { "type": "text", "key": "cardDescription", "label": "Description", "nested": true, - "defaultValue": "Description" + "resetOn": "dataSource" }, { "type": "text", "key": "cardImageURL", "label": "Image URL", - "nested": true + "nested": true, + "resetOn": "dataSource" }, { "type": "boolean", From e98a9f7f80792086cda9f4e1f0b470375673fddb Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Fri, 19 Apr 2024 10:55:58 +0100 Subject: [PATCH 2/8] Update when empty setting enrichment runs to ensure card blocks properly reset when changing datasource --- .../builder/src/stores/builder/components.js | 83 ++++++++++++------- 1 file changed, 54 insertions(+), 29 deletions(-) diff --git a/packages/builder/src/stores/builder/components.js b/packages/builder/src/stores/builder/components.js index 24789b6b05..33d21f57cc 100644 --- a/packages/builder/src/stores/builder/components.js +++ b/packages/builder/src/stores/builder/components.js @@ -299,39 +299,64 @@ export class ComponentStore extends BudiStore { // Add default bindings to card blocks if (component._component.endsWith("/cardsblock")) { - const { _id, dataSource } = component - if (dataSource) { - const { schema, table } = getSchemaForDatasource(screen, dataSource) - const readableTypes = [ - FieldType.STRING, - FieldType.OPTIONS, - FieldType.DATETIME, - FieldType.NUMBER, - ] + // Only proceed if the card is empty, i.e. we just changed datasource or + // just created the card + const cardKeys = ["cardTitle", "cardSubtitle", "cardDescription"] + if (cardKeys.every(key => !component[key]) && !component.cardImageURL) { + const { _id, dataSource } = component + if (dataSource) { + const { schema, table } = getSchemaForDatasource(screen, dataSource) + const findFieldTypes = fieldTypes => { + if (!Array.isArray(fieldTypes)) { + fieldTypes = [fieldTypes] + } + return Object.entries(schema || {}) + .filter(([name, fieldSchema]) => { + return ( + fieldTypes.includes(fieldSchema.type) && + !fieldSchema.autoColumn && + name !== table?.primaryDisplay + ) + }) + .map(([name]) => name) + } - // Extract good field candidates to prefil our cards with - const fields = Object.entries(schema || {}) - .filter(([name, fieldSchema]) => { - return ( - readableTypes.includes(fieldSchema.type) && - !fieldSchema.autoColumn && - name !== table?.primaryDisplay - ) + // Extract good field candidates to prefil our cards with + const fields = findFieldTypes([ + FieldType.STRING, + FieldType.OPTIONS, + FieldType.DATETIME, + FieldType.NUMBER, + ]) + + // Use the primary display as the best field, if it exists + if (schema?.[table?.primaryDisplay]) { + fields.unshift(table.primaryDisplay) + } + + // Fill our cards with as many bindings as we can + const prefix = safe(`${_id}-repeater`) + cardKeys.forEach(key => { + if (!fields[0]) return + component[key] = `{{ ${prefix}.${safe(fields[0])} }}` + fields.shift() }) - .map(([name]) => name) - // Use the primary display as the best field, if it exists - if (schema?.[table?.primaryDisplay]) { - fields.unshift(table.primaryDisplay) + // Attempt to fill the image setting + let imgFields = findFieldTypes([FieldType.ATTACHMENT_SINGLE]) + if (imgFields[0]) { + component.cardImageURL = `{{ ${prefix}.${safe( + imgFields[0] + )}.[url] }}` + } else { + imgFields = findFieldTypes([FieldType.ATTACHMENTS]) + if (imgFields[0]) { + component.cardImageURL = `{{ ${prefix}.${safe( + imgFields[0] + )}.[0].[url] }}` + } + } } - - // Fill our cards with as many bindings as we can - const cardKeys = ["cardTitle", "cardSubtitle", "cardDescription"] - cardKeys.forEach(key => { - if (!fields[0] || component[key]) return - component[key] = `{{ ${safe(`${_id}-repeater`)}.${safe(fields[0])} }}` - fields.shift() - }) } } } From 75bf928242386844a000a906e916ba08f8a4bbcd Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Fri, 19 Apr 2024 11:08:59 +0100 Subject: [PATCH 3/8] Tidy up card binding logic --- .../builder/src/stores/builder/components.js | 39 ++++++++++--------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/packages/builder/src/stores/builder/components.js b/packages/builder/src/stores/builder/components.js index 33d21f57cc..c1c16193c2 100644 --- a/packages/builder/src/stores/builder/components.js +++ b/packages/builder/src/stores/builder/components.js @@ -321,39 +321,42 @@ export class ComponentStore extends BudiStore { .map(([name]) => name) } - // Extract good field candidates to prefil our cards with + // Inserts a card binding for a certain setting + const addBinding = (key, ...parts) => { + parts.unshift(`${_id}-repeater`) + component[key] = `{{ ${parts.map(safe).join(".")} }}` + } + + // Extract good field candidates to prefil our cards with. + // Use the primary display as the best field, if it exists. const fields = findFieldTypes([ FieldType.STRING, FieldType.OPTIONS, FieldType.DATETIME, FieldType.NUMBER, ]) - - // Use the primary display as the best field, if it exists if (schema?.[table?.primaryDisplay]) { fields.unshift(table.primaryDisplay) } // Fill our cards with as many bindings as we can - const prefix = safe(`${_id}-repeater`) cardKeys.forEach(key => { - if (!fields[0]) return - component[key] = `{{ ${prefix}.${safe(fields[0])} }}` - fields.shift() + if (fields[0]) { + addBinding(key, fields[0]) + fields.shift() + } }) - // Attempt to fill the image setting - let imgFields = findFieldTypes([FieldType.ATTACHMENT_SINGLE]) - if (imgFields[0]) { - component.cardImageURL = `{{ ${prefix}.${safe( - imgFields[0] - )}.[url] }}` + // Attempt to fill the image setting. + // Check single attachment fields first. + let imgField = findFieldTypes(FieldType.ATTACHMENT_SINGLE)[0] + if (imgField) { + addBinding("cardImageURL", imgField, "url") } else { - imgFields = findFieldTypes([FieldType.ATTACHMENTS]) - if (imgFields[0]) { - component.cardImageURL = `{{ ${prefix}.${safe( - imgFields[0] - )}.[0].[url] }}` + // Then try multi-attachment fields if no single ones exist + imgField = findFieldTypes(FieldType.ATTACHMENTS)[0] + if (imgField) { + addBinding("cardImageURL", imgField, 0, "url") } } } From bc29d3515fcbf6acc78263b59838c58195ebead4 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Fri, 19 Apr 2024 11:35:16 +0100 Subject: [PATCH 4/8] Improve card binding autofill field selection --- .../builder/src/stores/builder/components.js | 63 +++++++++++-------- 1 file changed, 36 insertions(+), 27 deletions(-) diff --git a/packages/builder/src/stores/builder/components.js b/packages/builder/src/stores/builder/components.js index c1c16193c2..7eb212d81e 100644 --- a/packages/builder/src/stores/builder/components.js +++ b/packages/builder/src/stores/builder/components.js @@ -30,7 +30,7 @@ import { } from "constants/backend" import BudiStore from "../BudiStore" import { Utils } from "@budibase/frontend-core" -import { FieldType } from "@budibase/types" +import { FieldSubtype, FieldType } from "@budibase/types" export const INITIAL_COMPONENTS_STATE = { components: {}, @@ -306,6 +306,8 @@ export class ComponentStore extends BudiStore { const { _id, dataSource } = component if (dataSource) { const { schema, table } = getSchemaForDatasource(screen, dataSource) + + // Finds fields by types from the schema of the configured datasource const findFieldTypes = fieldTypes => { if (!Array.isArray(fieldTypes)) { fieldTypes = [fieldTypes] @@ -315,48 +317,55 @@ export class ComponentStore extends BudiStore { return ( fieldTypes.includes(fieldSchema.type) && !fieldSchema.autoColumn && - name !== table?.primaryDisplay + name !== table?.primaryDisplay && + !name.startsWith("_") ) }) .map(([name]) => name) } // Inserts a card binding for a certain setting - const addBinding = (key, ...parts) => { - parts.unshift(`${_id}-repeater`) - component[key] = `{{ ${parts.map(safe).join(".")} }}` - } - - // Extract good field candidates to prefil our cards with. - // Use the primary display as the best field, if it exists. - const fields = findFieldTypes([ - FieldType.STRING, - FieldType.OPTIONS, - FieldType.DATETIME, - FieldType.NUMBER, - ]) - if (schema?.[table?.primaryDisplay]) { - fields.unshift(table.primaryDisplay) - } - - // Fill our cards with as many bindings as we can - cardKeys.forEach(key => { - if (fields[0]) { - addBinding(key, fields[0]) - fields.shift() + const addBinding = (key, fallback, ...parts) => { + if (parts.some(x => x == null)) { + component[key] = fallback + } else { + parts.unshift(`${_id}-repeater`) + component[key] = `{{ ${parts.map(safe).join(".")} }}` } - }) + } + + // Extract good field candidates to prefill our cards with. + // Use the primary display as the best field, if it exists. + const shortFields = [ + ...findFieldTypes(FieldType.STRING), + ...findFieldTypes(FieldType.OPTIONS), + ...findFieldTypes(FieldType.ARRAY), + ...findFieldTypes(FieldType.DATETIME), + ...findFieldTypes(FieldType.NUMBER), + ] + const longFields = findFieldTypes(FieldType.LONGFORM) + if (schema?.[table?.primaryDisplay]) { + shortFields.unshift(table.primaryDisplay) + } + + // Fill title and subtitle with short fields + addBinding("cardTitle", "Title", shortFields[0]) + addBinding("cardSubtitle", "Subtitle", shortFields[1]) + + // Fill description with a long field if possible + const longField = longFields[0] ?? shortFields[2] + addBinding("cardDescription", "Description", longField) // Attempt to fill the image setting. // Check single attachment fields first. let imgField = findFieldTypes(FieldType.ATTACHMENT_SINGLE)[0] if (imgField) { - addBinding("cardImageURL", imgField, "url") + addBinding("cardImageURL", null, imgField, "url") } else { // Then try multi-attachment fields if no single ones exist imgField = findFieldTypes(FieldType.ATTACHMENTS)[0] if (imgField) { - addBinding("cardImageURL", imgField, 0, "url") + addBinding("cardImageURL", null, imgField, 0, "url") } } } From e670dc1e1b5c6601e5128e70308ca78ae4e716d5 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Fri, 19 Apr 2024 11:43:25 +0100 Subject: [PATCH 5/8] Lint --- packages/builder/src/stores/builder/components.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/builder/src/stores/builder/components.js b/packages/builder/src/stores/builder/components.js index 7eb212d81e..7f5d7a2022 100644 --- a/packages/builder/src/stores/builder/components.js +++ b/packages/builder/src/stores/builder/components.js @@ -1,4 +1,4 @@ -import { get, derived, readable } from "svelte/store" +import { get, derived } from "svelte/store" import { cloneDeep } from "lodash/fp" import { API } from "api" import { Helpers } from "@budibase/bbui" @@ -30,7 +30,7 @@ import { } from "constants/backend" import BudiStore from "../BudiStore" import { Utils } from "@budibase/frontend-core" -import { FieldSubtype, FieldType } from "@budibase/types" +import { FieldType } from "@budibase/types" export const INITIAL_COMPONENTS_STATE = { components: {}, From ebbd0a87d4b28c9bbae80b31753f99593fb22029 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Fri, 19 Apr 2024 11:48:26 +0100 Subject: [PATCH 6/8] Remove dates from card block autofill because the timestamps don't look nice --- packages/builder/src/stores/builder/components.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/builder/src/stores/builder/components.js b/packages/builder/src/stores/builder/components.js index 7f5d7a2022..7ee3db08cf 100644 --- a/packages/builder/src/stores/builder/components.js +++ b/packages/builder/src/stores/builder/components.js @@ -340,7 +340,6 @@ export class ComponentStore extends BudiStore { ...findFieldTypes(FieldType.STRING), ...findFieldTypes(FieldType.OPTIONS), ...findFieldTypes(FieldType.ARRAY), - ...findFieldTypes(FieldType.DATETIME), ...findFieldTypes(FieldType.NUMBER), ] const longFields = findFieldTypes(FieldType.LONGFORM) From a074cb6befe600bd56bf6c1ce9cd0b0261f818cb Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Tue, 21 May 2024 14:17:35 +0100 Subject: [PATCH 7/8] Remove enrichEmptySettings from component patch function as any screen update already invokes empty setting enrichment on every single component --- packages/builder/src/stores/builder/components.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/builder/src/stores/builder/components.js b/packages/builder/src/stores/builder/components.js index 1311c26d5d..c281c73dfe 100644 --- a/packages/builder/src/stores/builder/components.js +++ b/packages/builder/src/stores/builder/components.js @@ -555,7 +555,6 @@ export class ComponentStore extends BudiStore { const patchResult = patchFn(component, screen) // Post processing - this.enrichEmptySettings(component, { screen, useDefaultValues: false }) const migrated = this.migrateSettings(component) // Returning an explicit false signifies that we should skip this From 35c52203ce4b432fc811212f55f4043cdf0c35cc Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Tue, 21 May 2024 17:29:32 +0100 Subject: [PATCH 8/8] Add tests for enriching empty card block settings --- .../stores/builder/tests/component.test.js | 23 +++++++ .../stores/builder/tests/fixtures/index.js | 60 ++++++++++++++++++- 2 files changed, 81 insertions(+), 2 deletions(-) diff --git a/packages/builder/src/stores/builder/tests/component.test.js b/packages/builder/src/stores/builder/tests/component.test.js index b8baefc5e6..80a0c8077d 100644 --- a/packages/builder/src/stores/builder/tests/component.test.js +++ b/packages/builder/src/stores/builder/tests/component.test.js @@ -23,6 +23,7 @@ import { DB_TYPE_EXTERNAL, DEFAULT_BB_DATASOURCE_ID, } from "constants/backend" +import { makePropSafe as safe } from "@budibase/string-templates" // Could move to fixtures const COMP_PREFIX = "@budibase/standard-components" @@ -360,8 +361,30 @@ describe("Component store", () => { resourceId: internalTableDoc._id, type: "table", }) + + return comp } + it("enrichEmptySettings - initialise cards blocks with correct fields", async ctx => { + const comp = enrichSettingsDS("cardsblock", ctx) + const expectBinding = (setting, ...parts) => { + expect(comp[setting]).toStrictEqual( + `{{ ${safe(`${comp._id}-repeater`)}.${parts.map(safe).join(".")} }}` + ) + } + expectBinding("cardTitle", internalTableDoc.schema.MediaTitle.name) + expectBinding("cardSubtitle", internalTableDoc.schema.MediaVersion.name) + expectBinding( + "cardDescription", + internalTableDoc.schema.MediaDescription.name + ) + expectBinding( + "cardImageURL", + internalTableDoc.schema.MediaImage.name, + "url" + ) + }) + it("enrichEmptySettings - set default datasource for 'table' setting type", async ctx => { enrichSettingsDS("formblock", ctx) }) diff --git a/packages/builder/src/stores/builder/tests/fixtures/index.js b/packages/builder/src/stores/builder/tests/fixtures/index.js index f636790f53..fbad17e374 100644 --- a/packages/builder/src/stores/builder/tests/fixtures/index.js +++ b/packages/builder/src/stores/builder/tests/fixtures/index.js @@ -8,6 +8,7 @@ import { DB_TYPE_EXTERNAL, DEFAULT_BB_DATASOURCE_ID, } from "constants/backend" +import { FieldType } from "@budibase/types" const getDocId = () => { return v4().replace(/-/g, "") @@ -45,6 +46,52 @@ export const COMPONENT_DEFINITIONS = { }, ], }, + cardsblock: { + block: true, + name: "Cards Block", + settings: [ + { + type: "dataSource", + label: "Data", + key: "dataSource", + required: true, + }, + { + section: true, + name: "Cards", + settings: [ + { + type: "text", + key: "cardTitle", + label: "Title", + nested: true, + resetOn: "dataSource", + }, + { + type: "text", + key: "cardSubtitle", + label: "Subtitle", + nested: true, + resetOn: "dataSource", + }, + { + type: "text", + key: "cardDescription", + label: "Description", + nested: true, + resetOn: "dataSource", + }, + { + type: "text", + key: "cardImageURL", + label: "Image URL", + nested: true, + resetOn: "dataSource", + }, + ], + }, + ], + }, container: { name: "Container", }, @@ -262,14 +309,23 @@ export const internalTableDoc = { name: "Media", sourceId: BUDIBASE_INTERNAL_DB_ID, sourceType: DB_TYPE_INTERNAL, + primaryDisplay: "MediaTitle", schema: { MediaTitle: { name: "MediaTitle", - type: "string", + type: FieldType.STRING, }, MediaVersion: { name: "MediaVersion", - type: "string", + type: FieldType.STRING, + }, + MediaDescription: { + name: "MediaDescription", + type: FieldType.LONGFORM, + }, + MediaImage: { + name: "MediaImage", + type: FieldType.ATTACHMENT_SINGLE, }, }, }