Merge pull request #3249 from Budibase/blocks
Core blocks implementation + table with search block
This commit is contained in:
commit
d0755ce8ac
|
@ -21,6 +21,7 @@
|
||||||
<label
|
<label
|
||||||
class="spectrum-Checkbox spectrum-Checkbox--emphasized {sizeClass}"
|
class="spectrum-Checkbox spectrum-Checkbox--emphasized {sizeClass}"
|
||||||
class:is-invalid={!!error}
|
class:is-invalid={!!error}
|
||||||
|
class:checked={value}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
checked={value}
|
checked={value}
|
||||||
|
@ -50,6 +51,16 @@
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
.spectrum-Checkbox--sizeL .spectrum-Checkbox-checkmark {
|
||||||
|
transform: scale(1.1);
|
||||||
|
left: 55%;
|
||||||
|
top: 55%;
|
||||||
|
}
|
||||||
|
.spectrum-Checkbox--sizeXL .spectrum-Checkbox-checkmark {
|
||||||
|
transform: scale(1.2);
|
||||||
|
left: 60%;
|
||||||
|
top: 60%;
|
||||||
|
}
|
||||||
.spectrum-Checkbox-input {
|
.spectrum-Checkbox-input {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import {
|
||||||
findAllMatchingComponents,
|
findAllMatchingComponents,
|
||||||
findComponent,
|
findComponent,
|
||||||
findComponentPath,
|
findComponentPath,
|
||||||
|
getComponentSettings,
|
||||||
} from "./storeUtils"
|
} from "./storeUtils"
|
||||||
import { store } from "builderStore"
|
import { store } from "builderStore"
|
||||||
import { queries as queriesStores, tables as tablesStore } from "stores/backend"
|
import { queries as queriesStores, tables as tablesStore } from "stores/backend"
|
||||||
|
@ -38,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.
|
* Gets all data provider components above a component.
|
||||||
*/
|
*/
|
||||||
|
@ -82,13 +102,10 @@ export const getActionProviderComponents = (asset, componentId, actionType) => {
|
||||||
* Gets a datasource object for a certain data provider component
|
* Gets a datasource object for a certain data provider component
|
||||||
*/
|
*/
|
||||||
export const getDatasourceForProvider = (asset, component) => {
|
export const getDatasourceForProvider = (asset, component) => {
|
||||||
const def = store.actions.components.getDefinition(component?._component)
|
const settings = getComponentSettings(component?._component)
|
||||||
if (!def) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
// If this component has a dataProvider setting, go up the stack and use it
|
// If this component has a dataProvider setting, go up the stack and use it
|
||||||
const dataProviderSetting = def.settings.find(setting => {
|
const dataProviderSetting = settings.find(setting => {
|
||||||
return setting.type === "dataProvider"
|
return setting.type === "dataProvider"
|
||||||
})
|
})
|
||||||
if (dataProviderSetting) {
|
if (dataProviderSetting) {
|
||||||
|
@ -100,7 +117,7 @@ export const getDatasourceForProvider = (asset, component) => {
|
||||||
|
|
||||||
// Extract datasource from component instance
|
// Extract datasource from component instance
|
||||||
const validSettingTypes = ["dataSource", "table", "schema"]
|
const validSettingTypes = ["dataSource", "table", "schema"]
|
||||||
const datasourceSetting = def.settings.find(setting => {
|
const datasourceSetting = settings.find(setting => {
|
||||||
return validSettingTypes.includes(setting.type)
|
return validSettingTypes.includes(setting.type)
|
||||||
})
|
})
|
||||||
if (!datasourceSetting) {
|
if (!datasourceSetting) {
|
||||||
|
@ -127,9 +144,26 @@ export const getDatasourceForProvider = (asset, component) => {
|
||||||
const getContextBindings = (asset, componentId) => {
|
const getContextBindings = (asset, componentId) => {
|
||||||
// Extract any components which provide data contexts
|
// Extract any components which provide data contexts
|
||||||
const dataProviders = getDataProviderComponents(asset, componentId)
|
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
|
// Create bindings for each data provider
|
||||||
|
let bindings = []
|
||||||
dataProviders.forEach(component => {
|
dataProviders.forEach(component => {
|
||||||
const def = store.actions.components.getDefinition(component._component)
|
const def = store.actions.components.getDefinition(component._component)
|
||||||
const contexts = Array.isArray(def.context) ? def.context : [def.context]
|
const contexts = Array.isArray(def.context) ? def.context : [def.context]
|
||||||
|
@ -142,6 +176,7 @@ const getContextBindings = (asset, componentId) => {
|
||||||
|
|
||||||
let schema
|
let schema
|
||||||
let readablePrefix
|
let readablePrefix
|
||||||
|
let runtimeSuffix = context.suffix
|
||||||
|
|
||||||
if (context.type === "form") {
|
if (context.type === "form") {
|
||||||
// Forms do not need table schemas
|
// Forms do not need table schemas
|
||||||
|
@ -171,8 +206,14 @@ const getContextBindings = (asset, componentId) => {
|
||||||
|
|
||||||
const keys = Object.keys(schema).sort()
|
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
|
// Create bindable properties for each schema field
|
||||||
const safeComponentId = makePropSafe(component._id)
|
|
||||||
keys.forEach(key => {
|
keys.forEach(key => {
|
||||||
const fieldSchema = schema[key]
|
const fieldSchema = schema[key]
|
||||||
|
|
||||||
|
@ -184,6 +225,7 @@ const getContextBindings = (asset, componentId) => {
|
||||||
} else if (fieldSchema.type === "attachment") {
|
} else if (fieldSchema.type === "attachment") {
|
||||||
runtimeBoundKey = `${key}_first`
|
runtimeBoundKey = `${key}_first`
|
||||||
}
|
}
|
||||||
|
|
||||||
const runtimeBinding = `${safeComponentId}.${makePropSafe(
|
const runtimeBinding = `${safeComponentId}.${makePropSafe(
|
||||||
runtimeBoundKey
|
runtimeBoundKey
|
||||||
)}`
|
)}`
|
||||||
|
@ -374,8 +416,8 @@ const buildFormSchema = component => {
|
||||||
if (!component) {
|
if (!component) {
|
||||||
return schema
|
return schema
|
||||||
}
|
}
|
||||||
const def = store.actions.components.getDefinition(component._component)
|
const settings = getComponentSettings(component._component)
|
||||||
const fieldSetting = def?.settings?.find(
|
const fieldSetting = settings.find(
|
||||||
setting => setting.key === "field" && setting.type.startsWith("field/")
|
setting => setting.key === "field" && setting.type.startsWith("field/")
|
||||||
)
|
)
|
||||||
if (fieldSetting && component.field) {
|
if (fieldSetting && component.field) {
|
||||||
|
|
|
@ -25,6 +25,7 @@ import {
|
||||||
findClosestMatchingComponent,
|
findClosestMatchingComponent,
|
||||||
findAllMatchingComponents,
|
findAllMatchingComponents,
|
||||||
findComponent,
|
findComponent,
|
||||||
|
getComponentSettings,
|
||||||
} from "../storeUtils"
|
} from "../storeUtils"
|
||||||
import { uuid } from "../uuid"
|
import { uuid } from "../uuid"
|
||||||
import { removeBindings } from "../dataBinding"
|
import { removeBindings } from "../dataBinding"
|
||||||
|
@ -368,14 +369,13 @@ export const getFrontendStore = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate default props
|
// Generate default props
|
||||||
|
const settings = getComponentSettings(componentName)
|
||||||
let props = { ...presetProps }
|
let props = { ...presetProps }
|
||||||
if (definition.settings) {
|
settings.forEach(setting => {
|
||||||
definition.settings.forEach(setting => {
|
if (setting.defaultValue !== undefined) {
|
||||||
if (setting.defaultValue !== undefined) {
|
props[setting.key] = setting.defaultValue
|
||||||
props[setting.key] = setting.defaultValue
|
}
|
||||||
}
|
})
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add any extra properties the component needs
|
// Add any extra properties the component needs
|
||||||
let extras = {}
|
let extras = {}
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { store } from "./index"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Recursively searches for a specific component ID
|
* Recursively searches for a specific component ID
|
||||||
*/
|
*/
|
||||||
|
@ -123,3 +125,20 @@ const searchComponentTree = (rootComponent, matchComponent) => {
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Searches a component's definition for a setting matching a certin predicate.
|
||||||
|
*/
|
||||||
|
export const getComponentSettings = componentType => {
|
||||||
|
const def = store.actions.components.getDefinition(componentType)
|
||||||
|
if (!def) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
let settings = def.settings?.filter(setting => !setting.section) ?? []
|
||||||
|
def.settings
|
||||||
|
?.filter(setting => setting.section)
|
||||||
|
.forEach(section => {
|
||||||
|
settings = settings.concat(section.settings || [])
|
||||||
|
})
|
||||||
|
return settings
|
||||||
|
}
|
||||||
|
|
|
@ -1,4 +1,12 @@
|
||||||
[
|
[
|
||||||
|
{
|
||||||
|
"name": "Blocks",
|
||||||
|
"icon": "Article",
|
||||||
|
"children": [
|
||||||
|
"tablewithsearch",
|
||||||
|
"cardlistwithsearch"
|
||||||
|
]
|
||||||
|
},
|
||||||
"section",
|
"section",
|
||||||
"container",
|
"container",
|
||||||
"dataprovider",
|
"dataprovider",
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
export let componentInstance
|
export let componentInstance
|
||||||
export let assetInstance
|
export let assetInstance
|
||||||
export let bindings
|
export let bindings
|
||||||
|
export let componentBindings
|
||||||
|
|
||||||
const layoutDefinition = []
|
const layoutDefinition = []
|
||||||
const screenDefinition = [
|
const screenDefinition = [
|
||||||
|
@ -21,10 +22,24 @@
|
||||||
{ key: "layoutId", label: "Layout", control: LayoutSelect },
|
{ key: "layoutId", label: "Layout", control: LayoutSelect },
|
||||||
]
|
]
|
||||||
|
|
||||||
$: settings = componentDefinition?.settings ?? []
|
$: sections = getSections(componentDefinition)
|
||||||
$: isLayout = assetInstance && assetInstance.favicon
|
$: isLayout = assetInstance && assetInstance.favicon
|
||||||
$: assetDefinition = isLayout ? layoutDefinition : screenDefinition
|
$: 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 updateProp = store.actions.components.updateProp
|
||||||
|
|
||||||
const canRenderControl = setting => {
|
const canRenderControl = setting => {
|
||||||
|
@ -59,19 +74,19 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DetailSummary name="General" collapsible={false}>
|
{#each sections as section, idx (section.name)}
|
||||||
{#if !componentInstance._component.endsWith("/layout")}
|
<DetailSummary name={section.name} collapsible={false}>
|
||||||
<PropertyControl
|
{#if idx === 0 && !componentInstance._component.endsWith("/layout")}
|
||||||
bindable={false}
|
<PropertyControl
|
||||||
control={Input}
|
bindable={false}
|
||||||
label="Name"
|
control={Input}
|
||||||
key="_instanceName"
|
label="Name"
|
||||||
value={componentInstance._instanceName}
|
key="_instanceName"
|
||||||
onChange={val => updateProp("_instanceName", val)}
|
value={componentInstance._instanceName}
|
||||||
/>
|
onChange={val => updateProp("_instanceName", val)}
|
||||||
{/if}
|
/>
|
||||||
{#if settings && settings.length > 0}
|
{/if}
|
||||||
{#each settings as setting (setting.key)}
|
{#each section.settings as setting (setting.key)}
|
||||||
{#if canRenderControl(setting)}
|
{#if canRenderControl(setting)}
|
||||||
<PropertyControl
|
<PropertyControl
|
||||||
type={setting.type}
|
type={setting.type}
|
||||||
|
@ -80,7 +95,7 @@
|
||||||
key={setting.key}
|
key={setting.key}
|
||||||
value={componentInstance[setting.key] ??
|
value={componentInstance[setting.key] ??
|
||||||
componentInstance[setting.key]?.defaultValue}
|
componentInstance[setting.key]?.defaultValue}
|
||||||
{componentInstance}
|
nested={setting.nested}
|
||||||
onChange={val => updateProp(setting.key, val)}
|
onChange={val => updateProp(setting.key, val)}
|
||||||
props={{
|
props={{
|
||||||
options: setting.options || [],
|
options: setting.options || [],
|
||||||
|
@ -89,20 +104,22 @@
|
||||||
max: setting.max || null,
|
max: setting.max || null,
|
||||||
}}
|
}}
|
||||||
{bindings}
|
{bindings}
|
||||||
|
{componentBindings}
|
||||||
|
{componentInstance}
|
||||||
{componentDefinition}
|
{componentDefinition}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{#if idx === 0 && componentDefinition?.component?.endsWith("/fieldgroup")}
|
||||||
{#if componentDefinition?.component?.endsWith("/fieldgroup")}
|
<ResetFieldsButton {componentInstance} />
|
||||||
<ResetFieldsButton {componentInstance} />
|
{/if}
|
||||||
{/if}
|
{#if section?.info}
|
||||||
{#if componentDefinition?.info}
|
<div class="text">
|
||||||
<div class="text">
|
{@html section.info}
|
||||||
{@html componentDefinition?.info}
|
</div>
|
||||||
</div>
|
{/if}
|
||||||
{/if}
|
</DetailSummary>
|
||||||
</DetailSummary>
|
{/each}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.text {
|
.text {
|
||||||
|
|
|
@ -6,13 +6,20 @@
|
||||||
import DesignSection from "./DesignSection.svelte"
|
import DesignSection from "./DesignSection.svelte"
|
||||||
import CustomStylesSection from "./CustomStylesSection.svelte"
|
import CustomStylesSection from "./CustomStylesSection.svelte"
|
||||||
import ConditionalUISection from "./ConditionalUISection.svelte"
|
import ConditionalUISection from "./ConditionalUISection.svelte"
|
||||||
import { getBindableProperties } from "builderStore/dataBinding"
|
import {
|
||||||
|
getBindableProperties,
|
||||||
|
getComponentBindableProperties,
|
||||||
|
} from "builderStore/dataBinding"
|
||||||
|
|
||||||
$: componentInstance = $selectedComponent
|
$: componentInstance = $selectedComponent
|
||||||
$: componentDefinition = store.actions.components.getDefinition(
|
$: componentDefinition = store.actions.components.getDefinition(
|
||||||
$selectedComponent?._component
|
$selectedComponent?._component
|
||||||
)
|
)
|
||||||
$: bindings = getBindableProperties($currentAsset, $store.selectedComponentId)
|
$: bindings = getBindableProperties($currentAsset, $store.selectedComponentId)
|
||||||
|
$: componentBindings = getComponentBindableProperties(
|
||||||
|
$currentAsset,
|
||||||
|
$store.selectedComponentId
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Tabs selected="Settings" noPadding>
|
<Tabs selected="Settings" noPadding>
|
||||||
|
@ -28,6 +35,7 @@
|
||||||
{componentInstance}
|
{componentInstance}
|
||||||
{componentDefinition}
|
{componentDefinition}
|
||||||
{bindings}
|
{bindings}
|
||||||
|
{componentBindings}
|
||||||
/>
|
/>
|
||||||
<DesignSection {componentInstance} {componentDefinition} {bindings} />
|
<DesignSection {componentInstance} {componentDefinition} {bindings} />
|
||||||
<CustomStylesSection
|
<CustomStylesSection
|
||||||
|
|
|
@ -17,14 +17,24 @@
|
||||||
export let props = {}
|
export let props = {}
|
||||||
export let onChange = () => {}
|
export let onChange = () => {}
|
||||||
export let bindings = []
|
export let bindings = []
|
||||||
|
export let componentBindings = []
|
||||||
|
export let nested = false
|
||||||
|
|
||||||
let bindingDrawer
|
let bindingDrawer
|
||||||
let anchor
|
let anchor
|
||||||
let valid
|
let valid
|
||||||
|
|
||||||
$: safeValue = getSafeValue(value, props.defaultValue, bindings)
|
$: allBindings = getAllBindings(bindings, componentBindings, nested)
|
||||||
|
$: safeValue = getSafeValue(value, props.defaultValue, allBindings)
|
||||||
$: tempValue = safeValue
|
$: 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 = () => {
|
const handleClose = () => {
|
||||||
handleChange(tempValue)
|
handleChange(tempValue)
|
||||||
|
@ -78,7 +88,7 @@
|
||||||
updateOnChange={false}
|
updateOnChange={false}
|
||||||
on:change={handleChange}
|
on:change={handleChange}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
{bindings}
|
bindings={allBindings}
|
||||||
name={key}
|
name={key}
|
||||||
text={label}
|
text={label}
|
||||||
{type}
|
{type}
|
||||||
|
@ -104,7 +114,7 @@
|
||||||
bind:valid
|
bind:valid
|
||||||
value={safeValue}
|
value={safeValue}
|
||||||
on:change={e => (tempValue = e.detail)}
|
on:change={e => (tempValue = e.detail)}
|
||||||
bindableProperties={bindings}
|
bindableProperties={allBindings}
|
||||||
allowJS
|
allowJS
|
||||||
/>
|
/>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
|
@ -122,7 +132,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
text-transform: capitalize;
|
|
||||||
padding-bottom: var(--spectrum-global-dimension-size-65);
|
padding-bottom: var(--spectrum-global-dimension-size-65);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2457,12 +2457,12 @@
|
||||||
"settings": [
|
"settings": [
|
||||||
{
|
{
|
||||||
"type": "dataProvider",
|
"type": "dataProvider",
|
||||||
"label": "Provider",
|
"label": "Data provider",
|
||||||
"key": "dataProvider"
|
"key": "dataProvider"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"label": "Row Count",
|
"label": "Row count",
|
||||||
"key": "rowCount",
|
"key": "rowCount",
|
||||||
"defaultValue": 8
|
"defaultValue": 8
|
||||||
},
|
},
|
||||||
|
@ -2496,9 +2496,36 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"label": "Auto Columns",
|
"label": "Show auto columns",
|
||||||
"key": "showAutoColumns",
|
"key": "showAutoColumns",
|
||||||
"defaultValue": false
|
"defaultValue": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"label": "Link table rows",
|
||||||
|
"key": "linkRows"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"label": "Open link screens in modal",
|
||||||
|
"key": "linkPeek"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "url",
|
||||||
|
"label": "Link screen",
|
||||||
|
"key": "linkURL"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"section": true,
|
||||||
|
"name": "Advanced",
|
||||||
|
"settings": [
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "ID column for linking (appended to URL)",
|
||||||
|
"key": "linkColumn",
|
||||||
|
"placeholder": "Default"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"context": {
|
"context": {
|
||||||
|
@ -2572,7 +2599,297 @@
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"key": "horizontal",
|
"key": "horizontal",
|
||||||
"label": "Horizontal"
|
"label": "Horizontal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"label": "Show button",
|
||||||
|
"key": "showButton"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"key": "buttonText",
|
||||||
|
"label": "Button text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "event",
|
||||||
|
"label": "Button action",
|
||||||
|
"key": "buttonOnClick"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"tablewithsearch": {
|
||||||
|
"block": true,
|
||||||
|
"name": "Table 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": "select",
|
||||||
|
"label": "Size",
|
||||||
|
"key": "size",
|
||||||
|
"defaultValue": "spectrum--medium",
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"label": "Medium",
|
||||||
|
"value": "spectrum--medium"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Large",
|
||||||
|
"value": "spectrum--large"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"label": "Paginate",
|
||||||
|
"key": "paginate",
|
||||||
|
"defaultValue": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"section": true,
|
||||||
|
"name": "Table",
|
||||||
|
"settings": [
|
||||||
|
{
|
||||||
|
"type": "number",
|
||||||
|
"label": "Row Count",
|
||||||
|
"key": "rowCount",
|
||||||
|
"defaultValue": 8
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "multifield",
|
||||||
|
"label": "Table Columns",
|
||||||
|
"key": "tableColumns",
|
||||||
|
"dependsOn": "dataSource",
|
||||||
|
"placeholder": "All columns"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"label": "Quiet table variant",
|
||||||
|
"key": "quiet"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"label": "Show auto columns",
|
||||||
|
"key": "showAutoColumns"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"label": "Link table rows",
|
||||||
|
"key": "linkRows"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"label": "Open link in modal",
|
||||||
|
"key": "linkPeek"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "url",
|
||||||
|
"label": "Link screen",
|
||||||
|
"key": "linkURL"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"section": true,
|
||||||
|
"name": "Title button",
|
||||||
|
"settings": [
|
||||||
|
{
|
||||||
|
"type": "boolean",
|
||||||
|
"key": "showTitleButton",
|
||||||
|
"label": "Show button",
|
||||||
|
"defaultValue": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"key": "titleButtonText",
|
||||||
|
"label": "Button text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "event",
|
||||||
|
"label": "Button action",
|
||||||
|
"key": "titleButtonOnClick"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"section": true,
|
||||||
|
"name": "Advanced",
|
||||||
|
"settings": [
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "ID column for linking (appended to URL)",
|
||||||
|
"key": "linkColumn",
|
||||||
|
"placeholder": "Default"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"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": 8
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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",
|
||||||
|
"suffix": "repeater"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
import { fetchTableData } from "./tables"
|
import { fetchTableData, fetchTableDefinition } from "./tables"
|
||||||
import { fetchViewData } from "./views"
|
import { fetchViewData } from "./views"
|
||||||
import { fetchRelationshipData } from "./relationships"
|
import { fetchRelationshipData } from "./relationships"
|
||||||
import { executeQuery } from "./queries"
|
import { executeQuery, fetchQueryDefinition } from "./queries"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches all rows for a particular Budibase data source.
|
* Fetches all rows for a particular Budibase data source.
|
||||||
|
@ -39,3 +39,35 @@ export const fetchDatasource = async dataSource => {
|
||||||
// Enrich the result is always an array
|
// Enrich the result is always an array
|
||||||
return Array.isArray(rows) ? rows : []
|
return Array.isArray(rows) ? rows : []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the schema of any kind of datasource.
|
||||||
|
*/
|
||||||
|
export const fetchDatasourceSchema = async dataSource => {
|
||||||
|
if (!dataSource) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const { type } = dataSource
|
||||||
|
|
||||||
|
// Nested providers should already have exposed their own schema
|
||||||
|
if (type === "provider") {
|
||||||
|
return dataSource.value?.schema
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tables, views and links can be fetched by table ID
|
||||||
|
if (
|
||||||
|
(type === "table" || type === "view" || type === "link") &&
|
||||||
|
dataSource.tableId
|
||||||
|
) {
|
||||||
|
const table = await fetchTableDefinition(dataSource.tableId)
|
||||||
|
return table?.schema
|
||||||
|
}
|
||||||
|
|
||||||
|
// Queries can be fetched by query ID
|
||||||
|
if (type === "query" && dataSource._id) {
|
||||||
|
const definition = await fetchQueryDefinition(dataSource._id)
|
||||||
|
return definition?.schema
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@ import API from "./api"
|
||||||
* Executes a query against an external data connector.
|
* Executes a query against an external data connector.
|
||||||
*/
|
*/
|
||||||
export const executeQuery = async ({ queryId, parameters }) => {
|
export const executeQuery = async ({ queryId, parameters }) => {
|
||||||
const query = await API.get({ url: `/api/queries/${queryId}` })
|
const query = await fetchQueryDefinition(queryId)
|
||||||
if (query?.datasourceId == null) {
|
if (query?.datasourceId == null) {
|
||||||
notificationStore.actions.error("That query couldn't be found")
|
notificationStore.actions.error("That query couldn't be found")
|
||||||
return
|
return
|
||||||
|
@ -24,3 +24,10 @@ export const executeQuery = async ({ queryId, parameters }) => {
|
||||||
}
|
}
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the definition of an external query.
|
||||||
|
*/
|
||||||
|
export const fetchQueryDefinition = async queryId => {
|
||||||
|
return await API.get({ url: `/api/queries/${queryId}`, cache: true })
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
<script>
|
||||||
|
import { getContext, setContext } from "svelte"
|
||||||
|
|
||||||
|
const component = getContext("component")
|
||||||
|
|
||||||
|
// We need to set a block context to know we're inside a block, but also
|
||||||
|
// to be able to reference the actual component ID of the block from
|
||||||
|
// any depth
|
||||||
|
setContext("block", { id: $component.id })
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<slot />
|
|
@ -0,0 +1,35 @@
|
||||||
|
<script>
|
||||||
|
import { getContext } from "svelte"
|
||||||
|
import { generate } from "shortid"
|
||||||
|
import Component from "components/Component.svelte"
|
||||||
|
|
||||||
|
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
|
||||||
|
export let id
|
||||||
|
|
||||||
|
const block = getContext("block")
|
||||||
|
const rand = generate()
|
||||||
|
|
||||||
|
// 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}-${context ?? rand}`
|
||||||
|
$: instance = {
|
||||||
|
_component: `@budibase/standard-components/${type}`,
|
||||||
|
_id: id,
|
||||||
|
_styles: {
|
||||||
|
normal: {
|
||||||
|
...styles,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...props,
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Component {instance}>
|
||||||
|
<slot />
|
||||||
|
</Component>
|
|
@ -1,3 +1,7 @@
|
||||||
|
<script context="module">
|
||||||
|
let SettingsDefinitionCache = {}
|
||||||
|
</script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { getContext, setContext } from "svelte"
|
import { getContext, setContext } from "svelte"
|
||||||
import { writable, get } from "svelte/store"
|
import { writable, get } from "svelte/store"
|
||||||
|
@ -20,13 +24,13 @@
|
||||||
// Any prop overrides that need to be applied due to conditional UI
|
// Any prop overrides that need to be applied due to conditional UI
|
||||||
let conditionalSettings
|
let conditionalSettings
|
||||||
|
|
||||||
// Props are hashed when inside the builder preview and used as a key, so that
|
// Settings are hashed when inside the builder preview and used as a key,
|
||||||
// components fully remount whenever any props change
|
// so that components fully remount whenever any settings change
|
||||||
let propsHash = 0
|
let hash = 0
|
||||||
|
|
||||||
// Latest timestamp that we started a props update.
|
// Latest timestamp that we started a props update.
|
||||||
// Due to enrichment now being async, we need to avoid overwriting newer
|
// 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
|
let latestUpdateTime
|
||||||
|
|
||||||
// Keep track of stringified representations of context and instance
|
// Keep track of stringified representations of context and instance
|
||||||
|
@ -40,6 +44,7 @@
|
||||||
// Get contexts
|
// Get contexts
|
||||||
const context = getContext("context")
|
const context = getContext("context")
|
||||||
const insideScreenslot = !!getContext("screenslot")
|
const insideScreenslot = !!getContext("screenslot")
|
||||||
|
const insideBlock = !!getContext("block")
|
||||||
|
|
||||||
// Create component context
|
// Create component context
|
||||||
const componentStore = writable({})
|
const componentStore = writable({})
|
||||||
|
@ -48,24 +53,59 @@
|
||||||
// Extract component instance info
|
// Extract component instance info
|
||||||
$: constructor = getComponentConstructor(instance._component)
|
$: constructor = getComponentConstructor(instance._component)
|
||||||
$: definition = getComponentDefinition(instance._component)
|
$: definition = getComponentDefinition(instance._component)
|
||||||
|
$: settingsDefinition = getSettingsDefinition(definition)
|
||||||
$: children = instance._children || []
|
$: children = instance._children || []
|
||||||
$: id = instance._id
|
$: id = instance._id
|
||||||
$: name = instance._instanceName
|
$: 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 =
|
$: interactive =
|
||||||
$builderStore.inBuilder &&
|
$builderStore.inBuilder &&
|
||||||
($builderStore.previewType === "layout" || insideScreenslot)
|
($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
|
$: empty = interactive && !children.length && definition?.hasChildren
|
||||||
$: emptyState = empty && definition?.showEmptyState !== false
|
$: emptyState = empty && definition?.showEmptyState !== false
|
||||||
$: rawProps = getRawProps(instance)
|
|
||||||
$: instanceKey = JSON.stringify(rawProps)
|
// Raw props are all props excluding internal props and children
|
||||||
$: updateComponentProps(rawProps, instanceKey, $context)
|
$: rawSettings = getRawSettings(instance)
|
||||||
$: selected =
|
$: instanceKey = hashString(JSON.stringify(rawSettings))
|
||||||
$builderStore.inBuilder &&
|
|
||||||
$builderStore.selectedComponentId === instance._id
|
// Component settings are those which are intended for this component and
|
||||||
$: inSelectedPath = $builderStore.selectedComponentPath?.includes(id)
|
// 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)
|
$: 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
|
// Update component context
|
||||||
$: componentStore.set({
|
$: componentStore.set({
|
||||||
|
@ -77,14 +117,14 @@
|
||||||
name,
|
name,
|
||||||
})
|
})
|
||||||
|
|
||||||
const getRawProps = instance => {
|
const getRawSettings = instance => {
|
||||||
let validProps = {}
|
let validSettings = {}
|
||||||
Object.entries(instance)
|
Object.entries(instance)
|
||||||
.filter(([name]) => name === "_conditions" || !name.startsWith("_"))
|
.filter(([name]) => name === "_conditions" || !name.startsWith("_"))
|
||||||
.forEach(([key, value]) => {
|
.forEach(([key, value]) => {
|
||||||
validProps[key] = value
|
validSettings[key] = value
|
||||||
})
|
})
|
||||||
return validProps
|
return validSettings
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gets the component constructor for the specified component
|
// Gets the component constructor for the specified component
|
||||||
|
@ -103,8 +143,47 @@
|
||||||
return type ? Manifest[type] : null
|
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
|
// Enriches any string component props using handlebars
|
||||||
const updateComponentProps = (rawProps, instanceKey, context) => {
|
const enrichComponentSettings = (rawSettings, instanceKey, context) => {
|
||||||
const instanceSame = instanceKey === lastInstanceKey
|
const instanceSame = instanceKey === lastInstanceKey
|
||||||
const contextSame = context.key === lastContextKey
|
const contextSame = context.key === lastContextKey
|
||||||
|
|
||||||
|
@ -119,8 +198,8 @@
|
||||||
latestUpdateTime = Date.now()
|
latestUpdateTime = Date.now()
|
||||||
const enrichmentTime = latestUpdateTime
|
const enrichmentTime = latestUpdateTime
|
||||||
|
|
||||||
// Enrich props with context
|
// Enrich settings with context
|
||||||
const enrichedProps = enrichProps(rawProps, context)
|
const newEnrichedSettings = enrichProps(rawSettings, context)
|
||||||
|
|
||||||
// Abandon this update if a newer update has started
|
// Abandon this update if a newer update has started
|
||||||
if (enrichmentTime !== latestUpdateTime) {
|
if (enrichmentTime !== latestUpdateTime) {
|
||||||
|
@ -130,7 +209,7 @@
|
||||||
// Update the component props.
|
// Update the component props.
|
||||||
// Most props are deeply compared so that svelte will only trigger reactive
|
// Most props are deeply compared so that svelte will only trigger reactive
|
||||||
// statements on props that have actually changed.
|
// statements on props that have actually changed.
|
||||||
if (!enrichedProps) {
|
if (!newEnrichedSettings) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let propsChanged = false
|
let propsChanged = false
|
||||||
|
@ -138,17 +217,17 @@
|
||||||
enrichedSettings = {}
|
enrichedSettings = {}
|
||||||
propsChanged = true
|
propsChanged = true
|
||||||
}
|
}
|
||||||
Object.keys(enrichedProps).forEach(key => {
|
Object.keys(newEnrichedSettings).forEach(key => {
|
||||||
if (!propsAreSame(enrichedProps[key], enrichedSettings[key])) {
|
if (!propsAreSame(newEnrichedSettings[key], enrichedSettings[key])) {
|
||||||
propsChanged = true
|
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
|
// Update the hash if we're in the builder so we can fully remount this
|
||||||
// component
|
// component
|
||||||
if (get(builderStore).inBuilder && propsChanged) {
|
if (get(builderStore).inBuilder && propsChanged) {
|
||||||
propsHash = hashString(JSON.stringify(enrichedSettings))
|
hash = hashString(JSON.stringify(enrichedSettings))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -171,14 +250,10 @@
|
||||||
conditionalSettings = result.settingUpdates
|
conditionalSettings = result.settingUpdates
|
||||||
visible = nextVisible
|
visible = nextVisible
|
||||||
}
|
}
|
||||||
|
|
||||||
// Drag and drop helper tags
|
|
||||||
$: draggable = interactive && !isLayout && !isScreen
|
|
||||||
$: droppable = interactive && !isLayout && !isScreen
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#key renderKey}
|
{#key renderKey}
|
||||||
{#if constructor && componentSettings && (visible || inSelectedPath)}
|
{#if constructor && settings && (visible || inSelectedPath)}
|
||||||
<!-- The ID is used as a class because getElementsByClassName is O(1) -->
|
<!-- The ID is used as a class because getElementsByClassName is O(1) -->
|
||||||
<!-- and the performance matters for the selection indicators -->
|
<!-- and the performance matters for the selection indicators -->
|
||||||
<div
|
<div
|
||||||
|
@ -190,13 +265,15 @@
|
||||||
data-id={id}
|
data-id={id}
|
||||||
data-name={name}
|
data-name={name}
|
||||||
>
|
>
|
||||||
<svelte:component this={constructor} {...componentSettings}>
|
<svelte:component this={constructor} {...settings}>
|
||||||
{#if children.length}
|
{#if children.length}
|
||||||
{#each children as child (child._id)}
|
{#each children as child (child._id)}
|
||||||
<svelte:self instance={child} />
|
<svelte:self instance={child} />
|
||||||
{/each}
|
{/each}
|
||||||
{:else if emptyState}
|
{:else if emptyState}
|
||||||
<Placeholder />
|
<Placeholder />
|
||||||
|
{:else if insideBlock}
|
||||||
|
<slot />
|
||||||
{/if}
|
{/if}
|
||||||
</svelte:component>
|
</svelte:component>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -27,6 +27,9 @@
|
||||||
button {
|
button {
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
width: -moz-fit-content;
|
width: -moz-fit-content;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
.spectrum-Button--overBackground:hover {
|
.spectrum-Button--overBackground:hover {
|
||||||
color: #555;
|
color: #555;
|
||||||
|
|
|
@ -35,28 +35,36 @@
|
||||||
let pageNumber = 0
|
let pageNumber = 0
|
||||||
let query = null
|
let query = null
|
||||||
|
|
||||||
|
// Sorting can be overridden at run time, so we can't use the prop directly
|
||||||
|
let currentSortColumn = sortColumn
|
||||||
|
let currentSortOrder = sortOrder
|
||||||
|
|
||||||
$: query = buildLuceneQuery(filter)
|
$: query = buildLuceneQuery(filter)
|
||||||
$: internalTable = dataSource?.type === "table"
|
$: internalTable = dataSource?.type === "table"
|
||||||
$: nestedProvider = dataSource?.type === "provider"
|
$: nestedProvider = dataSource?.type === "provider"
|
||||||
$: hasNextPage = bookmarks[pageNumber + 1] != null
|
$: hasNextPage = bookmarks[pageNumber + 1] != null
|
||||||
$: hasPrevPage = pageNumber > 0
|
$: hasPrevPage = pageNumber > 0
|
||||||
$: getSchema(dataSource)
|
$: getSchema(dataSource)
|
||||||
$: sortType = getSortType(schema, sortColumn)
|
$: sortType = getSortType(schema, currentSortColumn)
|
||||||
|
|
||||||
|
// Wait until schema loads before loading data, so that we can determine
|
||||||
|
// the correct sort type first time
|
||||||
$: {
|
$: {
|
||||||
// Wait until schema loads before loading data, so that we can determine
|
|
||||||
// the correct sort type first time
|
|
||||||
if (schemaLoaded) {
|
if (schemaLoaded) {
|
||||||
fetchData(
|
fetchData(
|
||||||
dataSource,
|
dataSource,
|
||||||
|
schema,
|
||||||
query,
|
query,
|
||||||
limit,
|
limit,
|
||||||
sortColumn,
|
currentSortColumn,
|
||||||
sortOrder,
|
currentSortOrder,
|
||||||
sortType,
|
sortType,
|
||||||
paginate
|
paginate
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reactively filter and sort rows if required
|
||||||
$: {
|
$: {
|
||||||
if (internalTable) {
|
if (internalTable) {
|
||||||
// Internal tables are already processed server-side
|
// Internal tables are already processed server-side
|
||||||
|
@ -65,10 +73,17 @@
|
||||||
// For anything else we use client-side implementations to filter, sort
|
// For anything else we use client-side implementations to filter, sort
|
||||||
// and limit
|
// and limit
|
||||||
const filtered = luceneQuery(allRows, query)
|
const filtered = luceneQuery(allRows, query)
|
||||||
const sorted = luceneSort(filtered, sortColumn, sortOrder, sortType)
|
const sorted = luceneSort(
|
||||||
|
filtered,
|
||||||
|
currentSortColumn,
|
||||||
|
currentSortOrder,
|
||||||
|
sortType
|
||||||
|
)
|
||||||
rows = luceneLimit(sorted, limit)
|
rows = luceneLimit(sorted, limit)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Build our action context
|
||||||
$: actions = [
|
$: actions = [
|
||||||
{
|
{
|
||||||
type: ActionTypes.RefreshDatasource,
|
type: ActionTypes.RefreshDatasource,
|
||||||
|
@ -79,7 +94,20 @@
|
||||||
type: ActionTypes.SetDataProviderQuery,
|
type: ActionTypes.SetDataProviderQuery,
|
||||||
callback: newQuery => (query = newQuery),
|
callback: newQuery => (query = newQuery),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: ActionTypes.SetDataProviderSorting,
|
||||||
|
callback: ({ column, order }) => {
|
||||||
|
if (column) {
|
||||||
|
currentSortColumn = column
|
||||||
|
}
|
||||||
|
if (order) {
|
||||||
|
currentSortOrder = order
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// Build our data context
|
||||||
$: dataContext = {
|
$: dataContext = {
|
||||||
rows,
|
rows,
|
||||||
schema,
|
schema,
|
||||||
|
@ -88,7 +116,11 @@
|
||||||
// Undocumented properties. These aren't supposed to be used in builder
|
// Undocumented properties. These aren't supposed to be used in builder
|
||||||
// bindings, but are used internally by other components
|
// bindings, but are used internally by other components
|
||||||
id: $component?.id,
|
id: $component?.id,
|
||||||
state: { query },
|
state: {
|
||||||
|
query,
|
||||||
|
sortColumn: currentSortColumn,
|
||||||
|
sortOrder: currentSortOrder,
|
||||||
|
},
|
||||||
loaded,
|
loaded,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -104,10 +136,11 @@
|
||||||
if (schemaLoaded && !nestedProvider) {
|
if (schemaLoaded && !nestedProvider) {
|
||||||
fetchData(
|
fetchData(
|
||||||
dataSource,
|
dataSource,
|
||||||
|
schema,
|
||||||
query,
|
query,
|
||||||
limit,
|
limit,
|
||||||
sortColumn,
|
currentSortColumn,
|
||||||
sortOrder,
|
currentSortOrder,
|
||||||
sortType,
|
sortType,
|
||||||
paginate
|
paginate
|
||||||
)
|
)
|
||||||
|
@ -116,6 +149,7 @@
|
||||||
|
|
||||||
const fetchData = async (
|
const fetchData = async (
|
||||||
dataSource,
|
dataSource,
|
||||||
|
schema,
|
||||||
query,
|
query,
|
||||||
limit,
|
limit,
|
||||||
sortColumn,
|
sortColumn,
|
||||||
|
@ -125,12 +159,16 @@
|
||||||
) => {
|
) => {
|
||||||
loading = true
|
loading = true
|
||||||
if (dataSource?.type === "table") {
|
if (dataSource?.type === "table") {
|
||||||
|
// Sanity check sort column, as using a non-existant column will prevent
|
||||||
|
// results coming back at all
|
||||||
|
const sort = schema?.[sortColumn] ? sortColumn : undefined
|
||||||
|
|
||||||
// For internal tables we use server-side processing
|
// For internal tables we use server-side processing
|
||||||
const res = await API.searchTable({
|
const res = await API.searchTable({
|
||||||
tableId: dataSource.tableId,
|
tableId: dataSource.tableId,
|
||||||
query,
|
query,
|
||||||
limit,
|
limit,
|
||||||
sort: sortColumn,
|
sort,
|
||||||
sortOrder: sortOrder?.toLowerCase() ?? "ascending",
|
sortOrder: sortOrder?.toLowerCase() ?? "ascending",
|
||||||
sortType,
|
sortType,
|
||||||
paginate,
|
paginate,
|
||||||
|
@ -156,34 +194,24 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const getSchema = async dataSource => {
|
const getSchema = async dataSource => {
|
||||||
if (dataSource?.schema) {
|
let newSchema = (await API.fetchDatasourceSchema(dataSource)) || {}
|
||||||
schema = dataSource.schema
|
|
||||||
} else if (dataSource?.tableId) {
|
|
||||||
const definition = await API.fetchTableDefinition(dataSource.tableId)
|
|
||||||
schema = definition?.schema ?? {}
|
|
||||||
} else if (dataSource?.type === "provider") {
|
|
||||||
schema = dataSource.value?.schema ?? {}
|
|
||||||
} else {
|
|
||||||
schema = {}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure there are "name" properties for all fields and that field schema
|
// Ensure there are "name" properties for all fields and that field schema
|
||||||
// are objects
|
// are objects
|
||||||
let fixedSchema = {}
|
Object.entries(newSchema).forEach(([fieldName, fieldSchema]) => {
|
||||||
Object.entries(schema || {}).forEach(([fieldName, fieldSchema]) => {
|
|
||||||
if (typeof fieldSchema === "string") {
|
if (typeof fieldSchema === "string") {
|
||||||
fixedSchema[fieldName] = {
|
newSchema[fieldName] = {
|
||||||
type: fieldSchema,
|
type: fieldSchema,
|
||||||
name: fieldName,
|
name: fieldName,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
fixedSchema[fieldName] = {
|
newSchema[fieldName] = {
|
||||||
...fieldSchema,
|
...fieldSchema,
|
||||||
name: fieldName,
|
name: fieldName,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
schema = fixedSchema
|
schema = newSchema
|
||||||
schemaLoaded = true
|
schemaLoaded = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -191,13 +219,14 @@
|
||||||
if (!hasNextPage || !internalTable) {
|
if (!hasNextPage || !internalTable) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
const sort = schema?.[currentSortColumn] ? currentSortColumn : undefined
|
||||||
const res = await API.searchTable({
|
const res = await API.searchTable({
|
||||||
tableId: dataSource?.tableId,
|
tableId: dataSource?.tableId,
|
||||||
query,
|
query,
|
||||||
bookmark: bookmarks[pageNumber + 1],
|
bookmark: bookmarks[pageNumber + 1],
|
||||||
limit,
|
limit,
|
||||||
sort: sortColumn,
|
sort,
|
||||||
sortOrder: sortOrder?.toLowerCase() ?? "ascending",
|
sortOrder: currentSortOrder?.toLowerCase() ?? "ascending",
|
||||||
sortType,
|
sortType,
|
||||||
paginate: true,
|
paginate: true,
|
||||||
})
|
})
|
||||||
|
@ -212,13 +241,14 @@
|
||||||
if (!hasPrevPage || !internalTable) {
|
if (!hasPrevPage || !internalTable) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
const sort = schema?.[currentSortColumn] ? currentSortColumn : undefined
|
||||||
const res = await API.searchTable({
|
const res = await API.searchTable({
|
||||||
tableId: dataSource?.tableId,
|
tableId: dataSource?.tableId,
|
||||||
query,
|
query,
|
||||||
bookmark: bookmarks[pageNumber - 1],
|
bookmark: bookmarks[pageNumber - 1],
|
||||||
limit,
|
limit,
|
||||||
sort: sortColumn,
|
sort,
|
||||||
sortOrder: sortOrder?.toLowerCase() ?? "ascending",
|
sortOrder: currentSortOrder?.toLowerCase() ?? "ascending",
|
||||||
sortType,
|
sortType,
|
||||||
paginate: true,
|
paginate: true,
|
||||||
})
|
})
|
||||||
|
@ -234,7 +264,7 @@
|
||||||
<ProgressCircle />
|
<ProgressCircle />
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
{#if !$component.children}
|
{#if $component.emptyState}
|
||||||
<Placeholder />
|
<Placeholder />
|
||||||
{:else}
|
{:else}
|
||||||
<slot />
|
<slot />
|
||||||
|
|
|
@ -38,6 +38,7 @@
|
||||||
padding: var(--spacing-l);
|
padding: var(--spacing-l);
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
|
grid-column: 1 / -1;
|
||||||
}
|
}
|
||||||
.noRows i {
|
.noRows i {
|
||||||
margin-bottom: var(--spacing-m);
|
margin-bottom: var(--spacing-m);
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import "@spectrum-css/card/dist/index-vars.css"
|
import "@spectrum-css/card/dist/index-vars.css"
|
||||||
import { getContext } from "svelte"
|
import { getContext } from "svelte"
|
||||||
|
import { Button } from "@budibase/bbui"
|
||||||
|
|
||||||
export let title
|
export let title
|
||||||
export let subtitle
|
export let subtitle
|
||||||
|
@ -8,6 +9,9 @@
|
||||||
export let imageURL
|
export let imageURL
|
||||||
export let linkURL
|
export let linkURL
|
||||||
export let horizontal
|
export let horizontal
|
||||||
|
export let showButton
|
||||||
|
export let buttonText
|
||||||
|
export let buttonOnClick
|
||||||
|
|
||||||
const { styleable, linkable } = getContext("sdk")
|
const { styleable, linkable } = getContext("sdk")
|
||||||
const component = getContext("component")
|
const component = getContext("component")
|
||||||
|
@ -60,12 +64,17 @@
|
||||||
{description}
|
{description}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if showButton}
|
||||||
|
<div class="spectrum-Card-footer button-container">
|
||||||
|
<Button on:click={buttonOnClick} secondary>{buttonText || ""}</Button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.spectrum-Card {
|
.spectrum-Card {
|
||||||
width: 240px;
|
width: 300px;
|
||||||
border-color: var(--spectrum-global-color-gray-300) !important;
|
border-color: var(--spectrum-global-color-gray-300) !important;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -76,6 +85,9 @@
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
width: 420px;
|
width: 420px;
|
||||||
}
|
}
|
||||||
|
.spectrum-Card-container {
|
||||||
|
padding: var(--spectrum-global-dimension-size-50) 0;
|
||||||
|
}
|
||||||
.spectrum-Card-title :global(a) {
|
.spectrum-Card-title :global(a) {
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
@ -114,6 +126,19 @@
|
||||||
.spectrum-Card-footer {
|
.spectrum-Card-footer {
|
||||||
border-top: none;
|
border-top: none;
|
||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
margin-top: -8px;
|
margin-top: -8px;
|
||||||
|
margin-bottom: var(
|
||||||
|
--spectrum-card-body-padding-bottom,
|
||||||
|
var(--spectrum-global-dimension-size-300)
|
||||||
|
);
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-container {
|
||||||
|
margin-bottom: var(--spectrum-global-dimension-size-300);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -0,0 +1,237 @@
|
||||||
|
<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 : 300
|
||||||
|
|
||||||
|
// 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: "stretch",
|
||||||
|
vAlign: "top",
|
||||||
|
gap: "M",
|
||||||
|
noRowsMessage: "No rows found",
|
||||||
|
}}
|
||||||
|
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>
|
||||||
|
.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>
|
|
@ -0,0 +1,218 @@
|
||||||
|
<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 tableColumns
|
||||||
|
export let showAutoColumns
|
||||||
|
export let rowCount
|
||||||
|
export let quiet
|
||||||
|
export let size
|
||||||
|
export let linkRows
|
||||||
|
export let linkURL
|
||||||
|
export let linkColumn
|
||||||
|
export let linkPeek
|
||||||
|
export let showTitleButton
|
||||||
|
export let titleButtonText
|
||||||
|
export let titleButtonOnClick
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
// 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={size} 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: rowCount,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<BlockComponent
|
||||||
|
type="table"
|
||||||
|
props={{
|
||||||
|
dataProvider: `{{ literal [${dataProviderId}] }}`,
|
||||||
|
columns: tableColumns,
|
||||||
|
showAutoColumns,
|
||||||
|
rowCount,
|
||||||
|
quiet,
|
||||||
|
size,
|
||||||
|
linkRows,
|
||||||
|
linkURL,
|
||||||
|
linkColumn,
|
||||||
|
linkPeek,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</BlockComponent>
|
||||||
|
</BlockComponent>
|
||||||
|
</div>
|
||||||
|
</Block>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.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>
|
|
@ -0,0 +1,2 @@
|
||||||
|
export { default as tablewithsearch } from "./TableWithSearch.svelte"
|
||||||
|
export { default as cardlistwithsearch } from "./CardListWithSearch.svelte"
|
|
@ -34,25 +34,34 @@
|
||||||
return closestContext || {}
|
return closestContext || {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetches the form schema from this form's dataSource, if one exists
|
// Fetches the form schema from this form's dataSource
|
||||||
const fetchSchema = async () => {
|
const fetchSchema = async () => {
|
||||||
if (!dataSource?.tableId) {
|
if (!dataSource) {
|
||||||
schema = {}
|
schema = {}
|
||||||
table = null
|
|
||||||
} else {
|
|
||||||
table = await API.fetchTableDefinition(dataSource?.tableId)
|
|
||||||
if (table) {
|
|
||||||
if (dataSource?.type === "query") {
|
|
||||||
schema = {}
|
|
||||||
const params = table.parameters || []
|
|
||||||
params.forEach(param => {
|
|
||||||
schema[param.name] = { ...param, type: "string" }
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
schema = table.schema || {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the datasource is a query, then we instead use a schema of the query
|
||||||
|
// parameters rather than the output schema
|
||||||
|
else if (
|
||||||
|
dataSource.type === "query" &&
|
||||||
|
dataSource._id &&
|
||||||
|
actionType === "Create"
|
||||||
|
) {
|
||||||
|
const query = await API.fetchQueryDefinition(dataSource._id)
|
||||||
|
let paramSchema = {}
|
||||||
|
const params = query.parameters || []
|
||||||
|
params.forEach(param => {
|
||||||
|
paramSchema[param.name] = { ...param, type: "string" }
|
||||||
|
})
|
||||||
|
schema = paramSchema
|
||||||
|
}
|
||||||
|
|
||||||
|
// For all other cases, just grab the normal schema
|
||||||
|
else {
|
||||||
|
const dataSourceSchema = await API.fetchDatasourceSchema(dataSource)
|
||||||
|
schema = dataSourceSchema || {}
|
||||||
|
}
|
||||||
|
|
||||||
loaded = true
|
loaded = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -32,6 +32,7 @@ export { default as spectrumcard } from "./SpectrumCard.svelte"
|
||||||
export * from "./charts"
|
export * from "./charts"
|
||||||
export * from "./forms"
|
export * from "./forms"
|
||||||
export * from "./table"
|
export * from "./table"
|
||||||
|
export * from "./blocks"
|
||||||
|
|
||||||
// Deprecated component left for compatibility in old apps
|
// Deprecated component left for compatibility in old apps
|
||||||
export { default as navigation } from "./deprecated/Navigation.svelte"
|
export { default as navigation } from "./deprecated/Navigation.svelte"
|
||||||
|
|
|
@ -9,9 +9,17 @@
|
||||||
export let rowCount
|
export let rowCount
|
||||||
export let quiet
|
export let quiet
|
||||||
export let size
|
export let size
|
||||||
|
export let linkRows
|
||||||
|
export let linkURL
|
||||||
|
export let linkColumn
|
||||||
|
export let linkPeek
|
||||||
|
|
||||||
const component = getContext("component")
|
const component = getContext("component")
|
||||||
const { styleable } = getContext("sdk")
|
const { styleable, getAction, ActionTypes, routeStore } = getContext("sdk")
|
||||||
|
const setSorting = getAction(
|
||||||
|
dataProvider?.id,
|
||||||
|
ActionTypes.SetDataProviderSorting
|
||||||
|
)
|
||||||
const customColumnKey = `custom-${Math.random()}`
|
const customColumnKey = `custom-${Math.random()}`
|
||||||
const customRenderers = [
|
const customRenderers = [
|
||||||
{
|
{
|
||||||
|
@ -70,6 +78,26 @@
|
||||||
})
|
})
|
||||||
return newSchema
|
return newSchema
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onSort = e => {
|
||||||
|
setSorting({
|
||||||
|
column: e.detail.column,
|
||||||
|
order: e.detail.order,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const onClick = e => {
|
||||||
|
if (!linkRows || !linkURL) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const col = linkColumn || "_id"
|
||||||
|
const id = e.detail?.[col]
|
||||||
|
if (!id) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const split = linkURL.split("/:")
|
||||||
|
routeStore.actions.navigate(`${split[0]}/${id}`, linkPeek)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div use:styleable={$component.styles} class={size}>
|
<div use:styleable={$component.styles} class={size}>
|
||||||
|
@ -84,6 +112,9 @@
|
||||||
allowEditRows={false}
|
allowEditRows={false}
|
||||||
allowEditColumns={false}
|
allowEditColumns={false}
|
||||||
showAutoColumns={true}
|
showAutoColumns={true}
|
||||||
|
disableSorting
|
||||||
|
on:sort={onSort}
|
||||||
|
on:click={onClick}
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
</Table>
|
</Table>
|
||||||
|
|
|
@ -6,6 +6,7 @@ export const ActionTypes = {
|
||||||
ValidateForm: "ValidateForm",
|
ValidateForm: "ValidateForm",
|
||||||
RefreshDatasource: "RefreshDatasource",
|
RefreshDatasource: "RefreshDatasource",
|
||||||
SetDataProviderQuery: "SetDataProviderQuery",
|
SetDataProviderQuery: "SetDataProviderQuery",
|
||||||
|
SetDataProviderSorting: "SetDataProviderSorting",
|
||||||
ClearForm: "ClearForm",
|
ClearForm: "ClearForm",
|
||||||
ChangeFormStep: "ChangeFormStep",
|
ChangeFormStep: "ChangeFormStep",
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { writable, derived } from "svelte/store"
|
import { writable, derived } from "svelte/store"
|
||||||
|
import { hashString } from "../utils/helpers"
|
||||||
|
|
||||||
export const createContextStore = oldContext => {
|
export const createContextStore = oldContext => {
|
||||||
const newContext = writable({})
|
const newContext = writable({})
|
||||||
|
@ -9,7 +10,7 @@ export const createContextStore = oldContext => {
|
||||||
for (let i = 0; i < $contexts.length - 1; i++) {
|
for (let i = 0; i < $contexts.length - 1; i++) {
|
||||||
key += $contexts[i].key
|
key += $contexts[i].key
|
||||||
}
|
}
|
||||||
key += JSON.stringify($contexts[$contexts.length - 1])
|
key = hashString(key + JSON.stringify($contexts[$contexts.length - 1]))
|
||||||
|
|
||||||
// Reduce global state
|
// Reduce global state
|
||||||
const reducer = (total, context) => ({ ...total, ...context })
|
const reducer = (total, context) => ({ ...total, ...context })
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import { writable } from "svelte/store"
|
import { get, writable } from "svelte/store"
|
||||||
import { push } from "svelte-spa-router"
|
import { push } from "svelte-spa-router"
|
||||||
import * as API from "../api"
|
import * as API from "../api"
|
||||||
|
import { peekStore } from "./peek"
|
||||||
|
import { builderStore } from "./builder"
|
||||||
|
|
||||||
const createRouteStore = () => {
|
const createRouteStore = () => {
|
||||||
const initialState = {
|
const initialState = {
|
||||||
|
@ -59,7 +61,25 @@ const createRouteStore = () => {
|
||||||
return state
|
return state
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
const navigate = push
|
const navigate = (url, peek) => {
|
||||||
|
if (get(builderStore).inBuilder) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (url) {
|
||||||
|
// If we're already peeking, don't peek again
|
||||||
|
const isPeeking = get(store).queryParams?.peek
|
||||||
|
if (peek && !isPeeking) {
|
||||||
|
peekStore.actions.showPeek(url)
|
||||||
|
} else {
|
||||||
|
const external = !url.startsWith("/")
|
||||||
|
if (external) {
|
||||||
|
window.location.href = url
|
||||||
|
} else {
|
||||||
|
push(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
const setRouterLoaded = () => {
|
const setRouterLoaded = () => {
|
||||||
store.update(state => ({ ...state, routerLoaded: true }))
|
store.update(state => ({ ...state, routerLoaded: true }))
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,6 @@ import {
|
||||||
builderStore,
|
builderStore,
|
||||||
confirmationStore,
|
confirmationStore,
|
||||||
authStore,
|
authStore,
|
||||||
peekStore,
|
|
||||||
stateStore,
|
stateStore,
|
||||||
} from "stores"
|
} from "stores"
|
||||||
import { saveRow, deleteRow, executeQuery, triggerAutomation } from "api"
|
import { saveRow, deleteRow, executeQuery, triggerAutomation } from "api"
|
||||||
|
@ -42,20 +41,7 @@ const triggerAutomationHandler = async action => {
|
||||||
|
|
||||||
const navigationHandler = action => {
|
const navigationHandler = action => {
|
||||||
const { url, peek } = action.parameters
|
const { url, peek } = action.parameters
|
||||||
if (url) {
|
routeStore.actions.navigate(url, peek)
|
||||||
// If we're already peeking, don't peek again
|
|
||||||
const isPeeking = get(routeStore).queryParams?.peek
|
|
||||||
if (peek && !isPeeking) {
|
|
||||||
peekStore.actions.showPeek(url)
|
|
||||||
} else {
|
|
||||||
const external = !url.startsWith("/")
|
|
||||||
if (external) {
|
|
||||||
window.location.href = url
|
|
||||||
} else {
|
|
||||||
routeStore.actions.navigate(action.parameters.url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const queryExecutionHandler = async action => {
|
const queryExecutionHandler = async action => {
|
||||||
|
@ -161,9 +147,15 @@ const confirmTextMap = {
|
||||||
*/
|
*/
|
||||||
export const enrichButtonActions = (actions, context) => {
|
export const enrichButtonActions = (actions, context) => {
|
||||||
// Prevent button actions in the builder preview
|
// Prevent button actions in the builder preview
|
||||||
if (get(builderStore).inBuilder) {
|
if (!actions || get(builderStore).inBuilder) {
|
||||||
return () => {}
|
return () => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If this is a function then it has already been enriched
|
||||||
|
if (typeof actions === "function") {
|
||||||
|
return actions
|
||||||
|
}
|
||||||
|
|
||||||
const handlers = actions.map(def => handlerMap[def["##eventHandlerType"]])
|
const handlers = actions.map(def => handlerMap[def["##eventHandlerType"]])
|
||||||
return async () => {
|
return async () => {
|
||||||
for (let i = 0; i < handlers.length; i++) {
|
for (let i = 0; i < handlers.length; i++) {
|
||||||
|
|
|
@ -36,17 +36,19 @@ export const enrichProps = (props, context) => {
|
||||||
let enrichedProps = enrichDataBindings(props, totalContext)
|
let enrichedProps = enrichDataBindings(props, totalContext)
|
||||||
|
|
||||||
// Enrich click actions if they exist
|
// Enrich click actions if they exist
|
||||||
if (enrichedProps.onClick) {
|
Object.keys(enrichedProps).forEach(prop => {
|
||||||
enrichedProps.onClick = enrichButtonActions(
|
if (prop?.toLowerCase().includes("onclick")) {
|
||||||
enrichedProps.onClick,
|
enrichedProps[prop] = enrichButtonActions(
|
||||||
totalContext
|
enrichedProps[prop],
|
||||||
)
|
totalContext
|
||||||
}
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// Enrich any click actions in conditions
|
// Enrich any click actions in conditions
|
||||||
if (enrichedProps._conditions) {
|
if (enrichedProps._conditions) {
|
||||||
enrichedProps._conditions.forEach(condition => {
|
enrichedProps._conditions.forEach(condition => {
|
||||||
if (condition.setting === "onClick") {
|
if (condition.setting?.toLowerCase().includes("onclick")) {
|
||||||
condition.settingValue = enrichButtonActions(
|
condition.settingValue = enrichButtonActions(
|
||||||
condition.settingValue,
|
condition.settingValue,
|
||||||
totalContext
|
totalContext
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { get } from "svelte/store"
|
|
||||||
import { builderStore } from "stores"
|
import { builderStore } from "stores"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -64,9 +63,7 @@ export const styleable = (node, styles = {}) => {
|
||||||
// Handler to select a component in the builder when clicking it in the
|
// Handler to select a component in the builder when clicking it in the
|
||||||
// builder preview
|
// builder preview
|
||||||
selectComponent = event => {
|
selectComponent = event => {
|
||||||
if (newStyles.interactive) {
|
builderStore.actions.selectComponent(componentId)
|
||||||
builderStore.actions.selectComponent(componentId)
|
|
||||||
}
|
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
return false
|
return false
|
||||||
|
@ -77,7 +74,7 @@ export const styleable = (node, styles = {}) => {
|
||||||
node.addEventListener("mouseout", applyNormalStyles)
|
node.addEventListener("mouseout", applyNormalStyles)
|
||||||
|
|
||||||
// Add builder preview click listener
|
// Add builder preview click listener
|
||||||
if (get(builderStore).inBuilder) {
|
if (newStyles.interactive) {
|
||||||
node.addEventListener("click", selectComponent, false)
|
node.addEventListener("click", selectComponent, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -89,11 +86,7 @@ export const styleable = (node, styles = {}) => {
|
||||||
const removeListeners = () => {
|
const removeListeners = () => {
|
||||||
node.removeEventListener("mouseover", applyHoverStyles)
|
node.removeEventListener("mouseover", applyHoverStyles)
|
||||||
node.removeEventListener("mouseout", applyNormalStyles)
|
node.removeEventListener("mouseout", applyNormalStyles)
|
||||||
|
node.removeEventListener("click", selectComponent)
|
||||||
// Remove builder preview click listener
|
|
||||||
if (get(builderStore).inBuilder) {
|
|
||||||
node.removeEventListener("click", selectComponent)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply initial styles
|
// Apply initial styles
|
||||||
|
|
|
@ -61,7 +61,11 @@ exports.validate = async ({ appId, tableId, row, table }) => {
|
||||||
let res
|
let res
|
||||||
|
|
||||||
// Validate.js doesn't seem to handle array
|
// Validate.js doesn't seem to handle array
|
||||||
if (table.schema[fieldName].type === FieldTypes.ARRAY) {
|
if (
|
||||||
|
table.schema[fieldName].type === FieldTypes.ARRAY &&
|
||||||
|
row[fieldName] &&
|
||||||
|
row[fieldName].length
|
||||||
|
) {
|
||||||
row[fieldName].map(val => {
|
row[fieldName].map(val => {
|
||||||
if (!constraints.inclusion.includes(val)) {
|
if (!constraints.inclusion.includes(val)) {
|
||||||
errors[fieldName] = "Field not in list"
|
errors[fieldName] = "Field not in list"
|
||||||
|
|
Loading…
Reference in New Issue