Skip to content

Commit 2997789

Browse files
authored
fix: if() formulas in the Application Builder silently rendering nothing when the condition references an empty field (baserow#5241)
1 parent 87c1487 commit 2997789

3 files changed

Lines changed: 103 additions & 8 deletions

File tree

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"type": "bug",
3+
"message": "Fixes if() formulas in the Application Builder silently rendering nothing when the condition references an empty field",
4+
"issue_origin": "github",
5+
"issue_number": null,
6+
"domain": "builder",
7+
"bullet_points": [],
8+
"created_at": "2026-04-21"
9+
}

web-frontend/modules/core/utils/object.js

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -111,26 +111,31 @@ export function isPromise(p) {
111111
/**
112112
* Get the value at `path` of `obj`, similar to Lodash `get` function.
113113
*
114-
* @param {Object} obj The object that holds the value
114+
* @param {Object} context The object that holds the value
115115
* @param {string | Array[string]} path The path to the value or a list with the path parts
116-
* @param {any} defaultValue The value to return if the path is not found
117-
* @return {Object} The value held by the path
116+
* @param {any} defaultValue The value to return if the path is not found (defaults to `null`)
117+
* @return {Object} The value held by the path, or `defaultValue` if not found
118118
*/
119-
export function getValueAtPath(context, path) {
119+
export function getValueAtPath(context, path, defaultValue = null) {
120120
function _getValueAtPath(obj, keys) {
121121
const [first, ...rest] = keys
122122
if (first === undefined || first === null) {
123123
return obj
124124
}
125125

126126
if (obj === null || obj === undefined) {
127-
throw new Error(`Path '${path}' not found in context '${obj}'`)
127+
return defaultValue
128128
}
129129

130130
if (first in obj) {
131131
return _getValueAtPath(obj[first], rest)
132132
}
133133
if (Array.isArray(obj) && first === '*') {
134+
// When wildcarding an empty array with no remaining path parts,
135+
// return an empty list (mirrors the backend's `get_value_at_path`).
136+
if (obj.length === 0 && rest.length === 0) {
137+
return []
138+
}
134139
const results = obj
135140
// Call recursively this function transforming the `*` in the path in a list
136141
// of indexes present in the object, e.g:
@@ -139,10 +144,9 @@ export function getValueAtPath(context, path) {
139144
// Remove empty results
140145
// Note: Don't exclude false values such as booleans, empty strings, etc.
141146
.filter((result) => result !== null && result !== undefined)
142-
// Return null in case there are no results
143-
return results.length ? results : null
147+
return results.length ? results : defaultValue
144148
}
145-
return null
149+
return defaultValue
146150
}
147151
const keys = typeof path === 'string' ? _.toPath(path) : path
148152
return _getValueAtPath(context, keys)

web-frontend/test/unit/core/utils/object.spec.js

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,4 +110,86 @@ describe('test utils object', () => {
110110
}
111111
expect(getValueAtPath(obj, path)).toStrictEqual(result)
112112
})
113+
114+
describe('getValueAtPath defaultValue', () => {
115+
const obj = {
116+
a: { b: { c: 123 } },
117+
nullable: null,
118+
list: [{ d: 456 }, { d: 789, e: 111 }],
119+
nested: [{ nested: [{ a: 1 }, { a: 2 }] }, { nested: [{ a: 3 }] }],
120+
}
121+
122+
test('returns null by default when the path is not found', () => {
123+
expect(getValueAtPath(obj, 'a.b.x')).toBe(null)
124+
expect(getValueAtPath(obj, 'a.x.b')).toBe(null)
125+
expect(getValueAtPath(obj, 'list.5.d')).toBe(null)
126+
expect(getValueAtPath(obj, 'list.0.x')).toBe(null)
127+
expect(getValueAtPath(obj, 'nested.0.nested.5.a')).toBe(null)
128+
expect(getValueAtPath(obj, 'nested.5.nested.0.a')).toBe(null)
129+
})
130+
131+
test('returns the provided default value when the path is not found', () => {
132+
expect(getValueAtPath(obj, 'a.b.x', 'fallback')).toBe('fallback')
133+
expect(getValueAtPath(obj, 'a.b.x', 0)).toBe(0)
134+
expect(getValueAtPath(obj, 'a.b.x', '')).toBe('')
135+
expect(getValueAtPath(obj, 'a.b.x', false)).toBe(false)
136+
expect(getValueAtPath(obj, 'list.5.d', 'fallback')).toBe('fallback')
137+
expect(getValueAtPath(obj, 'list.0.x', 'fallback')).toBe('fallback')
138+
expect(getValueAtPath(obj, 'nested.0.nested.5.a', 'fallback')).toBe(
139+
'fallback'
140+
)
141+
expect(getValueAtPath(obj, 'nested.5.nested.0.a', 'fallback')).toBe(
142+
'fallback'
143+
)
144+
})
145+
})
146+
147+
describe('getValueAtPath defaultValue with wildcards', () => {
148+
const obj = {
149+
list: [{ d: 456 }, { d: 789, e: 111 }],
150+
nested: [{ nested: [{ a: 1 }, { a: 2 }] }, { nested: [{ a: 3 }] }],
151+
empty: [],
152+
}
153+
154+
test('returns null when every wildcard branch misses (default)', () => {
155+
expect(getValueAtPath(obj, 'list.*.x')).toBe(null)
156+
expect(getValueAtPath(obj, 'nested.*.nested.*.x')).toBe(null)
157+
})
158+
159+
test('substitutes the default value at every missing leaf (backend-aligned)', () => {
160+
expect(getValueAtPath(obj, 'list.*.x', 'fallback')).toStrictEqual([
161+
'fallback',
162+
'fallback',
163+
])
164+
expect(
165+
getValueAtPath(obj, 'nested.*.nested.*.x', 'fallback')
166+
).toStrictEqual([['fallback', 'fallback'], ['fallback']])
167+
})
168+
169+
test('mixes existing values with the default value when only some branches miss', () => {
170+
expect(getValueAtPath(obj, 'list.*.e', 'fallback')).toStrictEqual([
171+
'fallback',
172+
111,
173+
])
174+
})
175+
176+
test('does not substitute the default value when wildcard branches all exist', () => {
177+
expect(getValueAtPath(obj, 'list.*.d', 'fallback')).toStrictEqual([
178+
456, 789,
179+
])
180+
expect(
181+
getValueAtPath(obj, 'nested.*.nested.*.a', 'fallback')
182+
).toStrictEqual([[1, 2], [3]])
183+
})
184+
185+
test('returns an empty list when wildcarding an empty array with no remaining path', () => {
186+
expect(getValueAtPath(obj, 'empty.*')).toStrictEqual([])
187+
expect(getValueAtPath(obj, 'empty.*', 'fallback')).toStrictEqual([])
188+
})
189+
190+
test('returns the default value when wildcarding an empty array with a remaining path', () => {
191+
expect(getValueAtPath(obj, 'empty.*.x')).toBe(null)
192+
expect(getValueAtPath(obj, 'empty.*.x', 'fallback')).toBe('fallback')
193+
})
194+
})
113195
})

0 commit comments

Comments
 (0)