diff --git a/packages/bbui/src/Drawer/Drawer.svelte b/packages/bbui/src/Drawer/Drawer.svelte index 5e3de88e61..4bc8a69445 100644 --- a/packages/bbui/src/Drawer/Drawer.svelte +++ b/packages/bbui/src/Drawer/Drawer.svelte @@ -21,16 +21,8 @@ } visible = false } - - function handleKey(e) { - if (visible && e.key === "Escape") { - hide() - } - } - - {#if visible}
diff --git a/packages/bbui/src/Table/Table.svelte b/packages/bbui/src/Table/Table.svelte index bcd84e7112..09ade22627 100644 --- a/packages/bbui/src/Table/Table.svelte +++ b/packages/bbui/src/Table/Table.svelte @@ -4,6 +4,7 @@ import CellRenderer from "./CellRenderer.svelte" import SelectEditRenderer from "./SelectEditRenderer.svelte" import { cloneDeep } from "lodash" + import { deepGet } from "../utils/helpers" /** * The expected schema is our normal couch schemas for our tables. @@ -318,7 +319,7 @@ {customRenderers} {row} schema={schema[field]} - value={row[field]} + value={deepGet(row, field)} on:clickrelationship > diff --git a/packages/bbui/src/index.js b/packages/bbui/src/index.js index c30047f73f..13fb7f1770 100644 --- a/packages/bbui/src/index.js +++ b/packages/bbui/src/index.js @@ -77,3 +77,6 @@ export { default as clickOutside } from "./Actions/click_outside" // Stores export { notifications, createNotificationStore } from "./Stores/notifications" + +// Utils +export * from "./utils/helpers" diff --git a/packages/bbui/src/utils/helpers.js b/packages/bbui/src/utils/helpers.js index 83d305d573..6cf432f356 100644 --- a/packages/bbui/src/utils/helpers.js +++ b/packages/bbui/src/utils/helpers.js @@ -6,3 +6,61 @@ export const generateID = () => { } export const capitalise = s => s.substring(0, 1).toUpperCase() + s.substring(1) + +/** + * Gets 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. getting "a.b" from { "a.b": "foo", a: { b: "bar" } } + * 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 (Object.prototype.hasOwnProperty.call(obj, key)) { + return obj[key] + } + const split = key.split(".") + for (let i = 0; i < split.length; i++) { + obj = obj?.[split[i]] + } + 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". + * If a deep path is specified and the parent keys don't exist then these will + * be created. + * @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++) { + const nextKey = split[i] + if (obj && obj[nextKey] == null) { + obj[nextKey] = {} + } + obj = obj?.[nextKey] + } + if (!obj) { + return + } + obj[split[split.length - 1]] = value +} diff --git a/packages/builder/src/builderStore/storeUtils.js b/packages/builder/src/builderStore/componentUtils.js similarity index 80% rename from packages/builder/src/builderStore/storeUtils.js rename to packages/builder/src/builderStore/componentUtils.js index e25949000f..04a87998fe 100644 --- a/packages/builder/src/builderStore/storeUtils.js +++ b/packages/builder/src/builderStore/componentUtils.js @@ -127,18 +127,37 @@ const searchComponentTree = (rootComponent, matchComponent) => { } /** - * Searches a component's definition for a setting matching a certin predicate. + * Searches a component's definition for a setting matching a certain predicate. + * These settings are cached because they cannot change at run time. */ +let componentSettingCache = {} export const getComponentSettings = componentType => { - const def = store.actions.components.getDefinition(componentType) - if (!def) { + if (!componentType) { return [] } - let settings = def.settings?.filter(setting => !setting.section) ?? [] - def.settings - ?.filter(setting => setting.section) - .forEach(section => { - settings = settings.concat(section.settings || []) - }) + + // Ensure whole component name is used + if (!componentType.startsWith("@budibase")) { + componentType = `@budibase/standard-components/${componentType}` + } + + // Check if we have cached this type already + if (componentSettingCache[componentType]) { + return componentSettingCache[componentType] + } + + // Otherwise get the settings and cache them + const def = store.actions.components.getDefinition(componentType) + let settings = [] + if (def) { + settings = def.settings?.filter(setting => !setting.section) ?? [] + def.settings + ?.filter(setting => setting.section) + .forEach(section => { + settings = settings.concat(section.settings || []) + }) + } + componentSettingCache[componentType] = settings + return settings } diff --git a/packages/builder/src/builderStore/dataBinding.js b/packages/builder/src/builderStore/dataBinding.js index b36613fbc5..0f3cffc4fb 100644 --- a/packages/builder/src/builderStore/dataBinding.js +++ b/packages/builder/src/builderStore/dataBinding.js @@ -5,7 +5,7 @@ import { findComponent, findComponentPath, getComponentSettings, -} from "./storeUtils" +} from "./componentUtils" import { store } from "builderStore" import { queries as queriesStores, tables as tablesStore } from "stores/backend" import { @@ -15,6 +15,11 @@ import { encodeJSBinding, } from "@budibase/string-templates" import { TableNames } from "../constants" +import { + convertJSONSchemaToTableSchema, + getJSONArrayDatasourceSchema, +} from "./jsonUtils" +import ActionDefinitions from "components/design/PropertiesPanel/PropertyControls/ButtonActionEditor/manifest.json" // Regex to match all instances of template strings const CAPTURE_VAR_INSIDE_TEMPLATE = /{{([^}]+)}}/g @@ -186,6 +191,7 @@ const getProviderContextBindings = (asset, dataProviders) => { } let schema + let table let readablePrefix let runtimeSuffix = context.suffix @@ -209,7 +215,16 @@ const getProviderContextBindings = (asset, dataProviders) => { } const info = getSchemaForDatasource(asset, datasource) schema = info.schema - readablePrefix = info.table?.name + table = info.table + + // For JSON arrays, use the array name as the readable prefix. + // Otherwise use the table name + if (datasource.type === "jsonarray") { + const split = datasource.label.split(".") + readablePrefix = split[split.length - 1] + } else { + readablePrefix = info.table?.name + } } if (!schema) { return @@ -229,7 +244,8 @@ const getProviderContextBindings = (asset, dataProviders) => { const fieldSchema = schema[key] // Make safe runtime binding - const runtimeBinding = `${safeComponentId}.${makePropSafe(key)}` + const safeKey = key.split(".").map(makePropSafe).join(".") + const runtimeBinding = `${safeComponentId}.${safeKey}` // Optionally use a prefix with readable bindings let readableBinding = component._instanceName @@ -247,6 +263,8 @@ const getProviderContextBindings = (asset, dataProviders) => { // datasource options, based on bindable properties fieldSchema, providerId, + // Table ID is used by JSON fields to know what table the field is in + tableId: table?._id, }) }) }) @@ -339,6 +357,36 @@ const getUrlBindings = asset => { })) } +/** + * Gets all bindable properties exposed in a button actions flow up until + * the specified action ID. + */ +export const getButtonContextBindings = (actions, actionId) => { + // Get the steps leading up to this value + const index = actions?.findIndex(action => action.id === actionId) + if (index == null || index === -1) { + return [] + } + const prevActions = actions.slice(0, index) + + // Generate bindings for any steps which provide context + let bindings = [] + prevActions.forEach((action, idx) => { + const def = ActionDefinitions.actions.find( + x => x.name === action["##eventHandlerType"] + ) + if (def.context) { + def.context.forEach(contextValue => { + bindings.push({ + readableBinding: `Action ${idx + 1}.${contextValue.label}`, + runtimeBinding: `actions.${idx}.${contextValue.value}`, + }) + }) + } + }) + return bindings +} + /** * Gets a schema for a datasource object. */ @@ -347,16 +395,26 @@ export const getSchemaForDatasource = (asset, datasource, isForm = false) => { if (datasource) { const { type } = datasource + const tables = get(tablesStore).list - // Determine the source table from the datasource type + // Determine the entity which backs this datasource. + // "provider" datasources are those targeting another data provider if (type === "provider") { const component = findComponent(asset.props, datasource.providerId) const source = getDatasourceForProvider(asset, component) return getSchemaForDatasource(asset, source, isForm) - } else if (type === "query") { + } + + // "query" datasources are those targeting non-plus datasources or + // custom queries + else if (type === "query") { const queries = get(queriesStores).list table = queries.find(query => query._id === datasource._id) - } else if (type === "field") { + } + + // "field" datasources are array-like fields of rows, such as attachments + // or multi-select fields + else if (type === "field") { table = { name: datasource.fieldName } const { fieldType } = datasource if (fieldType === "attachment") { @@ -375,12 +433,22 @@ export const getSchemaForDatasource = (asset, datasource, isForm = false) => { }, } } - } else { - const tables = get(tablesStore).list + } + + // "jsonarray" datasources are arrays inside JSON fields + else if (type === "jsonarray") { + table = tables.find(table => table._id === datasource.tableId) + let tableSchema = table?.schema + schema = getJSONArrayDatasourceSchema(tableSchema, datasource) + } + + // Otherwise we assume we're targeting an internal table or a plus + // datasource, and we can treat it as a table with a schema + else { table = tables.find(table => table._id === datasource.tableId) } - // Determine the schema from the table if not already determined + // Determine the schema from the backing entity if not already determined if (table && !schema) { if (type === "view") { schema = cloneDeep(table.views?.[datasource.name]?.schema) @@ -397,6 +465,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" } @@ -450,15 +538,58 @@ const buildFormSchema = component => { return schema } +/** + * Returns an array of the keys of any state variables which are set anywhere + * in the app. + */ +export const getAllStateVariables = () => { + // Get all component containing assets + let allAssets = [] + allAssets = allAssets.concat(get(store).layouts || []) + allAssets = allAssets.concat(get(store).screens || []) + + // Find all button action settings in all components + let eventSettings = [] + allAssets.forEach(asset => { + findAllMatchingComponents(asset.props, component => { + const settings = getComponentSettings(component._component) + settings + .filter(setting => setting.type === "event") + .forEach(setting => { + eventSettings.push(component[setting.key]) + }) + }) + }) + + // Extract all state keys from any "update state" actions in each setting + let bindingSet = new Set() + eventSettings.forEach(setting => { + if (!Array.isArray(setting)) { + return + } + setting.forEach(action => { + if ( + action["##eventHandlerType"] === "Update State" && + action.parameters?.type === "set" && + action.parameters?.key && + action.parameters?.value + ) { + bindingSet.add(action.parameters.key) + } + }) + }) + return Array.from(bindingSet) +} + /** * Recurses the input object to remove any instances of bindings. */ -export function removeBindings(obj) { +export const removeBindings = (obj, replacement = "Invalid binding") => { for (let [key, value] of Object.entries(obj)) { if (value && typeof value === "object") { - obj[key] = removeBindings(value) + obj[key] = removeBindings(value, replacement) } else if (typeof value === "string") { - obj[key] = value.replace(CAPTURE_HBS_TEMPLATE, "Invalid binding") + obj[key] = value.replace(CAPTURE_HBS_TEMPLATE, replacement) } } return obj @@ -468,8 +599,8 @@ export function removeBindings(obj) { * When converting from readable to runtime it can sometimes add too many square brackets, * this makes sure that doesn't happen. */ -function shouldReplaceBinding(currentValue, from, convertTo) { - if (!currentValue?.includes(from)) { +const shouldReplaceBinding = (currentValue, convertFrom, convertTo) => { + if (!currentValue?.includes(convertFrom)) { return false } if (convertTo === "readableBinding") { @@ -478,7 +609,7 @@ function shouldReplaceBinding(currentValue, from, convertTo) { // remove all the spaces, if the input is surrounded by spaces e.g. [ Auto ID ] then // this makes sure it is detected const noSpaces = currentValue.replace(/\s+/g, "") - const fromNoSpaces = from.replace(/\s+/g, "") + const fromNoSpaces = convertFrom.replace(/\s+/g, "") const invalids = [ `[${fromNoSpaces}]`, `"${fromNoSpaces}"`, @@ -487,14 +618,21 @@ function shouldReplaceBinding(currentValue, from, convertTo) { return !invalids.find(invalid => noSpaces?.includes(invalid)) } -function replaceBetween(string, start, end, replacement) { +/** + * Utility function which replaces a string between given indices. + */ +const replaceBetween = (string, start, end, replacement) => { return string.substring(0, start) + replacement + string.substring(end) } /** - * utility function for the readableToRuntimeBinding and runtimeToReadableBinding. + * Utility function for the readableToRuntimeBinding and runtimeToReadableBinding. */ -function bindingReplacement(bindableProperties, textWithBindings, convertTo) { +const bindingReplacement = ( + bindableProperties, + textWithBindings, + convertTo +) => { // Decide from base64 if using JS const isJS = isJSBinding(textWithBindings) if (isJS) { @@ -559,14 +697,17 @@ function bindingReplacement(bindableProperties, textWithBindings, convertTo) { * Extracts a component ID from a handlebars expression setting of * {{ literal [componentId] }} */ -function extractLiteralHandlebarsID(value) { +const extractLiteralHandlebarsID = value => { return value?.match(/{{\s*literal\s*\[+([^\]]+)].*}}/)?.[1] } /** * Converts a readable data binding into a runtime data binding */ -export function readableToRuntimeBinding(bindableProperties, textWithBindings) { +export const readableToRuntimeBinding = ( + bindableProperties, + textWithBindings +) => { return bindingReplacement( bindableProperties, textWithBindings, @@ -577,56 +718,13 @@ export function readableToRuntimeBinding(bindableProperties, textWithBindings) { /** * Converts a runtime data binding into a readable data binding */ -export function runtimeToReadableBinding(bindableProperties, textWithBindings) { +export const runtimeToReadableBinding = ( + bindableProperties, + textWithBindings +) => { return bindingReplacement( bindableProperties, textWithBindings, "readableBinding" ) } - -/** - * Returns an array of the keys of any state variables which are set anywhere - * in the app. - */ -export const getAllStateVariables = () => { - let allComponents = [] - - // Find all onClick settings in all layouts - get(store).layouts.forEach(layout => { - const components = findAllMatchingComponents( - layout.props, - c => c.onClick != null - ) - allComponents = allComponents.concat(components || []) - }) - - // Find all onClick settings in all screens - get(store).screens.forEach(screen => { - const components = findAllMatchingComponents( - screen.props, - c => c.onClick != null - ) - allComponents = allComponents.concat(components || []) - }) - - // Add state bindings for all state actions - let bindingSet = new Set() - allComponents.forEach(component => { - if (!Array.isArray(component.onClick)) { - return - } - component.onClick.forEach(action => { - if ( - action["##eventHandlerType"] === "Update State" && - action.parameters?.type === "set" && - action.parameters?.key && - action.parameters?.value - ) { - bindingSet.add(action.parameters.key) - } - }) - }) - - return Array.from(bindingSet) -} diff --git a/packages/builder/src/builderStore/index.js b/packages/builder/src/builderStore/index.js index f32dedd47e..23704556ad 100644 --- a/packages/builder/src/builderStore/index.js +++ b/packages/builder/src/builderStore/index.js @@ -4,7 +4,7 @@ import { getHostingStore } from "./store/hosting" import { getThemeStore } from "./store/theme" import { derived, writable } from "svelte/store" import { FrontendTypes, LAYOUT_NAMES } from "../constants" -import { findComponent } from "./storeUtils" +import { findComponent } from "./componentUtils" export const store = getFrontendStore() export const automationStore = getAutomationStore() diff --git a/packages/builder/src/builderStore/jsonUtils.js b/packages/builder/src/builderStore/jsonUtils.js new file mode 100644 index 0000000000..29bf2df34e --- /dev/null +++ b/packages/builder/src/builderStore/jsonUtils.js @@ -0,0 +1,121 @@ +/** + * Gets the schema for a datasource which is targeting a JSON array, including + * nested JSON arrays. The returned schema is a squashed, table-like schema + * which is fully compatible with the rest of the platform. + * @param tableSchema the full schema for the table this JSON field is in + * @param datasource the datasource configuration + */ +export const getJSONArrayDatasourceSchema = (tableSchema, datasource) => { + let jsonSchema = tableSchema + let keysToSchema = [] + + // If we are already deep inside a JSON field then we need to account + // for the keys that brought us here, so we can get the schema for the + // depth we're actually at + if (datasource.prefixKeys) { + keysToSchema = datasource.prefixKeys.concat(["schema"]) + } + + // We parse the label of the datasource to work out where we are inside + // the structure. We can use this to know which part of the schema + // is available underneath our current position. + keysToSchema = keysToSchema.concat(datasource.label.split(".").slice(2)) + + // Follow the JSON key path until we reach the schema for the level + // we are at + for (let i = 0; i < keysToSchema.length; i++) { + jsonSchema = jsonSchema?.[keysToSchema[i]] + if (jsonSchema?.schema) { + jsonSchema = jsonSchema.schema + } + } + + // We need to convert the JSON schema into a more typical looking table + // schema so that it works with the rest of the platform + return convertJSONSchemaToTableSchema(jsonSchema, { + squashObjects: true, + prefixKeys: keysToSchema, + }) +} + +/** + * Converts a JSON field schema (or sub-schema of a nested field) into a schema + * that looks like a typical table schema. + * @param jsonSchema the JSON field schema or sub-schema + * @param options + */ +export const convertJSONSchemaToTableSchema = (jsonSchema, options) => { + if (!jsonSchema) { + return null + } + + // Add default options + options = { squashObjects: false, prefixKeys: null, ...options } + + // Immediately strip the wrapper schema for objects, or wrap shallow values in + // a fake "value" schema + if (jsonSchema.schema) { + jsonSchema = jsonSchema.schema + } else { + jsonSchema = { + value: jsonSchema, + } + } + + // Extract all deep keys from the schema + const keys = extractJSONSchemaKeys(jsonSchema, options.squashObjects) + + // Form a full schema from all the deep schema keys + let schema = {} + keys.forEach(({ key, type }) => { + schema[key] = { type, name: key, prefixKeys: options.prefixKeys } + }) + return schema +} + +/** + * Recursively builds paths to all leaf fields in a JSON field schema structure, + * stopping when leaf nodes or arrays are reached. + * @param jsonSchema the JSON field schema or sub-schema + * @param squashObjects whether to recurse into objects or not + */ +const extractJSONSchemaKeys = (jsonSchema, squashObjects = false) => { + if (!jsonSchema || !Object.keys(jsonSchema).length) { + return [] + } + + // Iterate through every schema key + let keys = [] + Object.keys(jsonSchema).forEach(key => { + const type = jsonSchema[key].type + + // If we encounter an object, then only go deeper if we want to squash + // object paths + if (type === "json" && squashObjects) { + // Find all keys within this objects schema + const childKeys = extractJSONSchemaKeys( + jsonSchema[key].schema, + squashObjects + ) + + // Append child paths onto the current path to build the full path + keys = keys.concat( + childKeys.map(childKey => ({ + key: `${key}.${childKey.key}`, + type: childKey.type, + })) + ) + } + + // Otherwise add this as a lead node. + // We transform array types from "array" into "jsonarray" here to avoid + // confusion with the existing "array" type that represents a multi-select. + else { + keys.push({ + key, + type: type === "array" ? "jsonarray" : type, + }) + } + }) + return keys +} diff --git a/packages/builder/src/builderStore/schemaGenerator.js b/packages/builder/src/builderStore/schemaGenerator.js new file mode 100644 index 0000000000..33115fc997 --- /dev/null +++ b/packages/builder/src/builderStore/schemaGenerator.js @@ -0,0 +1,56 @@ +import { FIELDS } from "constants/backend" + +function baseConversion(type) { + if (type === "string") { + return { + type: FIELDS.STRING.type, + } + } else if (type === "boolean") { + return { + type: FIELDS.BOOLEAN.type, + } + } else if (type === "number") { + return { + type: FIELDS.NUMBER.type, + } + } +} + +function recurse(schemaLevel = {}, objectLevel) { + if (!objectLevel) { + return null + } + const baseType = typeof objectLevel + if (baseType !== "object") { + return baseConversion(baseType) + } + for (let [key, value] of Object.entries(objectLevel)) { + const type = typeof value + // check array first, since arrays are objects + if (Array.isArray(value)) { + const schema = recurse(schemaLevel[key], value[0]) + if (schema) { + schemaLevel[key] = { + type: FIELDS.ARRAY.type, + schema, + } + } + } else if (type === "object") { + const schema = recurse(schemaLevel[key], objectLevel[key]) + if (schema) { + schemaLevel[key] = schema + } + } else { + schemaLevel[key] = baseConversion(type) + } + } + if (!schemaLevel.type) { + return { type: FIELDS.JSON.type, schema: schemaLevel } + } else { + return schemaLevel + } +} + +export function generate(object) { + return recurse({}, object).schema +} diff --git a/packages/builder/src/builderStore/store/frontend.js b/packages/builder/src/builderStore/store/frontend.js index 9f1a20605f..fdfe450edf 100644 --- a/packages/builder/src/builderStore/store/frontend.js +++ b/packages/builder/src/builderStore/store/frontend.js @@ -26,7 +26,7 @@ import { findAllMatchingComponents, findComponent, getComponentSettings, -} from "../storeUtils" +} from "../componentUtils" import { uuid } from "../uuid" import { removeBindings } from "../dataBinding" @@ -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) @@ -537,7 +537,7 @@ export const getFrontendStore = () => { // immediately need to remove bindings, currently these aren't valid when pasted if (!cut && !preserveBindings) { - state.componentToPaste = removeBindings(state.componentToPaste) + state.componentToPaste = removeBindings(state.componentToPaste, "") } // Clone the component to paste diff --git a/packages/builder/src/builderStore/store/screenTemplates/utils/commonComponents.js b/packages/builder/src/builderStore/store/screenTemplates/utils/commonComponents.js index 5b3bc041ff..ae45b4f25d 100644 --- a/packages/builder/src/builderStore/store/screenTemplates/utils/commonComponents.js +++ b/packages/builder/src/builderStore/store/screenTemplates/utils/commonComponents.js @@ -137,6 +137,7 @@ const fieldTypeToComponentMap = { datetime: "datetimefield", attachment: "attachmentfield", link: "relationshipfield", + json: "jsonfield", } export function makeDatasourceFormComponents(datasource) { @@ -146,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 = diff --git a/packages/builder/src/components/backend/DataTable/RowFieldControl.svelte b/packages/builder/src/components/backend/DataTable/RowFieldControl.svelte index 25ad67b52e..0d9ca3644b 100644 --- a/packages/builder/src/components/backend/DataTable/RowFieldControl.svelte +++ b/packages/builder/src/components/backend/DataTable/RowFieldControl.svelte @@ -6,16 +6,20 @@ Toggle, TextArea, Multiselect, + Label, } from "@budibase/bbui" import Dropzone from "components/common/Dropzone.svelte" import { capitalise } from "helpers" import LinkedRowSelector from "components/common/LinkedRowSelector.svelte" + import Editor from "../../integration/QueryEditor.svelte" export let defaultValue export let meta export let value = defaultValue || (meta.type === "boolean" ? false : "") export let readonly + $: stringVal = + typeof value === "object" ? JSON.stringify(value, null, 2) : value $: type = meta?.type $: label = meta.name ? capitalise(meta.name) : "" @@ -40,6 +44,14 @@ {:else if type === "longform"}