niuma-ui 1.0.0

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 (220) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/LICENSE +202 -0
  3. package/NOTICE +25 -0
  4. package/README.en.md +99 -0
  5. package/README.md +161 -0
  6. package/package.json +106 -0
  7. package/src/__tests__/RsBadge.spec.ts +40 -0
  8. package/src/__tests__/RsBreadcrumb.spec.ts +172 -0
  9. package/src/__tests__/RsButton.spec.ts +108 -0
  10. package/src/__tests__/RsCalendarGrid.spec.ts +82 -0
  11. package/src/__tests__/RsCard.spec.ts +97 -0
  12. package/src/__tests__/RsCheckbox.spec.ts +36 -0
  13. package/src/__tests__/RsCodeEditor.spec.ts +111 -0
  14. package/src/__tests__/RsConfirmDialog.spec.ts +158 -0
  15. package/src/__tests__/RsContainer.spec.ts +89 -0
  16. package/src/__tests__/RsContextMenu.spec.ts +118 -0
  17. package/src/__tests__/RsDatePicker.spec.ts +88 -0
  18. package/src/__tests__/RsDateTimePicker.spec.ts +69 -0
  19. package/src/__tests__/RsDialog.spec.ts +404 -0
  20. package/src/__tests__/RsDrawer.spec.ts +145 -0
  21. package/src/__tests__/RsDropdown.spec.ts +135 -0
  22. package/src/__tests__/RsEmpty.spec.ts +99 -0
  23. package/src/__tests__/RsForm.spec.ts +173 -0
  24. package/src/__tests__/RsIcon.spec.ts +244 -0
  25. package/src/__tests__/RsInput.spec.ts +269 -0
  26. package/src/__tests__/RsInputNumber.spec.ts +118 -0
  27. package/src/__tests__/RsLabel.spec.ts +106 -0
  28. package/src/__tests__/RsLink.spec.ts +49 -0
  29. package/src/__tests__/RsLoading.spec.ts +84 -0
  30. package/src/__tests__/RsMarkdown.spec.ts +143 -0
  31. package/src/__tests__/RsMenu.spec.ts +119 -0
  32. package/src/__tests__/RsNewComponents.spec.ts +17 -0
  33. package/src/__tests__/RsPagination.spec.ts +158 -0
  34. package/src/__tests__/RsPopover.spec.ts +126 -0
  35. package/src/__tests__/RsScrollbar.spec.ts +53 -0
  36. package/src/__tests__/RsSelect.spec.ts +291 -0
  37. package/src/__tests__/RsSidebar.spec.ts +119 -0
  38. package/src/__tests__/RsSplitPane.spec.ts +177 -0
  39. package/src/__tests__/RsStatCard.spec.ts +79 -0
  40. package/src/__tests__/RsSteps.spec.ts +125 -0
  41. package/src/__tests__/RsTable.perf.spec.ts +103 -0
  42. package/src/__tests__/RsTable.spec.ts +1135 -0
  43. package/src/__tests__/RsTabs.spec.ts +328 -0
  44. package/src/__tests__/RsTerminal.spec.ts +218 -0
  45. package/src/__tests__/RsTimePicker.spec.ts +77 -0
  46. package/src/__tests__/RsToaster.spec.ts +95 -0
  47. package/src/__tests__/RsToolbar.spec.ts +75 -0
  48. package/src/__tests__/RsTooltip.spec.ts +111 -0
  49. package/src/__tests__/RsTree.spec.ts +572 -0
  50. package/src/__tests__/RsUpload.spec.ts +160 -0
  51. package/src/__tests__/RsVirtualList.spec.ts +77 -0
  52. package/src/__tests__/date-picker-utils.spec.ts +70 -0
  53. package/src/__tests__/dialog-viewport.spec.ts +38 -0
  54. package/src/__tests__/iso-local-datetime.spec.ts +58 -0
  55. package/src/__tests__/rs-clipboard.spec.ts +142 -0
  56. package/src/__tests__/setup.ts +12 -0
  57. package/src/__tests__/split-pane-utils.spec.ts +136 -0
  58. package/src/__tests__/table-drag.spec.ts +35 -0
  59. package/src/__tests__/table-edit-utils.spec.ts +194 -0
  60. package/src/__tests__/table-utils.spec.ts +286 -0
  61. package/src/__tests__/theme.spec.ts +22 -0
  62. package/src/__tests__/tree-utils.spec.ts +234 -0
  63. package/src/__tests__/useTableCellTooltip.spec.ts +101 -0
  64. package/src/components/RsAvatar.vue +115 -0
  65. package/src/components/RsBadge.vue +59 -0
  66. package/src/components/RsBreadcrumb.vue +65 -0
  67. package/src/components/RsBreadcrumbItems.vue +36 -0
  68. package/src/components/RsButton.vue +393 -0
  69. package/src/components/RsCalendarGrid.vue +284 -0
  70. package/src/components/RsCard.vue +237 -0
  71. package/src/components/RsCheckbox.vue +189 -0
  72. package/src/components/RsCodeBlock.vue +278 -0
  73. package/src/components/RsCodeEditor.vue +597 -0
  74. package/src/components/RsConfigProvider.vue +92 -0
  75. package/src/components/RsConfirmDialog.vue +178 -0
  76. package/src/components/RsContainer.vue +235 -0
  77. package/src/components/RsContextMenu.vue +238 -0
  78. package/src/components/RsContextMenuItems.vue +77 -0
  79. package/src/components/RsDatePicker.vue +756 -0
  80. package/src/components/RsDateTimePicker.vue +42 -0
  81. package/src/components/RsDialog.vue +486 -0
  82. package/src/components/RsDrawer.vue +175 -0
  83. package/src/components/RsDropdown.vue +252 -0
  84. package/src/components/RsDropdownItems.vue +90 -0
  85. package/src/components/RsEmpty.vue +107 -0
  86. package/src/components/RsForm.vue +131 -0
  87. package/src/components/RsIcon.vue +104 -0
  88. package/src/components/RsInput.vue +1101 -0
  89. package/src/components/RsInputNumber.vue +532 -0
  90. package/src/components/RsLabel.vue +54 -0
  91. package/src/components/RsLink.vue +106 -0
  92. package/src/components/RsLoading.vue +171 -0
  93. package/src/components/RsMarkdown.vue +329 -0
  94. package/src/components/RsMenu.vue +256 -0
  95. package/src/components/RsMenuItems.vue +125 -0
  96. package/src/components/RsMonacoEditor.vue +541 -0
  97. package/src/components/RsPagination.vue +184 -0
  98. package/src/components/RsPopover.vue +77 -0
  99. package/src/components/RsProseEditor.vue +160 -0
  100. package/src/components/RsScrollbar.vue +171 -0
  101. package/src/components/RsSelect.vue +1045 -0
  102. package/src/components/RsSidebar.vue +121 -0
  103. package/src/components/RsSidebarGroup.vue +32 -0
  104. package/src/components/RsSidebarItem.vue +72 -0
  105. package/src/components/RsSplitPane.vue +619 -0
  106. package/src/components/RsStatCard.vue +85 -0
  107. package/src/components/RsSteps.vue +138 -0
  108. package/src/components/RsTable.vue +2664 -0
  109. package/src/components/RsTableColgroup.vue +17 -0
  110. package/src/components/RsTableHeaderRow.vue +30 -0
  111. package/src/components/RsTabs.vue +951 -0
  112. package/src/components/RsTerminal.vue +697 -0
  113. package/src/components/RsTimePicker.vue +554 -0
  114. package/src/components/RsTimePickerColumns.vue +226 -0
  115. package/src/components/RsToaster.vue +396 -0
  116. package/src/components/RsToolbar.vue +135 -0
  117. package/src/components/RsTooltip.vue +142 -0
  118. package/src/components/RsTooltipProvider.vue +20 -0
  119. package/src/components/RsTree.vue +1248 -0
  120. package/src/components/RsUpload.vue +279 -0
  121. package/src/components/RsVirtualList.vue +217 -0
  122. package/src/components/avatar-utils.ts +19 -0
  123. package/src/components/breadcrumb-utils.ts +19 -0
  124. package/src/components/class-utils.ts +29 -0
  125. package/src/components/code-editor-utils.ts +106 -0
  126. package/src/components/code-mirror-completion.ts +40 -0
  127. package/src/components/code-mirror-diagnostics.ts +52 -0
  128. package/src/components/code-mirror-ghost.ts +158 -0
  129. package/src/components/code-mirror-inline-edit.ts +55 -0
  130. package/src/components/code-mirror-intel.ts +81 -0
  131. package/src/components/code-mirror-lang.ts +128 -0
  132. package/src/components/context-menu-utils.ts +20 -0
  133. package/src/components/date-picker-utils.ts +284 -0
  134. package/src/components/date-range-validation.ts +29 -0
  135. package/src/components/date-validation.ts +38 -0
  136. package/src/components/dialog-viewport.ts +48 -0
  137. package/src/components/dialog-window.ts +253 -0
  138. package/src/components/dropdown-utils.ts +31 -0
  139. package/src/components/form-utils.ts +77 -0
  140. package/src/components/input-number-utils.ts +128 -0
  141. package/src/components/input-rules.ts +84 -0
  142. package/src/components/loading-utils.ts +8 -0
  143. package/src/components/markdown-utils.ts +126 -0
  144. package/src/components/menu-utils.ts +48 -0
  145. package/src/components/overlay-utils.ts +47 -0
  146. package/src/components/pagination-utils.ts +38 -0
  147. package/src/components/popover-utils.ts +8 -0
  148. package/src/components/reka.ts +106 -0
  149. package/src/components/resolve-radius.ts +22 -0
  150. package/src/components/resolve-size.ts +29 -0
  151. package/src/components/scrollbar-utils.ts +10 -0
  152. package/src/components/select-utils.ts +72 -0
  153. package/src/components/split-pane-utils.ts +187 -0
  154. package/src/components/steps-utils.ts +22 -0
  155. package/src/components/table/RsTableBodyRow.vue +287 -0
  156. package/src/components/table/RsTableCell.vue +239 -0
  157. package/src/components/table/RsTableCellContent.ts +18 -0
  158. package/src/components/table/RsTableCellEditor.vue +351 -0
  159. package/src/components/table/RsTableEditGutter.vue +54 -0
  160. package/src/components/table/RsTableHeaderFilter.vue +107 -0
  161. package/src/components/table/RsTableRowStatus.vue +69 -0
  162. package/src/components/table/rs-table.css +1141 -0
  163. package/src/components/table/table-context-menu.ts +53 -0
  164. package/src/components/table/table-edit-utils.ts +496 -0
  165. package/src/components/table/table-keys.ts +12 -0
  166. package/src/components/table-drag.ts +215 -0
  167. package/src/components/table-utils.ts +950 -0
  168. package/src/components/tabs-utils.ts +107 -0
  169. package/src/components/terminal-utils.ts +301 -0
  170. package/src/components/terminal-wheel.ts +49 -0
  171. package/src/components/time-picker-utils.ts +167 -0
  172. package/src/components/time-range-validation.ts +23 -0
  173. package/src/components/time-validation.ts +20 -0
  174. package/src/components/tree-utils.ts +518 -0
  175. package/src/components/upload-utils.ts +55 -0
  176. package/src/components/virtual-list-utils.ts +17 -0
  177. package/src/composables/useClickOutside.ts +22 -0
  178. package/src/composables/useRsConfig.ts +91 -0
  179. package/src/composables/useRsI18n.ts +22 -0
  180. package/src/composables/useRsTabsNav.ts +187 -0
  181. package/src/composables/useRsToast.ts +48 -0
  182. package/src/composables/useTableCellTooltip.ts +239 -0
  183. package/src/composables/useTableEdit.ts +271 -0
  184. package/src/composables/useTableRowHighlight.ts +50 -0
  185. package/src/dev/vite-codemirror-deps.ts +20 -0
  186. package/src/icons/custom/api.ts +54 -0
  187. package/src/icons/custom/clickhouse.ts +52 -0
  188. package/src/icons/custom/dameng.ts +83 -0
  189. package/src/icons/custom/ftp.ts +61 -0
  190. package/src/icons/custom/kingbase.ts +52 -0
  191. package/src/icons/custom/mongodb.ts +50 -0
  192. package/src/icons/custom/mysql.ts +51 -0
  193. package/src/icons/custom/oracle.ts +52 -0
  194. package/src/icons/custom/redis.ts +51 -0
  195. package/src/icons/custom/sqlite.ts +50 -0
  196. package/src/icons/custom/sqlserver.ts +52 -0
  197. package/src/icons/custom/vastbase.ts +57 -0
  198. package/src/icons/lucide.ts +78 -0
  199. package/src/icons/registry.ts +35 -0
  200. package/src/index.ts +425 -0
  201. package/src/lib/iso-local-datetime.ts +152 -0
  202. package/src/lib/rs-dayjs.ts +56 -0
  203. package/src/locale/messages.ts +362 -0
  204. package/src/locale/types.ts +5 -0
  205. package/src/monaco/debug-decorations.css +20 -0
  206. package/src/monaco/debug-decorations.ts +70 -0
  207. package/src/monaco/index.ts +22 -0
  208. package/src/monaco/languages/index.ts +21 -0
  209. package/src/monaco/languages/mongodb-shell.ts +125 -0
  210. package/src/monaco/languages/types.ts +55 -0
  211. package/src/monaco/setup.ts +101 -0
  212. package/src/styles.css +1040 -0
  213. package/src/theme/apply.ts +12 -0
  214. package/src/theme/brand.example.css +37 -0
  215. package/src/theme/presets.ts +69 -0
  216. package/src/theme/types.ts +81 -0
  217. package/src/utils/rs-clipboard.ts +272 -0
  218. package/src/vite-env.d.ts +7 -0
  219. package/vite-plugins/monaco-zh-nls.ts +101 -0
  220. package/vite-plugins/silence-antlr-parse-console.ts +29 -0
