Skip to content

Commit e995e8f

Browse files
authored
feat: Show multiple formula expression examples (baserow#5981)
* Added support for multiple examples in formula expression help tooltip. * Show an icon to indicate that an example can be inserted.
1 parent a398de7 commit e995e8f

18 files changed

Lines changed: 900 additions & 124 deletions

File tree

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"type": "feature",
3+
"message": "Added support for multiple examples in formula expression help tooltip.",
4+
"issue_origin": "github",
5+
"issue_number": 5442,
6+
"domain": "core",
7+
"bullet_points": [],
8+
"created_at": "2026-09-03"
9+
}

web-frontend/locales/en.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -686,6 +686,7 @@
686686
"minuteDescription": "Returns the minute from the date time argument.",
687687
"secondDescription": "Returns the second from the date time argument.",
688688
"todayDescription": "Returns the current date.",
689+
"nowDescription": "Returns the current date and time.",
689690
"getPropertyDescription": "Returns the property from the object.",
690691
"randomIntDescription": "Returns a random integer from the range specified by the arguments.",
691692
"randomFloatDescription": "Returns a random float from the range specified by the arguments.",

web-frontend/modules/core/assets/scss/components/node_help_tooltip.scss

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,75 @@
4242

4343
@extend %mb-16;
4444
}
45+
46+
&__examples {
47+
display: flex;
48+
flex-direction: column;
49+
gap: 12px;
50+
51+
// Fits three examples, including the extra padding of the clickable
52+
// variant. Longer lists scroll.
53+
max-height: 260px;
54+
overflow-y: auto;
55+
56+
// Clickable examples carry their own padding so the hover background can
57+
// bleed past the snippet. Offset the container by the same amount so the
58+
// snippets stay aligned with the label above.
59+
&--clickable {
60+
margin: 0 -6px;
61+
padding: 0 6px;
62+
}
63+
}
64+
65+
&__example-footer {
66+
display: flex;
67+
align-items: center;
68+
justify-content: space-between;
69+
gap: 8px;
70+
margin-top: 12px;
71+
font-size: 12px;
72+
color: $palette-neutral-1100;
73+
}
74+
75+
&__example-result {
76+
min-width: 0;
77+
overflow-wrap: anywhere;
78+
}
79+
80+
// Signals that clicking the example inserts it into the formula. Pushed to
81+
// the far end of the footer so it sits opposite the result, and stays there
82+
// when the example has no result.
83+
&__example-insert-icon {
84+
margin-left: auto;
85+
flex-shrink: 0;
86+
font-size: 12px;
87+
color: $palette-neutral-700;
88+
}
89+
90+
&__example--clickable {
91+
cursor: pointer;
92+
padding: 6px;
93+
94+
@include rounded($rounded);
95+
96+
&:hover {
97+
background: $palette-neutral-200;
98+
99+
.node-help-tooltip__example-insert-icon {
100+
color: $palette-neutral-1200;
101+
}
102+
}
103+
104+
.node-help-tooltip__example-code {
105+
pointer-events: none;
106+
}
107+
}
108+
109+
&__examples-hint {
110+
margin: 8px 0 0;
111+
font-size: 11px;
112+
color: $palette-neutral-700;
113+
}
45114
}
46115

