Add card list with search block. Add concept of nested settings which can consume their own contexts and are not enriched at the top level
This commit is contained in:
parent
e2cbf054ce
commit
a7849eac7d
|
@ -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
|
||||
)}`
|
||||
|
|
|
@ -3,7 +3,8 @@
|
|||
"name": "Blocks",
|
||||
"icon": "Article",
|
||||
"children": [
|
||||
"tablewithsearch"
|
||||
"tablewithsearch",
|
||||
"cardlistwithsearch"
|
||||
]
|
||||
},
|
||||
"section",
|
||||
|
|
|
@ -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 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<DetailSummary name="General" collapsible={false}>
|
||||
{#if !componentInstance._component.endsWith("/layout")}
|
||||
<PropertyControl
|
||||
bindable={false}
|
||||
control={Input}
|
||||
label="Name"
|
||||
key="_instanceName"
|
||||
value={componentInstance._instanceName}
|
||||
onChange={val => updateProp("_instanceName", val)}
|
||||
/>
|
||||
{/if}
|
||||
{#if generalSettings.length}
|
||||
{#each generalSettings as setting (setting.key)}
|
||||
{#if canRenderControl(setting)}
|
||||
<PropertyControl
|
||||
type={setting.type}
|
||||
control={getComponentForSettingType(setting.type)}
|
||||
label={setting.label}
|
||||
key={setting.key}
|
||||
value={componentInstance[setting.key] ??
|
||||
componentInstance[setting.key]?.defaultValue}
|
||||
{componentInstance}
|
||||
onChange={val => 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")}
|
||||
<ResetFieldsButton {componentInstance} />
|
||||
{/if}
|
||||
{#if componentDefinition?.info}
|
||||
<div class="text">
|
||||
{@html componentDefinition.info}
|
||||
</div>
|
||||
{/if}
|
||||
</DetailSummary>
|
||||
|
||||
{#each sections as section (section.name)}
|
||||
{#each sections as section, idx (section.name)}
|
||||
<DetailSummary name={section.name} collapsible={false}>
|
||||
{#if idx === 0 && !componentInstance._component.endsWith("/layout")}
|
||||
<PropertyControl
|
||||
bindable={false}
|
||||
control={Input}
|
||||
label="Name"
|
||||
key="_instanceName"
|
||||
value={componentInstance._instanceName}
|
||||
onChange={val => updateProp("_instanceName", val)}
|
||||
/>
|
||||
{/if}
|
||||
{#each section.settings as setting (setting.key)}
|
||||
{#if canRenderControl(setting)}
|
||||
<PropertyControl
|
||||
|
@ -117,7 +95,7 @@
|
|||
key={setting.key}
|
||||
value={componentInstance[setting.key] ??
|
||||
componentInstance[setting.key]?.defaultValue}
|
||||
{componentInstance}
|
||||
nested={setting.nested}
|
||||
onChange={val => 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")}
|
||||
<ResetFieldsButton {componentInstance} />
|
||||
{/if}
|
||||
{#if section?.info}
|
||||
<div class="text">
|
||||
{@html section.info}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
</script>
|
||||
|
||||
<Tabs selected="Settings" noPadding>
|
||||
|
@ -28,6 +35,7 @@
|
|||
{componentInstance}
|
||||
{componentDefinition}
|
||||
{bindings}
|
||||
{componentBindings}
|
||||
/>
|
||||
<DesignSection {componentInstance} {componentDefinition} {bindings} />
|
||||
<CustomStylesSection
|
||||
|
|
|
@ -17,14 +17,24 @@
|
|||
export let props = {}
|
||||
export let onChange = () => {}
|
||||
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
|
||||
/>
|
||||
</Drawer>
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
<script context="module">
|
||||
let SettingsDefinitionCache = {}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import { getContext, setContext } from "svelte"
|
||||
import { writable, get } from "svelte/store"
|
||||
|
@ -20,13 +24,13 @@
|
|||
// Any prop overrides that need to be applied due to conditional UI
|
||||
let conditionalSettings
|
||||
|
||||
// Props are hashed when inside the builder preview and used as a key, so that
|
||||
// components fully remount whenever any props change
|
||||
let propsHash = 0
|
||||
// Settings are hashed when inside the builder preview and used as a key,
|
||||
// so that components fully remount whenever any settings change
|
||||
let hash = 0
|
||||
|
||||
// Latest timestamp that we started a props update.
|
||||
// Due to enrichment now being async, we need to avoid overwriting newer
|
||||
// props with old ones, depending on how long enrichment takes.
|
||||
// settings with old ones, depending on how long enrichment takes.
|
||||
let latestUpdateTime
|
||||
|
||||
// Keep track of stringified representations of context and instance
|
||||
|
@ -49,25 +53,59 @@
|
|||
// Extract component instance info
|
||||
$: constructor = getComponentConstructor(instance._component)
|
||||
$: definition = getComponentDefinition(instance._component)
|
||||
$: settingsDefinition = getSettingsDefinition(definition)
|
||||
$: children = instance._children || []
|
||||
$: id = instance._id
|
||||
$: name = instance._instanceName
|
||||
|
||||
// Determine if the component is selected or is part of the critical path
|
||||
// leading to the selected component
|
||||
$: selected =
|
||||
$builderStore.inBuilder && $builderStore.selectedComponentId === id
|
||||
$: inSelectedPath = $builderStore.selectedComponentPath?.includes(id)
|
||||
|
||||
// Interactive components can be selected, dragged and highlighted inside
|
||||
// the builder preview
|
||||
$: interactive =
|
||||
$builderStore.inBuilder &&
|
||||
($builderStore.previewType === "layout" || insideScreenslot) &&
|
||||
!insideBlock
|
||||
$: draggable = interactive && !isLayout && !isScreen
|
||||
$: droppable = interactive && !isLayout && !isScreen
|
||||
|
||||
// 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
|
||||
$: rawProps = getRawProps(instance)
|
||||
$: instanceKey = JSON.stringify(rawProps)
|
||||
$: updateComponentProps(rawProps, instanceKey, $context)
|
||||
$: selected =
|
||||
$builderStore.inBuilder &&
|
||||
$builderStore.selectedComponentId === instance._id
|
||||
$: inSelectedPath = $builderStore.selectedComponentPath?.includes(id)
|
||||
|
||||
// Raw props are all props excluding internal props and children
|
||||
$: rawSettings = getRawSettings(instance)
|
||||
$: instanceKey = hashString(JSON.stringify(rawSettings))
|
||||
|
||||
// Component settings are those which are intended for this component and
|
||||
// which need to be enriched
|
||||
$: componentSettings = getComponentSettings(rawSettings, settingsDefinition)
|
||||
$: enrichComponentSettings(rawSettings, instanceKey, $context)
|
||||
|
||||
// Nested settings are those which are intended for child components inside
|
||||
// blocks and which should not be enriched at this level
|
||||
$: nestedSettings = getNestedSettings(rawSettings, settingsDefinition)
|
||||
|
||||
// Evaluate conditional UI settings and store any component setting changes
|
||||
// which need to be made
|
||||
$: evaluateConditions(enrichedSettings?._conditions)
|
||||
$: componentSettings = { ...enrichedSettings, ...conditionalSettings }
|
||||
$: renderKey = `${propsHash}-${emptyState}`
|
||||
|
||||
// Build up the final settings object to be passed to the component
|
||||
$: settings = {
|
||||
...enrichedSettings,
|
||||
...nestedSettings,
|
||||
...conditionalSettings,
|
||||
}
|
||||
|
||||
// Render key is used when in the builder preview to fully remount
|
||||
// components when settings are changed
|
||||
$: renderKey = `${hash}-${emptyState}`
|
||||
|
||||
// Update component context
|
||||
$: componentStore.set({
|
||||
|
@ -79,14 +117,14 @@
|
|||
name,
|
||||
})
|
||||
|
||||
const getRawProps = instance => {
|
||||
let validProps = {}
|
||||
const getRawSettings = instance => {
|
||||
let validSettings = {}
|
||||
Object.entries(instance)
|
||||
.filter(([name]) => name === "_conditions" || !name.startsWith("_"))
|
||||
.forEach(([key, value]) => {
|
||||
validProps[key] = value
|
||||
validSettings[key] = value
|
||||
})
|
||||
return validProps
|
||||
return validSettings
|
||||
}
|
||||
|
||||
// Gets the component constructor for the specified component
|
||||
|
@ -105,8 +143,47 @@
|
|||
return type ? Manifest[type] : null
|
||||
}
|
||||
|
||||
const getSettingsDefinition = definition => {
|
||||
if (!definition) {
|
||||
return []
|
||||
}
|
||||
if (SettingsDefinitionCache[definition.name]) {
|
||||
return SettingsDefinitionCache[definition.name]
|
||||
}
|
||||
let settings = []
|
||||
definition.settings?.forEach(setting => {
|
||||
if (setting.section) {
|
||||
settings = settings.concat(setting.settings || [])
|
||||
} else {
|
||||
settings.push(setting)
|
||||
}
|
||||
})
|
||||
SettingsDefinitionCache[definition] = settings
|
||||
return settings
|
||||
}
|
||||
|
||||
const getComponentSettings = (rawSettings, settingsDefinition) => {
|
||||
let clone = { ...rawSettings }
|
||||
settingsDefinition?.forEach(setting => {
|
||||
if (setting.nested) {
|
||||
delete clone[setting.key]
|
||||
}
|
||||
})
|
||||
return clone
|
||||
}
|
||||
|
||||
const getNestedSettings = (rawSettings, settingsDefinition) => {
|
||||
let clone = { ...rawSettings }
|
||||
settingsDefinition?.forEach(setting => {
|
||||
if (!setting.nested) {
|
||||
delete clone[setting.key]
|
||||
}
|
||||
})
|
||||
return clone
|
||||
}
|
||||
|
||||
// Enriches any string component props using handlebars
|
||||
const updateComponentProps = (rawProps, instanceKey, context) => {
|
||||
const enrichComponentSettings = (rawSettings, instanceKey, context) => {
|
||||
const instanceSame = instanceKey === lastInstanceKey
|
||||
const contextSame = context.key === lastContextKey
|
||||
|
||||
|
@ -121,8 +198,8 @@
|
|||
latestUpdateTime = Date.now()
|
||||
const enrichmentTime = latestUpdateTime
|
||||
|
||||
// Enrich props with context
|
||||
const enrichedProps = enrichProps(rawProps, context)
|
||||
// Enrich settings with context
|
||||
const newEnrichedSettings = enrichProps(rawSettings, context)
|
||||
|
||||
// Abandon this update if a newer update has started
|
||||
if (enrichmentTime !== latestUpdateTime) {
|
||||
|
@ -132,7 +209,7 @@
|
|||
// Update the component props.
|
||||
// Most props are deeply compared so that svelte will only trigger reactive
|
||||
// statements on props that have actually changed.
|
||||
if (!enrichedProps) {
|
||||
if (!newEnrichedSettings) {
|
||||
return
|
||||
}
|
||||
let propsChanged = false
|
||||
|
@ -140,17 +217,17 @@
|
|||
enrichedSettings = {}
|
||||
propsChanged = true
|
||||
}
|
||||
Object.keys(enrichedProps).forEach(key => {
|
||||
if (!propsAreSame(enrichedProps[key], enrichedSettings[key])) {
|
||||
Object.keys(newEnrichedSettings).forEach(key => {
|
||||
if (!propsAreSame(newEnrichedSettings[key], enrichedSettings[key])) {
|
||||
propsChanged = true
|
||||
enrichedSettings[key] = enrichedProps[key]
|
||||
enrichedSettings[key] = newEnrichedSettings[key]
|
||||
}
|
||||
})
|
||||
|
||||
// Update the hash if we're in the builder so we can fully remount this
|
||||
// component
|
||||
if (get(builderStore).inBuilder && propsChanged) {
|
||||
propsHash = hashString(JSON.stringify(enrichedSettings))
|
||||
hash = hashString(JSON.stringify(enrichedSettings))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -173,14 +250,10 @@
|
|||
conditionalSettings = result.settingUpdates
|
||||
visible = nextVisible
|
||||
}
|
||||
|
||||
// Drag and drop helper tags
|
||||
$: draggable = interactive && !isLayout && !isScreen
|
||||
$: droppable = interactive && !isLayout && !isScreen
|
||||
</script>
|
||||
|
||||
{#key renderKey}
|
||||
{#if constructor && componentSettings && (visible || inSelectedPath)}
|
||||
{#if constructor && settings && (visible || inSelectedPath)}
|
||||
<!-- The ID is used as a class because getElementsByClassName is O(1) -->
|
||||
<!-- and the performance matters for the selection indicators -->
|
||||
<div
|
||||
|
@ -192,7 +265,7 @@
|
|||
data-id={id}
|
||||
data-name={name}
|
||||
>
|
||||
<svelte:component this={constructor} {...componentSettings}>
|
||||
<svelte:component this={constructor} {...settings}>
|
||||
{#if children.length}
|
||||
{#each children as child (child._id)}
|
||||
<svelte:self instance={child} />
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<script>
|
||||
import "@spectrum-css/card/dist/index-vars.css"
|
||||
import { getContext } from "svelte"
|
||||
import { Button } from "@budibase/bbui"
|
||||
|
||||
export let title
|
||||
export let subtitle
|
||||
|
@ -8,6 +9,9 @@
|
|||
export let imageURL
|
||||
export let linkURL
|
||||
export let horizontal
|
||||
export let showButton
|
||||
export let buttonText
|
||||
export let buttonOnClick
|
||||
|
||||
const { styleable, linkable } = getContext("sdk")
|
||||
const component = getContext("component")
|
||||
|
@ -60,6 +64,11 @@
|
|||
{description}
|
||||
</div>
|
||||
{/if}
|
||||
{#if showButton}
|
||||
<div class="spectrum-Card-footer button-container">
|
||||
<Button on:click={buttonOnClick} secondary>{buttonText}</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -116,4 +125,8 @@
|
|||
padding-top: 0;
|
||||
margin-top: -8px;
|
||||
}
|
||||
|
||||
.button-container {
|
||||
margin-top: -3px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,240 @@
|
|||
<script>
|
||||
import { onMount, getContext } from "svelte"
|
||||
import Block from "components/Block.svelte"
|
||||
import BlockComponent from "components/BlockComponent.svelte"
|
||||
import { Heading } from "@budibase/bbui"
|
||||
|
||||
export let title
|
||||
export let dataSource
|
||||
export let searchColumns
|
||||
export let filter
|
||||
export let sortColumn
|
||||
export let sortOrder
|
||||
export let paginate
|
||||
export let limit
|
||||
export let showTitleButton
|
||||
export let titleButtonText
|
||||
export let titleButtonOnClick
|
||||
export let cardTitle
|
||||
export let cardSubtitle
|
||||
export let cardDescription
|
||||
export let cardImageURL
|
||||
export let cardHorizontal
|
||||
export let showCardButton
|
||||
export let cardButtonText
|
||||
export let cardButtonOnClick
|
||||
|
||||
const { API, styleable } = getContext("sdk")
|
||||
const context = getContext("context")
|
||||
const component = getContext("component")
|
||||
const schemaComponentMap = {
|
||||
string: "stringfield",
|
||||
options: "optionsfield",
|
||||
number: "numberfield",
|
||||
datetime: "datetimefield",
|
||||
boolean: "booleanfield",
|
||||
}
|
||||
|
||||
let formId
|
||||
let dataProviderId
|
||||
let schema
|
||||
|
||||
$: enrichedSearchColumns = enrichSearchColumns(searchColumns, schema)
|
||||
$: enrichedFilter = enrichFilter(filter, enrichedSearchColumns, formId)
|
||||
$: cardWidth = cardHorizontal ? 420 : 240
|
||||
|
||||
// Enrich the default filter with the specified search fields
|
||||
const enrichFilter = (filter, columns, formId) => {
|
||||
let enrichedFilter = [...(filter || [])]
|
||||
columns?.forEach(column => {
|
||||
enrichedFilter.push({
|
||||
field: column.name,
|
||||
operator: "equal",
|
||||
type: "string",
|
||||
valueType: "Binding",
|
||||
value: `{{ [${formId}].[${column.name}] }}`,
|
||||
})
|
||||
})
|
||||
return enrichedFilter
|
||||
}
|
||||
|
||||
// Determine data types for search fields and only use those that are valid
|
||||
const enrichSearchColumns = (searchColumns, schema) => {
|
||||
let enrichedColumns = []
|
||||
searchColumns?.forEach(column => {
|
||||
const schemaType = schema?.[column]?.type
|
||||
const componentType = schemaComponentMap[schemaType]
|
||||
if (componentType) {
|
||||
enrichedColumns.push({
|
||||
name: column,
|
||||
componentType,
|
||||
})
|
||||
}
|
||||
})
|
||||
return enrichedColumns.slice(0, 3)
|
||||
}
|
||||
|
||||
// Load the datasource schema on mount so we can determine column types
|
||||
onMount(async () => {
|
||||
if (dataSource) {
|
||||
schema = await API.fetchDatasourceSchema(dataSource)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<Block>
|
||||
<div class="card-list" use:styleable={$component.styles}>
|
||||
<BlockComponent type="form" bind:id={formId} props={{ dataSource }}>
|
||||
{#if title || enrichedSearchColumns?.length || showTitleButton}
|
||||
<div class="header" class:mobile={$context.device.mobile}>
|
||||
<div class="title">
|
||||
<Heading>{title || ""}</Heading>
|
||||
</div>
|
||||
<div class="controls">
|
||||
{#if enrichedSearchColumns?.length}
|
||||
<div
|
||||
class="search"
|
||||
style="--cols:{enrichedSearchColumns?.length}"
|
||||
>
|
||||
{#each enrichedSearchColumns as column}
|
||||
<BlockComponent
|
||||
type={column.componentType}
|
||||
props={{
|
||||
field: column.name,
|
||||
placeholder: column.name,
|
||||
text: column.name,
|
||||
autoWidth: true,
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{#if showTitleButton}
|
||||
<BlockComponent
|
||||
type="button"
|
||||
props={{
|
||||
onClick: titleButtonOnClick,
|
||||
text: titleButtonText,
|
||||
type: "cta",
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<BlockComponent
|
||||
type="dataprovider"
|
||||
bind:id={dataProviderId}
|
||||
props={{
|
||||
dataSource,
|
||||
filter: enrichedFilter,
|
||||
sortColumn,
|
||||
sortOrder,
|
||||
paginate,
|
||||
limit,
|
||||
}}
|
||||
>
|
||||
<BlockComponent
|
||||
type="repeater"
|
||||
context="repeater"
|
||||
props={{
|
||||
dataProvider: `{{ literal [${dataProviderId}] }}`,
|
||||
direction: "row",
|
||||
hAlign: "left",
|
||||
vAlign: "top",
|
||||
gap: "M",
|
||||
}}
|
||||
styles={{
|
||||
display: "grid",
|
||||
"grid-template-columns": `repeat(auto-fill, minmax(${cardWidth}px, 1fr))`,
|
||||
}}
|
||||
>
|
||||
<BlockComponent
|
||||
type="spectrumcard"
|
||||
props={{
|
||||
title: cardTitle,
|
||||
subtitle: cardSubtitle,
|
||||
description: cardDescription,
|
||||
imageURL: cardImageURL,
|
||||
horizontal: cardHorizontal,
|
||||
showButton: showCardButton,
|
||||
buttonText: cardButtonText,
|
||||
buttonOnClick: cardButtonOnClick,
|
||||
}}
|
||||
styles={{
|
||||
width: "auto",
|
||||
}}
|
||||
/>
|
||||
</BlockComponent>
|
||||
</BlockComponent>
|
||||
</BlockComponent>
|
||||
</div>
|
||||
</Block>
|
||||
|
||||
<style>
|
||||
.card-list :global(.spectrum-Card) {
|
||||
width: auto !important;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.title {
|
||||
overflow: hidden;
|
||||
}
|
||||
.title :global(.spectrum-Heading) {
|
||||
flex: 1 1 auto;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.controls {
|
||||
flex: 0 1 auto;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
.controls :global(.spectrum-InputGroup .spectrum-InputGroup-input) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.search {
|
||||
flex: 0 1 auto;
|
||||
gap: 10px;
|
||||
max-width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(var(--cols), minmax(120px, 200px));
|
||||
}
|
||||
.search :global(.spectrum-InputGroup) {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* Mobile styles */
|
||||
.mobile {
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
}
|
||||
.mobile .controls {
|
||||
flex-direction: column-reverse;
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
}
|
||||
.mobile .search {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
|
@ -1 +1,2 @@
|
|||
export { default as tablewithsearch } from "./TableWithSearch.svelte"
|
||||
export { default as cardlistwithsearch } from "./CardListWithSearch.svelte"
|
||||
|
|
|
@ -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 })
|
||||
|
|
Loading…
Reference in New Issue