Add button to table with search block and support defining multiple settings sections in component manifest entries

This commit is contained in:
Andrew Kingston 2021-11-04 11:30:43 +00:00
parent df9f061d66
commit dc9b1a2a8c
9 changed files with 218 additions and 85 deletions

View File

@ -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"
@ -82,13 +83,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 +98,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) {
@ -374,8 +372,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) {

View File

@ -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 = {}

View File

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

View File

@ -22,6 +22,8 @@
] ]
$: settings = componentDefinition?.settings ?? [] $: settings = componentDefinition?.settings ?? []
$: generalSettings = settings.filter(setting => !setting.section)
$: sections = settings.filter(setting => setting.section)
$: isLayout = assetInstance && assetInstance.favicon $: isLayout = assetInstance && assetInstance.favicon
$: assetDefinition = isLayout ? layoutDefinition : screenDefinition $: assetDefinition = isLayout ? layoutDefinition : screenDefinition
@ -70,8 +72,8 @@
onChange={val => updateProp("_instanceName", val)} onChange={val => updateProp("_instanceName", val)}
/> />
{/if} {/if}
{#if settings && settings.length > 0} {#if generalSettings.length}
{#each settings as setting (setting.key)} {#each generalSettings as setting (setting.key)}
{#if canRenderControl(setting)} {#if canRenderControl(setting)}
<PropertyControl <PropertyControl
type={setting.type} type={setting.type}
@ -99,11 +101,43 @@
{/if} {/if}
{#if componentDefinition?.info} {#if componentDefinition?.info}
<div class="text"> <div class="text">
{@html componentDefinition?.info} {@html componentDefinition.info}
</div> </div>
{/if} {/if}
</DetailSummary> </DetailSummary>
{#each sections as section (section.name)}
<DetailSummary name={section.name} collapsible={false}>
{#each section.settings 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 section?.info}
<div class="text">
{@html section.info}
</div>
{/if}
</DetailSummary>
{/each}
<style> <style>
.text { .text {
font-size: var(--spectrum-global-dimension-font-size-75); font-size: var(--spectrum-global-dimension-font-size-75);

View File

@ -2576,9 +2576,11 @@
] ]
}, },
"tablewithsearch": { "tablewithsearch": {
"block": true,
"name": "Table with search", "name": "Table with search",
"icon": "Table", "icon": "Table",
"styles": ["size"], "styles": ["size"],
"info": "Only the first 3 search columns will be used.",
"settings": [ "settings": [
{ {
"type": "text", "type": "text",
@ -2613,18 +2615,6 @@
"options": ["Ascending", "Descending"], "options": ["Ascending", "Descending"],
"defaultValue": "Descending" "defaultValue": "Descending"
}, },
{
"type": "boolean",
"label": "Paginate",
"key": "paginate",
"defaultValue": true
},
{
"type": "number",
"label": "Row Count",
"key": "rowCount",
"defaultValue": 8
},
{ {
"type": "select", "type": "select",
"label": "Size", "label": "Size",
@ -2641,6 +2631,22 @@
} }
] ]
}, },
{
"type": "boolean",
"label": "Paginate",
"key": "paginate",
"defaultValue": true
},
{
"section": true,
"name": "Table",
"settings": [
{
"type": "number",
"label": "Row Count",
"key": "rowCount",
"defaultValue": 8
},
{ {
"type": "multifield", "type": "multifield",
"label": "Table Columns", "label": "Table Columns",
@ -2660,5 +2666,32 @@
"defaultValue": false "defaultValue": false
} }
] ]
},
{
"section": true,
"name": "Button",
"settings": [
{
"type": "boolean",
"key": "showTitleButton",
"label": "Show title button",
"defaultValue": false
},
{
"type": "text",
"key": "titleButtonText",
"label": "Title button text"
},
{
"type": "event",
"label": "Title Button Action",
"key": "titleButtonOnClick"
}
]
}
],
"context": {
"type": "schema"
}
} }
} }

View File

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

View File

@ -16,6 +16,9 @@
export let rowCount export let rowCount
export let quiet export let quiet
export let size export let size
export let showTitleButton
export let titleButtonText
export let titleButtonOnClick
const { API, styleable } = getContext("sdk") const { API, styleable } = getContext("sdk")
const context = getContext("context") const context = getContext("context")
@ -63,7 +66,7 @@
}) })
} }
}) })
return enrichedColumns return enrichedColumns.slice(0, 3)
} }
// Load the datasource schema on mount so we can determine column types // Load the datasource schema on mount so we can determine column types
@ -77,13 +80,17 @@
<Block> <Block>
<div class={size} use:styleable={$component.styles}> <div class={size} use:styleable={$component.styles}>
<BlockComponent type="form" bind:id={formId} props={{ dataSource }}> <BlockComponent type="form" bind:id={formId} props={{ dataSource }}>
{#if title || enrichedSearchColumns?.length} {#if title || enrichedSearchColumns?.length || showTitleButton}
<div class="header"> <div class="header" class:mobile={$context.device.mobile}>
<div class="title"> <div class="title">
<Heading>{title || ""}</Heading> <Heading>{title || ""}</Heading>
</div> </div>
<div class="controls">
{#if enrichedSearchColumns?.length} {#if enrichedSearchColumns?.length}
<div class="search" class:mobile={$context.device.mobile}> <div
class="search"
style="--cols:{enrichedSearchColumns?.length}"
>
{#each enrichedSearchColumns as column} {#each enrichedSearchColumns as column}
<BlockComponent <BlockComponent
type={column.componentType} type={column.componentType}
@ -97,6 +104,17 @@
{/each} {/each}
</div> </div>
{/if} {/if}
{#if showTitleButton}
<BlockComponent
type="button"
props={{
onClick: titleButtonOnClick,
text: titleButtonText,
type: "cta",
}}
/>
{/if}
</div>
</div> </div>
{/if} {/if}
<BlockComponent <BlockComponent
@ -134,39 +152,59 @@
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
gap: 20px; gap: 20px;
flex-wrap: wrap;
margin-bottom: 20px; margin-bottom: 20px;
} }
.title { .title {
flex: 1 1 auto; overflow: hidden;
} }
.search { .title :global(.spectrum-Heading) {
flex: 0 0 auto; flex: 1 1 auto;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.controls {
flex: 0 1 auto;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: flex-start; justify-content: flex-end;
align-items: center; align-items: center;
gap: 20px;
}
.controls :global(.spectrum-InputGroup .spectrum-InputGroup-input) {
width: 100%;
}
.search {
flex: 0 1 auto;
gap: 10px; gap: 10px;
max-width: 100%; max-width: 100%;
flex-wrap: wrap; display: grid;
} grid-template-columns: repeat(var(--cols), minmax(120px, 200px));
.search:not(.mobile) :global(.spectrum-Form-itemField > .spectrum-Picker) {
width: 200px;
}
.search :global(.spectrum-InputGroup .spectrum-InputGroup-input) {
width: 100%;
} }
.search :global(.spectrum-InputGroup) { .search :global(.spectrum-InputGroup) {
min-width: 0; min-width: 0;
} }
.search.mobile {
/* 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; flex-direction: column;
justify-content: flex-start; justify-content: flex-start;
align-items: stretch; align-items: stretch;
position: relative; position: relative;
width: 100%; width: 100%;
} }
.search.mobile :global(.component > *) {
width: 100%;
}
</style> </style>

View File

@ -161,9 +161,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++) {

View File

@ -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(
enrichedProps[prop],
totalContext 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