diff --git a/packages/builder/src/builderStore/dataBinding.js b/packages/builder/src/builderStore/dataBinding.js index 9cf00be3d4..9a41ad2afc 100644 --- a/packages/builder/src/builderStore/dataBinding.js +++ b/packages/builder/src/builderStore/dataBinding.js @@ -207,11 +207,11 @@ const getProviderContextBindings = (asset, dataProviders) => { const keys = Object.keys(schema).sort() // Generate safe unique runtime prefix - let runtimeId = component._id + let providerId = component._id if (runtimeSuffix) { - runtimeId += `-${runtimeSuffix}` + providerId += `-${runtimeSuffix}` } - const safeComponentId = makePropSafe(runtimeId) + const safeComponentId = makePropSafe(providerId) // Create bindable properties for each schema field keys.forEach(key => { @@ -235,7 +235,7 @@ const getProviderContextBindings = (asset, dataProviders) => { // Field schema and provider are required to construct relationship // datasource options, based on bindable properties fieldSchema, - providerId: component._id, + providerId, }) }) }) @@ -333,8 +333,11 @@ const getUrlBindings = asset => { */ export const getSchemaForDatasource = (asset, datasource, isForm = false) => { let schema, table + if (datasource) { const { type } = datasource + + // Determine the source table from the datasource type if (type === "provider") { const component = findComponent(asset.props, datasource.providerId) const source = getDatasourceForProvider(asset, component) @@ -342,11 +345,32 @@ export const getSchemaForDatasource = (asset, datasource, isForm = false) => { } else if (type === "query") { const queries = get(queriesStores).list table = queries.find(query => query._id === datasource._id) + } else if (type === "field") { + table = { name: datasource.fieldName } + const { fieldType } = datasource + if (fieldType === "attachment") { + schema = { + url: { + type: "string", + }, + name: { + type: "string", + }, + } + } else if (fieldType === "array") { + schema = { + value: { + type: "string", + }, + } + } } else { const tables = get(tablesStore).list table = tables.find(table => table._id === datasource.tableId) } - if (table) { + + // Determine the schema from the table if not already determined + if (table && !schema) { if (type === "view") { schema = cloneDeep(table.views?.[datasource.name]?.schema) } else if (type === "query" && isForm) { @@ -525,7 +549,7 @@ function bindingReplacement(bindableProperties, textWithBindings, convertTo) { * {{ literal [componentId] }} */ function extractLiteralHandlebarsID(value) { - return value?.match(/{{\s*literal[\s[]+([a-fA-F0-9]+)[\s\]]*}}/)?.[1] + return value?.match(/{{\s*literal\s*\[+([^\]]+)].*}}/)?.[1] } /** diff --git a/packages/builder/src/components/design/AppPreview/componentStructure.json b/packages/builder/src/components/design/AppPreview/componentStructure.json index c4fd33b084..357ea5a7be 100644 --- a/packages/builder/src/components/design/AppPreview/componentStructure.json +++ b/packages/builder/src/components/design/AppPreview/componentStructure.json @@ -4,7 +4,8 @@ "icon": "Article", "children": [ "tableblock", - "cardsblock" + "cardsblock", + "repeaterblock" ] }, "section", diff --git a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/DataProviderSelect.svelte b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/DataProviderSelect.svelte index d27a542f47..979443a403 100644 --- a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/DataProviderSelect.svelte +++ b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/DataProviderSelect.svelte @@ -11,10 +11,7 @@ const getValue = component => `{{ literal ${makePropSafe(component._id)} }}` $: path = findComponentPath($currentAsset.props, $store.selectedComponentId) - $: providers = path.filter( - component => - component._component === "@budibase/standard-components/dataprovider" - ) + $: providers = path.filter(c => c._component?.endsWith("/dataprovider")) // Set initial value to closest data provider onMount(() => { diff --git a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/DataSourceSelect.svelte b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/DataSourceSelect.svelte index d86f13e100..bc15110c09 100644 --- a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/DataSourceSelect.svelte +++ b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/DataSourceSelect.svelte @@ -20,16 +20,18 @@ import { notifications } from "@budibase/bbui" import ParameterBuilder from "components/integration/QueryParameterBuilder.svelte" import IntegrationQueryEditor from "components/integration/index.svelte" - - const dispatch = createEventDispatcher() - let anchorRight, dropdownRight - let drawer + import { makePropSafe as safe } from "@budibase/string-templates" export let value = {} export let otherSources export let showAllQueries export let bindings = [] + const dispatch = createEventDispatcher() + const arrayTypes = ["attachment", "array"] + let anchorRight, dropdownRight + let drawer + $: text = value?.label ?? "Choose an option" $: tables = $tablesStore.list.map(m => ({ label: m.name, @@ -54,8 +56,6 @@ name: query.name, tableId: query._id, ...query, - schema: query.schema, - parameters: query.parameters, type: "query", })) $: dataProviders = getDataProviderComponents( @@ -65,29 +65,40 @@ label: provider._instanceName, name: provider._instanceName, providerId: provider._id, - value: `{{ literal [${provider._id}] }}`, + value: `{{ literal ${safe(provider._id)} }}`, type: "provider", - schema: provider.schema, - })) - $: queryBindableProperties = bindings.map(property => ({ - ...property, - category: property.type === "instance" ? "Component" : "Table", - label: property.readableBinding, - path: property.readableBinding, })) $: links = bindings .filter(x => x.fieldSchema?.type === "link") - .map(property => { + .map(binding => { + const { providerId, readableBinding, fieldSchema } = binding || {} + const { name, tableId } = fieldSchema || {} + const safeProviderId = safe(providerId) return { - providerId: property.providerId, - label: property.readableBinding, - fieldName: property.fieldSchema.name, - tableId: property.fieldSchema.tableId, + providerId, + label: readableBinding, + fieldName: name, + tableId, type: "link", // These properties will be enriched by the client library and provide // details of the parent row of the relationship field, from context - rowId: `{{ ${property.providerId}._id }}`, - rowTableId: `{{ ${property.providerId}.tableId }}`, + rowId: `{{ ${safeProviderId}.${safe("_id")} }}`, + rowTableId: `{{ ${safeProviderId}.${safe("tableId")} }}`, + } + }) + $: fields = bindings + .filter(x => arrayTypes.includes(x.fieldSchema?.type)) + .map(binding => { + const { providerId, readableBinding, runtimeBinding } = binding + const { name, type, tableId } = binding.fieldSchema + return { + providerId, + label: readableBinding, + fieldName: name, + fieldType: type, + tableId, + type: "field", + value: `{{ literal ${runtimeBinding} }}`, } }) @@ -102,6 +113,14 @@ ).source return $integrations[source].query[query.queryVerb] } + + const getQueryParams = query => { + return $queriesStore.list.find(q => q._id === query?._id)?.parameters || [] + } + + const getQueryDatasource = query => { + return $datasources.list.find(ds => ds._id === query?.datasourceId) + }
@@ -127,11 +146,10 @@ - {#if value.parameters.length > 0} + {#if getQueryParams(value._id).length > 0} query._id === value._id) - .parameters} + parameters={getQueryParams(value)} {bindings} /> {/if} @@ -139,9 +157,7 @@ height={200} query={value} schema={fetchQueryDefinition(value)} - datasource={$datasources.list.find( - ds => ds._id === value.datasourceId - )} + datasource={getQueryDatasource(value)} editable={false} /> @@ -159,52 +175,71 @@
  • handleSelected(table)}>{table.label}
  • {/each} - -
    - Views -
    -
      - {#each views as view} -
    • handleSelected(view)}>{view.label}
    • - {/each} -
    - -
    - Relationships -
    -
      - {#each links as link} -
    • handleSelected(link)}>{link.label}
    • - {/each} -
    - -
    - Queries -
    -
      - {#each queries as query} -
    • handleSelected(query)} - > - {query.label} -
    • - {/each} -
    - -
    - Data Providers -
    -
      - {#each dataProviders as provider} -
    • handleSelected(provider)} - > - {provider.label} -
    • - {/each} -
    + {#if views?.length} + +
    + Views +
    +
      + {#each views as view} +
    • handleSelected(view)}>{view.label}
    • + {/each} +
    + {/if} + {#if queries?.length} + +
    + Queries +
    +
      + {#each queries as query} +
    • handleSelected(query)} + > + {query.label} +
    • + {/each} +
    + {/if} + {#if links?.length} + +
    + Relationships +
    +
      + {#each links as link} +
    • handleSelected(link)}>{link.label}
    • + {/each} +
    + {/if} + {#if fields?.length} + +
    + Fields +
    +
      + {#each fields as field} +
    • handleSelected(field)}>{field.label}
    • + {/each} +
    + {/if} + {#if dataProviders?.length} + +
    + Data Providers +
    +
      + {#each dataProviders as provider} +
    • handleSelected(provider)} + > + {provider.label} +
    • + {/each} +
    + {/if} {#if otherSources?.length}
    diff --git a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/PropertyControl.svelte b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/PropertyControl.svelte index 72b54f3b96..911688b30c 100644 --- a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/PropertyControl.svelte +++ b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/PropertyControl.svelte @@ -6,7 +6,6 @@ } from "builderStore/dataBinding" export let label = "" - export let bindable = true export let componentInstance = {} export let control = null export let key = "" diff --git a/packages/client/manifest.json b/packages/client/manifest.json index bf9810769a..050ac4a63a 100644 --- a/packages/client/manifest.json +++ b/packages/client/manifest.json @@ -2933,5 +2933,203 @@ "type": "schema", "suffix": "repeater" } + }, + "repeaterblock": { + "name": "Repeater block", + "icon": "ViewList", + "illegalChildren": ["section"], + "hasChildren": true, + "showSettingsBar": true, + "settings": [ + { + "type": "dataSource", + "label": "Data", + "key": "dataSource" + }, + { + "type": "filter", + "label": "Filtering", + "key": "filter" + }, + { + "type": "field", + "label": "Sort Column", + "key": "sortColumn" + }, + { + "type": "select", + "label": "Sort Order", + "key": "sortOrder", + "options": ["Ascending", "Descending"], + "defaultValue": "Descending" + }, + { + "type": "number", + "label": "Limit", + "key": "limit", + "defaultValue": 10 + }, + { + "type": "boolean", + "label": "Paginate", + "key": "paginate" + }, + { + "section": true, + "name": "Layout settings", + "settings": [ + { + "type": "text", + "label": "Empty Text", + "key": "noRowsMessage", + "defaultValue": "No rows found" + }, + { + "type": "select", + "label": "Direction", + "key": "direction", + "showInBar": true, + "barStyle": "buttons", + "options": [ + { + "label": "Column", + "value": "column", + "barIcon": "ViewRow", + "barTitle": "Column layout" + }, + { + "label": "Row", + "value": "row", + "barIcon": "ViewColumn", + "barTitle": "Row layout" + } + ], + "defaultValue": "column" + }, + { + "type": "select", + "label": "Horiz. Align", + "key": "hAlign", + "showInBar": true, + "barStyle": "buttons", + "options": [ + { + "label": "Left", + "value": "left", + "barIcon": "AlignLeft", + "barTitle": "Align left" + }, + { + "label": "Center", + "value": "center", + "barIcon": "AlignCenter", + "barTitle": "Align center" + }, + { + "label": "Right", + "value": "right", + "barIcon": "AlignRight", + "barTitle": "Align right" + }, + { + "label": "Stretch", + "value": "stretch", + "barIcon": "MoveLeftRight", + "barTitle": "Align stretched horizontally" + } + ], + "defaultValue": "stretch" + }, + { + "type": "select", + "label": "Vert. Align", + "key": "vAlign", + "showInBar": true, + "barStyle": "buttons", + "options": [ + { + "label": "Top", + "value": "top", + "barIcon": "AlignTop", + "barTitle": "Align top" + }, + { + "label": "Middle", + "value": "middle", + "barIcon": "AlignMiddle", + "barTitle": "Align middle" + }, + { + "label": "Bottom", + "value": "bottom", + "barIcon": "AlignBottom", + "barTitle": "Align bottom" + }, + { + "label": "Stretch", + "value": "stretch", + "barIcon": "MoveUpDown", + "barTitle": "Align stretched vertically" + } + ], + "defaultValue": "top" + }, + { + "type": "select", + "label": "Gap", + "key": "gap", + "showInBar": true, + "barStyle": "picker", + "options": [ + { + "label": "None", + "value": "N" + }, + { + "label": "Small", + "value": "S" + }, + { + "label": "Medium", + "value": "M" + }, + { + "label": "Large", + "value": "L" + } + ], + "defaultValue": "M" + } + ] + } + ], + "context": [ + { + "type": "static", + "suffix": "provider", + "values": [ + { + "label": "Rows", + "key": "rows" + }, + { + "label": "Rows Length", + "key": "rowsLength" + }, + { + "label": "Schema", + "key": "schema" + }, + { + "label": "Page Number", + "key": "pageNumber" + } + ] + }, + { + "type": "schema", + "suffix": "repeater" + } + ] } } diff --git a/packages/client/src/api/datasources.js b/packages/client/src/api/datasources.js index d2b05899cb..981d8301ca 100644 --- a/packages/client/src/api/datasources.js +++ b/packages/client/src/api/datasources.js @@ -55,6 +55,26 @@ export const fetchDatasourceSchema = async dataSource => { return dataSource.value?.schema } + // Field sources have their schema statically defined + if (type === "field") { + if (dataSource.fieldType === "attachment") { + return { + url: { + type: "string", + }, + name: { + type: "string", + }, + } + } else if (dataSource.fieldType === "array") { + return { + value: { + type: "string", + }, + } + } + } + // Tables, views and links can be fetched by table ID if ( (type === "table" || type === "view" || type === "link") && diff --git a/packages/client/src/components/BlockComponent.svelte b/packages/client/src/components/BlockComponent.svelte index 589998994d..c23f18f55c 100644 --- a/packages/client/src/components/BlockComponent.svelte +++ b/packages/client/src/components/BlockComponent.svelte @@ -30,6 +30,6 @@ } - + diff --git a/packages/client/src/components/Component.svelte b/packages/client/src/components/Component.svelte index 346de98f2f..443691595e 100644 --- a/packages/client/src/components/Component.svelte +++ b/packages/client/src/components/Component.svelte @@ -17,6 +17,7 @@ export let instance = {} export let isLayout = false export let isScreen = false + export let isBlock = false // The enriched component settings let enrichedSettings @@ -44,7 +45,6 @@ // Get contexts const context = getContext("context") const insideScreenslot = !!getContext("screenslot") - const insideBlock = !!getContext("block") // Create component context const componentStore = writable({}) @@ -69,7 +69,7 @@ $: interactive = $builderStore.inBuilder && ($builderStore.previewType === "layout" || insideScreenslot) && - !insideBlock + !isBlock $: draggable = interactive && !isLayout && !isScreen $: droppable = interactive && !isLayout && !isScreen @@ -262,6 +262,7 @@ class:droppable class:empty class:interactive + class:block={isBlock} data-id={id} data-name={name} > @@ -272,7 +273,7 @@ {/each} {:else if emptyState} - {:else if insideBlock} + {:else if isBlock} {/if} diff --git a/packages/client/src/components/app/DataProvider.svelte b/packages/client/src/components/app/DataProvider.svelte index a8e53f4906..68f6cc6c07 100644 --- a/packages/client/src/components/app/DataProvider.svelte +++ b/packages/client/src/components/app/DataProvider.svelte @@ -183,7 +183,16 @@ } else if (dataSource?.type === "provider") { // For providers referencing another provider, just use the rows it // provides - allRows = dataSource?.value?.rows ?? [] + allRows = dataSource?.value?.rows || [] + } else if (dataSource?.type === "field") { + // Field sources will be available from context. + // Enrich non object elements into object to ensure a valid schema. + const data = dataSource?.value || [] + if (Array.isArray(data) && data[0] && typeof data[0] !== "object") { + allRows = data.map(value => ({ value })) + } else { + allRows = data + } } else { // For other data sources like queries or views, fetch all rows from the // server diff --git a/packages/client/src/components/app/blocks/CardsBlock.svelte b/packages/client/src/components/app/blocks/CardsBlock.svelte index 9eccb2b2d1..1a8df189af 100644 --- a/packages/client/src/components/app/blocks/CardsBlock.svelte +++ b/packages/client/src/components/app/blocks/CardsBlock.svelte @@ -3,6 +3,7 @@ import Block from "components/Block.svelte" import BlockComponent from "components/BlockComponent.svelte" import { Heading } from "@budibase/bbui" + import { makePropSafe as safe } from "@budibase/string-templates" export let title export let dataSource @@ -103,7 +104,7 @@ } const col = linkColumn || "_id" const split = url.split("/:") - return `${split[0]}/{{ [${repeaterId}].[${col}] }}` + return `${split[0]}/{{ ${safe(repeaterId)}.${safe(col)} }}` } // Load the datasource schema on mount so we can determine column types @@ -171,7 +172,7 @@ bind:id={repeaterId} context="repeater" props={{ - dataProvider: `{{ literal [${dataProviderId}] }}`, + dataProvider: `{{ literal ${safe(dataProviderId)} }}`, direction: "row", hAlign: "stretch", vAlign: "top", diff --git a/packages/client/src/components/app/blocks/RepeaterBlock.svelte b/packages/client/src/components/app/blocks/RepeaterBlock.svelte new file mode 100644 index 0000000000..413d1df65e --- /dev/null +++ b/packages/client/src/components/app/blocks/RepeaterBlock.svelte @@ -0,0 +1,61 @@ + + + +
    + + {#if $component.empty} + + {:else} + + + + {/if} + +
    +
    diff --git a/packages/client/src/components/app/blocks/TableBlock.svelte b/packages/client/src/components/app/blocks/TableBlock.svelte index ac2a36adc7..255cbf44cf 100644 --- a/packages/client/src/components/app/blocks/TableBlock.svelte +++ b/packages/client/src/components/app/blocks/TableBlock.svelte @@ -3,6 +3,7 @@ import Block from "components/Block.svelte" import BlockComponent from "components/BlockComponent.svelte" import { Heading } from "@budibase/bbui" + import { makePropSafe as safe } from "@budibase/string-templates" export let title export let dataSource @@ -61,7 +62,7 @@ operator: column.type === "string" ? "string" : "equal", type: "string", valueType: "Binding", - value: `{{ [${formId}].[${column.name}] }}`, + value: `{{ ${safe(formId)}.${safe(column.name)} }}`, }) }) return enrichedFilter @@ -147,7 +148,7 @@ setting.showInBar) ?? [] + $: settings = getBarSettings(definition) + + const getBarSettings = definition => { + let allSettings = [] + definition?.settings?.forEach(setting => { + if (setting.section) { + allSettings = allSettings.concat(setting.settings || []) + } else { + allSettings.push(setting) + } + }) + return allSettings.filter(setting => setting.showInBar) + } const updatePosition = () => { if (!showBar) { diff --git a/packages/string-templates/src/helpers/index.js b/packages/string-templates/src/helpers/index.js index b43fef44dc..6b9195047e 100644 --- a/packages/string-templates/src/helpers/index.js +++ b/packages/string-templates/src/helpers/index.js @@ -46,6 +46,9 @@ const HELPERS = [ }), // adds a note for post-processor new Helper(HelperFunctionNames.LITERAL, value => { + if (value === undefined) { + return "" + } const type = typeof value const outputVal = type === "object" ? JSON.stringify(value) : value return `{{${LITERAL_MARKER} ${type}-${outputVal}}}`