Skip to content

Commit 576967e

Browse files
authored
Merge pull request #2120 from undb-io/release/v1.0.0-113
Release version v1.0.0-113
2 parents 66f9ccb + cb05974 commit 576967e

File tree

10 files changed

+542
-187
lines changed

10 files changed

+542
-187
lines changed

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
11
# Changelog
22

3+
## v1.0.0-113
4+
5+
6+
### 🏡 Chore
7+
8+
- Add formula return type ([3ff9a59](https://github.com/undb-io/undb/commit/3ff9a59))
9+
10+
### ❤️ Contributors
11+
12+
- Nichenqin ([@nichenqin](http://github.com/nichenqin))
13+
314
## v1.0.0-112
415

516

apps/frontend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@
8585
"@codemirror/language": "^6.10.3",
8686
"@codemirror/state": "^6.4.1",
8787
"@codemirror/view": "^6.34.1",
88+
"@floating-ui/dom": "^1.6.12",
8889
"@formkit/auto-animate": "^0.8.2",
8990
"@internationalized/date": "^3.5.6",
9091
"@svelte-put/clickoutside": "^3.0.2",

apps/frontend/src/lib/components/formula/formula-editor.svelte

Lines changed: 141 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
import { derived } from "svelte/store"
1717
import FieldIcon from "../blocks/field-icon/field-icon.svelte"
1818
import { type Field } from "@undb/table"
19+
import { computePosition, flip, shift, offset } from "@floating-ui/dom"
20+
import { globalFormulaRegistry } from "@undb/formula/src/formula/formula.registry"
1921
2022
const functions = FORMULA_FUNCTIONS
2123
@@ -31,8 +33,15 @@
3133
export let value: string = ""
3234
3335
let editor: EditorView
34-
let suggestions: string[] = [...functions, ...$fields]
36+
let formulaSuggestions: string[] = [...functions]
37+
let fieldSuggestions: string[] = [...$fields]
38+
39+
$: suggestions = [...formulaSuggestions, ...fieldSuggestions]
40+
3541
let selectedSuggestion: string = ""
42+
let hoverSuggestion: string = ""
43+
44+
$: hoverFormula = hoverSuggestion ? globalFormulaRegistry.get(hoverSuggestion as FormulaFunction) : undefined
3645
3746
const highlightStyle = HighlightStyle.define([
3847
{ tag: tags.keyword, color: "#5c6bc0" },
@@ -281,8 +290,8 @@
281290
if (!isInsideParens) {
282291
const functionNode = visitor.getNearestFunctionNode()
283292
if (functionNode) {
284-
const functionStart = functionNode.start.startIndex
285-
const functionNameLength = functionNode.IDENTIFIER().text.length
293+
const functionStart = functionNode.start.tokenIndex
294+
const functionNameLength = functionNode.IDENTIFIER().getText().length
286295
const transaction = editor.state.update({
287296
changes: {
288297
from: functionStart,
@@ -311,47 +320,50 @@
311320
editor.dispatch(transaction)
312321
} else {
313322
const fieldWithBrackets = `{{${suggestion}}}`
314-
315-
// 使用正则表达式找到光标位置最近的完整变量
316323
const fullText = editor.state.doc.toString()
317-
let start = cursor
318-
let end = cursor
324+
325+
// 检查光标是否在变量内部
326+
let isInsideVariable = false
327+
let variableStart = -1
328+
let variableEnd = -1
319329
320330
// 向前搜索 {{
321331
for (let i = cursor; i >= 0; i--) {
322332
if (fullText.slice(i, i + 2) === "{{") {
323-
start = i
333+
variableStart = i
324334
break
325335
}
326336
}
327337
328338
// 向后搜索 }}
329339
for (let i = cursor; i < fullText.length; i++) {
330340
if (fullText.slice(i, i + 2) === "}}") {
331-
end = i + 2
341+
variableEnd = i + 2
332342
break
333343
}
334344
}
335345
336-
// 检查找到的范围是否是一个有效的变量(不超过最近的逗号)
337-
const textBetween = fullText.slice(start, end)
338-
const isValidVariable = textBetween.includes("{{") && textBetween.includes("}}") && !textBetween.includes(",")
346+
// 判断光标是否在变量内部
347+
if (variableStart !== -1 && variableEnd !== -1) {
348+
const textBetween = fullText.slice(variableStart, variableEnd)
349+
isInsideVariable = textBetween.includes("{{") && textBetween.includes("}}") && !textBetween.includes(",")
350+
}
339351
340-
if (isValidVariable) {
341-
// 替换找到的变量
352+
// 如果光标在变量内部,替换变量
353+
// 如果光标在变量后面或其他位置,直接在当前位置插入
354+
if (isInsideVariable && cursor < variableEnd) {
342355
const transaction = editor.state.update({
343356
changes: {
344-
from: start,
345-
to: end,
357+
from: variableStart,
358+
to: variableEnd,
346359
insert: fieldWithBrackets,
347360
},
348361
selection: {
349-
anchor: start + fieldWithBrackets.length,
362+
anchor: variableStart + fieldWithBrackets.length,
350363
},
351364
})
352365
editor.dispatch(transaction)
353366
} else {
354-
// 不在变量内部或范��无效,直接在当前位置插入新变量
355367
const transaction = editor.state.update({
356368
changes: {
357369
from: cursor,
@@ -380,9 +392,30 @@
380392
errorMessage = (error as Error).message
381393
}
382394
}
395+
396+
let hoverSuggestionContainer: HTMLElement
397+
let editorContainerWrapper: HTMLElement
398+
399+
function update() {
400+
if (hoverSuggestionContainer && editorContainerWrapper && hoverFormula) {
401+
computePosition(editorContainerWrapper, hoverSuggestionContainer, {
402+
placement: "left-start",
403+
middleware: [flip(), shift({ padding: 5 }), offset(10)],
404+
}).then(({ x, y }) => {
405+
Object.assign(hoverSuggestionContainer.style, {
406+
left: `${x}px`,
407+
top: `${y}px`,
408+
})
409+
})
410+
}
411+
}
412+
413+
onMount(() => {
414+
update()
415+
})
383416
</script>
384417

385-
<div>
418+
<div bind:this={editorContainerWrapper} id="editor-container-wrapper" class="relative">
386419
<div id="editor-container" class="mb-2 rounded-sm border"></div>
387420
{#if errorMessage}
388421
<p class="text-destructive flex items-center gap-1 text-xs">
@@ -391,33 +424,103 @@
391424
</p>
392425
{/if}
393426

394-
<ul class="mt-2 flex h-[250px] flex-col overflow-auto rounded-lg border border-gray-200">
395-
{#each suggestions as suggestion}
427+
<ul class="mt-2 flex h-[250px] flex-col divide-y overflow-auto rounded-lg border border-gray-200">
428+
<div class="sticky top-0 z-10 border-b bg-gray-100 px-2 py-1.5 text-xs font-semibold">Formula</div>
429+
{#each formulaSuggestions as suggestion}
430+
{@const isSelected = suggestion === selectedSuggestion}
431+
{@const isHovered = suggestion === hoverSuggestion}
432+
<button
433+
type="button"
434+
on:click={() => insertSuggestion(suggestion)}
435+
on:mouseenter={() => {
436+
hoverSuggestion = suggestion
437+
update()
438+
}}
439+
class="group relative w-full text-left text-xs font-medium"
440+
>
441+
<li
442+
class={cn("flex w-full items-center gap-1 p-2 hover:bg-gray-100", (isSelected || isHovered) && "bg-gray-100")}
443+
>
444+
<span class="font-normal">
445+
<SquareFunctionIcon class="size-4" />
446+
</span>
447+
<span>
448+
{suggestion}()
449+
</span>
450+
</li>
451+
452+
<div class="absolute left-0 top-0 z-50 -translate-x-[100%] group-hover:block">hello</div>
453+
</button>
454+
{/each}
455+
<div class="sticky top-0 z-10 border-b bg-gray-100 px-2 py-1.5 text-xs font-semibold">Field</div>
456+
{#each fieldSuggestions as suggestion}
396457
{@const isSelected = suggestion === selectedSuggestion}
397-
{@const isFunction = functions.includes(suggestion)}
398-
{@const isField = !isFunction}
399-
<button type="button" on:click={() => insertSuggestion(suggestion)} class="w-full text-left text-xs font-medium">
458+
{@const field = $table.schema.getFieldByIdOrName(suggestion).into(null)}
459+
<button
460+
type="button"
461+
on:mouseenter={() => {
462+
hoverSuggestion = ""
463+
}}
464+
on:click={() => insertSuggestion(suggestion)}
465+
class="w-full text-left text-xs font-medium"
466+
>
400467
<li class={cn("flex w-full items-center gap-1 p-2 hover:bg-gray-100", isSelected && "bg-gray-100")}>
401-
{#if isFunction}
402-
<span class="font-normal">
403-
<SquareFunctionIcon class="size-4" />
468+
{#if field}
469+
<span class="flex items-center gap-1">
470+
<FieldIcon class="size-4" type={field.type} {field} />
471+
{field.name.value}
404472
</span>
405-
<span>
406-
{suggestion}()
407-
</span>
408-
{:else}
409-
{@const field = $table.schema.getFieldByIdOrName(suggestion).into(null)}
410-
{#if field}
411-
<span class="flex items-center gap-1">
412-
<FieldIcon class="size-4" type={field.type} {field} />
413-
{field.name.value}
414-
</span>
415-
{/if}
416473
{/if}
417474
</li>
418475
</button>
419476
{/each}
420477
</ul>
478+
479+
<div bind:this={hoverSuggestionContainer} class="fixed left-0 top-0 w-80 rounded-md border bg-white shadow-md">
480+
{#if hoverFormula}
481+
<div class="flex items-center justify-between border-b bg-gray-100 px-2 py-1">
482+
<div class="flex items-center gap-2 text-sm">
483+
<SquareFunctionIcon class="size-4" />
484+
{hoverSuggestion}()
485+
</div>
486+
487+
{#if hoverFormula.returnType}
488+
<span
489+
class="me-2 rounded bg-blue-100 px-2.5 py-0.5 text-xs font-medium uppercase text-blue-800 dark:bg-blue-900 dark:text-blue-300"
490+
>
491+
{hoverFormula.returnType}
492+
</span>
493+
{/if}
494+
</div>
495+
496+
<div class="space-y-2 p-2">
497+
<p class="overflow-hidden whitespace-normal break-words text-xs text-gray-500">{hoverFormula.description}</p>
498+
<div class="space-y-2">
499+
<p class="text-xs font-semibold text-gray-500">Syntax</p>
500+
{#each hoverFormula.syntax as syntax}
501+
<div class="whitespace-normal break-words rounded-sm border px-2 py-1 text-xs leading-6 text-gray-800">
502+
{syntax}
503+
</div>
504+
{/each}
505+
</div>
506+
{#if hoverFormula.examples && hoverFormula.examples.length > 0}
507+
<p class="text-xs font-semibold text-gray-500">Examples</p>
508+
<div class="space-y-2">
509+
{#each hoverFormula.examples as example}
510+
<div class="whitespace-normal break-words rounded-sm border px-2 py-1 text-xs leading-6 text-gray-800">
511+
{example[0]}
512+
{#if example[1]}
513+
<span class="text-gray-500">
514+
=> {example[1]}
515+
</span>
516+
{/if}
517+
</div>
518+
{/each}
519+
</div>
520+
{/if}
521+
</div>
522+
{/if}
523+
</div>
421524
</div>
422525

423526
<style lang="postcss">

bun.lockb

456 Bytes
Binary file not shown.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "undb",
3-
"version": "1.0.0-112",
3+
"version": "1.0.0-113",
44
"private": true,
55
"scripts": {
66
"build": "NODE_ENV=production bun --bun turbo build",

packages/formula/src/formula.visitor.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1+
import { globalFormulaRegistry } from "./formula/formula.registry"
12
import { FormulaFunction } from "./formula/formula.type"
2-
import { globalFunctionRegistry } from "./formula/registry"
33
import {
44
AddSubExprContext,
55
AndExprContext,
@@ -172,15 +172,15 @@ export class FormulaVisitor extends FormulaParserVisitor<ExpressionResult> {
172172
const funcName = ctx.IDENTIFIER().getText() as FormulaFunction
173173
const args = ctx.argumentList() ? (this.visit(ctx.argumentList()!) as FunctionExpressionResult) : undefined
174174

175-
if (!globalFunctionRegistry.isValid(funcName)) {
175+
if (!globalFormulaRegistry.isValid(funcName)) {
176176
throw new Error(`Unknown function: ${funcName}`)
177177
}
178178

179179
if (args) {
180-
globalFunctionRegistry.validateArgs(funcName, args.arguments)
180+
globalFormulaRegistry.validateArgs(funcName, args.arguments)
181181
}
182182

183-
const returnType = globalFunctionRegistry.get(funcName)!.returnType
183+
const returnType = globalFormulaRegistry.get(funcName)!.returnType
184184

185185
return {
186186
type: "functionCall",
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { describe, expect, it } from "bun:test"
2+
import { FormulaRegistry } from "./formula.registry"
3+
4+
describe("FormulaRegistry", () => {
5+
it("should register ADD functions", () => {
6+
const registry = new FormulaRegistry()
7+
registry.register("ADD", [["number", "number"]], "number")
8+
expect(registry.isValid("ADD")).toBe(true)
9+
expect(registry.get("ADD")?.syntax).toEqual(["ADD(number1, number2)"])
10+
})
11+
12+
it("should register SUM functions", () => {
13+
const registry = new FormulaRegistry()
14+
registry.register("SUM", [["number", "variadic"]], "number")
15+
expect(registry.isValid("SUM")).toBe(true)
16+
expect(registry.get("SUM")?.syntax).toEqual(["SUM(number1, [number2, ...])"])
17+
})
18+
19+
it("should register ABS functions", () => {
20+
const registry = new FormulaRegistry()
21+
registry.register("ABS", [["number"]], "number")
22+
expect(registry.isValid("ABS")).toBe(true)
23+
expect(registry.get("ABS")?.syntax).toEqual(["ABS(number)"])
24+
})
25+
})

0 commit comments

Comments
 (0)