@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.
Files changed (180) hide show
  1. package/dist/components/action/action-group.js +110 -3
  2. package/dist/components/action/action-group.js.map +1 -1
  3. package/dist/components/action/action-group.vue.d.ts +16 -3
  4. package/dist/components/action/action.js +26 -13
  5. package/dist/components/action/action.js.map +1 -1
  6. package/dist/components/action/action.vue.d.ts +2 -2
  7. package/dist/components/action/style.js +2 -1
  8. package/dist/components/action/style2.css +35 -7
  9. package/dist/components/auto-complete/auto-complete.vue.d.ts +2 -2
  10. package/dist/components/batch-edit/batch-edit-form.js +106 -42
  11. package/dist/components/batch-edit/batch-edit-form.js.map +1 -1
  12. package/dist/components/batch-edit/batch-edit-list.js +84 -47
  13. package/dist/components/batch-edit/batch-edit-list.js.map +1 -1
  14. package/dist/components/batch-edit/batch-edit.js +42 -10
  15. package/dist/components/batch-edit/batch-edit.js.map +1 -1
  16. package/dist/components/batch-edit/style.js +2 -4
  17. package/dist/components/batch-edit/style2.css +202 -30
  18. package/dist/components/batch-edit/use-edit.js +45 -9
  19. package/dist/components/batch-edit/use-edit.js.map +1 -1
  20. package/dist/components/cascade/cascade.js +3 -1
  21. package/dist/components/cascade/cascade.js.map +1 -1
  22. package/dist/components/cascade/cascade.vue.d.ts +2 -2
  23. package/dist/components/checkbox/checkbox-button.vue.d.ts +1 -1
  24. package/dist/components/checkbox-group/checkbox-group.vue.d.ts +2 -2
  25. package/dist/components/code-editor/code-editor.js +145 -49
  26. package/dist/components/code-editor/code-editor.js.map +1 -1
  27. package/dist/components/code-editor/code-editor.vue.d.ts +2 -0
  28. package/dist/components/code-editor/style2.css +87 -19
  29. package/dist/components/date-panel/date-panel.vue.d.ts +1 -1
  30. package/dist/components/date-picker/date-picker.vue.d.ts +2 -2
  31. package/dist/components/date-range-picker/date-range-picker.vue.d.ts +2 -2
  32. package/dist/components/dialog/dialog.js +6 -2
  33. package/dist/components/dialog/dialog.js.map +1 -1
  34. package/dist/components/dialog/style2.css +2 -3
  35. package/dist/components/expression-editor/components/path-preview.js +27 -0
  36. package/dist/components/expression-editor/components/path-preview.js.map +1 -0
  37. package/dist/components/expression-editor/components/variable-picker.js +199 -153
  38. package/dist/components/expression-editor/components/variable-picker.js.map +1 -1
  39. package/dist/components/expression-editor/core/chip.js +54 -0
  40. package/dist/components/expression-editor/core/chip.js.map +1 -0
  41. package/dist/components/expression-editor/core/editor.js +424 -0
  42. package/dist/components/expression-editor/core/editor.js.map +1 -0
  43. package/dist/components/expression-editor/core/mention.js +91 -0
  44. package/dist/components/expression-editor/core/mention.js.map +1 -0
  45. package/dist/components/expression-editor/core/model.js +64 -0
  46. package/dist/components/expression-editor/core/model.js.map +1 -0
  47. package/dist/components/expression-editor/di.js +4 -10
  48. package/dist/components/expression-editor/di.js.map +1 -1
  49. package/dist/components/expression-editor/expression-editor.js +168 -114
  50. package/dist/components/expression-editor/expression-editor.js.map +1 -1
  51. package/dist/components/expression-editor/expression-editor.vue.d.ts +2 -1
  52. package/dist/components/expression-editor/style.js +0 -2
  53. package/dist/components/expression-editor/style2.css +145 -219
  54. package/dist/components/index.js +54 -54
  55. package/dist/components/input/input.vue.d.ts +2 -2
  56. package/dist/components/layout/layout.js +12 -11
  57. package/dist/components/layout/layout.js.map +1 -1
  58. package/dist/components/multi-select/multi-select.vue.d.ts +3 -3
  59. package/dist/components/multi-tree-select/multi-tree-select.vue.d.ts +3 -3
  60. package/dist/components/notification/notification.vue.d.ts +1 -1
  61. package/dist/components/number/number.vue.d.ts +1 -1
  62. package/dist/components/number-input/number-input.js +4 -4
  63. package/dist/components/number-input/number-input.js.map +1 -1
  64. package/dist/components/number-input/number-input.vue.d.ts +2 -2
  65. package/dist/components/number-range-input/number-range-input.vue.d.ts +1 -1
  66. package/dist/components/password-input/password-input.vue.d.ts +1 -1
  67. package/dist/components/pop-confirm/pop-confirm.vue.d.ts +1 -1
  68. package/dist/components/radio-group/radio-group.vue.d.ts +2 -2
  69. package/dist/components/rich-text-editor/rich-text-editor.js +1 -1
  70. package/dist/components/rich-text-editor/rich-text-editor.vue.d.ts +1 -1
  71. package/dist/components/rich-text-editor/toolbar.js +2 -2
  72. package/dist/components/select/select.vue.d.ts +2 -2
  73. package/dist/components/steps/steps.vue.d.ts +1 -1
  74. package/dist/components/table/use-table.js +16 -0
  75. package/dist/components/table/use-table.js.map +1 -1
  76. package/dist/components/tabs/tabs-horizontal.vue.d.ts +1 -1
  77. package/dist/components/tabs/tabs-vertical.vue.d.ts +1 -1
  78. package/dist/components/tabs/tabs.vue.d.ts +1 -1
  79. package/dist/components/textarea/textarea.vue.d.ts +2 -2
  80. package/dist/components/tip/tip.vue.d.ts +1 -1
  81. package/dist/components/tree/style2.css +18 -12
  82. package/dist/components/tree/tree-node.js +7 -4
  83. package/dist/components/tree/tree-node.js.map +1 -1
  84. package/dist/components/tree-select/tree-select.vue.d.ts +3 -3
  85. package/dist/index.d.ts +3 -3
  86. package/dist/index.js +83 -83
  87. package/dist/style.js +3 -3
  88. package/dist/types/action.d.ts +38 -4
  89. package/dist/types/cascade.d.ts +2 -3
  90. package/dist/types/code-editor.d.ts +9 -2
  91. package/dist/types/expression-editor.d.ts +9 -1
  92. package/dist/types/index.d.ts +2 -2
  93. package/package.json +11 -12
  94. package/src/components/action/action-group.vue +126 -56
  95. package/src/components/action/action.vue +35 -12
  96. package/src/components/action/style.scss +46 -10
  97. package/src/components/action/style.ts +2 -1
  98. package/src/components/batch-edit/batch-edit-form.vue +121 -45
  99. package/src/components/batch-edit/batch-edit-list.vue +56 -39
  100. package/src/components/batch-edit/batch-edit.vue +57 -6
  101. package/src/components/batch-edit/style.scss +251 -37
  102. package/src/components/batch-edit/style.ts +5 -8
  103. package/src/components/batch-edit/use-edit.ts +58 -11
  104. package/src/components/cascade/cascade.vue +3 -1
  105. package/src/components/code-editor/code-editor.vue +198 -63
  106. package/src/components/code-editor/style.scss +110 -12
  107. package/src/components/dialog/dialog.vue +6 -2
  108. package/src/components/dialog/style.scss +4 -4
  109. package/src/components/expression-editor/__test__/chip.test.ts +86 -0
  110. package/src/components/expression-editor/__test__/mention.test.ts +122 -0
  111. package/src/components/expression-editor/__test__/model.test.ts +127 -0
  112. package/src/components/expression-editor/components/path-preview.vue +32 -0
  113. package/src/components/expression-editor/components/variable-picker.vue +224 -219
  114. package/src/components/expression-editor/core/chip.ts +101 -0
  115. package/src/components/expression-editor/core/editor.ts +513 -0
  116. package/src/components/expression-editor/core/mention.ts +121 -0
  117. package/src/components/expression-editor/core/model.ts +109 -0
  118. package/src/components/expression-editor/di.ts +4 -32
  119. package/src/components/expression-editor/expression-editor.vue +184 -151
  120. package/src/components/expression-editor/style.scss +148 -220
  121. package/src/components/expression-editor/style.ts +0 -2
  122. package/src/components/layout/layout.vue +17 -19
  123. package/src/components/number-input/number-input.vue +4 -4
  124. package/src/components/table/use-table.ts +16 -0
  125. package/src/components/tree/style.scss +30 -33
  126. package/src/components/tree/tree-node.vue +4 -2
  127. package/src/types/action.ts +42 -4
  128. package/src/types/cascade.ts +4 -3
  129. package/src/types/code-editor.ts +9 -2
  130. package/src/types/expression-editor.ts +9 -0
  131. package/dist/components/action/action-group.vue_vue_type_script_setup_true_lang.js +0 -65
  132. package/dist/components/action/action-group.vue_vue_type_script_setup_true_lang.js.map +0 -1
  133. package/dist/components/expression-editor/constants.js +0 -9
  134. package/dist/components/expression-editor/constants.js.map +0 -1
  135. package/dist/components/expression-editor/internal/editor-runtime/create-runtime.js +0 -47
  136. package/dist/components/expression-editor/internal/editor-runtime/create-runtime.js.map +0 -1
  137. package/dist/components/expression-editor/internal/editor-runtime/model-sync.js +0 -49
  138. package/dist/components/expression-editor/internal/editor-runtime/model-sync.js.map +0 -1
  139. package/dist/components/expression-editor/internal/features/commands/register-command-packs.js +0 -215
  140. package/dist/components/expression-editor/internal/features/commands/register-command-packs.js.map +0 -1
  141. package/dist/components/expression-editor/internal/features/drag-drop/drag-drop-service.js +0 -33
  142. package/dist/components/expression-editor/internal/features/drag-drop/drag-drop-service.js.map +0 -1
  143. package/dist/components/expression-editor/internal/features/insertion/insertion-service.js +0 -33
  144. package/dist/components/expression-editor/internal/features/insertion/insertion-service.js.map +0 -1
  145. package/dist/components/expression-editor/nodes/variable-node.js +0 -94
  146. package/dist/components/expression-editor/nodes/variable-node.js.map +0 -1
  147. package/dist/components/expression-editor/parser.js +0 -32
  148. package/dist/components/expression-editor/parser.js.map +0 -1
  149. package/dist/components/expression-editor/plain-text.js +0 -19
  150. package/dist/components/expression-editor/plain-text.js.map +0 -1
  151. package/dist/components/expression-editor/use-context.js +0 -69
  152. package/dist/components/expression-editor/use-context.js.map +0 -1
  153. package/dist/components/expression-editor/use-decorators.js +0 -27
  154. package/dist/components/expression-editor/use-decorators.js.map +0 -1
  155. package/dist/components/expression-editor/use-expression-drag-drop.js +0 -275
  156. package/dist/components/expression-editor/use-expression-drag-drop.js.map +0 -1
  157. package/src/components/batch-edit/batch-edit-new.vue +0 -168
  158. package/src/components/batch-edit/style-new.scss +0 -35
  159. package/src/components/batch-edit/use-edit-new.ts +0 -68
  160. package/src/components/batch-edit/use-tip.ts +0 -47
  161. package/src/components/expression-editor/__test__/architecture-boundaries.test.ts +0 -103
  162. package/src/components/expression-editor/__test__/architecture-compatibility.test.ts +0 -52
  163. package/src/components/expression-editor/__test__/drag-drop.test.ts +0 -171
  164. package/src/components/expression-editor/__test__/parser.test.ts +0 -62
  165. package/src/components/expression-editor/constants.ts +0 -6
  166. package/src/components/expression-editor/internal/contracts/editor-runtime.ts +0 -11
  167. package/src/components/expression-editor/internal/editor-runtime/create-runtime.ts +0 -64
  168. package/src/components/expression-editor/internal/editor-runtime/index.ts +0 -1
  169. package/src/components/expression-editor/internal/editor-runtime/model-sync.ts +0 -68
  170. package/src/components/expression-editor/internal/features/commands/register-command-packs.ts +0 -517
  171. package/src/components/expression-editor/internal/features/drag-drop/drag-drop-service.ts +0 -51
  172. package/src/components/expression-editor/internal/features/insertion/insertion-service.ts +0 -56
  173. package/src/components/expression-editor/nodes/variable-node.tsx +0 -118
  174. package/src/components/expression-editor/parser.ts +0 -46
  175. package/src/components/expression-editor/plain-text.ts +0 -25
  176. package/src/components/expression-editor/plan.md +0 -22
  177. package/src/components/expression-editor/use-context.ts +0 -134
  178. package/src/components/expression-editor/use-decorators.tsx +0 -29
  179. package/src/components/expression-editor/use-editor.ts +0 -12
  180. 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>