Merge pull request #3355 from Budibase/repeater-array

Data block + array fields as data sources
This commit is contained in:
Andrew Kingston 2021-11-16 11:03:30 +00:00 committed by GitHub
commit 4c84f713b9
17 changed files with 459 additions and 96 deletions

View File

@ -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]
}
/**

View File

@ -4,7 +4,8 @@
"icon": "Article",
"children": [
"tableblock",
"cardsblock"
"cardsblock",
"repeaterblock"
]
},
"section",

View File

@ -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(() => {

View File

@ -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)
}
</script>
<div class="container" bind:this={anchorRight}>
@ -127,11 +146,10 @@
</Button>
<DrawerContent slot="body">
<Layout noPadding>
{#if value.parameters.length > 0}
{#if getQueryParams(value._id).length > 0}
<ParameterBuilder
bind:customParams={value.queryParams}
parameters={queries.find(query => 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}
/>
</Layout>
@ -159,52 +175,71 @@
<li on:click={() => handleSelected(table)}>{table.label}</li>
{/each}
</ul>
<Divider size="S" />
<div class="title">
<Heading size="XS">Views</Heading>
</div>
<ul>
{#each views as view}
<li on:click={() => handleSelected(view)}>{view.label}</li>
{/each}
</ul>
<Divider size="S" />
<div class="title">
<Heading size="XS">Relationships</Heading>
</div>
<ul>
{#each links as link}
<li on:click={() => handleSelected(link)}>{link.label}</li>
{/each}
</ul>
<Divider size="S" />
<div class="title">
<Heading size="XS">Queries</Heading>
</div>
<ul>
{#each queries as query}
<li
class:selected={value === query}
on:click={() => handleSelected(query)}
>
{query.label}
</li>
{/each}
</ul>
<Divider size="S" />
<div class="title">
<Heading size="XS">Data Providers</Heading>
</div>
<ul>
{#each dataProviders as provider}
<li
class:selected={value === provider}
on:click={() => handleSelected(provider)}
>
{provider.label}
</li>
{/each}
</ul>
{#if views?.length}
<Divider size="S" />
<div class="title">
<Heading size="XS">Views</Heading>
</div>
<ul>
{#each views as view}
<li on:click={() => handleSelected(view)}>{view.label}</li>
{/each}
</ul>
{/if}
{#if queries?.length}
<Divider size="S" />
<div class="title">
<Heading size="XS">Queries</Heading>
</div>
<ul>
{#each queries as query}
<li
class:selected={value === query}
on:click={() => handleSelected(query)}
>
{query.label}
</li>
{/each}
</ul>
{/if}
{#if links?.length}
<Divider size="S" />
<div class="title">
<Heading size="XS">Relationships</Heading>
</div>
<ul>
{#each links as link}
<li on:click={() => handleSelected(link)}>{link.label}</li>
{/each}
</ul>
{/if}
{#if fields?.length}
<Divider size="S" />
<div class="title">
<Heading size="XS">Fields</Heading>
</div>
<ul>
{#each fields as field}
<li on:click={() => handleSelected(field)}>{field.label}</li>
{/each}
</ul>
{/if}
{#if dataProviders?.length}
<Divider size="S" />
<div class="title">
<Heading size="XS">Data Providers</Heading>
</div>
<ul>
{#each dataProviders as provider}
<li
class:selected={value === provider}
on:click={() => handleSelected(provider)}
>
{provider.label}
</li>
{/each}
</ul>
{/if}
{#if otherSources?.length}
<Divider size="S" />
<div class="title">

View File

@ -6,7 +6,6 @@
} from "builderStore/dataBinding"
export let label = ""
export let bindable = true
export let componentInstance = {}
export let control = null
export let key = ""

View File

@ -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"
}
]
}
}

View File

@ -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") &&

View File

@ -30,6 +30,6 @@
}
</script>
<Component {instance}>
<Component {instance} isBlock>
<slot />
</Component>

View File

@ -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}
<Placeholder />
{:else if insideBlock}
{:else if isBlock}
<slot />
{/if}
</svelte:component>

View File

@ -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

View File

@ -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",

View File

@ -0,0 +1,61 @@
<script>
import BlockComponent from "components/BlockComponent.svelte"
import Block from "components/Block.svelte"
import Placeholder from "components/app/Placeholder.svelte"
import { getContext } from "svelte"
import { makePropSafe as safe } from "@budibase/string-templates"
export let dataSource
export let filter
export let sortColumn
export let sortOrder
export let limit
export let paginate
export let noRowsMessage
export let direction
export let hAlign
export let vAlign
export let gap
let providerId
const component = getContext("component")
const { styleable } = getContext("sdk")
</script>
<Block>
<div use:styleable={$component.styles}>
<BlockComponent
type="dataprovider"
context="provider"
bind:id={providerId}
props={{
dataSource,
filter,
sortColumn,
sortOrder,
limit,
paginate,
}}
>
{#if $component.empty}
<Placeholder text={$component.name} />
{:else}
<BlockComponent
type="repeater"
context="repeater"
props={{
dataProvider: `{{ literal ${safe(providerId)} }}`,
noRowsMessage,
direction,
hAlign,
vAlign,
gap,
}}
>
<slot />
</BlockComponent>
{/if}
</BlockComponent>
</div>
</Block>

View File

@ -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 @@
<BlockComponent
type="table"
props={{
dataProvider: `{{ literal [${dataProviderId}] }}`,
dataProvider: `{{ literal ${safe(dataProviderId)} }}`,
columns: tableColumns,
showAutoColumns,
rowCount,

View File

@ -1,2 +1,3 @@
export { default as tableblock } from "./TableBlock.svelte"
export { default as cardsblock } from "./CardsBlock.svelte"
export { default as repeaterblock } from "./RepeaterBlock.svelte"

View File

@ -147,7 +147,7 @@
return
}
const element = e.target.closest(".component")
const element = e.target.closest(".component:not(.block)")
if (
element &&
element.classList.contains("droppable") &&

View File

@ -17,7 +17,19 @@
$: definition = $builderStore.selectedComponentDefinition
$: showBar = definition?.showSettingsBar && !$builderStore.isDragging
$: settings = definition?.settings?.filter(setting => 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) {

View File

@ -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}}}`