Add support for binding forms to deep json fields
This commit is contained in:
parent
891538eaa4
commit
dc7d22e5cb
|
@ -15,18 +15,43 @@ export const capitalise = s => s.substring(0, 1).toUpperCase() + s.substring(1)
|
|||
* will return "foo" over "bar".
|
||||
* @param obj the object
|
||||
* @param key the key
|
||||
* @return {*|null} the value or null if a value was not found for this key
|
||||
*/
|
||||
export const deepGet = (obj, key) => {
|
||||
if (!obj || !key) {
|
||||
return null
|
||||
}
|
||||
if (obj[key] != null) {
|
||||
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
||||
return obj[key]
|
||||
}
|
||||
const split = key.split(".")
|
||||
let value = obj
|
||||
for (let i = 0; i < split.length; i++) {
|
||||
value = value?.[split[i]]
|
||||
obj = obj?.[split[i]]
|
||||
}
|
||||
return value
|
||||
return obj
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a key within an object. The key supports dot syntax for retrieving deep
|
||||
* fields - e.g. "a.b.c".
|
||||
* Exact matches of keys with dots in them take precedence over nested keys of
|
||||
* the same path - e.g. setting "a.b" of { "a.b": "foo", a: { b: "bar" } }
|
||||
* will override the value "foo" rather than "bar".
|
||||
* @param obj the object
|
||||
* @param key the key
|
||||
* @param value the value
|
||||
*/
|
||||
export const deepSet = (obj, key, value) => {
|
||||
if (!obj || !key) {
|
||||
return
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
||||
obj[key] = value
|
||||
return
|
||||
}
|
||||
const split = key.split(".")
|
||||
for (let i = 0; i < split.length - 1; i++) {
|
||||
obj = obj?.[split[i]]
|
||||
}
|
||||
obj[split[split.length - 1]] = value
|
||||
}
|
||||
|
|
|
@ -220,23 +220,6 @@ const getProviderContextBindings = (asset, dataProviders) => {
|
|||
schema = info.schema
|
||||
table = info.table
|
||||
|
||||
// Check for any JSON fields so we can add any top level properties
|
||||
let jsonAdditions = {}
|
||||
Object.keys(schema).forEach(fieldKey => {
|
||||
const fieldSchema = schema[fieldKey]
|
||||
if (fieldSchema.type === "json") {
|
||||
const jsonSchema = convertJSONSchemaToTableSchema(fieldSchema, {
|
||||
squashObjects: true,
|
||||
})
|
||||
Object.keys(jsonSchema).forEach(jsonKey => {
|
||||
jsonAdditions[`${fieldKey}.${jsonKey}`] = {
|
||||
type: jsonSchema[jsonKey].type,
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
schema = { ...schema, ...jsonAdditions }
|
||||
|
||||
// For JSON arrays, use the array name as the readable prefix.
|
||||
// Otherwise use the table name
|
||||
if (datasource.type === "jsonarray") {
|
||||
|
@ -485,6 +468,26 @@ export const getSchemaForDatasource = (asset, datasource, isForm = false) => {
|
|||
}
|
||||
}
|
||||
|
||||
// Check for any JSON fields so we can add any top level properties
|
||||
if (schema) {
|
||||
let jsonAdditions = {}
|
||||
Object.keys(schema).forEach(fieldKey => {
|
||||
const fieldSchema = schema[fieldKey]
|
||||
if (fieldSchema?.type === "json") {
|
||||
const jsonSchema = convertJSONSchemaToTableSchema(fieldSchema, {
|
||||
squashObjects: true,
|
||||
})
|
||||
Object.keys(jsonSchema).forEach(jsonKey => {
|
||||
jsonAdditions[`${fieldKey}.${jsonKey}`] = {
|
||||
type: jsonSchema[jsonKey].type,
|
||||
nestedJSON: true,
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
schema = { ...schema, ...jsonAdditions }
|
||||
}
|
||||
|
||||
// Add _id and _rev fields for certain types
|
||||
if (schema && !isForm && ["table", "link"].includes(datasource.type)) {
|
||||
schema["_id"] = { type: "string" }
|
||||
|
|
|
@ -329,12 +329,12 @@ export const getFrontendStore = () => {
|
|||
},
|
||||
components: {
|
||||
select: component => {
|
||||
if (!component) {
|
||||
const asset = get(currentAsset)
|
||||
if (!asset || !component) {
|
||||
return
|
||||
}
|
||||
|
||||
// If this is the root component, select the asset instead
|
||||
const asset = get(currentAsset)
|
||||
const parent = findComponentParent(asset.props, component._id)
|
||||
if (parent == null) {
|
||||
const state = get(store)
|
||||
|
|
|
@ -147,7 +147,7 @@ export function makeDatasourceFormComponents(datasource) {
|
|||
fields.forEach(field => {
|
||||
const fieldSchema = schema[field]
|
||||
// skip autocolumns
|
||||
if (fieldSchema.autocolumn) {
|
||||
if (fieldSchema.autocolumn || fieldSchema.nestedJSON) {
|
||||
return
|
||||
}
|
||||
const fieldType =
|
||||
|
|
|
@ -4,7 +4,10 @@ import { fetchViewData } from "./views"
|
|||
import { fetchRelationshipData } from "./relationships"
|
||||
import { FieldTypes } from "../constants"
|
||||
import { executeQuery, fetchQueryDefinition } from "./queries"
|
||||
import { getJSONArrayDatasourceSchema } from "builder/src/builderStore/jsonUtils"
|
||||
import {
|
||||
convertJSONSchemaToTableSchema,
|
||||
getJSONArrayDatasourceSchema,
|
||||
} from "builder/src/builderStore/jsonUtils"
|
||||
|
||||
/**
|
||||
* Fetches all rows for a particular Budibase data source.
|
||||
|
@ -50,16 +53,17 @@ export const fetchDatasourceSchema = async dataSource => {
|
|||
return null
|
||||
}
|
||||
const { type } = dataSource
|
||||
let schema
|
||||
|
||||
// Nested providers should already have exposed their own schema
|
||||
if (type === "provider") {
|
||||
return dataSource.value?.schema
|
||||
schema = dataSource.value?.schema
|
||||
}
|
||||
|
||||
// Field sources have their schema statically defined
|
||||
if (type === "field") {
|
||||
if (dataSource.fieldType === "attachment") {
|
||||
return {
|
||||
schema = {
|
||||
url: {
|
||||
type: "string",
|
||||
},
|
||||
|
@ -68,7 +72,7 @@ export const fetchDatasourceSchema = async dataSource => {
|
|||
},
|
||||
}
|
||||
} else if (dataSource.fieldType === "array") {
|
||||
return {
|
||||
schema = {
|
||||
value: {
|
||||
type: "string",
|
||||
},
|
||||
|
@ -80,7 +84,7 @@ export const fetchDatasourceSchema = async dataSource => {
|
|||
// We can then extract their schema as a subset of the table schema.
|
||||
if (type === "jsonarray") {
|
||||
const table = await fetchTableDefinition(dataSource.tableId)
|
||||
return getJSONArrayDatasourceSchema(table?.schema, dataSource)
|
||||
schema = getJSONArrayDatasourceSchema(table?.schema, dataSource)
|
||||
}
|
||||
|
||||
// Tables, views and links can be fetched by table ID
|
||||
|
@ -89,14 +93,34 @@ export const fetchDatasourceSchema = async dataSource => {
|
|||
dataSource.tableId
|
||||
) {
|
||||
const table = await fetchTableDefinition(dataSource.tableId)
|
||||
return table?.schema
|
||||
schema = table?.schema
|
||||
}
|
||||
|
||||
// Queries can be fetched by query ID
|
||||
if (type === "query" && dataSource._id) {
|
||||
const definition = await fetchQueryDefinition(dataSource._id)
|
||||
return definition?.schema
|
||||
schema = definition?.schema
|
||||
}
|
||||
|
||||
return null
|
||||
// Sanity check
|
||||
if (!schema) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Check for any JSON fields so we can add any top level properties
|
||||
let jsonAdditions = {}
|
||||
Object.keys(schema).forEach(fieldKey => {
|
||||
const fieldSchema = schema[fieldKey]
|
||||
if (fieldSchema?.type === "json") {
|
||||
const jsonSchema = convertJSONSchemaToTableSchema(fieldSchema, {
|
||||
squashObjects: true,
|
||||
})
|
||||
Object.keys(jsonSchema).forEach(jsonKey => {
|
||||
jsonAdditions[`${fieldKey}.${jsonKey}`] = {
|
||||
type: jsonSchema[jsonKey].type,
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
return { ...schema, ...jsonAdditions }
|
||||
}
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
import { derived, get, writable } from "svelte/store"
|
||||
import { createValidatorFromConstraints } from "./validation"
|
||||
import { generateID } from "utils/helpers"
|
||||
import { deepGet, deepSet } from "@budibase/bbui"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
|
||||
export let dataSource
|
||||
export let disabled = false
|
||||
|
@ -49,6 +51,19 @@
|
|||
})
|
||||
}
|
||||
|
||||
// Derive value of whole form
|
||||
$: formValue = deriveFormValue(initialValues, $values, $enrichments)
|
||||
|
||||
// Create data context to provide
|
||||
$: dataContext = {
|
||||
...formValue,
|
||||
|
||||
// These static values are prefixed to avoid clashes with actual columns
|
||||
__valid: valid,
|
||||
__currentStep: $currentStep,
|
||||
__currentStepValid: $currentStepValid,
|
||||
}
|
||||
|
||||
// Generates a derived store from an array of fields, comprised of a map of
|
||||
// extracted values from the field array
|
||||
const deriveFieldProperty = (fieldStores, getProp) => {
|
||||
|
@ -78,6 +93,35 @@
|
|||
})
|
||||
}
|
||||
|
||||
// Derive the overall form value and deeply set all field paths so that we
|
||||
// can support things like JSON fields.
|
||||
const deriveFormValue = (initialValues, values, enrichments) => {
|
||||
let formValue = cloneDeep(initialValues || {})
|
||||
|
||||
// We need to sort the keys to avoid a JSON field overwriting a nested field
|
||||
const sortedFields = Object.entries(values || {})
|
||||
.map(([key, value]) => {
|
||||
const field = getField(key)
|
||||
return {
|
||||
key,
|
||||
value,
|
||||
lastUpdate: get(field).fieldState?.lastUpdate || 0,
|
||||
}
|
||||
})
|
||||
.sort((a, b) => {
|
||||
return a.lastUpdate > b.lastUpdate
|
||||
})
|
||||
|
||||
// Merge all values and enrichments into a single value
|
||||
sortedFields.forEach(({ key, value }) => {
|
||||
deepSet(formValue, key, value)
|
||||
})
|
||||
Object.entries(enrichments || {}).forEach(([key, value]) => {
|
||||
deepSet(formValue, key, value)
|
||||
})
|
||||
return formValue
|
||||
}
|
||||
|
||||
// Searches the field array for a certain field
|
||||
const getField = name => {
|
||||
return fields.find(field => get(field).name === name)
|
||||
|
@ -97,7 +141,7 @@
|
|||
}
|
||||
|
||||
// If we've already registered this field then keep some existing state
|
||||
let initialValue = initialValues[field] ?? defaultValue
|
||||
let initialValue = deepGet(initialValues, field) ?? defaultValue
|
||||
let fieldId = `id-${generateID()}`
|
||||
const existingField = getField(field)
|
||||
if (existingField) {
|
||||
|
@ -137,6 +181,7 @@
|
|||
disabled: disabled || fieldDisabled || isAutoColumn,
|
||||
defaultValue,
|
||||
validator,
|
||||
lastUpdate: Date.now(),
|
||||
},
|
||||
fieldApi: makeFieldApi(field, defaultValue),
|
||||
fieldSchema: schema?.[field] ?? {},
|
||||
|
@ -211,6 +256,7 @@
|
|||
fieldInfo.update(state => {
|
||||
state.fieldState.value = value
|
||||
state.fieldState.error = error
|
||||
state.fieldState.lastUpdate = Date.now()
|
||||
return state
|
||||
})
|
||||
|
||||
|
@ -227,6 +273,7 @@
|
|||
fieldInfo.update(state => {
|
||||
state.fieldState.value = newValue
|
||||
state.fieldState.error = null
|
||||
state.fieldState.lastUpdate = Date.now()
|
||||
return state
|
||||
})
|
||||
}
|
||||
|
@ -306,18 +353,6 @@
|
|||
{ type: ActionTypes.ClearForm, callback: formApi.clear },
|
||||
{ type: ActionTypes.ChangeFormStep, callback: formApi.changeStep },
|
||||
]
|
||||
|
||||
// Create data context to provide
|
||||
$: dataContext = {
|
||||
...initialValues,
|
||||
...$values,
|
||||
...$enrichments,
|
||||
|
||||
// These static values are prefixed to avoid clashes with actual columns
|
||||
__valid: valid,
|
||||
__currentStep: $currentStep,
|
||||
__currentStepValid: $currentStepValid,
|
||||
}
|
||||
</script>
|
||||
|
||||
<Provider {actions} data={dataContext}>
|
||||
|
|
|
@ -9,6 +9,7 @@ import {
|
|||
import { saveRow, deleteRow, executeQuery, triggerAutomation } from "api"
|
||||
import { ActionTypes } from "constants"
|
||||
import { enrichDataBindings } from "./enrichDataBinding"
|
||||
import { deepSet } from "@budibase/bbui"
|
||||
|
||||
const saveRowHandler = async (action, context) => {
|
||||
const { fields, providerId, tableId } = action.parameters
|
||||
|
@ -20,7 +21,7 @@ const saveRowHandler = async (action, context) => {
|
|||
}
|
||||
if (fields) {
|
||||
for (let [field, value] of Object.entries(fields)) {
|
||||
payload[field] = value
|
||||
deepSet(payload, field, value)
|
||||
}
|
||||
}
|
||||
if (tableId) {
|
||||
|
@ -35,18 +36,18 @@ const saveRowHandler = async (action, context) => {
|
|||
const duplicateRowHandler = async (action, context) => {
|
||||
const { fields, providerId, tableId } = action.parameters
|
||||
if (providerId) {
|
||||
let draft = { ...context[providerId] }
|
||||
let payload = { ...context[providerId] }
|
||||
if (fields) {
|
||||
for (let [field, value] of Object.entries(fields)) {
|
||||
draft[field] = value
|
||||
deepSet(payload, field, value)
|
||||
}
|
||||
}
|
||||
if (tableId) {
|
||||
draft.tableId = tableId
|
||||
payload.tableId = tableId
|
||||
}
|
||||
delete draft._id
|
||||
delete draft._rev
|
||||
const row = await saveRow(draft)
|
||||
delete payload._id
|
||||
delete payload._rev
|
||||
const row = await saveRow(payload)
|
||||
return {
|
||||
row,
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue