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/package.json b/packages/server/package.json
index 32d9c5c7aa..d2ccad4e57 100644
--- a/packages/server/package.json
+++ b/packages/server/package.json
@@ -1,7 +1,7 @@
{
"name": "@budibase/server",
"email": "hi@budibase.com",
- "version": "1.0.27-alpha.0",
+ "version": "1.0.27-alpha.2",
"description": "Budibase Web Server",
"main": "src/index.ts",
"repository": {
@@ -70,9 +70,9 @@
"license": "GPL-3.0",
"dependencies": {
"@apidevtools/swagger-parser": "^10.0.3",
- "@budibase/auth": "^1.0.27-alpha.0",
- "@budibase/client": "^1.0.27-alpha.0",
- "@budibase/string-templates": "^1.0.27-alpha.0",
+ "@budibase/auth": "^1.0.27-alpha.2",
+ "@budibase/client": "^1.0.27-alpha.2",
+ "@budibase/string-templates": "^1.0.27-alpha.2",
"@bull-board/api": "^3.7.0",
"@bull-board/koa": "^3.7.0",
"@elastic/elasticsearch": "7.10.0",
diff --git a/packages/server/src/api/controllers/row/internalSearch.js b/packages/server/src/api/controllers/row/internalSearch.js
index 793454e601..d8ae77156a 100644
--- a/packages/server/src/api/controllers/row/internalSearch.js
+++ b/packages/server/src/api/controllers/row/internalSearch.js
@@ -191,7 +191,8 @@ class QueryBuilder {
}
if (this.query.equal) {
build(this.query.equal, (key, value) => {
- if (!value) {
+ // 0 evaluates to false, which means we would return all rows if we don't check it
+ if (!value && value !== 0) {
return null
}
return `${key}:${builder.preprocess(value, allPreProcessingOpts)}`
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/package.json b/packages/string-templates/package.json
index 08c2fd20d6..ce1da2cb67 100644
--- a/packages/string-templates/package.json
+++ b/packages/string-templates/package.json
@@ -1,6 +1,6 @@
{
"name": "@budibase/string-templates",
- "version": "1.0.27-alpha.0",
+ "version": "1.0.27-alpha.2",
"description": "Handlebars wrapper for Budibase templating.",
"main": "src/index.cjs",
"module": "dist/bundle.mjs",
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} The structure input, as fully updated as possible.
*/
-module.exports.processObject = async (
- object,
- context,
- opts = { noHelpers: false }
-) => {
+module.exports.processObject = async (object, context, opts) => {
testObject(object)
for (let key of Object.keys(object || {})) {
if (object[key] != null) {
@@ -63,11 +60,7 @@ module.exports.processObject = async (
* @param {object} opts optional - specify some options for processing.
* @returns {Promise} The enriched string, all templates should have been replaced if they can be.
*/
-module.exports.processString = async (
- string,
- context,
- opts = { noHelpers: false }
-) => {
+module.exports.processString = async (string, context, opts) => {
// TODO: carry out any async calls before carrying out async call
return module.exports.processStringSync(string, context, opts)
}
@@ -81,11 +74,7 @@ module.exports.processString = async (
* @param {object} opts optional - specify some options for processing.
* @returns {object|array} The structure input, as fully updated as possible.
*/
-module.exports.processObjectSync = (
- object,
- context,
- opts = { noHelpers: false }
-) => {
+module.exports.processObjectSync = (object, context, opts) => {
testObject(object)
for (let key of Object.keys(object || {})) {
let val = object[key]
@@ -106,26 +95,20 @@ module.exports.processObjectSync = (
* @param {object} opts optional - specify some options for processing.
* @returns {string} The enriched string, all templates should have been replaced if they can be.
*/
-module.exports.processStringSync = (
- string,
- context,
- opts = { noHelpers: false }
-) => {
- if (!exports.isValid(string)) {
- return string
- }
- // take a copy of input incase error
+module.exports.processStringSync = (string, context, opts) => {
+ opts = { ...defaultOpts, ...opts }
+
+ // take a copy of input in case of error
const input = string
if (typeof string !== "string") {
throw "Cannot process non-string types."
}
try {
- const noHelpers = opts && opts.noHelpers
// finalising adds a helper, can't do this with no helpers
- const shouldFinalise = !noHelpers
+ const shouldFinalise = !opts.noHelpers
string = processors.preprocess(string, shouldFinalise)
// this does not throw an error when template can't be fulfilled, have to try correct beforehand
- const instance = noHelpers ? hbsInstanceNoHelpers : hbsInstance
+ const instance = opts.noHelpers ? hbsInstanceNoHelpers : hbsInstance
const template = instance.compile(string, {
strict: false,
})
@@ -136,7 +119,7 @@ module.exports.processStringSync = (
})
)
} catch (err) {
- return removeHandlebarsStatements(input)
+ return input
}
}
@@ -155,7 +138,8 @@ module.exports.makePropSafe = property => {
* @param opts optional - specify some options for processing.
* @returns {boolean} Whether or not the input string is valid.
*/
-module.exports.isValid = (string, opts = { noHelpers: false }) => {
+module.exports.isValid = (string, opts) => {
+ opts = { ...defaultOpts, ...opts }
const validCases = [
"string",
"number",
@@ -169,7 +153,7 @@ module.exports.isValid = (string, opts = { noHelpers: false }) => {
// don't really need a real context to check if its valid
const context = {}
try {
- const instance = opts && opts.noHelpers ? hbsInstanceNoHelpers : hbsInstance
+ const instance = opts.noHelpers ? hbsInstanceNoHelpers : hbsInstance
instance.compile(processors.preprocess(string, false))(context)
return true
} catch (err) {
diff --git a/packages/string-templates/src/utilities.js b/packages/string-templates/src/utilities.js
index 50d770c6ea..645aca78ba 100644
--- a/packages/string-templates/src/utilities.js
+++ b/packages/string-templates/src/utilities.js
@@ -10,7 +10,10 @@ module.exports.swapStrings = (string, start, length, swap) => {
return string.slice(0, start) + swap + string.slice(start + length)
}
-module.exports.removeHandlebarsStatements = string => {
+module.exports.removeHandlebarsStatements = (
+ string,
+ replacement = "Invalid binding"
+) => {
let regexp = new RegExp(exports.FIND_HBS_REGEX)
let matches = string.match(regexp)
if (matches == null) {
@@ -18,7 +21,7 @@ module.exports.removeHandlebarsStatements = string => {
}
for (let match of matches) {
const idx = string.indexOf(match)
- string = exports.swapStrings(string, idx, match.length, "Invalid Binding")
+ string = exports.swapStrings(string, idx, match.length, replacement)
}
return string
}
diff --git a/packages/string-templates/test/helpers.spec.js b/packages/string-templates/test/helpers.spec.js
index 2cf9310638..b4179475fb 100644
--- a/packages/string-templates/test/helpers.spec.js
+++ b/packages/string-templates/test/helpers.spec.js
@@ -13,10 +13,14 @@ describe("test the custom helpers we have applied", () => {
describe("test that it can run without helpers", () => {
it("should be able to run without helpers", async () => {
- const output = await processString("{{ avg 1 1 1 }}", {}, { noHelpers: true })
+ const output = await processString(
+ "{{ avg 1 1 1 }}",
+ {},
+ { noHelpers: true }
+ )
const valid = await processString("{{ avg 1 1 1 }}", {})
expect(valid).toBe("1")
- expect(output).toBe("Invalid Binding")
+ expect(output).toBe("{{ avg 1 1 1 }}")
})
})
@@ -185,17 +189,22 @@ describe("test the date helpers", () => {
it("should test the timezone capabilities", async () => {
const date = new Date(1611577535000)
- const output = await processString("{{ date time 'HH-mm-ss Z' 'America/New_York' }}", {
- time: date.toUTCString(),
- })
- const formatted = new dayjs(date).tz("America/New_York").format("HH-mm-ss Z")
+ const output = await processString(
+ "{{ date time 'HH-mm-ss Z' 'America/New_York' }}",
+ {
+ time: date.toUTCString(),
+ }
+ )
+ const formatted = new dayjs(date)
+ .tz("America/New_York")
+ .format("HH-mm-ss Z")
expect(output).toBe(formatted)
})
it("should guess the users timezone when not specified", async () => {
const date = new Date()
const output = await processString("{{ date time 'Z' }}", {
- time: date.toUTCString()
+ time: date.toUTCString(),
})
const timezone = dayjs.tz.guess()
const offset = new dayjs(date).tz(timezone).format("Z")
@@ -307,12 +316,12 @@ describe("test the comparison helpers", () => {
describe("Test the object/array helper", () => {
it("should allow plucking from an array of objects", async () => {
const context = {
- items: [
- { price: 20 },
- { price: 30 },
- ]
+ items: [{ price: 20 }, { price: 30 }],
}
- const output = await processString("{{ literal ( sum ( pluck items 'price' ) ) }}", context)
+ const output = await processString(
+ "{{ literal ( sum ( pluck items 'price' ) ) }}",
+ context
+ )
expect(output).toBe(50)
})
@@ -442,15 +451,15 @@ describe("Cover a few complex use cases", () => {
it("should only invalidate a single string in an object", async () => {
const input = {
- dataProvider:"{{ literal [c670254c9e74e40518ee5becff53aa5be] }}",
- theme:"spectrum--lightest",
- showAutoColumns:false,
- quiet:true,
- size:"spectrum--medium",
- rowCount:8,
+ dataProvider: "{{ literal [c670254c9e74e40518ee5becff53aa5be] }}",
+ theme: "spectrum--lightest",
+ showAutoColumns: false,
+ quiet: true,
+ size: "spectrum--medium",
+ rowCount: 8,
}
const output = await processObject(input, tableJson)
- expect(output.dataProvider).not.toBe("Invalid Binding")
+ expect(output.dataProvider).not.toBe("Invalid binding")
})
it("should be able to handle external ids", async () => {
diff --git a/packages/worker/package.json b/packages/worker/package.json
index 0e856c68bb..f0a32afb09 100644
--- a/packages/worker/package.json
+++ b/packages/worker/package.json
@@ -1,7 +1,7 @@
{
"name": "@budibase/worker",
"email": "hi@budibase.com",
- "version": "1.0.27-alpha.0",
+ "version": "1.0.27-alpha.2",
"description": "Budibase background service",
"main": "src/index.js",
"repository": {
@@ -29,8 +29,8 @@
"author": "Budibase",
"license": "GPL-3.0",
"dependencies": {
- "@budibase/auth": "^1.0.27-alpha.0",
- "@budibase/string-templates": "^1.0.27-alpha.0",
+ "@budibase/auth": "^1.0.27-alpha.2",
+ "@budibase/string-templates": "^1.0.27-alpha.2",
"@koa/router": "^8.0.0",
"@sentry/node": "^6.0.0",
"@techpass/passport-openidconnect": "^0.3.0",
diff --git a/packages/worker/src/api/routes/tests/email.spec.js b/packages/worker/src/api/routes/tests/email.spec.js
index c8c93658f7..a66857249a 100644
--- a/packages/worker/src/api/routes/tests/email.spec.js
+++ b/packages/worker/src/api/routes/tests/email.spec.js
@@ -8,7 +8,7 @@ jest.mock("nodemailer")
const nodemailer = require("nodemailer")
nodemailer.createTransport.mockReturnValue({
sendMail: sendMailMock,
- verify: jest.fn()
+ verify: jest.fn(),
})
describe("/api/global/email", () => {
@@ -39,6 +39,6 @@ describe("/api/global/email", () => {
expect(sendMailMock).toHaveBeenCalled()
const emailCall = sendMailMock.mock.calls[0][0]
expect(emailCall.subject).toBe("Hello!")
- expect(emailCall.html).not.toContain("Invalid Binding")
+ expect(emailCall.html).not.toContain("Invalid binding")
})
-})
\ No newline at end of file
+})