@veltra/desktop 1.0.13 → 1.0.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/action/action-group.js +110 -3
- package/dist/components/action/action-group.js.map +1 -1
- package/dist/components/action/action-group.vue.d.ts +16 -3
- package/dist/components/action/action.js +26 -13
- package/dist/components/action/action.js.map +1 -1
- package/dist/components/action/action.vue.d.ts +2 -2
- package/dist/components/action/style.js +2 -1
- package/dist/components/action/style2.css +35 -7
- package/dist/components/auto-complete/auto-complete.vue.d.ts +2 -2
- package/dist/components/batch-edit/batch-edit-form.js +106 -42
- package/dist/components/batch-edit/batch-edit-form.js.map +1 -1
- package/dist/components/batch-edit/batch-edit-list.js +84 -47
- package/dist/components/batch-edit/batch-edit-list.js.map +1 -1
- package/dist/components/batch-edit/batch-edit.js +42 -10
- package/dist/components/batch-edit/batch-edit.js.map +1 -1
- package/dist/components/batch-edit/style.js +2 -4
- package/dist/components/batch-edit/style2.css +202 -30
- package/dist/components/batch-edit/use-edit.js +45 -9
- package/dist/components/batch-edit/use-edit.js.map +1 -1
- package/dist/components/cascade/cascade.js +3 -1
- package/dist/components/cascade/cascade.js.map +1 -1
- package/dist/components/cascade/cascade.vue.d.ts +2 -2
- package/dist/components/checkbox/checkbox-button.vue.d.ts +1 -1
- package/dist/components/checkbox-group/checkbox-group.vue.d.ts +2 -2
- package/dist/components/code-editor/code-editor.js +145 -49
- package/dist/components/code-editor/code-editor.js.map +1 -1
- package/dist/components/code-editor/code-editor.vue.d.ts +2 -0
- package/dist/components/code-editor/style2.css +87 -19
- package/dist/components/date-panel/date-panel.vue.d.ts +1 -1
- package/dist/components/date-picker/date-picker.vue.d.ts +2 -2
- package/dist/components/date-range-picker/date-range-picker.vue.d.ts +2 -2
- package/dist/components/dialog/dialog.js +6 -2
- package/dist/components/dialog/dialog.js.map +1 -1
- package/dist/components/dialog/style2.css +2 -3
- package/dist/components/expression-editor/components/path-preview.js +27 -0
- package/dist/components/expression-editor/components/path-preview.js.map +1 -0
- package/dist/components/expression-editor/components/variable-picker.js +199 -153
- package/dist/components/expression-editor/components/variable-picker.js.map +1 -1
- package/dist/components/expression-editor/core/chip.js +54 -0
- package/dist/components/expression-editor/core/chip.js.map +1 -0
- package/dist/components/expression-editor/core/editor.js +424 -0
- package/dist/components/expression-editor/core/editor.js.map +1 -0
- package/dist/components/expression-editor/core/mention.js +91 -0
- package/dist/components/expression-editor/core/mention.js.map +1 -0
- package/dist/components/expression-editor/core/model.js +64 -0
- package/dist/components/expression-editor/core/model.js.map +1 -0
- package/dist/components/expression-editor/di.js +4 -10
- package/dist/components/expression-editor/di.js.map +1 -1
- package/dist/components/expression-editor/expression-editor.js +168 -114
- package/dist/components/expression-editor/expression-editor.js.map +1 -1
- package/dist/components/expression-editor/expression-editor.vue.d.ts +2 -1
- package/dist/components/expression-editor/style.js +0 -2
- package/dist/components/expression-editor/style2.css +145 -219
- package/dist/components/index.js +54 -54
- package/dist/components/input/input.vue.d.ts +2 -2
- package/dist/components/layout/layout.js +12 -11
- package/dist/components/layout/layout.js.map +1 -1
- package/dist/components/multi-select/multi-select.vue.d.ts +3 -3
- package/dist/components/multi-tree-select/multi-tree-select.vue.d.ts +3 -3
- package/dist/components/notification/notification.vue.d.ts +1 -1
- package/dist/components/number/number.vue.d.ts +1 -1
- package/dist/components/number-input/number-input.js +4 -4
- package/dist/components/number-input/number-input.js.map +1 -1
- package/dist/components/number-input/number-input.vue.d.ts +2 -2
- package/dist/components/number-range-input/number-range-input.vue.d.ts +1 -1
- package/dist/components/password-input/password-input.vue.d.ts +1 -1
- package/dist/components/pop-confirm/pop-confirm.vue.d.ts +1 -1
- package/dist/components/radio-group/radio-group.vue.d.ts +2 -2
- package/dist/components/rich-text-editor/rich-text-editor.js +1 -1
- package/dist/components/rich-text-editor/rich-text-editor.vue.d.ts +1 -1
- package/dist/components/rich-text-editor/toolbar.js +2 -2
- package/dist/components/select/select.vue.d.ts +2 -2
- package/dist/components/steps/steps.vue.d.ts +1 -1
- package/dist/components/table/use-table.js +16 -0
- package/dist/components/table/use-table.js.map +1 -1
- package/dist/components/tabs/tabs-horizontal.vue.d.ts +1 -1
- package/dist/components/tabs/tabs-vertical.vue.d.ts +1 -1
- package/dist/components/tabs/tabs.vue.d.ts +1 -1
- package/dist/components/textarea/textarea.vue.d.ts +2 -2
- package/dist/components/tip/tip.vue.d.ts +1 -1
- package/dist/components/tree/style2.css +18 -12
- package/dist/components/tree/tree-node.js +7 -4
- package/dist/components/tree/tree-node.js.map +1 -1
- package/dist/components/tree-select/tree-select.vue.d.ts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +83 -83
- package/dist/style.js +3 -3
- package/dist/types/action.d.ts +38 -4
- package/dist/types/cascade.d.ts +2 -3
- package/dist/types/code-editor.d.ts +9 -2
- package/dist/types/expression-editor.d.ts +9 -1
- package/dist/types/index.d.ts +2 -2
- package/package.json +11 -12
- package/src/components/action/action-group.vue +126 -56
- package/src/components/action/action.vue +35 -12
- package/src/components/action/style.scss +46 -10
- package/src/components/action/style.ts +2 -1
- package/src/components/batch-edit/batch-edit-form.vue +121 -45
- package/src/components/batch-edit/batch-edit-list.vue +56 -39
- package/src/components/batch-edit/batch-edit.vue +57 -6
- package/src/components/batch-edit/style.scss +251 -37
- package/src/components/batch-edit/style.ts +5 -8
- package/src/components/batch-edit/use-edit.ts +58 -11
- package/src/components/cascade/cascade.vue +3 -1
- package/src/components/code-editor/code-editor.vue +198 -63
- package/src/components/code-editor/style.scss +110 -12
- package/src/components/dialog/dialog.vue +6 -2
- package/src/components/dialog/style.scss +4 -4
- package/src/components/expression-editor/__test__/chip.test.ts +86 -0
- package/src/components/expression-editor/__test__/mention.test.ts +122 -0
- package/src/components/expression-editor/__test__/model.test.ts +127 -0
- package/src/components/expression-editor/components/path-preview.vue +32 -0
- package/src/components/expression-editor/components/variable-picker.vue +224 -219
- package/src/components/expression-editor/core/chip.ts +101 -0
- package/src/components/expression-editor/core/editor.ts +513 -0
- package/src/components/expression-editor/core/mention.ts +121 -0
- package/src/components/expression-editor/core/model.ts +109 -0
- package/src/components/expression-editor/di.ts +4 -32
- package/src/components/expression-editor/expression-editor.vue +184 -151
- package/src/components/expression-editor/style.scss +148 -220
- package/src/components/expression-editor/style.ts +0 -2
- package/src/components/layout/layout.vue +17 -19
- package/src/components/number-input/number-input.vue +4 -4
- package/src/components/table/use-table.ts +16 -0
- package/src/components/tree/style.scss +30 -33
- package/src/components/tree/tree-node.vue +4 -2
- package/src/types/action.ts +42 -4
- package/src/types/cascade.ts +4 -3
- package/src/types/code-editor.ts +9 -2
- package/src/types/expression-editor.ts +9 -0
- package/dist/components/action/action-group.vue_vue_type_script_setup_true_lang.js +0 -65
- package/dist/components/action/action-group.vue_vue_type_script_setup_true_lang.js.map +0 -1
- package/dist/components/expression-editor/constants.js +0 -9
- package/dist/components/expression-editor/constants.js.map +0 -1
- package/dist/components/expression-editor/internal/editor-runtime/create-runtime.js +0 -47
- package/dist/components/expression-editor/internal/editor-runtime/create-runtime.js.map +0 -1
- package/dist/components/expression-editor/internal/editor-runtime/model-sync.js +0 -49
- package/dist/components/expression-editor/internal/editor-runtime/model-sync.js.map +0 -1
- package/dist/components/expression-editor/internal/features/commands/register-command-packs.js +0 -215
- package/dist/components/expression-editor/internal/features/commands/register-command-packs.js.map +0 -1
- package/dist/components/expression-editor/internal/features/drag-drop/drag-drop-service.js +0 -33
- package/dist/components/expression-editor/internal/features/drag-drop/drag-drop-service.js.map +0 -1
- package/dist/components/expression-editor/internal/features/insertion/insertion-service.js +0 -33
- package/dist/components/expression-editor/internal/features/insertion/insertion-service.js.map +0 -1
- package/dist/components/expression-editor/nodes/variable-node.js +0 -94
- package/dist/components/expression-editor/nodes/variable-node.js.map +0 -1
- package/dist/components/expression-editor/parser.js +0 -32
- package/dist/components/expression-editor/parser.js.map +0 -1
- package/dist/components/expression-editor/plain-text.js +0 -19
- package/dist/components/expression-editor/plain-text.js.map +0 -1
- package/dist/components/expression-editor/use-context.js +0 -69
- package/dist/components/expression-editor/use-context.js.map +0 -1
- package/dist/components/expression-editor/use-decorators.js +0 -27
- package/dist/components/expression-editor/use-decorators.js.map +0 -1
- package/dist/components/expression-editor/use-expression-drag-drop.js +0 -275
- package/dist/components/expression-editor/use-expression-drag-drop.js.map +0 -1
- package/src/components/batch-edit/batch-edit-new.vue +0 -168
- package/src/components/batch-edit/style-new.scss +0 -35
- package/src/components/batch-edit/use-edit-new.ts +0 -68
- package/src/components/batch-edit/use-tip.ts +0 -47
- package/src/components/expression-editor/__test__/architecture-boundaries.test.ts +0 -103
- package/src/components/expression-editor/__test__/architecture-compatibility.test.ts +0 -52
- package/src/components/expression-editor/__test__/drag-drop.test.ts +0 -171
- package/src/components/expression-editor/__test__/parser.test.ts +0 -62
- package/src/components/expression-editor/constants.ts +0 -6
- package/src/components/expression-editor/internal/contracts/editor-runtime.ts +0 -11
- package/src/components/expression-editor/internal/editor-runtime/create-runtime.ts +0 -64
- package/src/components/expression-editor/internal/editor-runtime/index.ts +0 -1
- package/src/components/expression-editor/internal/editor-runtime/model-sync.ts +0 -68
- package/src/components/expression-editor/internal/features/commands/register-command-packs.ts +0 -517
- package/src/components/expression-editor/internal/features/drag-drop/drag-drop-service.ts +0 -51
- package/src/components/expression-editor/internal/features/insertion/insertion-service.ts +0 -56
- package/src/components/expression-editor/nodes/variable-node.tsx +0 -118
- package/src/components/expression-editor/parser.ts +0 -46
- package/src/components/expression-editor/plain-text.ts +0 -25
- package/src/components/expression-editor/plan.md +0 -22
- package/src/components/expression-editor/use-context.ts +0 -134
- package/src/components/expression-editor/use-decorators.tsx +0 -29
- package/src/components/expression-editor/use-editor.ts +0 -12
- package/src/components/expression-editor/use-expression-drag-drop.ts +0 -396
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import { ChipAttrs, createChip, isChipElement, readChipValue, setChipFocused } from '../core/chip'
|
|
5
|
+
|
|
6
|
+
// 本地最小 BEM 实例:避免引入 @veltra/utils → vue 间接依赖在 happy-dom 环境下触发 .d.mts 解析失败
|
|
7
|
+
const B = 'u-expression-editor'
|
|
8
|
+
const cls = {
|
|
9
|
+
b: B,
|
|
10
|
+
e: (n: string) => `${B}__${n}`,
|
|
11
|
+
m: (n: string) => `${B}--${n}`,
|
|
12
|
+
em: (e: string, m: string) => `${B}__${e}--${m}`,
|
|
13
|
+
create: () => cls
|
|
14
|
+
} as any
|
|
15
|
+
|
|
16
|
+
function makeChip(overrides: Partial<{ value: string; label: string; type?: string }> = {}) {
|
|
17
|
+
const onRemove = vi.fn<(el: HTMLElement) => void>()
|
|
18
|
+
const onReselect = vi.fn<(el: HTMLElement) => void>()
|
|
19
|
+
const segment = {
|
|
20
|
+
kind: 'var' as const,
|
|
21
|
+
value: overrides.value ?? 'form.user.name',
|
|
22
|
+
label: overrides.label ?? '用户姓名',
|
|
23
|
+
...(overrides.type ? { type: overrides.type } : {})
|
|
24
|
+
}
|
|
25
|
+
const el = createChip(segment, { cls, onRemove, onReselect })
|
|
26
|
+
return { el, onRemove, onReselect, segment }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe('createChip', () => {
|
|
30
|
+
it('renders chip with data-seg, data-value and contenteditable=false', () => {
|
|
31
|
+
const { el, segment } = makeChip()
|
|
32
|
+
expect(el.getAttribute(ChipAttrs.seg)).toBe('var')
|
|
33
|
+
expect(el.getAttribute(ChipAttrs.value)).toBe(segment.value)
|
|
34
|
+
expect(el.getAttribute('contenteditable')).toBe('false')
|
|
35
|
+
expect(el.className).toContain('u-expression-editor__chip')
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('renders label with type suffix when type provided', () => {
|
|
39
|
+
const { el } = makeChip({ value: 'x', label: '名称', type: 'string' })
|
|
40
|
+
expect(el.textContent).toContain('名称 (string)')
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('renders label without type suffix when type is absent', () => {
|
|
44
|
+
const { el } = makeChip({ value: 'x', label: '名称' })
|
|
45
|
+
expect(el.textContent).toContain('名称')
|
|
46
|
+
expect(el.textContent).not.toContain('(')
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('isChipElement detects chip nodes', () => {
|
|
50
|
+
const { el } = makeChip()
|
|
51
|
+
expect(isChipElement(el)).toBe(true)
|
|
52
|
+
expect(isChipElement(document.createElement('span'))).toBe(false)
|
|
53
|
+
expect(isChipElement(null)).toBe(false)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('mousedown on chip body triggers onReselect', () => {
|
|
57
|
+
const { el, onReselect, onRemove } = makeChip()
|
|
58
|
+
const label = el.querySelector(`.${cls.e('chip-label')}`) as HTMLElement
|
|
59
|
+
label.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }))
|
|
60
|
+
expect(onReselect).toHaveBeenCalledTimes(1)
|
|
61
|
+
expect(onReselect).toHaveBeenCalledWith(el)
|
|
62
|
+
expect(onRemove).not.toHaveBeenCalled()
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('mousedown on close (×) triggers onRemove and not onReselect', () => {
|
|
66
|
+
const { el, onReselect, onRemove } = makeChip()
|
|
67
|
+
const close = el.querySelector(`[${ChipAttrs.close}]`) as HTMLElement
|
|
68
|
+
close.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }))
|
|
69
|
+
expect(onRemove).toHaveBeenCalledTimes(1)
|
|
70
|
+
expect(onRemove).toHaveBeenCalledWith(el)
|
|
71
|
+
expect(onReselect).not.toHaveBeenCalled()
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('readChipValue returns the bound value', () => {
|
|
75
|
+
const { el } = makeChip({ value: 'a.b.c' })
|
|
76
|
+
expect(readChipValue(el)).toBe('a.b.c')
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('setChipFocused toggles is-focused class', () => {
|
|
80
|
+
const { el } = makeChip()
|
|
81
|
+
setChipFocused(el, true)
|
|
82
|
+
expect(el.classList.contains('is-focused')).toBe(true)
|
|
83
|
+
setChipFocused(el, false)
|
|
84
|
+
expect(el.classList.contains('is-focused')).toBe(false)
|
|
85
|
+
})
|
|
86
|
+
})
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { createMention } from '../core/mention'
|
|
4
|
+
import { parse, type Doc } from '../core/model'
|
|
5
|
+
|
|
6
|
+
function docFromText(text: string): Doc {
|
|
7
|
+
return parse(text)
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
describe('mention state machine', () => {
|
|
11
|
+
it('idle: empty doc and caret=null → no mention', () => {
|
|
12
|
+
const m = createMention()
|
|
13
|
+
expect(m.update([], null)).toBeNull()
|
|
14
|
+
expect(m.getState()).toBeNull()
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('typing @ activates mention with empty filter', () => {
|
|
18
|
+
const m = createMention()
|
|
19
|
+
const doc = docFromText('hello@')
|
|
20
|
+
const s = m.update(doc, 6)
|
|
21
|
+
expect(s).toEqual({ anchorOffset: 5, filter: '' })
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('typing characters after @ extends filter', () => {
|
|
25
|
+
const m = createMention()
|
|
26
|
+
const doc = docFromText('hello@form')
|
|
27
|
+
const s = m.update(doc, 10)
|
|
28
|
+
expect(s).toEqual({ anchorOffset: 5, filter: 'form' })
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('typing whitespace exits mention (no activation)', () => {
|
|
32
|
+
const m = createMention()
|
|
33
|
+
const doc = docFromText('hello@form ')
|
|
34
|
+
const s = m.update(doc, 11)
|
|
35
|
+
expect(s).toBeNull()
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('moving caret to before @ returns null', () => {
|
|
39
|
+
const m = createMention()
|
|
40
|
+
const doc = docFromText('hello@form')
|
|
41
|
+
expect(m.update(doc, 4)).toBeNull()
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('typing a second @ resets anchor to the new @', () => {
|
|
45
|
+
const m = createMention()
|
|
46
|
+
let doc = docFromText('hello@form')
|
|
47
|
+
expect(m.update(doc, 10)).toEqual({ anchorOffset: 5, filter: 'form' })
|
|
48
|
+
doc = docFromText('hello@form@user')
|
|
49
|
+
const s = m.update(doc, 15)
|
|
50
|
+
expect(s).toEqual({ anchorOffset: 10, filter: 'user' })
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('Backspace shrinking filter still keeps mention active', () => {
|
|
54
|
+
const m = createMention()
|
|
55
|
+
let doc = docFromText('hello@form')
|
|
56
|
+
expect(m.update(doc, 10)).toEqual({ anchorOffset: 5, filter: 'form' })
|
|
57
|
+
doc = docFromText('hello@for')
|
|
58
|
+
expect(m.update(doc, 9)).toEqual({ anchorOffset: 5, filter: 'for' })
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('Backspace through the @ exits mention', () => {
|
|
62
|
+
const m = createMention()
|
|
63
|
+
let doc = docFromText('hello@')
|
|
64
|
+
expect(m.update(doc, 6)).toEqual({ anchorOffset: 5, filter: '' })
|
|
65
|
+
doc = docFromText('hello')
|
|
66
|
+
expect(m.update(doc, 5)).toBeNull()
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('Esc / space / arrow → dismiss(): same anchor cannot reactivate by sync', () => {
|
|
70
|
+
const m = createMention()
|
|
71
|
+
let doc = docFromText('hello@form')
|
|
72
|
+
expect(m.update(doc, 10)).toEqual({ anchorOffset: 5, filter: 'form' })
|
|
73
|
+
m.dismiss()
|
|
74
|
+
expect(m.getState()).toBeNull()
|
|
75
|
+
|
|
76
|
+
// 用户继续在同一处键入字符
|
|
77
|
+
doc = docFromText('hello@formx')
|
|
78
|
+
expect(m.update(doc, 11)).toBeNull()
|
|
79
|
+
// 移动光标也不会重新激活
|
|
80
|
+
expect(m.update(doc, 9)).toBeNull()
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('after dismiss(), inserting a new @ creates a new mention with new anchor', () => {
|
|
84
|
+
const m = createMention()
|
|
85
|
+
let doc = docFromText('hello@form')
|
|
86
|
+
m.update(doc, 10)
|
|
87
|
+
m.dismiss()
|
|
88
|
+
doc = docFromText('hello@form @x')
|
|
89
|
+
const s = m.update(doc, 13)
|
|
90
|
+
expect(s).toEqual({ anchorOffset: 11, filter: 'x' })
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('mention does not span across a chip boundary', () => {
|
|
94
|
+
const m = createMention()
|
|
95
|
+
// 文本 "@a" + chip "{x}" + 文本 "@b" → 新 anchor 应是第二个 @
|
|
96
|
+
const doc: Doc = [
|
|
97
|
+
{ kind: 'text', value: '@a' },
|
|
98
|
+
{ kind: 'var', value: 'x', label: 'x' },
|
|
99
|
+
{ kind: 'text', value: '@b' }
|
|
100
|
+
]
|
|
101
|
+
// 序列化后:"@a{x}@b",caret 在 '@b' 后即 offset=2+3+2=7
|
|
102
|
+
const s = m.update(doc, 7)
|
|
103
|
+
expect(s).toEqual({ anchorOffset: 5, filter: 'b' })
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('caret at very end (text segment) activates mention', () => {
|
|
107
|
+
const m = createMention()
|
|
108
|
+
const doc = docFromText('@')
|
|
109
|
+
expect(m.update(doc, 1)).toEqual({ anchorOffset: 0, filter: '' })
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('commit() clears dismissedAnchor so next sync can produce fresh state', () => {
|
|
113
|
+
const m = createMention()
|
|
114
|
+
let doc = docFromText('hello@form')
|
|
115
|
+
m.update(doc, 10)
|
|
116
|
+
m.commit()
|
|
117
|
+
|
|
118
|
+
doc = docFromText('hello@form@')
|
|
119
|
+
const s = m.update(doc, 11)
|
|
120
|
+
expect(s).toEqual({ anchorOffset: 10, filter: '' })
|
|
121
|
+
})
|
|
122
|
+
})
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { normalize, parse, serialize, splitAt, type Doc } from '../core/model'
|
|
4
|
+
|
|
5
|
+
function roundTrip(input: string): string {
|
|
6
|
+
return serialize(parse(input))
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
describe('parse / serialize round-trip', () => {
|
|
10
|
+
it('round-trips empty string', () => {
|
|
11
|
+
expect(roundTrip('')).toBe('')
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('round-trips plain text without variables', () => {
|
|
15
|
+
expect(roundTrip('hello')).toBe('hello')
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('round-trips a single variable', () => {
|
|
19
|
+
expect(roundTrip('{a}')).toBe('{a}')
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('round-trips text with one variable in middle', () => {
|
|
23
|
+
expect(roundTrip('hello{foo}world')).toBe('hello{foo}world')
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('round-trips text with prefix and suffix around a variable', () => {
|
|
27
|
+
expect(roundTrip('prefix{a}suffix')).toBe('prefix{a}suffix')
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('round-trips adjacent variables', () => {
|
|
31
|
+
expect(roundTrip('{a}{b}')).toBe('{a}{b}')
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('preserves text containing an unclosed brace as plain text', () => {
|
|
35
|
+
expect(roundTrip('hello{unclosed')).toBe('hello{unclosed')
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('round-trips multi-line content with variables', () => {
|
|
39
|
+
const input = '行一{form.user.name}\n行二{form.company.name}\n行三'
|
|
40
|
+
expect(roundTrip(input)).toBe(input)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('round-trips with dotted variable paths', () => {
|
|
44
|
+
expect(roundTrip('你好{form.user.name}')).toBe('你好{form.user.name}')
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
describe('parse with variableMap fills label/type', () => {
|
|
49
|
+
it('attaches label/type from map for known variables', () => {
|
|
50
|
+
const map = new Map([
|
|
51
|
+
['form.user.name', { label: '用户姓名', value: 'form.user.name', type: 'string' }]
|
|
52
|
+
])
|
|
53
|
+
const doc = parse('{form.user.name}', map)
|
|
54
|
+
expect(doc).toEqual([
|
|
55
|
+
{ kind: 'var', value: 'form.user.name', label: '用户姓名', type: 'string' }
|
|
56
|
+
])
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('falls back to value when not found in map', () => {
|
|
60
|
+
const doc = parse('{unknown}')
|
|
61
|
+
expect(doc).toEqual([{ kind: 'var', value: 'unknown', label: 'unknown' }])
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
describe('normalize', () => {
|
|
66
|
+
it('merges adjacent text segments', () => {
|
|
67
|
+
const doc: Doc = [
|
|
68
|
+
{ kind: 'text', value: 'a' },
|
|
69
|
+
{ kind: 'text', value: 'b' },
|
|
70
|
+
{ kind: 'text', value: 'c' }
|
|
71
|
+
]
|
|
72
|
+
expect(normalize(doc)).toEqual([{ kind: 'text', value: 'abc' }])
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('drops empty text segments', () => {
|
|
76
|
+
const doc: Doc = [
|
|
77
|
+
{ kind: 'text', value: '' },
|
|
78
|
+
{ kind: 'var', value: 'x', label: 'X' },
|
|
79
|
+
{ kind: 'text', value: '' }
|
|
80
|
+
]
|
|
81
|
+
expect(normalize(doc)).toEqual([{ kind: 'var', value: 'x', label: 'X' }])
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('keeps a single text segment as is', () => {
|
|
85
|
+
const doc: Doc = [{ kind: 'text', value: 'hi' }]
|
|
86
|
+
expect(normalize(doc)).toEqual([{ kind: 'text', value: 'hi' }])
|
|
87
|
+
})
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
describe('splitAt', () => {
|
|
91
|
+
it('splits inside a text segment', () => {
|
|
92
|
+
const doc = parse('hello world')
|
|
93
|
+
const { before, after } = splitAt(doc, 5)
|
|
94
|
+
expect(before).toEqual([{ kind: 'text', value: 'hello' }])
|
|
95
|
+
expect(after).toEqual([{ kind: 'text', value: ' world' }])
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('treats a variable as atomic and never splits inside', () => {
|
|
99
|
+
const doc = parse('a{x}b')
|
|
100
|
+
const { before, after } = splitAt(doc, 2) // 落到 {x} 内部
|
|
101
|
+
expect(before).toEqual([{ kind: 'text', value: 'a' }])
|
|
102
|
+
expect(after).toEqual([
|
|
103
|
+
{ kind: 'var', value: 'x', label: 'x' },
|
|
104
|
+
{ kind: 'text', value: 'b' }
|
|
105
|
+
])
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('splits exactly at variable boundary', () => {
|
|
109
|
+
const doc = parse('a{x}b')
|
|
110
|
+
const split = splitAt(doc, 1)
|
|
111
|
+
expect(split.before).toEqual([{ kind: 'text', value: 'a' }])
|
|
112
|
+
expect(split.after).toEqual([
|
|
113
|
+
{ kind: 'var', value: 'x', label: 'x' },
|
|
114
|
+
{ kind: 'text', value: 'b' }
|
|
115
|
+
])
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('returns full doc on the right when offset is 0', () => {
|
|
119
|
+
const doc = parse('hello')
|
|
120
|
+
expect(splitAt(doc, 0)).toEqual({ before: [], after: [{ kind: 'text', value: 'hello' }] })
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('returns full doc on the left when offset >= length', () => {
|
|
124
|
+
const doc = parse('hello')
|
|
125
|
+
expect(splitAt(doc, 100)).toEqual({ before: [{ kind: 'text', value: 'hello' }], after: [] })
|
|
126
|
+
})
|
|
127
|
+
})
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div :class="cls.e('path-preview')">
|
|
3
|
+
<div :class="cls.e('path-preview-title')">变量路径</div>
|
|
4
|
+
<ul :class="cls.e('path-tree')">
|
|
5
|
+
<li
|
|
6
|
+
v-for="(node, i) in path"
|
|
7
|
+
:key="`${i}-${node.value || node.label}`"
|
|
8
|
+
:class="[cls.e('path-tree-item'), bem.is('active', i === path.length - 1)]"
|
|
9
|
+
:style="{ paddingLeft: `${i * 14}px` }"
|
|
10
|
+
>
|
|
11
|
+
<span v-if="i > 0" :class="cls.e('path-tree-branch')">└</span>
|
|
12
|
+
<span :class="cls.e('path-tree-label')">{{ node.label }}</span>
|
|
13
|
+
</li>
|
|
14
|
+
</ul>
|
|
15
|
+
</div>
|
|
16
|
+
</template>
|
|
17
|
+
|
|
18
|
+
<script lang="ts" setup>
|
|
19
|
+
import { bem } from '@veltra/utils'
|
|
20
|
+
|
|
21
|
+
import type { VariableItem } from '../../../types'
|
|
22
|
+
|
|
23
|
+
defineOptions({
|
|
24
|
+
name: 'ExpressionPathPreview'
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
defineProps<{
|
|
28
|
+
path: VariableItem[]
|
|
29
|
+
}>()
|
|
30
|
+
|
|
31
|
+
const cls = bem('expression-editor')
|
|
32
|
+
</script>
|