Skip to content

Commit 7bd7569

Browse files
authored
Advanced formula editor fixings (baserow#4203)
1 parent 5de958d commit 7bd7569

20 files changed

+1089
-828
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,8 @@
169169
@import 'formula_input_field';
170170
@import 'node_help_tooltip';
171171
@import 'get_formula_component';
172+
@import 'function_formula_component';
173+
@import 'operator_formula_component';
172174
@import 'color_input';
173175
@import 'group_bys';
174176
@import 'node_explorer/node_explorer';
Lines changed: 18 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,9 @@
1-
.function-name-highlight {
2-
color: $palette-cyan-800;
3-
font-weight: 500;
4-
background-color: $palette-cyan-50;
5-
padding: 4px 8px;
6-
height: 24px;
7-
display: inline-block;
8-
vertical-align: top;
9-
10-
@include rounded;
11-
}
12-
13-
.operator-highlight {
14-
color: $palette-green-800;
15-
font-weight: 500;
16-
background-color: $palette-green-50;
17-
padding: 3px 8px;
18-
height: 24px;
19-
box-sizing: border-box;
20-
display: inline-block;
21-
vertical-align: top;
22-
23-
@include rounded;
24-
}
25-
26-
.text-segment {
27-
min-height: 24px;
28-
display: inline-block;
29-
vertical-align: top;
30-
padding: 3px 0;
31-
margin-right: 4px;
32-
line-height: 18px;
33-
}
34-
35-
.function-comma-highlight {
36-
margin-right: 4px;
37-
}
38-
39-
.function-comma-highlight,
40-
.function-paren-highlight {
41-
color: $palette-cyan-800;
42-
font-weight: 500;
43-
background-color: $palette-cyan-50;
44-
padding: 4px 8px;
45-
height: 24px;
46-
box-sizing: border-box;
47-
display: inline-block;
48-
vertical-align: top;
49-
50-
@include rounded;
51-
}
52-
531
.formula-input-field {
542
height: auto;
553
font-size: 13px;
564
min-height: 36px;
57-
padding: 5px 12px 1px;
5+
line-height: 25px;
6+
padding: 5px 12px;
587

598
// If the field is empty, then give it the
609
// same padding as a normal form input field.
@@ -68,27 +17,13 @@
6817
padding: 10px 12px;
6918
min-height: 48px;
7019
}
71-
72-
// Remove margin from the last element to avoid trailing space
73-
/* stylelint-disable-next-line selector-class-pattern */
74-
.ProseMirror {
75-
span:last-child {
76-
margin-right: 0;
77-
}
78-
79-
> div {
80-
> span:not(.text-segment) {
81-
margin: 0 4px 4px 0;
82-
}
83-
}
84-
}
8520
}
8621

8722
.formula-input-field--focused {
8823
border-color: $palette-blue-500;
8924

9025
&.formula-input-field--error {
91-
border-color: $palette-red-400;
26+
border-color: $palette-red-400 !important;
9227
}
9328
}
9429

