diff --git a/packages/builder/src/builderStore/dataBinding.js b/packages/builder/src/builderStore/dataBinding.js index 742c9052ad..165ed37fbb 100644 --- a/packages/builder/src/builderStore/dataBinding.js +++ b/packages/builder/src/builderStore/dataBinding.js @@ -39,6 +39,25 @@ export const getBindableProperties = (asset, componentId) => { ] } +/** + * Gets the bindable properties exposed by a certain component. + */ +export const getComponentBindableProperties = (asset, componentId) => { + if (!asset || !componentId) { + return [] + } + + // Ensure that the component exists and exposes context + const component = findComponent(asset.props, componentId) + const def = store.actions.components.getDefinition(component?._component) + if (!def?.context) { + return [] + } + + // Get the bindings for the component + return getProviderContextBindings(asset, component) +} + /** * Gets all data provider components above a component. */ @@ -125,9 +144,26 @@ export const getDatasourceForProvider = (asset, component) => { const getContextBindings = (asset, componentId) => { // Extract any components which provide data contexts const dataProviders = getDataProviderComponents(asset, componentId) - let bindings = [] + + // Generate bindings for all matching components + return getProviderContextBindings(asset, dataProviders) +} + +/** + * Gets the context bindings exposed by a set of data provider components. + */ +const getProviderContextBindings = (asset, dataProviders) => { + if (!asset || !dataProviders) { + return [] + } + + // Ensure providers is an array + if (!Array.isArray(dataProviders)) { + dataProviders = [dataProviders] + } // Create bindings for each data provider + let bindings = [] dataProviders.forEach(component => { const def = store.actions.components.getDefinition(component._component) const contexts = Array.isArray(def.context) ? def.context : [def.context] @@ -140,6 +176,7 @@ const getContextBindings = (asset, componentId) => { let schema let readablePrefix + let runtimeSuffix = context.suffix if (context.type === "form") { // Forms do not need table schemas @@ -169,8 +206,14 @@ const getContextBindings = (asset, componentId) => { const keys = Object.keys(schema).sort() + // Generate safe unique runtime prefix + let runtimeId = component._id + if (runtimeSuffix) { + runtimeId += `-${runtimeSuffix}` + } + const safeComponentId = makePropSafe(runtimeId) + // Create bindable properties for each schema field - const safeComponentId = makePropSafe(component._id) keys.forEach(key => { const fieldSchema = schema[key] @@ -182,6 +225,7 @@ const getContextBindings = (asset, componentId) => { } else if (fieldSchema.type === "attachment") { runtimeBoundKey = `${key}_first` } + const runtimeBinding = `${safeComponentId}.${makePropSafe( runtimeBoundKey )}` diff --git a/packages/builder/src/components/design/AppPreview/componentStructure.json b/packages/builder/src/components/design/AppPreview/componentStructure.json index b6675c2480..9a87bb5761 100644 --- a/packages/builder/src/components/design/AppPreview/componentStructure.json +++ b/packages/builder/src/components/design/AppPreview/componentStructure.json @@ -3,7 +3,8 @@ "name": "Blocks", "icon": "Article", "children": [ - "tablewithsearch" + "tablewithsearch", + "cardlistwithsearch" ] }, "section", diff --git a/packages/builder/src/components/design/PropertiesPanel/ComponentSettingsSection.svelte b/packages/builder/src/components/design/PropertiesPanel/ComponentSettingsSection.svelte index f7123eb9e3..356e86b20a 100644 --- a/packages/builder/src/components/design/PropertiesPanel/ComponentSettingsSection.svelte +++ b/packages/builder/src/components/design/PropertiesPanel/ComponentSettingsSection.svelte @@ -12,6 +12,7 @@ export let componentInstance export let assetInstance export let bindings + export let componentBindings const layoutDefinition = [] const screenDefinition = [ @@ -21,12 +22,24 @@ { key: "layoutId", label: "Layout", control: LayoutSelect }, ] - $: settings = componentDefinition?.settings ?? [] - $: generalSettings = settings.filter(setting => !setting.section) - $: sections = settings.filter(setting => setting.section) + $: sections = getSections(componentDefinition) $: isLayout = assetInstance && assetInstance.favicon $: assetDefinition = isLayout ? layoutDefinition : screenDefinition + const getSections = definition => { + const settings = definition?.settings ?? [] + const generalSettings = settings.filter(setting => !setting.section) + const customSections = settings.filter(setting => setting.section) + return [ + { + name: "General", + info: componentDefinition?.info, + settings: generalSettings, + }, + ...(customSections || []), + ] + } + const updateProp = store.actions.components.updateProp const canRenderControl = setting => { @@ -61,53 +74,18 @@ } - - {#if !componentInstance._component.endsWith("/layout")} - updateProp("_instanceName", val)} - /> - {/if} - {#if generalSettings.length} - {#each generalSettings as setting (setting.key)} - {#if canRenderControl(setting)} - updateProp(setting.key, val)} - props={{ - options: setting.options || [], - placeholder: setting.placeholder || null, - min: setting.min || null, - max: setting.max || null, - }} - {bindings} - {componentDefinition} - /> - {/if} - {/each} - {/if} - {#if componentDefinition?.component?.endsWith("/fieldgroup")} - - {/if} - {#if componentDefinition?.info} -
- {@html componentDefinition.info} -
- {/if} -
- -{#each sections as section (section.name)} +{#each sections as section, idx (section.name)} + {#if idx === 0 && !componentInstance._component.endsWith("/layout")} + updateProp("_instanceName", val)} + /> + {/if} {#each section.settings as setting (setting.key)} {#if canRenderControl(setting)} updateProp(setting.key, val)} props={{ options: setting.options || [], @@ -126,10 +104,15 @@ max: setting.max || null, }} {bindings} + {componentBindings} + {componentInstance} {componentDefinition} /> {/if} {/each} + {#if idx === 0 && componentDefinition?.component?.endsWith("/fieldgroup")} + + {/if} {#if section?.info}
{@html section.info} diff --git a/packages/builder/src/components/design/PropertiesPanel/PropertiesPanel.svelte b/packages/builder/src/components/design/PropertiesPanel/PropertiesPanel.svelte index 27f6650cde..df91a87456 100644 --- a/packages/builder/src/components/design/PropertiesPanel/PropertiesPanel.svelte +++ b/packages/builder/src/components/design/PropertiesPanel/PropertiesPanel.svelte @@ -6,13 +6,20 @@ import DesignSection from "./DesignSection.svelte" import CustomStylesSection from "./CustomStylesSection.svelte" import ConditionalUISection from "./ConditionalUISection.svelte" - import { getBindableProperties } from "builderStore/dataBinding" + import { + getBindableProperties, + getComponentBindableProperties, + } from "builderStore/dataBinding" $: componentInstance = $selectedComponent $: componentDefinition = store.actions.components.getDefinition( $selectedComponent?._component ) $: bindings = getBindableProperties($currentAsset, $store.selectedComponentId) + $: componentBindings = getComponentBindableProperties( + $currentAsset, + $store.selectedComponentId + ) @@ -28,6 +35,7 @@ {componentInstance} {componentDefinition} {bindings} + {componentBindings} /> {} export let bindings = [] + export let componentBindings = [] + export let nested = false let bindingDrawer let anchor let valid - $: safeValue = getSafeValue(value, props.defaultValue, bindings) + $: allBindings = getAllBindings(bindings, componentBindings, nested) + $: safeValue = getSafeValue(value, props.defaultValue, allBindings) $: tempValue = safeValue - $: replaceBindings = val => readableToRuntimeBinding(bindings, val) + $: replaceBindings = val => readableToRuntimeBinding(allBindings, val) + + const getAllBindings = (bindings, componentBindings, nested) => { + if (!nested) { + return bindings + } + return [...(bindings || []), ...(componentBindings || [])] + } const handleClose = () => { handleChange(tempValue) @@ -78,7 +88,7 @@ updateOnChange={false} on:change={handleChange} onChange={handleChange} - {bindings} + bindings={allBindings} name={key} text={label} {type} @@ -104,7 +114,7 @@ bind:valid value={safeValue} on:change={e => (tempValue = e.detail)} - bindableProperties={bindings} + bindableProperties={allBindings} allowJS /> diff --git a/packages/client/manifest.json b/packages/client/manifest.json index 982f96ef90..f3ba71223c 100644 --- a/packages/client/manifest.json +++ b/packages/client/manifest.json @@ -2599,6 +2599,21 @@ "type": "boolean", "key": "horizontal", "label": "Horizontal" + }, + { + "type": "boolean", + "label": "Show button", + "key": "showButton" + }, + { + "type": "text", + "key": "buttonText", + "label": "Button text" + }, + { + "type": "event", + "label": "Button action", + "key": "buttonOnClick" } ] }, @@ -2710,22 +2725,22 @@ }, { "section": true, - "name": "Button", + "name": "Title button", "settings": [ { "type": "boolean", "key": "showTitleButton", - "label": "Show title button", + "label": "Show button", "defaultValue": false }, { "type": "text", "key": "titleButtonText", - "label": "Title button text" + "label": "Button text" }, { "type": "event", - "label": "Title button action", + "label": "Button action", "key": "titleButtonOnClick" } ] @@ -2742,9 +2757,139 @@ } ] } + ] + }, + "cardlistwithsearch": { + "block": true, + "name": "Card list with search", + "icon": "Table", + "styles": ["size"], + "info": "Only the first 3 search columns will be used.", + "settings": [ + { + "type": "text", + "label": "Title", + "key": "title" + }, + { + "type": "dataSource", + "label": "Data", + "key": "dataSource" + }, + { + "type": "multifield", + "label": "Search Columns", + "key": "searchColumns", + "placeholder": "Choose search columns" + }, + { + "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": "Cards", + "settings": [ + { + "type": "text", + "key": "cardTitle", + "label": "Title", + "nested": true + }, + { + "type": "text", + "key": "cardSubtitle", + "label": "Subtitle", + "nested": true + }, + { + "type": "text", + "key": "cardDescription", + "label": "Description", + "nested": true + + }, + { + "type": "text", + "key": "cardImageURL", + "label": "Image URL", + "nested": true + + }, + { + "type": "boolean", + "key": "cardHorizontal", + "label": "Horizontal" + }, + { + "type": "boolean", + "label": "Show button", + "key": "showCardButton" + }, + { + "type": "text", + "key": "cardButtonText", + "label": "Button text", + "nested": true + + }, + { + "type": "event", + "label": "Button action", + "key": "cardButtonOnClick", + "nested": true + } + ] + }, + { + "section": true, + "name": "Title button", + "settings": [ + { + "type": "boolean", + "key": "showTitleButton", + "label": "Show button" + }, + { + "type": "text", + "key": "titleButtonText", + "label": "Button text" + }, + { + "type": "event", + "label": "Button action", + "key": "titleButtonOnClick" + } + ] + } ], "context": { - "type": "schema" + "type": "schema", + "suffix": "repeater" } } } diff --git a/packages/client/src/components/BlockComponent.svelte b/packages/client/src/components/BlockComponent.svelte index 02456322da..589998994d 100644 --- a/packages/client/src/components/BlockComponent.svelte +++ b/packages/client/src/components/BlockComponent.svelte @@ -6,6 +6,7 @@ export let type export let props export let styles + export let context // ID is only exposed as a prop so that it can be bound to from parent // block components @@ -16,7 +17,7 @@ // Create a fake component instance so that we can use the core Component // to render this part of the block, taking advantage of binding enrichment - $: id = block.id + rand + $: id = `${block.id}-${context ?? rand}` $: instance = { _component: `@budibase/standard-components/${type}`, _id: id, diff --git a/packages/client/src/components/Component.svelte b/packages/client/src/components/Component.svelte index c13a50d52e..346de98f2f 100644 --- a/packages/client/src/components/Component.svelte +++ b/packages/client/src/components/Component.svelte @@ -1,3 +1,7 @@ + + {#key renderKey} - {#if constructor && componentSettings && (visible || inSelectedPath)} + {#if constructor && settings && (visible || inSelectedPath)}
- + {#if children.length} {#each children as child (child._id)} diff --git a/packages/client/src/components/app/SpectrumCard.svelte b/packages/client/src/components/app/SpectrumCard.svelte index bac0a81fb3..ba3e919cdd 100644 --- a/packages/client/src/components/app/SpectrumCard.svelte +++ b/packages/client/src/components/app/SpectrumCard.svelte @@ -1,6 +1,7 @@ + + +
+ + {#if title || enrichedSearchColumns?.length || showTitleButton} +
+
+ {title || ""} +
+
+ {#if enrichedSearchColumns?.length} + + {/if} + {#if showTitleButton} + + {/if} +
+
+ {/if} + + + + + + +
+
+ + diff --git a/packages/client/src/components/app/blocks/index.js b/packages/client/src/components/app/blocks/index.js index 16b123d17e..7d0d15080a 100644 --- a/packages/client/src/components/app/blocks/index.js +++ b/packages/client/src/components/app/blocks/index.js @@ -1 +1,2 @@ export { default as tablewithsearch } from "./TableWithSearch.svelte" +export { default as cardlistwithsearch } from "./CardListWithSearch.svelte" diff --git a/packages/client/src/stores/context.js b/packages/client/src/stores/context.js index e372b837bd..fcbcf0f592 100644 --- a/packages/client/src/stores/context.js +++ b/packages/client/src/stores/context.js @@ -1,4 +1,5 @@ import { writable, derived } from "svelte/store" +import { hashString } from "../utils/helpers" export const createContextStore = oldContext => { const newContext = writable({}) @@ -9,7 +10,7 @@ export const createContextStore = oldContext => { for (let i = 0; i < $contexts.length - 1; i++) { key += $contexts[i].key } - key += JSON.stringify($contexts[$contexts.length - 1]) + key = hashString(key + JSON.stringify($contexts[$contexts.length - 1])) // Reduce global state const reducer = (total, context) => ({ ...total, ...context })