47116
.node-help-tooltip-context {

web-frontend/modules/core/components/formula/FormulaInputExplorerContext.vue

Lines changed: 2 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
:loading="loading"
1616
@node-selected="$emit('node-selected', $event)"
1717
@node-unselected="$emit('node-unselected')"
18+
@example-click="$emit('example-click', $event)"
1819
/>
1920
<div
2021
v-if="advancedModeEnabled"
@@ -113,14 +114,10 @@ export default {
113114
required: true,
114115
},
115116
},
116-
emits: ['mode-changed', 'node-selected', 'node-unselected'],
117+
emits: ['mode-changed', 'node-selected', 'node-unselected', 'example-click'],
117118
data() {
118119
return {
119120
searchQuery: '',
120-
tooltip: {
121-
functionData: null,
122-
},
123-
tooltipTimer: null,
124121
tabs: [],
125122
isModalVisible: false,
126123
}
@@ -141,7 +138,6 @@ export default {
141138
},
142139
activeTabIndex() {
143140
this.searchQuery = ''
144-
this.hideTooltip()
145141
},
146142
},
147143
created() {
@@ -165,7 +161,6 @@ export default {
165161
},
166162
hide() {
167163
this.$refs.context.hide()
168-
this.hideTooltip()
169164
},
170165
getTabTitle(tabName) {
171166
const titleMap = {
@@ -203,48 +198,6 @@ export default {
203198
this.$emit('mode-changed', 'advanced')
204199
}
205200
},
206-
onFunctionHover(item, tabName, event) {
207-
if (tabName !== 'Functions') {
208-
return
209-
}
210-
211-
if (this.tooltipTimer) {
212-
clearTimeout(this.tooltipTimer)
213-
}
214-
215-
this.tooltip.functionData = {
216-
name: item.name,
217-
description: item.description,
218-
example: item.example,
219-
icon: item.icon,
220-
}
221-
222-
this.tooltipTimer = setTimeout(() => {
223-
if (this.$refs.functionHelpTooltip) {
224-
this.$refs.functionHelpTooltip.show(
225-
event.target,
226-
'bottom',
227-
'right',
228-
5,
229-
10
230-
)
231-
}
232-
}, 300)
233-
},
234-
onFunctionLeave() {
235-
if (this.tooltipTimer) {
236-
clearTimeout(this.tooltipTimer)
237-
this.tooltipTimer = null
238-
}
239-
240-
this.hideTooltip()
241-
},
242-
hideTooltip() {
243-
if (this.$refs.functionHelpTooltip) {
244-
this.$refs.functionHelpTooltip.hide()
245-
}
246-
this.tooltip.functionData = null
247-
},
248201
showAdvancedModeModal() {
249202
this.$refs.advancedModeModal.show()
250203
},

web-frontend/modules/core/components/formula/FormulaInputField.vue

Lines changed: 41 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -69,11 +69,13 @@
6969
:enabled-modes="enabledModes"
7070
@node-selected="handleNodeSelected"
7171
@node-unselected="unSelectNode"
72+
@example-click="handleExampleSelected"
7273
@mode-changed="handleModeChange"
7374
@mousedown="onContextMouseDown"
7475
/>
7576

7677
<NodeHelpTooltip
78+
v-if="!readOnly"
7779
ref="nodeHelpTooltip"
7880
:node="hoveredFunctionNode"
7981
:nodes-hierarchy="nodesHierarchy"
@@ -398,17 +400,24 @@ export default {
398400
this.$refs.formulaInputExplorerContext?.hide()
399401
},
400402
}),
401-
FunctionHelpTooltipExtension.configure({
402-
functionDefinitions: this.formulaRegistry.definitions,
403-
onShowTooltip: (el, node) => {
404-
this.hoveredFunctionNode = node
405-
this.$refs.nodeHelpTooltip?.show(el, 'bottom', 'right', 6, 10)
406-
},
407-
onHideTooltip: () => {
408-
this.$refs.nodeHelpTooltip?.hide()
409-
this.hoveredFunctionNode = null
410-
},
411-
}),
403+
// Read-only fields are display-only snippets (e.g. the examples
404+
// rendered inside `NodeHelpTooltip` itself), so they must not spawn
405+
// their own hover help tooltip.
406+
...(this.readOnly
407+
? []
408+
: [
409+
FunctionHelpTooltipExtension.configure({
410+
functionDefinitions: this.formulaRegistry.definitions,
411+
onShowTooltip: (el, node) => {
412+
this.hoveredFunctionNode = node
413+
this.$refs.nodeHelpTooltip?.show(el, 'bottom', 'right', 6, 10)
414+
},
415+
onHideTooltip: () => {
416+
this.$refs.nodeHelpTooltip?.hide()
417+
this.hoveredFunctionNode = null
418+
},
419+
}),
420+
]),
412421
...this.formulaComponents,
413422
]
414423
@@ -598,7 +607,11 @@ export default {
598607
* renders without any error styling until the field is touched.
599608
*/
600609
validateFormula(formula) {
601-
if (this.isRawMode) {
610+
// Raw mode holds plain text, not a formula. Read-only fields are
611+
// display-only snippets (e.g. the examples in the help tooltip) that
612+
// never show an error context, so don't paint them as errors either:
613+
// an example may deliberately be incomplete, like `get()` without a path.
614+
if (this.isRawMode || this.readOnly) {
602615
this.isFormulaInvalid = false
603616
this.formulaErrorContext = { scope: null, title: '', message: '' }
604617
return true
@@ -697,6 +710,22 @@ export default {
697710
break
698711
}
699712
},
713+
/**
714+
* Inserts a clicked help-tooltip example at the cursor. Only advanced
715+
* mode can host the function nodes examples parse into; the explorer's
716+
* function tab (and thus a clickable tooltip) only exists in that mode.
717+
*/
718+
handleExampleSelected(example) {
719+
if (this.mode !== 'advanced') {
720+
return
721+
}
722+
const content = this.toContent(example.formula)
723+
const fragment = content?.content?.[0]?.content
724+
if (!fragment) {
725+
return
726+
}
727+
this.editor.commands.insertFormulaFragment(fragment)
728+
},
700729
onContextMouseDown() {
701730
this.editor?.commands.handleContextMouseDown()
702731
},

web-frontend/modules/core/components/formula/extensions/FormulaNodes.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,17 @@ export const FormulaInsertionExtension = Extension.create({
363363

364364
commands.focus()
365365

366+
return true
367+
},
368+
insertFormulaFragment:
369+
(content) =>
370+
({ commands }) => {
371+
// ZWS-bracketed like the other insert commands; ZWSManagementExtension
372+
// removes any consecutive ZWS this may create.
373+
commands.insertContent([zwsTextJSON(), ...content, zwsTextJSON()])
374+
375+
commands.focus()
376+
366377
return true
367378
},
368379
insertOperator:

web-frontend/modules/core/components/nodeExplorer/NodeExplorer.vue

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
:allow-node-selection="allowNodeSelection"
2929
@reset-search="resetSearch"
3030
@node-selected="$emit('node-selected', $event)"
31+
@example-click="$emit('example-click', $event)"
3132
@toggle="toggleNode"
3233
/>
3334
</Tab>
@@ -43,6 +44,7 @@
4344
:allow-node-selection="allowNodeSelection"
4445
@reset-search="resetSearch"
4546
@node-selected="$emit('node-selected', $event)"
47+
@example-click="$emit('example-click', $event)"
4648
@toggle="toggleNode"
4749
/>
4850
</template>
@@ -96,7 +98,7 @@ export default {
9698
validator: (value) => ['none', 'all', 'array', 'object'].includes(value),
9799
},
98100
},
99-
emits: ['node-selected', 'node-toggled', 'node-unselected'],
101+
emits: ['node-selected', 'node-toggled', 'node-unselected', 'example-click'],
100102
data() {
101103
return {
102104
activeTabIndex: 0,

0 commit comments

Comments
 (0)