diff --git a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/index.js b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/index.js
deleted file mode 100644
index cca8ece484..0000000000
--- a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/actions/index.js
+++ /dev/null
@@ -1,80 +0,0 @@
-import { store } from "builderStore"
-import { get } from "svelte/store"
-
-import NavigateTo from "./NavigateTo.svelte"
-import SaveRow from "./SaveRow.svelte"
-import DeleteRow from "./DeleteRow.svelte"
-import ExecuteQuery from "./ExecuteQuery.svelte"
-import TriggerAutomation from "./TriggerAutomation.svelte"
-import ValidateForm from "./ValidateForm.svelte"
-import LogOut from "./LogOut.svelte"
-import ClearForm from "./ClearForm.svelte"
-import CloseScreenModal from "./CloseScreenModal.svelte"
-import ChangeFormStep from "./ChangeFormStep.svelte"
-import UpdateStateStep from "./UpdateState.svelte"
-import RefreshDataProvider from "./RefreshDataProvider.svelte"
-
-// Defines which actions are available to configure in the front end.
-// Unfortunately the "name" property is used as the identifier so please don't
-// change them.
-// The client library removes any spaces when processing actions, so they can
-// be considered as camel case too.
-// There is technical debt here to sanitize all these and standardise them
-// across the packages but it's a breaking change to existing apps.
-export const getAvailableActions = () => {
- let actions = [
- {
- name: "Save Row",
- component: SaveRow,
- },
- {
- name: "Delete Row",
- component: DeleteRow,
- },
- {
- name: "Navigate To",
- component: NavigateTo,
- },
- {
- name: "Execute Query",
- component: ExecuteQuery,
- },
- {
- name: "Trigger Automation",
- component: TriggerAutomation,
- },
- {
- name: "Validate Form",
- component: ValidateForm,
- },
- {
- name: "Log Out",
- component: LogOut,
- },
- {
- name: "Clear Form",
- component: ClearForm,
- },
- {
- name: "Close Screen Modal",
- component: CloseScreenModal,
- },
- {
- name: "Change Form Step",
- component: ChangeFormStep,
- },
- {
- name: "Refresh Data Provider",
- component: RefreshDataProvider,
- },
- ]
-
- if (get(store).clientFeatures?.state) {
- actions.push({
- name: "Update State",
- component: UpdateStateStep,
- })
- }
-
- return actions
-}
diff --git a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/index.js b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/index.js
deleted file mode 100644
index 8966c4ab26..0000000000
--- a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/EventsEditor/index.js
+++ /dev/null
@@ -1,2 +0,0 @@
-import EventsEditor from "./EventPropertyControl.svelte"
-export default EventsEditor
diff --git a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/FilterEditor/FilterDrawer.svelte b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/FilterEditor/FilterDrawer.svelte
index e30fd6d491..d9425c961d 100644
--- a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/FilterEditor/FilterDrawer.svelte
+++ b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/FilterEditor/FilterDrawer.svelte
@@ -21,7 +21,7 @@
export let panel = ClientBindingPanel
export let allowBindings = true
- const BannedTypes = ["link", "attachment", "formula"]
+ const BannedTypes = ["link", "attachment", "formula", "json", "jsonarray"]
$: fieldOptions = (schemaFields ?? [])
.filter(field => !BannedTypes.includes(field.type))
diff --git a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/FormFieldSelect.svelte b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/FormFieldSelect.svelte
index 5527941bd5..ba54de5478 100644
--- a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/FormFieldSelect.svelte
+++ b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/FormFieldSelect.svelte
@@ -5,7 +5,7 @@
getSchemaForDatasource,
} from "builderStore/dataBinding"
import { currentAsset } from "builderStore"
- import { findClosestMatchingComponent } from "builderStore/storeUtils"
+ import { findClosestMatchingComponent } from "builderStore/componentUtils"
export let componentInstance
export let value
diff --git a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/ResetFieldsButton.svelte b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/ResetFieldsButton.svelte
index d5b82176ce..fa2a0d6088 100644
--- a/packages/builder/src/components/design/PropertiesPanel/PropertyControls/ResetFieldsButton.svelte
+++ b/packages/builder/src/components/design/PropertiesPanel/PropertyControls/ResetFieldsButton.svelte
@@ -1,7 +1,7 @@
+
+
+ {componentText}
+ {#if closable}
+
+ {/if}
+
+
+
diff --git a/packages/client/src/components/app/blocks/CardsBlock.svelte b/packages/client/src/components/app/blocks/CardsBlock.svelte
index ec631ede36..3a136b5b20 100644
--- a/packages/client/src/components/app/blocks/CardsBlock.svelte
+++ b/packages/client/src/components/app/blocks/CardsBlock.svelte
@@ -73,7 +73,7 @@
enrichedFilter.push({
field: column.name,
operator: column.type === "string" ? "string" : "equal",
- type: "string",
+ type: column.type === "string" ? "string" : "number",
valueType: "Binding",
value: `{{ [${formId}].[${column.name}] }}`,
})
diff --git a/packages/client/src/components/app/blocks/TableBlock.svelte b/packages/client/src/components/app/blocks/TableBlock.svelte
index 55937d1b7f..1f561f9b5d 100644
--- a/packages/client/src/components/app/blocks/TableBlock.svelte
+++ b/packages/client/src/components/app/blocks/TableBlock.svelte
@@ -61,7 +61,7 @@
enrichedFilter.push({
field: column.name,
operator: column.type === "string" ? "string" : "equal",
- type: "string",
+ type: column.type === "string" ? "string" : "number",
valueType: "Binding",
value: `{{ ${safe(formId)}.${safe(column.name)} }}`,
})
diff --git a/packages/client/src/components/app/dynamic-filter/FilterModal.svelte b/packages/client/src/components/app/dynamic-filter/FilterModal.svelte
index 99b9a1a6f6..f303c69aaf 100644
--- a/packages/client/src/components/app/dynamic-filter/FilterModal.svelte
+++ b/packages/client/src/components/app/dynamic-filter/FilterModal.svelte
@@ -19,7 +19,7 @@
export let schemaFields
export let filters = []
- const BannedTypes = ["link", "attachment", "formula"]
+ const BannedTypes = ["link", "attachment", "formula", "json"]
$: fieldOptions = (schemaFields ?? [])
.filter(field => !BannedTypes.includes(field.type))
diff --git a/packages/client/src/components/app/forms/InnerForm.svelte b/packages/client/src/components/app/forms/InnerForm.svelte
index 7067d33e7d..47bc717d92 100644
--- a/packages/client/src/components/app/forms/InnerForm.svelte
+++ b/packages/client/src/components/app/forms/InnerForm.svelte
@@ -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,20 @@
})
}
+ // 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
+ __value: formValue,
+ __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 +94,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,13 +142,20 @@
}
// 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) {
const { fieldState } = get(existingField)
- initialValue = fieldState.value ?? initialValue
fieldId = fieldState.fieldId
+
+ // Use new default value if default value changed,
+ // otherwise use the current value if possible
+ if (defaultValue !== fieldState.defaultValue) {
+ initialValue = defaultValue
+ } else {
+ initialValue = fieldState.value ?? initialValue
+ }
}
// Auto columns are always disabled
@@ -130,6 +182,7 @@
disabled: disabled || fieldDisabled || isAutoColumn,
defaultValue,
validator,
+ lastUpdate: Date.now(),
},
fieldApi: makeFieldApi(field, defaultValue),
fieldSchema: schema?.[field] ?? {},
@@ -204,6 +257,7 @@
fieldInfo.update(state => {
state.fieldState.value = value
state.fieldState.error = error
+ state.fieldState.lastUpdate = Date.now()
return state
})
@@ -220,6 +274,7 @@
fieldInfo.update(state => {
state.fieldState.value = newValue
state.fieldState.error = null
+ state.fieldState.lastUpdate = Date.now()
return state
})
}
@@ -299,18 +354,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,
- }
diff --git a/packages/client/src/components/app/forms/JSONField.svelte b/packages/client/src/components/app/forms/JSONField.svelte
new file mode 100644
index 0000000000..d38a41b430
--- /dev/null
+++ b/packages/client/src/components/app/forms/JSONField.svelte
@@ -0,0 +1,71 @@
+
+
+
+ {#if fieldState}
+
+ fieldApi.setValue(parseValue(e.detail))}
+ disabled={fieldState.disabled}
+ error={fieldState.error}
+ id={fieldState.fieldId}
+ {placeholder}
+ />
+
+ {/if}
+
+
+
diff --git a/packages/client/src/components/app/forms/LongFormField.svelte b/packages/client/src/components/app/forms/LongFormField.svelte
index 81ad42bbcb..a58e1fe76c 100644
--- a/packages/client/src/components/app/forms/LongFormField.svelte
+++ b/packages/client/src/components/app/forms/LongFormField.svelte
@@ -1,6 +1,7 @@
{#if fieldState}
- fieldApi.setValue(e.detail)}
- disabled={fieldState.disabled}
- error={fieldState.error}
- id={fieldState.fieldId}
- {placeholder}
- />
+
+ fieldApi.setValue(e.detail)}
+ disabled={fieldState.disabled}
+ error={fieldState.error}
+ id={fieldState.fieldId}
+ {placeholder}
+ />
+
{/if}
+
+
diff --git a/packages/client/src/components/app/forms/index.js b/packages/client/src/components/app/forms/index.js
index f554f601d4..ab1f7d20ed 100644
--- a/packages/client/src/components/app/forms/index.js
+++ b/packages/client/src/components/app/forms/index.js
@@ -11,3 +11,4 @@ export { default as attachmentfield } from "./AttachmentField.svelte"
export { default as relationshipfield } from "./RelationshipField.svelte"
export { default as passwordfield } from "./PasswordField.svelte"
export { default as formstep } from "./FormStep.svelte"
+export { default as jsonfield } from "./JSONField.svelte"
diff --git a/packages/client/src/components/app/forms/validation.js b/packages/client/src/components/app/forms/validation.js
index f461ab00c0..4e06a640e9 100644
--- a/packages/client/src/components/app/forms/validation.js
+++ b/packages/client/src/components/app/forms/validation.js
@@ -206,6 +206,7 @@ const parseType = (value, type) => {
return value
}
+ // Parse array, treating no elements as null
if (type === FieldTypes.ARRAY) {
if (!Array.isArray(value) || !value.length) {
return null
@@ -213,6 +214,12 @@ const parseType = (value, type) => {
return value
}
+ // For JSON we don't touch the value at all as we want to verify it in its
+ // raw form
+ if (type === FieldTypes.JSON) {
+ return value
+ }
+
// If some unknown type, treat as null to avoid breaking validators
return null
}
@@ -290,6 +297,19 @@ const notContainsHandler = (value, rule) => {
return !containsHandler(value, rule)
}
+// Evaluates a constraint that the value must be a valid json object
+const jsonHandler = value => {
+ if (typeof value !== "object" || Array.isArray(value)) {
+ return false
+ }
+ try {
+ JSON.parse(JSON.stringify(value))
+ return true
+ } catch (error) {
+ return false
+ }
+}
+
/**
* Map of constraint types to handlers.
*/
@@ -306,6 +326,7 @@ const handlerMap = {
notRegex: notRegexHandler,
contains: containsHandler,
notContains: notContainsHandler,
+ json: jsonHandler,
}
/**
diff --git a/packages/client/src/components/app/index.js b/packages/client/src/components/app/index.js
index f5d085e97b..ef0f96ce59 100644
--- a/packages/client/src/components/app/index.js
+++ b/packages/client/src/components/app/index.js
@@ -29,6 +29,7 @@ export { default as backgroundimage } from "./BackgroundImage.svelte"
export { default as daterangepicker } from "./DateRangePicker.svelte"
export { default as cardstat } from "./CardStat.svelte"
export { default as spectrumcard } from "./SpectrumCard.svelte"
+export { default as tag } from "./Tag.svelte"
export * from "./charts"
export * from "./forms"
export * from "./table"
diff --git a/packages/client/src/components/preview/Indicator.svelte b/packages/client/src/components/preview/Indicator.svelte
index b97204abbc..ee969e3395 100644
--- a/packages/client/src/components/preview/Indicator.svelte
+++ b/packages/client/src/components/preview/Indicator.svelte
@@ -24,6 +24,7 @@
class:flipped
class:line
style="top: {top}px; left: {left}px; width: {width}px; height: {height}px; --color: {color}; --zIndex: {zIndex};"
+ class:withText={!!text}
>
{#if text}
@@ -39,12 +40,12 @@
z-index: var(--zIndex);
border: 2px solid var(--color);
pointer-events: none;
- border-top-right-radius: 4px;
- border-top-left-radius: 0;
- border-bottom-left-radius: 4px;
- border-bottom-right-radius: 4px;
+ border-radius: 4px;
}
- .indicator.flipped {
+ .indicator.withText {
+ border-top-left-radius: 0;
+ }
+ .indicator.withText.flipped {
border-top-left-radius: 4px;
}
.indicator.line {
@@ -74,8 +75,7 @@
border-radius: 4px;
}
.text.flipped {
- border-top-left-radius: 4px;
- border-bottom-left-radius: 4px;
+ border-radius: 4px;
transform: translateY(0%);
top: -2px;
}
diff --git a/packages/client/src/constants.js b/packages/client/src/constants.js
index 740f279b36..9d20177b52 100644
--- a/packages/client/src/constants.js
+++ b/packages/client/src/constants.js
@@ -13,6 +13,7 @@ export const FieldTypes = {
ATTACHMENT: "attachment",
LINK: "link",
FORMULA: "formula",
+ JSON: "json",
}
export const UnsortableTypes = [
diff --git a/packages/client/src/stores/dataSource.js b/packages/client/src/stores/dataSource.js
index efec755b99..46ac0b6c86 100644
--- a/packages/client/src/stores/dataSource.js
+++ b/packages/client/src/stores/dataSource.js
@@ -61,7 +61,8 @@ export const createDataSourceStore = () => {
// Emit this as a window event, so parent screens which are iframing us in
// can also invalidate the same datasource
- if (get(routeStore).queryParams?.peek) {
+ const inModal = get(routeStore).queryParams?.peek
+ if (inModal) {
window.parent.postMessage({
type: "invalidate-datasource",
detail: { dataSourceId },
diff --git a/packages/client/src/utils/buttonActions.js b/packages/client/src/utils/buttonActions.js
index 9c6ae73c1f..6b4dd4235a 100644
--- a/packages/client/src/utils/buttonActions.js
+++ b/packages/client/src/utils/buttonActions.js
@@ -8,20 +8,49 @@ import {
} from "stores"
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
+ let payload
if (providerId) {
- let draft = context[providerId]
+ payload = { ...context[providerId] }
+ } else {
+ payload = {}
+ }
+ if (fields) {
+ for (let [field, value] of Object.entries(fields)) {
+ deepSet(payload, field, value)
+ }
+ }
+ if (tableId) {
+ payload.tableId = tableId
+ }
+ const row = await saveRow(payload)
+ return {
+ row,
+ }
+}
+
+const duplicateRowHandler = async (action, context) => {
+ const { fields, providerId, tableId } = action.parameters
+ if (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 payload._id
+ delete payload._rev
+ const row = await saveRow(payload)
+ return {
+ row,
}
- await saveRow(draft)
}
}
@@ -46,11 +75,12 @@ const navigationHandler = action => {
const queryExecutionHandler = async action => {
const { datasourceId, queryId, queryParams } = action.parameters
- await executeQuery({
+ const result = await executeQuery({
datasourceId,
queryId,
parameters: queryParams,
})
+ return { result }
}
const executeActionHandler = async (
@@ -129,6 +159,7 @@ const updateStateHandler = action => {
const handlerMap = {
["Save Row"]: saveRowHandler,
+ ["Duplicate Row"]: duplicateRowHandler,
["Delete Row"]: deleteRowHandler,
["Navigate To"]: navigationHandler,
["Execute Query"]: queryExecutionHandler,
@@ -165,12 +196,27 @@ export const enrichButtonActions = (actions, context) => {
return actions
}
+ // Button context is built up as actions are executed.
+ // Inherit any previous button context which may have come from actions
+ // before a confirmable action since this breaks the chain.
+ let buttonContext = context.actions || []
+
const handlers = actions.map(def => handlerMap[def["##eventHandlerType"]])
return async () => {
for (let i = 0; i < handlers.length; i++) {
try {
- const action = actions[i]
- const callback = async () => handlers[i](action, context)
+ // Skip any non-existent action definitions
+ if (!handlers[i]) {
+ continue
+ }
+
+ // Built total context for this action
+ const totalContext = { ...context, actions: buttonContext }
+
+ // Get and enrich this button action with the total context
+ let action = actions[i]
+ action = enrichDataBindings(action, totalContext)
+ const callback = async () => handlers[i](action, totalContext)
// If this action is confirmable, show confirmation and await a
// callback to execute further actions
@@ -185,7 +231,15 @@ export const enrichButtonActions = (actions, context) => {
// then execute the rest of the actions in the chain
const result = await callback()
if (result !== false) {
- const next = enrichButtonActions(actions.slice(i + 1), context)
+ // Generate a new total context to pass into the next enrichment
+ buttonContext.push(result)
+ const newContext = { ...context, actions: buttonContext }
+
+ // Enrich and call the next button action
+ const next = enrichButtonActions(
+ actions.slice(i + 1),
+ newContext
+ )
await next()
}
}
@@ -201,6 +255,8 @@ export const enrichButtonActions = (actions, context) => {
const result = await callback()
if (result === false) {
return
+ } else {
+ buttonContext.push(result)
}
}
} catch (error) {
diff --git a/packages/client/src/utils/componentProps.js b/packages/client/src/utils/componentProps.js
index d600fdef04..b6b2cf7a99 100644
--- a/packages/client/src/utils/componentProps.js
+++ b/packages/client/src/utils/componentProps.js
@@ -32,35 +32,56 @@ export const enrichProps = (props, context) => {
data: context[context.closestComponentId],
}
- // Enrich all data bindings in top level props
- let enrichedProps = enrichDataBindings(props, totalContext)
-
- // Enrich click actions if they exist
- Object.keys(enrichedProps).forEach(prop => {
+ // We want to exclude any button actions from enrichment at this stage.
+ // Extract top level button action settings.
+ let normalProps = { ...props }
+ let actionProps = {}
+ Object.keys(normalProps).forEach(prop => {
if (prop?.toLowerCase().includes("onclick")) {
- enrichedProps[prop] = enrichButtonActions(
- enrichedProps[prop],
- totalContext
- )
+ actionProps[prop] = normalProps[prop]
+ delete normalProps[prop]
}
})
- // Enrich any click actions in conditions
- if (enrichedProps._conditions) {
- enrichedProps._conditions.forEach(condition => {
- if (condition.setting?.toLowerCase().includes("onclick")) {
- condition.settingValue = enrichButtonActions(
- condition.settingValue,
- totalContext
- )
+ // Handle conditional UI separately after normal settings
+ let conditions = normalProps._conditions
+ delete normalProps._conditions
- // If there is an onclick function in here then it won't be serialised
- // properly, and therefore will not be updated properly.
- // The solution to this is add a rand which will ensure diffs happen
- // every time.
- condition.rand = Math.random()
+ // Enrich all props except button actions
+ let enrichedProps = enrichDataBindings(normalProps, totalContext)
+
+ // Enrich button actions.
+ // Actions are enriched into a function at this stage, but actual data
+ // binding enrichment is done dynamically at runtime.
+ Object.keys(actionProps).forEach(prop => {
+ enrichedProps[prop] = enrichButtonActions(actionProps[prop], totalContext)
+ })
+
+ // Conditions
+ if (conditions?.length) {
+ let enrichedConditions = []
+ conditions.forEach(condition => {
+ if (condition.setting?.toLowerCase().includes("onclick")) {
+ // Copy and remove the setting value from the condition as it needs
+ // enriched separately
+ let toEnrich = { ...condition }
+ delete toEnrich.settingValue
+
+ // Join the condition back together
+ enrichedConditions.push({
+ ...enrichDataBindings(toEnrich, totalContext),
+ settingValue: enrichButtonActions(
+ condition.settingValue,
+ totalContext
+ ),
+ rand: Math.random(),
+ })
+ } else {
+ // Normal condition
+ enrichedConditions.push(enrichDataBindings(condition, totalContext))
}
})
+ enrichedProps._conditions = enrichedConditions
}
return enrichedProps
diff --git a/packages/client/yarn.lock b/packages/client/yarn.lock
index e57aed8eca..f61cc01f61 100644
--- a/packages/client/yarn.lock
+++ b/packages/client/yarn.lock
@@ -305,6 +305,11 @@
resolved "https://registry.yarnpkg.com/@spectrum-css/tabs/-/tabs-3.1.5.tgz#cc82e69c1fc721902345178231fb95d05938b983"
integrity sha512-UtfW8bA1quYnJM6v/lp6AVYGnQFkiUix2FHAf/4VHVrk4mh7ydtLiXS0IR3Kx+t/S8FWdSdSQHDZ8tHbY1ZLZg==
+"@spectrum-css/tag@^3.1.4":
+ version "3.1.4"
+ resolved "https://registry.yarnpkg.com/@spectrum-css/tag/-/tag-3.1.4.tgz#334384dd789ddf0562679cae62ef763883480ac5"
+ integrity sha512-9dYBMhCEkjy+p75XJIfCA2/zU4JAqsJrL7fkYIDXakS6/BzeVtIvAW/6JaIHtLIA9lrj0Sn4m+ZjceKnZNIv1w==
+
"@spectrum-css/tags@^3.0.2":
version "3.0.3"
resolved "https://registry.yarnpkg.com/@spectrum-css/tags/-/tags-3.0.3.tgz#fc76d2735cdc442de91b7eb3bee49a928c0767ac"
diff --git a/packages/server/src/api/controllers/row/utils.js b/packages/server/src/api/controllers/row/utils.js
index f7a4b13304..71b22375f7 100644
--- a/packages/server/src/api/controllers/row/utils.js
+++ b/packages/server/src/api/controllers/row/utils.js
@@ -50,10 +50,10 @@ exports.validate = async ({ appId, tableId, row, table }) => {
const errors = {}
for (let fieldName of Object.keys(table.schema)) {
const constraints = cloneDeep(table.schema[fieldName].constraints)
+ const type = table.schema[fieldName].type
// special case for options, need to always allow unselected (null)
if (
- table.schema[fieldName].type ===
- (FieldTypes.OPTIONS || FieldTypes.ARRAY) &&
+ (type === FieldTypes.OPTIONS || type === FieldTypes.ARRAY) &&
constraints.inclusion
) {
constraints.inclusion.push(null)
@@ -61,17 +61,20 @@ exports.validate = async ({ appId, tableId, row, table }) => {
let res
// Validate.js doesn't seem to handle array
- if (
- table.schema[fieldName].type === FieldTypes.ARRAY &&
- row[fieldName] &&
- row[fieldName].length
- ) {
+ if (type === FieldTypes.ARRAY && row[fieldName] && row[fieldName].length) {
row[fieldName].map(val => {
if (!constraints.inclusion.includes(val)) {
errors[fieldName] = "Field not in list"
}
})
- } else if (table.schema[fieldName].type === FieldTypes.FORMULA) {
+ } else if (type === FieldTypes.JSON && typeof row[fieldName] === "string") {
+ // this should only happen if there is an error
+ try {
+ JSON.parse(row[fieldName])
+ } catch (err) {
+ errors[fieldName] = [`Contains invalid JSON`]
+ }
+ } else if (type === FieldTypes.FORMULA) {
res = validateJs.single(
processStringSync(table.schema[fieldName].formula, row),
constraints
diff --git a/packages/server/src/utilities/rowProcessor/index.js b/packages/server/src/utilities/rowProcessor/index.js
index ea63c23f7d..860063f173 100644
--- a/packages/server/src/utilities/rowProcessor/index.js
+++ b/packages/server/src/utilities/rowProcessor/index.js
@@ -81,6 +81,18 @@ const TYPE_TRANSFORM_MAP = {
[FieldTypes.AUTO]: {
parse: () => undefined,
},
+ [FieldTypes.JSON]: {
+ parse: input => {
+ try {
+ if (input === "") {
+ return undefined
+ }
+ return JSON.parse(input)
+ } catch (err) {
+ return input
+ }
+ },
+ },
}
/**
diff --git a/packages/string-templates/src/helpers/javascript.js b/packages/string-templates/src/helpers/javascript.js
index 42e8a1508a..9231283e89 100644
--- a/packages/string-templates/src/helpers/javascript.js
+++ b/packages/string-templates/src/helpers/javascript.js
@@ -1,4 +1,5 @@
const { atob } = require("../utilities")
+const { cloneDeep } = require("lodash/fp")
// The method of executing JS scripts depends on the bundle being built.
// This setter is used in the entrypoint (either index.cjs or index.mjs).
@@ -38,8 +39,12 @@ module.exports.processJS = (handlebars, context) => {
// This is required to allow the final `return` statement to be valid.
const js = `function run(){${atob(handlebars)}};run();`
- // Our $ context function gets a value from context
- const sandboxContext = { $: path => getContextValue(path, context) }
+ // Our $ context function gets a value from context.
+ // We clone the context to avoid mutation in the binding affecting real
+ // app context.
+ const sandboxContext = {
+ $: path => getContextValue(path, cloneDeep(context)),
+ }
// Create a sandbox with out context and run the JS
return runJS(js, sandboxContext)
diff --git a/packages/string-templates/src/index.js b/packages/string-templates/src/index.js
index 2523b763ba..820b8da290 100644
--- a/packages/string-templates/src/index.js
+++ b/packages/string-templates/src/index.js
@@ -1,12 +1,13 @@
const handlebars = require("handlebars")
const { registerAll } = require("./helpers/index")
const processors = require("./processors")
-const { removeHandlebarsStatements, atob, btoa } = require("./utilities")
+const { atob, btoa } = require("./utilities")
const manifest = require("../manifest.json")
const hbsInstance = handlebars.create()
registerAll(hbsInstance)
const hbsInstanceNoHelpers = handlebars.create()
+const defaultOpts = { noHelpers: false }
/**
* utility function to check if the object is valid
@@ -28,11 +29,7 @@ function testObject(object) {
* @param {object} opts optional - specify some options for processing.
* @returns {Promise