Initial commit

This commit is contained in:
Dean 2023-08-03 09:29:12 +01:00
parent 3167fcdc76
commit cb2a19620b
9 changed files with 591 additions and 161 deletions

View File

@ -44,7 +44,9 @@
align-items: stretch;
border-bottom: var(--border-light);
}
.property-group-container:last-child {
border-bottom: 0px;
}
.property-group-name {
cursor: pointer;
display: flex;

View File

@ -93,6 +93,40 @@ const INITIAL_FRONTEND_STATE = {
tourNodes: null,
}
export const updateComponentSetting = (name, value) => {
return component => {
if (!name || !component) {
return false
}
// Skip update if the value is the same
if (component[name] === value) {
return false
}
const settings = getComponentSettings(component._component)
const updatedSetting = settings.find(setting => setting.key === name)
if (
updatedSetting?.type === "dataSource" ||
updatedSetting?.type === "table"
) {
const { schema } = getSchemaForDatasource(null, value)
const columnNames = Object.keys(schema || {})
const multifieldKeysToSelectAll = settings
.filter(setting => {
return setting.type === "multifield" && setting.selectAllFields
})
.map(setting => setting.key)
multifieldKeysToSelectAll.forEach(key => {
component[key] = columnNames
})
}
component[name] = value
}
}
export const getFrontendStore = () => {
const store = writable({ ...INITIAL_FRONTEND_STATE })
let websocket
@ -111,13 +145,18 @@ export const getFrontendStore = () => {
}
let clone = cloneDeep(screen)
const result = patchFn(clone)
console.log("sequentialScreenPatch ", result)
if (result === false) {
return
}
return await store.actions.screens.save(clone)
return
//return await store.actions.screens.save(clone)
})
store.actions = {
tester: (name, value) => {
return updateComponentSetting(name, value)
},
reset: () => {
store.set({ ...INITIAL_FRONTEND_STATE })
websocket?.disconnect()
@ -825,6 +864,7 @@ export const getFrontendStore = () => {
},
patch: async (patchFn, componentId, screenId) => {
// Use selected component by default
console.log("front end patch")
if (!componentId || !screenId) {
const state = get(store)
componentId = componentId || state.selectedComponentId
@ -834,6 +874,7 @@ export const getFrontendStore = () => {
return
}
const patchScreen = screen => {
// findComponent looks in the tree not comp.settings[0]
let component = findComponent(screen.props, componentId)
if (!component) {
return false
@ -842,6 +883,18 @@ export const getFrontendStore = () => {
}
await store.actions.screens.patch(patchScreen, screenId)
},
// Temporary
customPatch: async (patchFn, componentId, screenId) => {
console.log("patchUpdate :")
if (!componentId || !screenId) {
const state = get(store)
componentId = componentId || state.selectedComponentId
screenId = screenId || state.selectedScreenId
}
if (!componentId || !screenId || !patchFn) {
return
}
},
delete: async component => {
if (!component) {
return
@ -1207,37 +1260,9 @@ export const getFrontendStore = () => {
})
},
updateSetting: async (name, value) => {
await store.actions.components.patch(component => {
if (!name || !component) {
return false
}
// Skip update if the value is the same
if (component[name] === value) {
return false
}
const settings = getComponentSettings(component._component)
const updatedSetting = settings.find(setting => setting.key === name)
if (
updatedSetting?.type === "dataSource" ||
updatedSetting?.type === "table"
) {
const { schema } = getSchemaForDatasource(null, value)
const columnNames = Object.keys(schema || {})
const multifieldKeysToSelectAll = settings
.filter(setting => {
return setting.type === "multifield" && setting.selectAllFields
})
.map(setting => setting.key)
multifieldKeysToSelectAll.forEach(key => {
component[key] = columnNames
})
}
component[name] = value
})
await store.actions.components.patch(
updateComponentSetting(name, value)
)
},
requestEjectBlock: componentId => {
store.actions.preview.sendEvent("eject-block", componentId)

View File

@ -0,0 +1,161 @@
<!-- FormBlockFieldSettingsPopover -->
<script>
import { Icon, Popover, Layout } from "@budibase/bbui"
import { store } from "builderStore"
import { cloneDeep } from "lodash/fp"
import ComponentSettingsSection from "../../../../../pages/builder/app/[application]/design/[screenId]/components/[componentId]/_components/settings/ComponentSettingsSection.svelte"
export let anchor
export let field
export let componentBindings
export let bindings
export let parent
let popover
$: sudoComponentInstance = buildSudoInstance(field)
$: componentDef = store.actions.components.getDefinition(field._component)
$: parsedComponentDef = processComponentDefinitionSettings(componentDef)
const buildSudoInstance = instance => {
let clone = cloneDeep(instance)
// only do this IF necessary
const instanceCheck = store.actions.components.createInstance(
clone._component,
{
_instanceName: instance.displayName,
field: instance.name, //Must be fixed
label: instance.displayName,
placeholder: instance.displayName,
},
{} //?
)
// mutating on load would achieve this.
// Would need to replace the entire config at this point
// console.log(instanceCheck)
return instanceCheck
}
// Ensures parent bindings are pushed down
// Confirm this
const processComponentDefinitionSettings = componentDef => {
const clone = cloneDeep(componentDef)
clone.settings.forEach(setting => {
if (setting.type === "text") {
setting.nested = true
}
})
return clone
}
// Current core update setting fn
const updateSetting = async (setting, value) => {
console.log("Custom Save Setting", setting, value)
console.log("The parent", parent)
//updateBlockFieldSetting in frontend?
const nestedFieldInstance = cloneDeep(sudoComponentInstance)
// Parse the current fields on load and check for unbuilt instances
// This is icky
let parentFieldsSettings = parent.fields.find(
pSetting => pSetting.name === nestedFieldInstance.field
)
//In this scenario it may be best to extract
store.actions.tester(setting.key, value)(nestedFieldInstance) //mods the internal val
parentFieldsSettings = cloneDeep(nestedFieldInstance)
console.log("UPDATED nestedFieldInstance", nestedFieldInstance)
//Overwrite all the fields
await store.actions.components.updateSetting("fields", parent.fields)
/*
ignore/disabled _instanceName > this will be handled in the new header field.
ignore/disabled field > this should be populated and hidden.
*/
}
</script>
<Icon
name="Settings"
hoverable
size="S"
on:click={() => {
popover.show()
}}
/>
<Popover bind:this={popover} {anchor} align="left-outside">
<Layout noPadding>
<!--
property-group-container - has a border, is there a scenario where it doesnt render?
FormBlock Default behaviour.
validation: field.validation,
field: field.name,
label: field.displayName,
placeholder: field.displayName,
Block differences
_instanceName:
Filtered as it has been moved to own area.
field:
Fixed - not visible.
componentBindings
These appear to be removed/invalid
Bindings
{bindings} - working
{componentBindings}
componentdefinition.settings[x].nested needs to be true
Are these appropriate for the form block
FormBlock will have to pull the settings from fields:[]
Frontend Store > updateSetting: async (name, value)
Performs a patch for the component settings change
PropertyControl
Would this behaviour require a flag?
highlighted={$store.highlightedSettingKey === setting.key}
propertyFocus={$store.propertyFocus === setting.key}
Mode filtering of fields
Create
Update
View > do we filter fields here or disable them?
Default value?? Makes no sense
Drawer actions
CRUD - how to persist to the correct location?
Its just not a thing now
- Validation
- Bindings
- Custom options.
** Options source - should this be shaped too?
Schema,
Datasource
Custom
-->
<ComponentSettingsSection
componentInstance={sudoComponentInstance}
componentDefinition={parsedComponentDef}
isScreen={false}
{bindings}
{componentBindings}
onUpdateSetting={updateSetting}
/>
</Layout>
</Popover>

View File

@ -1,45 +1,125 @@
<script>
import { Button, ActionButton, Drawer } from "@budibase/bbui"
import { createEventDispatcher } from "svelte"
import ColumnDrawer from "./ColumnDrawer.svelte"
import { cloneDeep } from "lodash/fp"
import { generate } from "shortid"
import {
getDatasourceForProvider,
getSchemaForDatasource,
} from "builderStore/dataBinding"
import { currentAsset } from "builderStore"
import SettingsList from "../SettingsList.svelte"
import { createEventDispatcher } from "svelte"
import { store, selectedScreen } from "builderStore"
import {
getBindableProperties,
getComponentBindableProperties,
} from "builderStore/dataBinding"
import EditFieldPopover from "./EditFieldPopover.svelte"
export let componentInstance
export let value = []
export let value
const dispatch = createEventDispatcher()
let drawer
let boundValue
// Dean - From the inner form block - make a util
const FieldTypeToComponentMap = {
string: "stringfield",
number: "numberfield",
bigint: "bigintfield",
options: "optionsfield",
array: "multifieldselect",
boolean: "booleanfield",
longform: "longformfield",
datetime: "datetimefield",
attachment: "attachmentfield",
link: "relationshipfield",
json: "jsonfield",
barcodeqr: "codescanner",
}
// Dean - From the inner form block - make a util
const getComponentForField = field => {
if (!field || !schema?.[field]) {
return null
}
const type = schema[field].type
return FieldTypeToComponentMap[type]
}
let fieldConfigList
$: text = getText(value)
$: convertOldColumnFormat(value)
$: datasource = getDatasourceForProvider($currentAsset, componentInstance)
$: schema = getSchema($currentAsset, datasource)
$: options = Object.keys(schema || {})
$: sanitisedValue = getValidColumns(value, options)
$: sanitisedValue = getValidColumns(convertOldFieldFormat(value), options)
$: updateBoundValue(sanitisedValue)
const getText = value => {
if (!value?.length) {
return "All fields"
$: fieldConfigList.forEach(column => {
if (!column.id) {
column.id = generate()
}
let text = `${value.length} field`
if (value.length !== 1) {
text += "s"
}
return text
})
$: bindings = getBindableProperties(
$selectedScreen,
$store.selectedComponentId
)
$: console.log("bindings ", bindings)
$: componentBindings = getComponentBindableProperties(
$selectedScreen,
$store.selectedComponentId
)
$: console.log("componentBindings ", componentBindings)
// Builds unused ones only
const buildListOptions = (schema, selected) => {
let schemaClone = cloneDeep(schema)
selected.forEach(val => {
delete schemaClone[val.name]
})
return Object.keys(schemaClone)
.filter(key => !schemaClone[key].autocolumn)
.map(key => {
const col = schemaClone[key]
return {
name: key,
displayName: key,
id: generate(),
active: typeof col.active != "boolean" ? !value : col.active,
}
})
}
const convertOldColumnFormat = oldColumns => {
if (typeof oldColumns?.[0] === "string") {
value = oldColumns.map(field => ({ name: field, displayName: field }))
/*
SUPPORT
- ["FIELD1", "FIELD2"...]
"fields": [ "First Name", "Last Name" ]
- [{name: "FIELD1", displayName: "FIELD1"}, ... only the currentlyadded fields]
* [{name: "FIELD1", displayName: "FIELD1", active: true|false}, all currently available fields]
*/
$: unconfigured = buildListOptions(schema, fieldConfigList)
const convertOldFieldFormat = fields => {
let formFields
if (typeof fields?.[0] === "string") {
formFields = fields.map(field => ({
name: field,
displayName: field,
active: true,
}))
} else {
formFields = fields
}
return (formFields || []).map(field => {
return {
...field,
active: typeof field?.active != "boolean" ? true : field?.active,
}
})
}
const getSchema = (asset, datasource) => {
@ -55,7 +135,7 @@
}
const updateBoundValue = value => {
boundValue = cloneDeep(value)
fieldConfigList = cloneDeep(value)
}
const getValidColumns = (columns, options) => {
@ -75,29 +155,32 @@
})
}
const open = () => {
updateBoundValue(sanitisedValue)
drawer.show()
let listOptions
$: if (fieldConfigList) {
listOptions = [...fieldConfigList, ...unconfigured].map(column => {
const type = getComponentForField(column.name)
const _component = `@budibase/standard-components/${type}`
return { ...column, _component } //only necessary if it doesnt exist
})
console.log(listOptions)
}
const save = () => {
dispatch("change", getValidColumns(boundValue, options))
drawer.hide()
const listUpdated = e => {
const parsedColumns = getValidColumns(e.detail, options)
dispatch("change", parsedColumns)
}
</script>
<div class="field-configuration">
<ActionButton on:click={open}>{text}</ActionButton>
<SettingsList
value={listOptions}
on:change={listUpdated}
rightButton={EditFieldPopover}
rightProps={{ componentBindings, bindings, parent: componentInstance }}
/>
</div>
<Drawer bind:this={drawer} title="Form Fields">
<svelte:fragment slot="description">
Configure the fields in your form.
</svelte:fragment>
<Button cta slot="buttons" on:click={save}>Save</Button>
<ColumnDrawer slot="body" bind:columns={boundValue} {options} {schema} />
</Drawer>
<style>
.field-configuration :global(.spectrum-ActionButton) {
width: 100%;

View File

@ -0,0 +1,133 @@
<script>
import { Toggle, Icon } from "@budibase/bbui"
import { dndzone } from "svelte-dnd-action"
import { flip } from "svelte/animate"
import { createEventDispatcher } from "svelte"
export let value = []
export let showHandle = true
export let rightButton
export let rightProps = {}
const dispatch = createEventDispatcher()
const flipDurationMs = 150
let dragDisabled = false
let listOptions = [...value]
let anchors = {}
const updateColumnOrder = e => {
listOptions = e.detail.items
}
const handleFinalize = e => {
updateColumnOrder(e)
dispatch("change", listOptions)
dragDisabled = false
}
// This is optional and should be moved.
const onToggle = item => {
return e => {
console.log(`${item.name} toggled: ${e.detail}`)
item.active = e.detail
dispatch("change", listOptions)
}
}
</script>
<ul
class="list-wrap"
use:dndzone={{
items: listOptions,
flipDurationMs,
dropTargetStyle: { outline: "none" },
dragDisabled,
}}
on:finalize={handleFinalize}
on:consider={updateColumnOrder}
>
{#each listOptions as item (item.id)}
<li
animate:flip={{ duration: flipDurationMs }}
bind:this={anchors[item.id]}
>
<div class="left-content">
{#if showHandle}
<div
class="handle"
aria-label="drag-handle"
style={dragDisabled ? "cursor: grab" : "cursor: grabbing"}
>
<Icon name="DragHandle" size="XL" />
</div>
{/if}
<!-- slot - left action -->
<Toggle on:change={onToggle(item)} text="" value={item.active} thin />
{item.name}
</div>
<!-- slot - right action -->
<div class="right-content">
{#if rightButton}
<svelte:component
this={rightButton}
anchor={anchors[item.id]}
field={item}
componentBindings={rightProps.componentBindings}
bindings={rightProps.bindings}
parent={rightProps.parent}
/>
{/if}
</div>
</li>
{/each}
</ul>
<style>
.list-wrap {
list-style-type: none;
margin: 0;
padding: 0;
width: 100%;
border-radius: 4px;
background-color: var(
--spectrum-table-background-color,
var(--spectrum-global-color-gray-50)
);
border: 1px solid
var(--spectrum-table-border-color, var(--spectrum-alias-border-color-mid));
}
.list-wrap > li {
background-color: var(
--spectrum-table-background-color,
var(--spectrum-global-color-gray-50)
);
transition: background-color ease-in-out 130ms;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid
var(--spectrum-table-border-color, var(--spectrum-alias-border-color-mid));
}
.list-wrap > li:hover {
background-color: var(
--spectrum-table-row-background-color-hover,
var(--spectrum-alias-highlight-hover)
);
}
.list-wrap > li:first-child {
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
.list-wrap > li:last-child {
border-top-left-radius: var(--spectrum-table-regular-border-radius);
border-top-right-radius: var(--spectrum-table-regular-border-radius);
}
.left-content {
display: flex;
align-items: center;
}
.list-wrap li {
padding-left: var(--spacing-s);
padding-right: var(--spacing-s);
}
</style>

View File

@ -13,11 +13,16 @@
export let bindings
export let componentBindings
export let isScreen = false
export let onUpdateSetting
$: sections = getSections(componentInstance, componentDefinition, isScreen)
const getSections = (instance, definition, isScreen) => {
const settings = definition?.settings ?? []
console.log(
"ComponentSettingsSection::definition?.settings",
definition?.settings
)
const generalSettings = settings.filter(setting => !setting.section)
const customSections = settings.filter(setting => setting.section)
let sections = [
@ -46,19 +51,23 @@
}
const updateSetting = async (setting, value) => {
try {
await store.actions.components.updateSetting(setting.key, value)
if (typeof onUpdateSetting === "function") {
onUpdateSetting(setting, value)
} else {
try {
await store.actions.components.updateSetting(setting.key, value)
// Send event if required
if (setting.sendEvents) {
analytics.captureEvent(Events.COMPONENT_UPDATED, {
name: componentInstance._component,
setting: setting.key,
value,
})
// Send event if required
if (setting.sendEvents) {
analytics.captureEvent(Events.COMPONENT_UPDATED, {
name: componentInstance._component,
setting: setting.key,
value,
})
}
} catch (error) {
notifications.error("Error updating component prop")
}
} catch (error) {
notifications.error("Error updating component prop")
}
}
@ -129,10 +138,13 @@
{/if}
{#each section.settings as setting (setting.key)}
{#if setting.visible}
<!-- DEAN - Remove fieldConfiguration label config -->
<PropertyControl
type={setting.type}
control={getComponentForSetting(setting)}
label={setting.label}
label={setting.type != "fieldConfiguration"
? setting.label
: undefined}
labelHidden={setting.labelHidden}
key={setting.key}
value={componentInstance[setting.key]}

View File

@ -5106,22 +5106,6 @@
"key": "title",
"nested": true
},
{
"type": "select",
"label": "Size",
"key": "size",
"options": [
{
"label": "Medium",
"value": "spectrum--medium"
},
{
"label": "Large",
"value": "spectrum--large"
}
],
"defaultValue": "spectrum--medium"
},
{
"type": "text",
"label": "Empty text",
@ -5133,45 +5117,6 @@
"invert": true
}
},
{
"section": true,
"name": "Fields",
"settings": [
{
"type": "fieldConfiguration",
"label": "Fields",
"key": "fields",
"selectAllFields": true
},
{
"type": "select",
"label": "Field labels",
"key": "labelPosition",
"defaultValue": "left",
"options": [
{
"label": "Left",
"value": "left"
},
{
"label": "Above",
"value": "above"
}
]
},
{
"type": "boolean",
"label": "Disabled",
"key": "disabled",
"defaultValue": false,
"dependsOn": {
"setting": "actionType",
"value": "View",
"invert": true
}
}
]
},
{
"section": true,
"name": "Buttons",
@ -5241,6 +5186,61 @@
}
}
]
},
{
"section": true,
"name": "Fields",
"settings": [
{
"type": "select",
"label": "Align labels",
"key": "labelPosition",
"defaultValue": "left",
"options": [
{
"label": "Left",
"value": "left"
},
{
"label": "Above",
"value": "above"
}
]
},
{
"type": "select",
"label": "Size",
"key": "size",
"options": [
{
"label": "Medium",
"value": "spectrum--medium"
},
{
"label": "Large",
"value": "spectrum--large"
}
],
"defaultValue": "spectrum--medium"
},
{
"type": "fieldConfiguration",
"label": "Fields",
"key": "fields",
"selectAllFields": true
},
{
"type": "boolean",
"label": "Disabled",
"key": "disabled",
"defaultValue": false,
"dependsOn": {
"setting": "actionType",
"value": "View",
"invert": true
}
}
]
}
],
"context": [

View File

@ -24,14 +24,20 @@
const { fetchDatasourceSchema } = getContext("sdk")
const convertOldFieldFormat = fields => {
if (typeof fields?.[0] === "string") {
return fields.map(field => ({ name: field, displayName: field }))
}
return fields
return typeof fields?.[0] === "string"
? fields.map(field => ({
name: field,
displayName: field,
active: true,
}))
: fields
}
//All settings need to derive from the block config now
// Parse the fields here too. Not present means false.
const getDefaultFields = (fields, schema) => {
let formFields
if (schema && (!fields || fields.length === 0)) {
const defaultFields = []
@ -41,13 +47,20 @@
defaultFields.push({
name: field.name,
displayName: field.name,
active: true,
})
})
return defaultFields
formFields = [...defaultFields]
} else {
formFields = (fields || []).map(field => {
return {
...field,
active: typeof field?.active != "boolean" ? true : field?.active,
}
})
}
return fields
return formFields.filter(field => field.active)
}
let schema
@ -56,7 +69,6 @@
$: formattedFields = convertOldFieldFormat(fields)
$: fieldsOrDefault = getDefaultFields(formattedFields, schema)
$: fetchSchema(dataSource)
$: dataProvider = `{{ literal ${safe(providerId)} }}`
$: filter = [

View File

@ -197,22 +197,24 @@
{/if}
</BlockComponent>
{/if}
<BlockComponent type="fieldgroup" props={{ labelPosition }} order={1}>
{#each fields as field, idx}
{#if getComponentForField(field.name)}
<BlockComponent
type={getComponentForField(field.name)}
props={{
validation: field.validation,
field: field.name,
label: field.displayName,
placeholder: field.displayName,
}}
order={idx}
/>
{/if}
{/each}
</BlockComponent>
{#key fields}
<BlockComponent type="fieldgroup" props={{ labelPosition }} order={1}>
{#each fields as field, idx}
{#if getComponentForField(field.name)}
<BlockComponent
type={getComponentForField(field.name)}
props={{
validation: field.validation,
field: field.name,
label: field.displayName,
placeholder: field.displayName,
}}
order={idx}
/>
{/if}
{/each}
</BlockComponent>
{/key}
</BlockComponent>
</BlockComponent>
{:else}