@@ -0,0 +1,136 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import {
3
+ applySplitResize,
4
+ collapseSplitPane,
5
+ expandSplitPane,
6
+ isSplitPaneCollapsed,
7
+ normalizeSplitSizes,
8
+ resolveSplitConstraints,
9
+ splitSizesEqual,
10
+ type RsSplitPaneItem,
11
+ } from '../components/split-pane-utils'
12
+
13
+ const sum = (values: number[]) => values.reduce((acc, value) => acc + value, 0)
14
+
15
+ describe('resolveSplitConstraints', () => {
16
+ it('applies defaults and clamps ranges', () => {
17
+ const result = resolveSplitConstraints([{ key: 'a' }])
18
+ expect(result[0]).toEqual({ min: 0, max: 100, collapsible: false, collapsedSize: 0 })
19
+ })
20
+
21
+ it('orders swapped min/max and caps collapsedSize at min', () => {
22
+ const result = resolveSplitConstraints([
23
+ { key: 'a', min: 60, max: 20, collapsible: true, collapsedSize: 40 },
24
+ ])
25
+ expect(result[0]).toEqual({ min: 20, max: 60, collapsible: true, collapsedSize: 20 })
26
+ })
27
+ })
28
+
29
+ describe('normalizeSplitSizes', () => {
30
+ it('distributes remaining space to unspecified panes', () => {
31
+ const panes: RsSplitPaneItem[] = [{ key: 'a', size: 40 }, { key: 'b' }, { key: 'c' }]
32
+ const sizes = normalizeSplitSizes(panes)
33
+ expect(sum(sizes)).toBeCloseTo(100)
34
+ expect(sizes[0]).toBeCloseTo(40)
35
+ expect(sizes[1]).toBeCloseTo(30)
36
+ expect(sizes[2]).toBeCloseTo(30)
37
+ })
38
+
39
+ it('rescales values that do not sum to 100', () => {
40
+ const sizes = normalizeSplitSizes([{ key: 'a', size: 1 }, { key: 'b', size: 3 }])
41
+ expect(sizes).toEqual([25, 75])
42
+ })
43
+
44
+ it('prefers provided (controlled) sizes over pane defaults', () => {
45
+ const panes: RsSplitPaneItem[] = [{ key: 'a', size: 20 }, { key: 'b', size: 80 }]
46
+ const sizes = normalizeSplitSizes(panes, [50, 50])
47
+ expect(sizes).toEqual([50, 50])
48
+ })
49
+
50
+ it('falls back to equal split when all sizes are zero', () => {
51
+ const sizes = normalizeSplitSizes([{ key: 'a', size: 0 }, { key: 'b', size: 0 }])
52
+ expect(sizes).toEqual([50, 50])
53
+ })
54
+ })
55
+
56
+ describe('applySplitResize', () => {
57
+ const constraints = resolveSplitConstraints([{ key: 'a' }, { key: 'b' }])
58
+
59
+ it('moves space between adjacent panes and preserves total', () => {
60
+ const next = applySplitResize([50, 50], constraints, 0, 10)
61
+ expect(next).toEqual([60, 40])
62
+ expect(sum(next)).toBe(100)
63
+ })
64
+
65
+ it('is immutable', () => {
66
+ const input = [50, 50]
67
+ const next = applySplitResize(input, constraints, 0, 10)
68
+ expect(input).toEqual([50, 50])
69
+ expect(next).not.toBe(input)
70
+ })
71
+
72
+ it('respects min/max of both neighbors', () => {
73
+ const bounded = resolveSplitConstraints([
74
+ { key: 'a', min: 30 },
75
+ { key: 'b', min: 30 },
76
+ ])
77
+ expect(applySplitResize([50, 50], bounded, 0, -40)).toEqual([30, 70])
78
+ expect(applySplitResize([50, 50], bounded, 0, 40)).toEqual([70, 30])
79
+ })
80
+
81
+ it('ignores dividers that are out of range', () => {
82
+ expect(applySplitResize([50, 50], constraints, 1, 10)).toEqual([50, 50])
83
+ expect(applySplitResize([50, 50], constraints, -1, 10)).toEqual([50, 50])
84
+ })
85
+
86
+ it('snaps a collapsible pane to collapsedSize past the halfway threshold', () => {
87
+ const collapsible = resolveSplitConstraints([
88
+ { key: 'a', min: 20, collapsible: true, collapsedSize: 0 },
89
+ { key: 'b' },
90
+ ])
91
+ // 50 - 45 = 5 < (20+0)/2 = 10 → 折叠到 0
92
+ expect(applySplitResize([50, 50], collapsible, 0, -45)).toEqual([0, 100])
93
+ // 50 - 38 = 12 > 10 → 夹到 min 20
94
+ expect(applySplitResize([50, 50], collapsible, 0, -38)).toEqual([20, 80])
95
+ })
96
+ })
97
+
98
+ describe('collapseSplitPane / expandSplitPane', () => {
99
+ const constraints = resolveSplitConstraints([
100
+ { key: 'a', min: 20, collapsible: true, collapsedSize: 0 },
101
+ { key: 'b' },
102
+ ])
103
+
104
+ it('collapses to collapsedSize and gives space to the neighbor', () => {
105
+ expect(collapseSplitPane([40, 60], constraints, 0)).toEqual([0, 100])
106
+ })
107
+
108
+ it('expands back to min by default', () => {
109
+ expect(expandSplitPane([0, 100], constraints, 0)).toEqual([20, 80])
110
+ })
111
+
112
+ it('expands to an explicit size clamped within bounds', () => {
113
+ expect(expandSplitPane([0, 100], constraints, 0, 35)).toEqual([35, 65])
114
+ })
115
+ })
116
+
117
+ describe('isSplitPaneCollapsed', () => {
118
+ const [collapsible, plain] = resolveSplitConstraints([
119
+ { key: 'a', collapsible: true, collapsedSize: 0 },
120
+ { key: 'b' },
121
+ ])
122
+
123
+ it('is true only for collapsible panes at collapsedSize', () => {
124
+ expect(isSplitPaneCollapsed(0, collapsible)).toBe(true)
125
+ expect(isSplitPaneCollapsed(10, collapsible)).toBe(false)
126
+ expect(isSplitPaneCollapsed(0, plain)).toBe(false)
127
+ })
128
+ })
129
+
130
+ describe('splitSizesEqual', () => {
131
+ it('compares with tolerance', () => {
132
+ expect(splitSizesEqual([50, 50], [50.005, 49.995])).toBe(true)
133
+ expect(splitSizesEqual([50, 50], [60, 40])).toBe(false)
134
+ expect(splitSizesEqual([50], [50, 50])).toBe(false)
135
+ })
136
+ })
@@ -0,0 +1,35 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import {
3
+ resolveDragRowKeys,
4
+ resolveTableRowDropPosition,
5
+ } from '../components/table-drag'
6
+
7
+ describe('table-drag', () => {
8
+ it('resolves reorder drop position from pointer', () => {
9
+ const element = {
10
+ getBoundingClientRect: () => ({ top: 0, height: 100 }),
11
+ } as HTMLElement
12
+
13
+ expect(
14
+ resolveTableRowDropPosition({ clientY: 20 } as MouseEvent, element, 'reorder'),
15
+ ).toBe('before')
16
+ expect(
17
+ resolveTableRowDropPosition({ clientY: 80 } as MouseEvent, element, 'reorder'),
18
+ ).toBe('after')
19
+ })
20
+
21
+ it('uses into position for folder drop mode', () => {
22
+ const element = {
23
+ getBoundingClientRect: () => ({ top: 0, height: 100 }),
24
+ } as HTMLElement
25
+
26
+ expect(
27
+ resolveTableRowDropPosition({ clientY: 20 } as MouseEvent, element, 'into'),
28
+ ).toBe('into')
29
+ })
30
+
31
+ it('drags all selected rows when dragging a selected row', () => {
32
+ expect(resolveDragRowKeys('b', ['a', 'b', 'c'])).toEqual(['a', 'b', 'c'])
33
+ expect(resolveDragRowKeys('d', ['a', 'b', 'c'])).toEqual(['d'])
34
+ })
35
+ })
@@ -0,0 +1,194 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { h, isVNode } from 'vue'
3
+ import {
4
+ RS_TABLE_NULL_DRAFT,
5
+ booleanToEditText,
6
+ coerceBoolean,
7
+ isBooleanToggleColumn,
8
+ isNullDraft,
9
+ navigateEditableCell,
10
+ parseCellEditInput,
11
+ parseClipboardGrid,
12
+ plainTextFromCellDisplay,
13
+ resolveColumnCommitOn,
14
+ resolveColumnDisplayContent,
15
+ resolveColumnEditTrigger,
16
+ resolveColumnRawValue,
17
+ stripNumberDecorations,
18
+ resolveCellEditText,
19
+ usesOverlayEditor,
20
+ validateCellValue,
21
+ } from '../components/table/table-edit-utils'
22
+
23
+ describe('table-edit-utils editors', () => {
24
+ it('normalizes ISO datetime for date picker binding', () => {
25
+ expect(
26
+ resolveCellEditText(
27
+ { updatedAt: '2026-06-15T10:30:00' },
28
+ { key: 'updatedAt', title: '更新时间', valueType: 'datetime' },
29
+ 0,
30
+ ),
31
+ ).toBe('2026-06-15 10:30:00')
32
+ expect(
33
+ resolveCellEditText(
34
+ { birthday: '2024-01-15' },
35
+ { key: 'birthday', title: '生日', valueType: 'date' },
36
+ 0,
37
+ ),
38
+ ).toBe('2024-01-15')
39
+ })
40
+
41
+ it('edits UTC ISO as local and writes back without losing ms when unchanged', () => {
42
+ const raw = '2026-06-25T08:06:36.2263624Z'
43
+ const col = { key: 'addTime', title: 'addTime', valueType: 'datetime' as const }
44
+ const draft = resolveCellEditText({ addTime: raw }, col, 0)
45
+ const d = new Date('2026-06-25T08:06:36.226Z')
46
+ const pad = (n: number, w = 2) => String(n).padStart(w, '0')
47
+ expect(draft).toBe(
48
+ `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ` +
49
+ `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}.${pad(d.getMilliseconds(), 3)}`,
50
+ )
51
+ // 选择器通常去掉毫秒:秒级未变应还原原始串
52
+ const withoutMs = draft.replace(/\.\d+$/, '')
53
+ expect(parseCellEditInput(withoutMs, { addTime: raw }, col, 0)).toBe(raw)
54
+ })
55
+
56
+ it('stringifies object cell values for edit draft', () => {
57
+ const binary = { $binary: '566h55CG5ZGYMDAx' }
58
+ expect(
59
+ resolveCellEditText(
60
+ { userName: binary },
61
+ { key: 'userName', title: 'userName' },
62
+ 0,
63
+ ),
64
+ ).toBe(JSON.stringify(binary))
65
+ expect(
66
+ resolveCellEditText(
67
+ { userName: binary },
68
+ {
69
+ key: 'userName',
70
+ title: 'userName',
71
+ formatter: (v) => (typeof v === 'object' ? JSON.stringify(v) : String(v)),
72
+ },
73
+ 0,
74
+ ),
75
+ ).toBe(JSON.stringify(binary))
76
+ })
77
+
78
+ it('detects boolean toggle columns', () => {
79
+ expect(isBooleanToggleColumn({ key: 'a', title: 'A', valueType: 'boolean' })).toBe(true)
80
+ expect(isBooleanToggleColumn({ key: 'a', title: 'A', valueType: 'text' })).toBe(false)
81
+ })
82
+
83
+ it('uses change commit and click trigger for overlay editors', () => {
84
+ expect(usesOverlayEditor('select')).toBe(true)
85
+ expect(usesOverlayEditor('date')).toBe(true)
86
+ expect(resolveColumnCommitOn({ key: 'k', title: 'K', valueType: 'select' }, false)).toBe(
87
+ 'change',
88
+ )
89
+ expect(resolveColumnCommitOn({ key: 'k', title: 'K', valueType: 'date' }, false)).toBe('enter')
90
+ expect(resolveColumnCommitOn({ key: 'k', title: 'K', valueType: 'datetime' }, false)).toBe(
91
+ 'enter',
92
+ )
93
+ expect(resolveColumnEditTrigger({ key: 'k', title: 'K', valueType: 'select' }, 'dblclick')).toBe(
94
+ 'click',
95
+ )
96
+ })
97
+
98
+ it('parses boolean edit text', () => {
99
+ expect(coerceBoolean(true)).toBe(true)
100
+ expect(coerceBoolean('YES')).toBe(true)
101
+ expect(booleanToEditText(false)).toBe('false')
102
+ expect(
103
+ parseCellEditInput('true', { a: false }, { key: 'a', title: 'A', valueType: 'boolean' }, 0),
104
+ ).toBe(true)
105
+ })
106
+
107
+ it('parses explicit NULL draft and emptyAsNull', () => {
108
+ expect(isNullDraft(RS_TABLE_NULL_DRAFT)).toBe(true)
109
+ expect(
110
+ parseCellEditInput(RS_TABLE_NULL_DRAFT, { a: 'x' }, { key: 'a', title: 'A' }, 0),
111
+ ).toBeNull()
112
+ expect(
113
+ parseCellEditInput('', { a: 'x' }, { key: 'a', title: 'A', emptyAsNull: true, nullable: true }, 0),
114
+ ).toBeNull()
115
+ })
116
+
117
+ it('supports async validators', async () => {
118
+ const { validateCellValueAsync, listBatchColumnTargets } = await import(
119
+ '../components/table/table-edit-utils'
120
+ )
121
+ await expect(
122
+ validateCellValueAsync(
123
+ -1,
124
+ { n: 0 },
125
+ {
126
+ key: 'n',
127
+ title: 'N',
128
+ validator: async (value) => (Number(value) >= 0 ? null : '异步不能为负'),
129
+ },
130
+ 0,
131
+ ),
132
+ ).resolves.toBe('异步不能为负')
133
+
134
+ const targets = listBatchColumnTargets({
135
+ rows: [
136
+ { row: { id: '1' }, rowIndex: 0, rowKey: '1' },
137
+ { row: { id: '2' }, rowIndex: 1, rowKey: '2' },
138
+ { row: { id: '3' }, rowIndex: 2, rowKey: '3' },
139
+ ],
140
+ column: { key: 'name', title: '名称', editable: true },
141
+ tableEditable: true,
142
+ selectedKeys: ['1', '3'],
143
+ anchorRowKey: '1',
144
+ })
145
+ expect(targets.map((item) => item.rowKey)).toEqual(['1', '3'])
146
+ })
147
+
148
+ it('strips number decorations and validates', () => {
149
+ expect(stripNumberDecorations('1,234.5')).toBe('1234.5')
150
+ expect(
151
+ parseCellEditInput('1,200', { n: 0 }, { key: 'n', title: 'N', valueType: 'number' }, 0),
152
+ ).toBe(1200)
153
+ expect(
154
+ validateCellValue(-1, { n: 0 }, {
155
+ key: 'n',
156
+ title: 'N',
157
+ validator: (value) => (Number(value) >= 0 ? null : '不能为负'),
158
+ }, 0),
159
+ ).toBe('不能为负')
160
+ })
161
+
162
+ it('parses clipboard TSV grid', () => {
163
+ expect(parseClipboardGrid('a\tb\nc\td')).toEqual([
164
+ ['a', 'b'],
165
+ ['c', 'd'],
166
+ ])
167
+ })
168
+
169
+ it('navigates editable cells', () => {
170
+ const cells = [
171
+ { rowKey: '1', colKey: 'a', rowIndex: 0 },
172
+ { rowKey: '1', colKey: 'b', rowIndex: 0 },
173
+ { rowKey: '2', colKey: 'a', rowIndex: 1 },
174
+ { rowKey: '2', colKey: 'b', rowIndex: 1 },
175
+ ]
176
+ expect(navigateEditableCell(cells, '1', 'a', 'next', 2)?.colKey).toBe('b')
177
+ expect(navigateEditableCell(cells, '1', 'b', 'next', 2)?.rowKey).toBe('2')
178
+ expect(navigateEditableCell(cells, '1', 'a', 'down', 2)?.rowKey).toBe('2')
179
+ expect(navigateEditableCell(cells, '2', 'b', 'prev', 2)?.colKey).toBe('a')
180
+ })
181
+
182
+ it('keeps raw value separate from render display (incl. VNode)', () => {
183
+ const row = { time: 42 }
184
+ const column = {
185
+ key: 'time',
186
+ title: 'Time',
187
+ render: (r: { time: number }) => h('span', { class: 'x' }, String(r.time)),
188
+ }
189
+ expect(resolveColumnRawValue(row, column, 0)).toBe(42)
190
+ const display = resolveColumnDisplayContent(row, column, 0)
191
+ expect(plainTextFromCellDisplay(display)).toBe('42')
192
+ expect(isVNode(display)).toBe(true)
193
+ })
194
+ })
@@ -0,0 +1,286 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import {
3
+ buildTableEntries,
4
+ clampColumnWidth,
5
+ filterTableRows,
6
+ filterTableRowsByColumnFilters,
7
+ isColumnFilterActive,
8
+ fixedCellStyle,
9
+ groupTableRows,
10
+ injectExpandRows,
11
+ isNearScrollBottom,
12
+ resolveFixedColumnStyles,
13
+ resolveSelectableRowKeys,
14
+ resolveSelectAllState,
15
+ resolveTableVirtualEnabled,
16
+ selectRowKeys,
17
+ selectRowKeysByClick,
18
+ sliceVirtualColumns,
19
+ sliceVirtualTableEntries,
20
+ sortTableRows,
21
+ toggleMultiSortState,
22
+ toggleRowSelection,
23
+ toggleSelectAll,
24
+ toggleSortState,
25
+ reorderColumnKeys,
26
+ reorderTableRows,
27
+ resolveColumnOrder,
28
+ resolveOrderedColumns,
29
+ sortTableRowsMulti,
30
+ } from '../components/table-utils'
31
+
32
+ describe('table-utils', () => {
33
+ const columns = [
34
+ { key: 'name', title: '名称', sortable: true },
35
+ { key: 'status', title: '状态' },
36
+ { key: 'count', title: '数量', sortable: true },
37
+ ]
38
+
39
+ const rows = [
40
+ { id: '1', name: 'Alpha', status: 'running', count: 12 },
41
+ { id: '2', name: 'Beta', status: 'stopped', count: 7 },
42
+ { id: '3', name: 'Gamma', status: 'running', count: 23 },
43
+ ]
44
+
45
+ it('filters rows by query across columns', () => {
46
+ expect(filterTableRows(rows, 'alp', columns).map((row) => row.name)).toEqual(['Alpha'])
47
+ expect(filterTableRows(rows, 'running', columns, ['status'])).toHaveLength(2)
48
+ expect(filterTableRows(rows, ' ', columns)).toHaveLength(3)
49
+ })
50
+
51
+ it('filters rows by per-column filters', () => {
52
+ expect(
53
+ filterTableRowsByColumnFilters(rows, columns, { name: 'beta' }).map((row) => row.name),
54
+ ).toEqual(['Beta'])
55
+ expect(filterTableRowsByColumnFilters(rows, columns, { status: 'running' })).toHaveLength(2)
56
+ expect(
57
+ filterTableRowsByColumnFilters(rows, columns, { name: 'alp', status: 'running' }).map((row) => row.name),
58
+ ).toEqual(['Alpha'])
59
+ expect(isColumnFilterActive({ name: 'x' }, 'name')).toBe(true)
60
+ expect(isColumnFilterActive({ name: ' ' }, 'name')).toBe(false)
61
+ })
62
+
63
+ it('applies column filters in buildTableEntries', () => {
64
+ const entries = buildTableEntries(rows, columns, { columnFilters: { name: 'gamma' } })
65
+ expect(entries).toHaveLength(1)
66
+ expect(entries[0]?.type).toBe('row')
67
+ if (entries[0]?.type === 'row') expect(entries[0].row.name).toBe('Gamma')
68
+ })
69
+
70
+ it('toggles sort state', () => {
71
+ expect(toggleSortState(null, 'name')).toEqual({ key: 'name', order: 'asc' })
72
+ expect(toggleSortState({ key: 'name', order: 'asc' }, 'name')).toEqual({ key: 'name', order: 'desc' })
73
+ expect(toggleSortState({ key: 'name', order: 'desc' }, 'name')).toBeNull()
74
+ })
75
+
76
+ it('sorts rows by numeric and string columns', () => {
77
+ expect(sortTableRows(rows, columns, { key: 'name', order: 'asc' }).map((row) => row.name)).toEqual([
78
+ 'Alpha',
79
+ 'Beta',
80
+ 'Gamma',
81
+ ])
82
+ expect(sortTableRows(rows, columns, { key: 'count', order: 'desc' }).map((row) => row.count)).toEqual([23, 12, 7])
83
+ })
84
+
85
+ it('groups rows and inserts group headers', () => {
86
+ const sorted = [...rows].sort((left, right) => left.status.localeCompare(right.status))
87
+ const grouped = groupTableRows(sorted, 'status', (key) => `状态:${key}`)
88
+ expect(grouped.filter((entry) => entry.type === 'group').map((entry) => entry.label)).toEqual([
89
+ '状态:running',
90
+ '状态:stopped',
91
+ ])
92
+ expect(grouped.filter((entry) => entry.type === 'row')).toHaveLength(3)
93
+ })
94
+
95
+ it('builds entries with filter, sort and group pipeline', () => {
96
+ const entries = buildTableEntries(rows, columns, {
97
+ filterText: 'running',
98
+ filterKeys: ['status'],
99
+ sort: { key: 'count', order: 'desc' },
100
+ groupBy: 'status',
101
+ groupLabel: (key) => key,
102
+ })
103
+ expect(entries.some((entry) => entry.type === 'group' && entry.key === 'running')).toBe(true)
104
+ const dataRows = entries.filter((entry) => entry.type === 'row')
105
+ expect(dataRows).toHaveLength(2)
106
+ expect(dataRows[0].type === 'row' && dataRows[0].row.name).toBe('Gamma')
107
+ })
108
+
109
+ it('slices virtual entries with padding', () => {
110
+ const entries = buildTableEntries(
111
+ Array.from({ length: 100 }, (_, index) => ({
112
+ id: String(index),
113
+ name: `Row ${index}`,
114
+ status: 'running',
115
+ count: index,
116
+ })),
117
+ columns,
118
+ )
119
+ const slice = sliceVirtualTableEntries(entries, 400, 200, 40, 36, 2)
120
+ expect(slice.entries.length).toBeLessThan(entries.length)
121
+ expect(slice.paddingTop).toBeGreaterThan(0)
122
+ expect(slice.paddingBottom).toBeGreaterThan(0)
123
+ })
124
+
125
+ it('clamps column width', () => {
126
+ expect(clampColumnWidth(10)).toBe(48)
127
+ expect(clampColumnWidth(900)).toBe(640)
128
+ expect(clampColumnWidth(120)).toBe(120)
129
+ })
130
+
131
+ it('resolves virtual scroll for infinite mode', () => {
132
+ expect(resolveTableVirtualEnabled({ infinite: true })).toBe(true)
133
+ expect(resolveTableVirtualEnabled({ infinite: true, virtualOnInfinite: false })).toBe(false)
134
+ expect(resolveTableVirtualEnabled({ virtual: true })).toBe(true)
135
+ })
136
+
137
+ it('detects near scroll bottom', () => {
138
+ expect(isNearScrollBottom(300, 400, 120, 80)).toBe(true)
139
+ expect(isNearScrollBottom(100, 400, 120, 80)).toBe(false)
140
+ })
141
+
142
+ it('resolves row selection helpers', () => {
143
+ const rows = [
144
+ { id: '1', name: 'A' },
145
+ { id: '2', name: 'B', disabled: true },
146
+ { id: '3', name: 'C' },
147
+ ]
148
+ const keys = resolveSelectableRowKeys(rows, 'id')
149
+ expect(keys).toEqual(['1', '3'])
150
+ expect(resolveSelectAllState(['1'], keys)).toBe('indeterminate')
151
+ expect(toggleSelectAll([], keys, true)).toEqual(['1', '3'])
152
+ expect(toggleRowSelection(['1', '3'], '1')).toEqual(['3'])
153
+ expect(
154
+ selectRowKeysByClick(['1'], '3', {
155
+ toggle: false,
156
+ range: false,
157
+ orderedKeys: keys,
158
+ anchorKey: '1',
159
+ }),
160
+ ).toEqual(['3'])
161
+ expect(
162
+ selectRowKeysByClick(['1'], '3', {
163
+ toggle: true,
164
+ range: false,
165
+ orderedKeys: keys,
166
+ anchorKey: '1',
167
+ }),
168
+ ).toEqual(['1', '3'])
169
+ expect(
170
+ selectRowKeysByClick(['1'], '3', {
171
+ toggle: false,
172
+ range: true,
173
+ orderedKeys: ['1', '2', '3'],
174
+ anchorKey: '1',
175
+ }),
176
+ ).toEqual(['1', '2', '3'])
177
+ })
178
+
179
+ it('injects expand rows after expanded keys', () => {
180
+ const entries = buildTableEntries(
181
+ [{ id: '1', name: 'A' }],
182
+ [{ key: 'name', title: '名称' }],
183
+ )
184
+ const expanded = injectExpandRows(entries, new Set(['1']), 'id')
185
+ expect(expanded).toHaveLength(2)
186
+ expect(expanded[1]?.type).toBe('expand')
187
+ })
188
+
189
+ it('resolves fixed column offsets', () => {
190
+ const styles = resolveFixedColumnStyles(
191
+ [
192
+ { key: 'name', title: '名称', fixed: 'left', width: 100 },
193
+ { key: 'action', title: '操作', fixed: 'right', width: 80 },
194
+ ],
195
+ {},
196
+ { expandable: true },
197
+ )
198
+ expect(styles.get('name')?.left).toBe(40)
199
+ expect(styles.get('action')?.right).toBe(0)
200
+ })
201
+
202
+ it('applies header top sticky for fixed cells', () => {
203
+ expect(fixedCellStyle({ fixed: 'left', left: 0 }, { header: true })).toEqual({
204
+ position: 'sticky',
205
+ zIndex: '4',
206
+ top: '0',
207
+ left: '0px',
208
+ })
209
+ expect(fixedCellStyle({ fixed: 'right', right: 12 })).toEqual({
210
+ position: 'sticky',
211
+ zIndex: '2',
212
+ right: '12px',
213
+ })
214
+ })
215
+
216
+ it('slices visible columns for horizontal virtualization', () => {
217
+ const cols = Array.from({ length: 20 }, (_, i) => ({
218
+ key: `c${i}`,
219
+ title: `C${i}`,
220
+ width: 100,
221
+ }))
222
+ const sliced = sliceVirtualColumns(cols, {
223
+ scrollLeft: 500,
224
+ viewportWidth: 300,
225
+ getWidth: (c) => (typeof c.width === 'number' ? c.width : 100),
226
+ overscan: 1,
227
+ })
228
+ // 500→Start near col 5; viewport 300px ≈ 3 cols + overscan
229
+ expect(sliced.startIndex).toBeGreaterThanOrEqual(3)
230
+ expect(sliced.endIndex).toBeLessThanOrEqual(12)
231
+ expect(sliced.paddingLeft).toBe(sliced.startIndex * 100)
232
+ expect(sliced.paddingLeft + sliced.columns.reduce((n, c) => n + 100, 0) + sliced.paddingRight).toBe(
233
+ 2000,
234
+ )
235
+ })
236
+
237
+ it('selects single row in radio mode', () => {
238
+ expect(selectRowKeys([], 'a', 'radio')).toEqual(['a'])
239
+ expect(selectRowKeys(['a'], 'b', 'radio')).toEqual(['b'])
240
+ })
241
+
242
+ it('toggles multi-column sort state', () => {
243
+ expect(toggleMultiSortState([], 'name')).toEqual([{ key: 'name', order: 'asc' }])
244
+ expect(toggleMultiSortState([{ key: 'name', order: 'asc' }], 'count')).toEqual([
245
+ { key: 'name', order: 'asc' },
246
+ { key: 'count', order: 'asc' },
247
+ ])
248
+ expect(toggleMultiSortState([{ key: 'name', order: 'desc' }], 'name')).toEqual([])
249
+ })
250
+
251
+ it('sorts rows by multiple columns', () => {
252
+ const mixed = [
253
+ { id: '1', name: 'A', status: 'running', count: 10 },
254
+ { id: '2', name: 'A', status: 'stopped', count: 5 },
255
+ { id: '3', name: 'B', status: 'running', count: 8 },
256
+ ]
257
+ const sorted = sortTableRowsMulti(mixed, columns, [
258
+ { key: 'name', order: 'asc' },
259
+ { key: 'count', order: 'desc' },
260
+ ])
261
+ expect(sorted.map((row) => row.id)).toEqual(['1', '2', '3'])
262
+ })
263
+
264
+ it('reorders columns and rows', () => {
265
+ expect(reorderColumnKeys(['a', 'b', 'c'], 'c', 'a')).toEqual(['c', 'a', 'b'])
266
+ expect(resolveOrderedColumns(columns, ['count', 'name']).map((column) => column.key)).toEqual([
267
+ 'count',
268
+ 'name',
269
+ 'status',
270
+ ])
271
+ expect(resolveColumnOrder(columns, ['status', 'name'])).toEqual(['status', 'name', 'count'])
272
+ expect(reorderTableRows(['a', 'b', 'c'], 2, 0, 'before')).toEqual(['c', 'a', 'b'])
273
+ })
274
+
275
+ it('builds entries with multi sort', () => {
276
+ const entries = buildTableEntries(rows, columns, {
277
+ multiSort: true,
278
+ sorts: [
279
+ { key: 'status', order: 'asc' },
280
+ { key: 'count', order: 'desc' },
281
+ ],
282
+ })
283
+ const dataRows = entries.filter((entry) => entry.type === 'row')
284
+ expect(dataRows[0].type === 'row' && dataRows[0].row.name).toBe('Gamma')
285
+ })
286
+ })
@@ -0,0 +1,22 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { createRsConfigState } from '../composables/useRsConfig'
3
+ import { applyTheme } from '../theme/apply'
4
+
5
+ describe('useRsConfig / theme', () => {
6
+ it('translates by locale', () => {
7
+ const zh = createRsConfigState('dark', 'zh-CN')
8
+ const en = createRsConfigState('dark', 'en-US')
9
+ expect(zh.t('select.placeholder')).toBe('请选择')
10
+ expect(en.t('select.placeholder')).toBe('Select')
11
+ expect(zh.t('input.required')).toBe('此项为必填')
12
+ expect(en.t('input.required')).toBe('This field is required')
13
+ expect(en.t('breadcrumb.label')).toBe('Breadcrumb')
14
+ })
15
+
16
+ it('applyTheme sets data-rs-theme on element', () => {
17
+ applyTheme('light')
18
+ expect(document.documentElement.dataset.rsTheme).toBe('light')
19
+ applyTheme('dark')
20
+ expect(document.documentElement.dataset.rsTheme).toBe('dark')
21
+ })
22
+ })