@@ -112,3 +47,18 @@
11247
pointer-events: none;
11348
height: 0;
11449
}
50+
51+
// Atomic comma style
52+
.formula-input-field__comma,
53+
.formula-input-field__parenthesis {
54+
color: $palette-cyan-800;
55+
font-weight: 500;
56+
background-color: $palette-cyan-50;
57+
padding: 0 8px;
58+
height: 24px;
59+
box-sizing: border-box;
60+
display: inline-block;
61+
vertical-align: top;
62+
63+
@include rounded;
64+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
.function-formula-component {
2+
height: 24px;
3+
display: inline-block;
4+
vertical-align: top;
5+
}
6+
7+
.function-formula-component__name {
8+
color: $palette-cyan-800;
9+
font-weight: 500;
10+
background-color: $palette-cyan-50;
11+
padding: 0 8px;
12+
display: inline-block;
13+
vertical-align: top;
14+
height: 24px;
15+
16+
@include rounded;
17+
18+
.function-formula-component:has(+ .function-formula-component) & {
19+
padding-right: 0;
20+
}
21+
}
22+
23+
.function-formula-component__parenthesis {
24+
color: $palette-cyan-800;
25+
font-weight: 500;
26+
background-color: $palette-cyan-50;
27+
padding: 0 8px;
28+
height: 24px;
29+
box-sizing: border-box;
30+
display: inline-block;
31+
vertical-align: top;
32+
33+
@include rounded;
34+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
.get-formula-component {
22
cursor: pointer;
33
display: inline-block;
4-
vertical-align: middle;
4+
vertical-align: top;
55
background-color: $palette-neutral-100;
66
font-size: 12px;
77
border-radius: 3px;
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
.operator-formula-component {
2+
display: inline-block;
3+
vertical-align: top;
4+
white-space: normal;
5+
user-select: none;
6+
cursor: default;
7+
}
8+
9+
.operator-formula-component__symbol {
10+
color: $palette-green-800;
11+
font-weight: 500;
12+
background-color: $palette-green-50;
13+
padding: 0 8px;
14+
height: 24px;
15+
box-sizing: border-box;
16+
display: inline-block;
17+
vertical-align: top;
18+
19+
@include rounded;
20+
}

web-frontend/modules/core/components/formula/ContextManagementExtension.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export const ContextManagementExtension = Extension.create({
5252
switch (contextPosition) {
5353
case 'left':
5454
config = {
55-
vertical: 'top',
55+
vertical: 'bottom',
5656
horizontal: 'left',
5757
needsDynamicOffset: true,
5858
}
@@ -67,7 +67,7 @@ export const ContextManagementExtension = Extension.create({
6767
break
6868
case 'right':
6969
config = {
70-
vertical: 'top',
70+
vertical: 'bottom',
7171
horizontal: 'left',
7272
needsDynamicOffset: true,
7373
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/**
2+
* Creates a clipboard text serializer for the formula editor
3+
* @param {Function} toFormula - Function to convert editor content to formula string
4+
* @returns {Function} Serializer function
5+
*/
6+
export function createClipboardTextSerializer(toFormula) {
7+
return (slice) => {
8+
// Serialize the slice to formula text
9+
const content = {
10+
type: 'doc',
11+
content: [{ type: 'wrapper', content: [] }],
12+
}
13+
14+
// Extract nodes from the slice
15+
const nodes = []
16+
slice.content.forEach((node) => {
17+
nodes.push(node.toJSON())
18+
})
19+
20+
content.content[0].content = nodes
21+
22+
// Convert to formula string
23+
const formula = toFormula(content)
24+
25+
return formula || ''
26+
}
27+
}
28+
29+
/**
30+
* Creates a paste handler for the formula editor
31+
* @param {Object} options - Handler options
32+
* @param {Function} options.toContent - Function to parse formula string to editor content
33+
* @param {Function} options.getMode - Function to get current editor mode
34+
* @returns {Function} Paste handler function
35+
*/
36+
export function createPasteHandler({ toContent, getMode }) {
37+
return (view, event, slice) => {
38+
// Only handle paste in advanced mode
39+
if (getMode() !== 'advanced') {
40+
return false
41+
}
42+
43+
// Get the pasted text
44+
const text = event.clipboardData.getData('text/plain')
45+
if (!text) {
46+
return false
47+
}
48+
49+
// Try to parse it as a formula
50+
try {
51+
const content = toContent(text)
52+
if (!content) {
53+
return false
54+
}
55+
56+
// Get the wrapper content (skip doc and wrapper nodes)
57+
const wrapperContent =
58+
content.content && content.content[0] && content.content[0].content
59+
? content.content[0].content
60+
: []
61+
62+
// Insert the parsed content at the current selection
63+
if (wrapperContent.length > 0) {
64+
const { tr } = view.state
65+
const { from, to } = view.state.selection
66+
67+
// Create nodes from the content
68+
const nodes = wrapperContent.map((item) =>
69+
view.state.schema.nodeFromJSON(item)
70+
)
71+
72+
// Replace the selection with the nodes
73+
tr.replaceWith(from, to, nodes)
74+
view.dispatch(tr)
75+
return true
76+
}
77+
} catch (error) {
78+
console.error('Error parsing pasted formula:', error)
79+
return false
80+
}
81+
82+
return false
83+
}
84+
}

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

Lines changed: 41 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,23 @@ import { Placeholder } from '@tiptap/extension-placeholder'
4242
import { Document } from '@tiptap/extension-document'
4343
import { Text } from '@tiptap/extension-text'
4444
import { History } from '@tiptap/extension-history'
45-
import { FunctionHighlightExtension } from '@baserow/modules/core/components/formula/FunctionHighlightExtension'
46-
import { FunctionAutoCompleteExtension } from '@baserow/modules/core/components/formula/FunctionAutoCompleteExtension'
47-
import { FunctionDeletionExtension } from '@baserow/modules/core/components/formula/FunctionDeletionExtension'
45+
import { HardBreak } from '@tiptap/extension-hard-break'
4846
import { FunctionHelpTooltipExtension } from '@baserow/modules/core/components/formula/FunctionHelpTooltipExtension'
49-
import { FormulaInsertionExtension } from '@baserow/modules/core/components/formula/FormulaInsertionExtension'
47+
import {
48+
FormulaInsertionExtension,
49+
FunctionFormulaComponentNode,
50+
FunctionArgumentCommaNode,
51+
FunctionClosingParenNode,
52+
OperatorFormulaComponentNode,
53+
} from '@baserow/modules/core/components/formula/FormulaInsertionExtension'
5054
import { NodeSelectionExtension } from '@baserow/modules/core/components/formula/NodeSelectionExtension'
5155
import { ContextManagementExtension } from '@baserow/modules/core/components/formula/ContextManagementExtension'
56+
import { FunctionDetectionExtension } from '@baserow/modules/core/components/formula/FunctionDetectionExtension'
57+
import { OperatorDetectionExtension } from '@baserow/modules/core/components/formula/OperatorDetectionExtension'
58+
import {
59+
createClipboardTextSerializer,
60+
createPasteHandler,
61+
} from '@baserow/modules/core/components/formula/FormulaClipboardHandler'
5262
import _ from 'lodash'
5363
import parseBaserowFormula from '@baserow/modules/core/formula/parser/parser'
5464
import { ToTipTapVisitor } from '@baserow/modules/core/formula/tiptap/toTipTapVisitor'
@@ -254,20 +264,31 @@ export default {
254264
FunctionHelpTooltipExtension.configure({
255265
vueComponent: this,
256266
}),
257-
FunctionHighlightExtension.configure({
258-
functionNames: this.mode === 'advanced' ? this.functionNames : [],
259-
operators: this.mode === 'advanced' ? this.operators : [],
260-
}),
261267
...this.formulaComponents,
262268
]
263269
264270
if (this.mode === 'advanced') {
271+
extensions.push(FunctionFormulaComponentNode)
272+
extensions.push(FunctionArgumentCommaNode)
273+
extensions.push(FunctionClosingParenNode)
274+
extensions.push(OperatorFormulaComponentNode)
275+
extensions.push(
276+
HardBreak.extend({
277+
addKeyboardShortcuts() {
278+
return {
279+
Enter: () => this.editor.commands.setHardBreak(),
280+
}
281+
},
282+
})
283+
)
265284
extensions.push(
266-
FunctionAutoCompleteExtension.configure({
285+
FunctionDetectionExtension.configure({
267286
functionNames: this.functionNames,
287+
vueComponent: this,
268288
}),
269-
FunctionDeletionExtension.configure({
270-
functionNames: this.functionNames,
289+
OperatorDetectionExtension.configure({
290+
operators: this.operators,
291+
vueComponent: this,
271292
})
272293
)
273294
}
@@ -382,11 +403,18 @@ export default {
382403
parseOptions: {
383404
preserveWhitespace: 'full',
384405
},
385-
editorProps: {},
406+
editorProps: {
407+
clipboardTextSerializer: createClipboardTextSerializer(
408+
this.toFormula.bind(this)
409+
),
410+
handlePaste: createPasteHandler({
411+
toContent: this.toContent.bind(this),
412+
getMode: () => this.mode,
413+
}),
414+
},
386415
})
387416
},
388417
recreateEditor(formula = null) {
389-
// If no formula is provided, save the current formula before destroying the editor
390418
const currentFormula =
391419
formula ||
392420
(this.editor ? this.toFormula(this.wrapperContent) : this.value)
@@ -435,23 +463,6 @@ export default {
435463
}
436464
}
437465
438-
if (this.readOnly) {
439-
return {
440-
type: 'doc',
441-
content: [
442-
{
443-
type: 'wrapper',
444-
content: [
445-
{
446-
type: 'text',
447-
text: formula,
448-
},
449-
],
450-
},
451-
],
452-
}
453-
}
454-
455466
try {
456467
const tree = parseBaserowFormula(formula)
457468
const functionCollection = new RuntimeFunctionCollection(this.$registry)

0 commit comments

Comments
 (0)