@adecore/database 0.0.1 → 0.16.0-beta.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 (400) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +152 -1
  3. package/dist/DatabaseProvider.d.ts +13 -0
  4. package/dist/DatabaseProvider.js +17 -0
  5. package/dist/actions.d.ts +40 -0
  6. package/dist/actions.js +1 -0
  7. package/dist/client/client.d.ts +5 -0
  8. package/dist/client/client.js +245 -0
  9. package/dist/client/index.d.ts +2 -0
  10. package/dist/client/index.js +2 -0
  11. package/dist/client/types.d.ts +97 -0
  12. package/dist/client/types.js +13 -0
  13. package/dist/client-context.d.ts +16 -0
  14. package/dist/client-context.js +17 -0
  15. package/dist/connections/ConnectionForm.d.ts +10 -0
  16. package/dist/connections/ConnectionForm.js +145 -0
  17. package/dist/connections/ConnectionManager.d.ts +12 -0
  18. package/dist/connections/ConnectionManager.js +93 -0
  19. package/dist/connections/EngineIcon.d.ts +9 -0
  20. package/dist/connections/EngineIcon.js +7 -0
  21. package/dist/connections/connection-config.d.ts +16 -0
  22. package/dist/connections/connection-config.js +107 -0
  23. package/dist/connections/docker.d.ts +9 -0
  24. package/dist/connections/docker.js +38 -0
  25. package/dist/connections/engine-icons.d.ts +3 -0
  26. package/dist/connections/engine-icons.js +3 -0
  27. package/dist/connections/use-docker-containers.d.ts +17 -0
  28. package/dist/connections/use-docker-containers.js +26 -0
  29. package/dist/console/DestructiveDialog.d.ts +9 -0
  30. package/dist/console/DestructiveDialog.js +9 -0
  31. package/dist/console/HistoryPanel.d.ts +10 -0
  32. package/dist/console/HistoryPanel.js +15 -0
  33. package/dist/console/QueryConsole.d.ts +14 -0
  34. package/dist/console/QueryConsole.js +261 -0
  35. package/dist/console/SchemaPicker.d.ts +11 -0
  36. package/dist/console/SchemaPicker.js +8 -0
  37. package/dist/console/SqlEditor.d.ts +12 -0
  38. package/dist/console/SqlEditor.js +61 -0
  39. package/dist/console/StatementResultView.d.ts +18 -0
  40. package/dist/console/StatementResultView.js +32 -0
  41. package/dist/console/TransactionControls.d.ts +13 -0
  42. package/dist/console/TransactionControls.js +12 -0
  43. package/dist/console/destructive.d.ts +7 -0
  44. package/dist/console/destructive.js +43 -0
  45. package/dist/console/editing.d.ts +12 -0
  46. package/dist/console/editing.js +60 -0
  47. package/dist/console/history.d.ts +13 -0
  48. package/dist/console/history.js +43 -0
  49. package/dist/console/labels.d.ts +1 -0
  50. package/dist/console/labels.js +6 -0
  51. package/dist/console/outcome.d.ts +6 -0
  52. package/dist/console/outcome.js +11 -0
  53. package/dist/console/schemas.d.ts +2 -0
  54. package/dist/console/schemas.js +5 -0
  55. package/dist/console/shortcuts.d.ts +2 -0
  56. package/dist/console/shortcuts.js +3 -0
  57. package/dist/console/useConsoleHistory.d.ts +7 -0
  58. package/dist/console/useConsoleHistory.js +22 -0
  59. package/dist/ddl/alter-mysql.d.ts +4 -0
  60. package/dist/ddl/alter-mysql.js +79 -0
  61. package/dist/ddl/alter-sqlite.d.ts +4 -0
  62. package/dist/ddl/alter-sqlite.js +80 -0
  63. package/dist/ddl/alter.d.ts +4 -0
  64. package/dist/ddl/alter.js +24 -0
  65. package/dist/ddl/column.d.ts +9 -0
  66. package/dist/ddl/column.js +62 -0
  67. package/dist/ddl/create.d.ts +12 -0
  68. package/dist/ddl/create.js +74 -0
  69. package/dist/ddl/dialect.d.ts +12 -0
  70. package/dist/ddl/dialect.js +34 -0
  71. package/dist/ddl/diff.d.ts +34 -0
  72. package/dist/ddl/diff.js +111 -0
  73. package/dist/ddl/draft.d.ts +46 -0
  74. package/dist/ddl/draft.js +70 -0
  75. package/dist/ddl/index.d.ts +10 -0
  76. package/dist/ddl/index.js +10 -0
  77. package/dist/ddl/parse.d.ts +17 -0
  78. package/dist/ddl/parse.js +131 -0
  79. package/dist/ddl/statements.d.ts +6 -0
  80. package/dist/ddl/statements.js +12 -0
  81. package/dist/ddl/types.d.ts +6 -0
  82. package/dist/ddl/types.js +52 -0
  83. package/dist/ddl/validate.d.ts +7 -0
  84. package/dist/ddl/validate.js +38 -0
  85. package/dist/designer/ColumnsEditor.d.ts +2 -0
  86. package/dist/designer/ColumnsEditor.js +24 -0
  87. package/dist/designer/DesignerView.d.ts +25 -0
  88. package/dist/designer/DesignerView.js +33 -0
  89. package/dist/designer/ForeignKeysEditor.d.ts +7 -0
  90. package/dist/designer/ForeignKeysEditor.js +24 -0
  91. package/dist/designer/IndexesEditor.d.ts +2 -0
  92. package/dist/designer/IndexesEditor.js +13 -0
  93. package/dist/designer/NamePicker.d.ts +11 -0
  94. package/dist/designer/NamePicker.js +13 -0
  95. package/dist/designer/OptionsEditor.d.ts +2 -0
  96. package/dist/designer/OptionsEditor.js +17 -0
  97. package/dist/designer/SqlPreview.d.ts +12 -0
  98. package/dist/designer/SqlPreview.js +17 -0
  99. package/dist/designer/TableDesigner.d.ts +10 -0
  100. package/dist/designer/TableDesigner.js +103 -0
  101. package/dist/designer/edit.d.ts +17 -0
  102. package/dist/designer/edit.js +115 -0
  103. package/dist/designer/editor-props.d.ts +10 -0
  104. package/dist/designer/editor-props.js +1 -0
  105. package/dist/designer/plan.d.ts +9 -0
  106. package/dist/designer/plan.js +13 -0
  107. package/dist/designer/recover.d.ts +2 -0
  108. package/dist/designer/recover.js +18 -0
  109. package/dist/designer/useReferenceColumns.d.ts +3 -0
  110. package/dist/designer/useReferenceColumns.js +36 -0
  111. package/dist/explorer/DatabaseExplorer.d.ts +13 -0
  112. package/dist/explorer/DatabaseExplorer.js +264 -0
  113. package/dist/explorer/RowMenu.d.ts +11 -0
  114. package/dist/explorer/RowMenu.js +89 -0
  115. package/dist/explorer/TableDialog.d.ts +15 -0
  116. package/dist/explorer/TableDialog.js +50 -0
  117. package/dist/explorer/expansion.d.ts +7 -0
  118. package/dist/explorer/expansion.js +31 -0
  119. package/dist/explorer/table-statements.d.ts +5 -0
  120. package/dist/explorer/table-statements.js +12 -0
  121. package/dist/explorer/tree.d.ts +125 -0
  122. package/dist/explorer/tree.js +295 -0
  123. package/dist/explorer/use-explorer-loads.d.ts +12 -0
  124. package/dist/explorer/use-explorer-loads.js +109 -0
  125. package/dist/explorer/use-stored-expansion.d.ts +9 -0
  126. package/dist/explorer/use-stored-expansion.js +47 -0
  127. package/dist/grid/CellEditor.d.ts +9 -0
  128. package/dist/grid/CellEditor.js +29 -0
  129. package/dist/grid/CopyAsMenu.d.ts +7 -0
  130. package/dist/grid/CopyAsMenu.js +10 -0
  131. package/dist/grid/DataGrid.d.ts +36 -0
  132. package/dist/grid/DataGrid.js +512 -0
  133. package/dist/grid/EnumPicker.d.ts +17 -0
  134. package/dist/grid/EnumPicker.js +53 -0
  135. package/dist/grid/GridHeaderCell.d.ts +36 -0
  136. package/dist/grid/GridHeaderCell.js +69 -0
  137. package/dist/grid/RecordView.d.ts +20 -0
  138. package/dist/grid/RecordView.js +126 -0
  139. package/dist/grid/ValueDock.d.ts +11 -0
  140. package/dist/grid/ValueDock.js +28 -0
  141. package/dist/grid/aggregates.d.ts +22 -0
  142. package/dist/grid/aggregates.js +62 -0
  143. package/dist/grid/cell-range.d.ts +14 -0
  144. package/dist/grid/cell-range.js +11 -0
  145. package/dist/grid/column-layout.d.ts +18 -0
  146. package/dist/grid/column-layout.js +49 -0
  147. package/dist/grid/column-selection.d.ts +11 -0
  148. package/dist/grid/column-selection.js +28 -0
  149. package/dist/grid/column-view.d.ts +10 -0
  150. package/dist/grid/column-view.js +23 -0
  151. package/dist/grid/copy-formats.d.ts +15 -0
  152. package/dist/grid/copy-formats.js +80 -0
  153. package/dist/grid/display.d.ts +20 -0
  154. package/dist/grid/display.js +81 -0
  155. package/dist/grid/edit-value.d.ts +5 -0
  156. package/dist/grid/edit-value.js +55 -0
  157. package/dist/grid/enum-type.d.ts +7 -0
  158. package/dist/grid/enum-type.js +52 -0
  159. package/dist/grid/focused-value.d.ts +4 -0
  160. package/dist/grid/focused-value.js +19 -0
  161. package/dist/grid/layout.d.ts +22 -0
  162. package/dist/grid/layout.js +74 -0
  163. package/dist/grid/navigation.d.ts +15 -0
  164. package/dist/grid/navigation.js +52 -0
  165. package/dist/grid/row-selection.d.ts +9 -0
  166. package/dist/grid/row-selection.js +24 -0
  167. package/dist/grid/sort.d.ts +7 -0
  168. package/dist/grid/sort.js +7 -0
  169. package/dist/grid/types.d.ts +30 -0
  170. package/dist/grid/types.js +9 -0
  171. package/dist/host/helper-path.d.ts +7 -0
  172. package/dist/host/helper-path.js +15 -0
  173. package/dist/host/host.d.ts +2 -0
  174. package/dist/host/host.js +343 -0
  175. package/dist/host/index.d.ts +5 -0
  176. package/dist/host/index.js +4 -0
  177. package/dist/host/spawn.d.ts +7 -0
  178. package/dist/host/spawn.js +98 -0
  179. package/dist/host/types.d.ts +19 -0
  180. package/dist/host/types.js +1 -0
  181. package/dist/host/validate.d.ts +10 -0
  182. package/dist/host/validate.js +339 -0
  183. package/dist/index.d.ts +14 -0
  184. package/dist/index.js +13 -0
  185. package/dist/locales/en.json +577 -0
  186. package/dist/locales/nl.json +577 -0
  187. package/dist/locales.d.ts +4 -0
  188. package/dist/locales.js +14 -0
  189. package/dist/protocol/connection.d.ts +56 -0
  190. package/dist/protocol/connection.js +1 -0
  191. package/dist/protocol/errors.d.ts +7 -0
  192. package/dist/protocol/errors.js +1 -0
  193. package/dist/protocol/index.d.ts +5 -0
  194. package/dist/protocol/index.js +3 -0
  195. package/dist/protocol/messages.d.ts +250 -0
  196. package/dist/protocol/messages.js +2 -0
  197. package/dist/protocol/schema.d.ts +53 -0
  198. package/dist/protocol/schema.js +1 -0
  199. package/dist/protocol/values.d.ts +21 -0
  200. package/dist/protocol/values.js +15 -0
  201. package/dist/sql-split.d.ts +15 -0
  202. package/dist/sql-split.js +201 -0
  203. package/dist/sql.d.ts +29 -0
  204. package/dist/sql.js +104 -0
  205. package/dist/structure/StructureView.d.ts +14 -0
  206. package/dist/structure/StructureView.js +99 -0
  207. package/dist/structure/structure-text.d.ts +2 -0
  208. package/dist/structure/structure-text.js +2 -0
  209. package/dist/table/CommandField.d.ts +14 -0
  210. package/dist/table/CommandField.js +157 -0
  211. package/dist/table/ImportDialog.d.ts +27 -0
  212. package/dist/table/ImportDialog.js +28 -0
  213. package/dist/table/TableStatusBar.d.ts +19 -0
  214. package/dist/table/TableStatusBar.js +22 -0
  215. package/dist/table/TableToolbar.d.ts +43 -0
  216. package/dist/table/TableToolbar.js +16 -0
  217. package/dist/table/TableView.d.ts +13 -0
  218. package/dist/table/TableView.js +508 -0
  219. package/dist/table/command-field.d.ts +57 -0
  220. package/dist/table/command-field.js +203 -0
  221. package/dist/table/grid-rows.d.ts +23 -0
  222. package/dist/table/grid-rows.js +55 -0
  223. package/dist/table/import-mapping.d.ts +7 -0
  224. package/dist/table/import-mapping.js +28 -0
  225. package/dist/table/layout-store.d.ts +13 -0
  226. package/dist/table/layout-store.js +43 -0
  227. package/dist/table/paging.d.ts +12 -0
  228. package/dist/table/paging.js +20 -0
  229. package/dist/table/pending.d.ts +32 -0
  230. package/dist/table/pending.js +94 -0
  231. package/dist/table/references.d.ts +12 -0
  232. package/dist/table/references.js +21 -0
  233. package/dist/table/useLoaded.d.ts +7 -0
  234. package/dist/table/useLoaded.js +36 -0
  235. package/dist/table/useStoredLayout.d.ts +8 -0
  236. package/dist/table/useStoredLayout.js +39 -0
  237. package/dist/table/useTableTransfer.d.ts +51 -0
  238. package/dist/table/useTableTransfer.js +125 -0
  239. package/dist/testing/fake.d.ts +21 -0
  240. package/dist/testing/fake.js +372 -0
  241. package/dist/testing/index.d.ts +1 -0
  242. package/dist/testing/index.js +1 -0
  243. package/dist/use-popup-press.d.ts +1 -0
  244. package/dist/use-popup-press.js +24 -0
  245. package/dist/use-schema-change.d.ts +1 -0
  246. package/dist/use-schema-change.js +13 -0
  247. package/dist/use-stable-callback.d.ts +1 -0
  248. package/dist/use-stable-callback.js +9 -0
  249. package/dist/value/ValuePanel.d.ts +14 -0
  250. package/dist/value/ValuePanel.js +103 -0
  251. package/dist/value/draft.d.ts +33 -0
  252. package/dist/value/draft.js +44 -0
  253. package/dist/value/hex.d.ts +14 -0
  254. package/dist/value/hex.js +46 -0
  255. package/dist/value/json.d.ts +9 -0
  256. package/dist/value/json.js +71 -0
  257. package/dist/value/views.d.ts +22 -0
  258. package/dist/value/views.js +54 -0
  259. package/dist/workbench/DatabaseWorkbench.d.ts +9 -0
  260. package/dist/workbench/DatabaseWorkbench.js +179 -0
  261. package/dist/workbench/DesignerTab.d.ts +10 -0
  262. package/dist/workbench/DesignerTab.js +22 -0
  263. package/dist/workbench/TableTab.d.ts +10 -0
  264. package/dist/workbench/TableTab.js +8 -0
  265. package/dist/workbench/tabs.d.ts +42 -0
  266. package/dist/workbench/tabs.js +139 -0
  267. package/package.json +79 -3
  268. package/src/DatabaseProvider.tsx +37 -0
  269. package/src/actions.ts +34 -0
  270. package/src/client/client.ts +324 -0
  271. package/src/client/index.ts +18 -0
  272. package/src/client/types.ts +161 -0
  273. package/src/client-context.ts +35 -0
  274. package/src/connections/ConnectionForm.tsx +474 -0
  275. package/src/connections/ConnectionManager.tsx +224 -0
  276. package/src/connections/EngineIcon.tsx +18 -0
  277. package/src/connections/connection-config.ts +143 -0
  278. package/src/connections/docker.ts +50 -0
  279. package/src/connections/engine-icons.ts +5 -0
  280. package/src/connections/use-docker-containers.ts +41 -0
  281. package/src/console/DestructiveDialog.tsx +40 -0
  282. package/src/console/HistoryPanel.tsx +67 -0
  283. package/src/console/QueryConsole.tsx +439 -0
  284. package/src/console/SchemaPicker.tsx +32 -0
  285. package/src/console/SqlEditor.tsx +95 -0
  286. package/src/console/StatementResultView.tsx +176 -0
  287. package/src/console/TransactionControls.tsx +53 -0
  288. package/src/console/destructive.ts +60 -0
  289. package/src/console/editing.ts +77 -0
  290. package/src/console/history.ts +62 -0
  291. package/src/console/labels.ts +7 -0
  292. package/src/console/outcome.ts +14 -0
  293. package/src/console/schemas.ts +7 -0
  294. package/src/console/shortcuts.ts +4 -0
  295. package/src/console/useConsoleHistory.ts +39 -0
  296. package/src/ddl/alter-mysql.ts +90 -0
  297. package/src/ddl/alter-sqlite.ts +93 -0
  298. package/src/ddl/alter.ts +29 -0
  299. package/src/ddl/column.ts +74 -0
  300. package/src/ddl/create.ts +92 -0
  301. package/src/ddl/dialect.ts +53 -0
  302. package/src/ddl/diff.ts +164 -0
  303. package/src/ddl/draft.ts +136 -0
  304. package/src/ddl/index.ts +10 -0
  305. package/src/ddl/parse.ts +161 -0
  306. package/src/ddl/statements.ts +20 -0
  307. package/src/ddl/types.ts +60 -0
  308. package/src/ddl/validate.ts +47 -0
  309. package/src/designer/ColumnsEditor.tsx +204 -0
  310. package/src/designer/DesignerView.tsx +184 -0
  311. package/src/designer/ForeignKeysEditor.tsx +132 -0
  312. package/src/designer/IndexesEditor.tsx +63 -0
  313. package/src/designer/NamePicker.tsx +44 -0
  314. package/src/designer/OptionsEditor.tsx +101 -0
  315. package/src/designer/SqlPreview.tsx +56 -0
  316. package/src/designer/TableDesigner.tsx +186 -0
  317. package/src/designer/edit.ts +137 -0
  318. package/src/designer/editor-props.ts +13 -0
  319. package/src/designer/plan.ts +25 -0
  320. package/src/designer/recover.ts +20 -0
  321. package/src/designer/useReferenceColumns.ts +54 -0
  322. package/src/explorer/DatabaseExplorer.tsx +450 -0
  323. package/src/explorer/RowMenu.tsx +134 -0
  324. package/src/explorer/TableDialog.tsx +80 -0
  325. package/src/explorer/expansion.ts +47 -0
  326. package/src/explorer/table-statements.ts +18 -0
  327. package/src/explorer/tree.ts +453 -0
  328. package/src/explorer/use-explorer-loads.ts +157 -0
  329. package/src/explorer/use-stored-expansion.ts +66 -0
  330. package/src/grid/CellEditor.tsx +53 -0
  331. package/src/grid/CopyAsMenu.tsx +33 -0
  332. package/src/grid/DataGrid.tsx +871 -0
  333. package/src/grid/EnumPicker.tsx +108 -0
  334. package/src/grid/GridHeaderCell.tsx +243 -0
  335. package/src/grid/RecordView.tsx +293 -0
  336. package/src/grid/ValueDock.tsx +52 -0
  337. package/src/grid/aggregates.ts +94 -0
  338. package/src/grid/cell-range.ts +26 -0
  339. package/src/grid/column-layout.ts +71 -0
  340. package/src/grid/column-selection.ts +42 -0
  341. package/src/grid/column-view.ts +35 -0
  342. package/src/grid/copy-formats.ts +108 -0
  343. package/src/grid/display.ts +109 -0
  344. package/src/grid/edit-value.ts +60 -0
  345. package/src/grid/enum-type.ts +62 -0
  346. package/src/grid/focused-value.ts +23 -0
  347. package/src/grid/layout.ts +91 -0
  348. package/src/grid/navigation.ts +74 -0
  349. package/src/grid/row-selection.ts +35 -0
  350. package/src/grid/sort.ts +12 -0
  351. package/src/grid/types.ts +59 -0
  352. package/src/host/helper-path.ts +41 -0
  353. package/src/host/host.ts +456 -0
  354. package/src/host/index.ts +5 -0
  355. package/src/host/spawn.ts +124 -0
  356. package/src/host/types.ts +46 -0
  357. package/src/host/validate.ts +402 -0
  358. package/src/index.ts +37 -0
  359. package/src/locales/en.json +577 -0
  360. package/src/locales/nl.json +577 -0
  361. package/src/locales.ts +18 -0
  362. package/src/protocol/connection.ts +86 -0
  363. package/src/protocol/errors.ts +38 -0
  364. package/src/protocol/index.ts +33 -0
  365. package/src/protocol/messages.ts +168 -0
  366. package/src/protocol/schema.ts +76 -0
  367. package/src/protocol/values.ts +52 -0
  368. package/src/sql-split.ts +220 -0
  369. package/src/sql.ts +152 -0
  370. package/src/structure/StructureView.tsx +269 -0
  371. package/src/structure/structure-text.ts +4 -0
  372. package/src/table/CommandField.tsx +318 -0
  373. package/src/table/ImportDialog.tsx +168 -0
  374. package/src/table/TableStatusBar.tsx +91 -0
  375. package/src/table/TableToolbar.tsx +217 -0
  376. package/src/table/TableView.tsx +880 -0
  377. package/src/table/command-field.ts +285 -0
  378. package/src/table/grid-rows.ts +80 -0
  379. package/src/table/import-mapping.ts +37 -0
  380. package/src/table/layout-store.ts +62 -0
  381. package/src/table/paging.ts +33 -0
  382. package/src/table/pending.ts +143 -0
  383. package/src/table/references.ts +38 -0
  384. package/src/table/useLoaded.ts +58 -0
  385. package/src/table/useStoredLayout.ts +53 -0
  386. package/src/table/useTableTransfer.ts +187 -0
  387. package/src/testing/fake.ts +533 -0
  388. package/src/testing/index.ts +1 -0
  389. package/src/use-popup-press.ts +27 -0
  390. package/src/use-schema-change.ts +19 -0
  391. package/src/use-stable-callback.ts +12 -0
  392. package/src/value/ValuePanel.tsx +254 -0
  393. package/src/value/draft.ts +72 -0
  394. package/src/value/hex.ts +62 -0
  395. package/src/value/json.ts +74 -0
  396. package/src/value/views.ts +68 -0
  397. package/src/workbench/DatabaseWorkbench.tsx +348 -0
  398. package/src/workbench/DesignerTab.tsx +37 -0
  399. package/src/workbench/TableTab.tsx +27 -0
  400. package/src/workbench/tabs.ts +184 -0
@@ -0,0 +1,871 @@
1
+ import {
2
+ useCallback,
3
+ useEffect,
4
+ useId,
5
+ useMemo,
6
+ useRef,
7
+ useState,
8
+ type KeyboardEvent,
9
+ type MouseEvent,
10
+ type PointerEvent,
11
+ type ReactNode,
12
+ type Ref
13
+ } from 'react';
14
+ import clsx from 'clsx';
15
+ import { ArrowUpRight, Ban, Copy, RotateCcw } from 'lucide-react';
16
+ import { useTranslation } from 'react-i18next';
17
+ import { ContextMenu, copyText, EDIT_SHORTCUTS, Icon, IconButton, isApplePlatform, isModHeld, Kbd, Spinner, useContentSize } from '@adecore/ui';
18
+ import { formatNumber } from '@adecore/ui/format';
19
+ import type { EditValue, Value } from '../protocol/index.ts';
20
+ import type { SqlTarget } from '../sql.ts';
21
+ import type { RangeBlock } from './aggregates.ts';
22
+ import { fullRect, rectBetween, rectContains } from './cell-range.ts';
23
+ import { CellEditor, type EditMove } from './CellEditor.tsx';
24
+ import { captureLayout, restoreLayout, sameLayout, type GridLayout } from './column-layout.ts';
25
+ import { NO_COLUMN_SELECTION, selectColumn, shownSelection, type ColumnSelection } from './column-selection.ts';
26
+ import { displayOrder, hideColumn, pinnedCount, showAllColumns, togglePin, type ColumnView } from './column-view.ts';
27
+ import { COPY_FORMATS, formatCopy, type CopyFormat, type CopyInput } from './copy-formats.ts';
28
+ import { CopyAsMenu } from './CopyAsMenu.tsx';
29
+ import { cellView, copyTextOf, isPreview, type CellView, type NumberNotation } from './display.ts';
30
+ import { draftOf, parseDraft } from './edit-value.ts';
31
+ import { parseEnumType } from './enum-type.ts';
32
+ import { EnumPicker } from './EnumPicker.tsx';
33
+ import { GridHeaderCell, type HeaderActions } from './GridHeaderCell.tsx';
34
+ import {
35
+ clampColumnWidth,
36
+ columnOffsets,
37
+ estimateColumnWidth,
38
+ fitColumnWidth,
39
+ FALLBACK_VIEWPORT_HEIGHT,
40
+ growWidths,
41
+ gutterWidth,
42
+ HEADER_HEIGHT,
43
+ OVERSCAN_ROWS,
44
+ ROW_HEIGHT,
45
+ scrollToReveal,
46
+ visibleRange
47
+ } from './layout.ts';
48
+ import { moveFocus, type CellPosition, type NavigationKey } from './navigation.ts';
49
+ import { selectRow } from './row-selection.ts';
50
+ import { sortOnly, sortStateOf, type GridSort } from './sort.ts';
51
+ import type { ColumnRequest, FocusedCell, GridColumn, GridMenuContext, GridRow } from './types.ts';
52
+ import { useNumberNotation } from '../client-context.ts';
53
+ import { usePopupPress } from '../use-popup-press.ts';
54
+
55
+ export interface DataGridProps {
56
+ columns: readonly GridColumn[];
57
+ rows: readonly GridRow[];
58
+ /* The accessible name of the grid. */
59
+ label: string;
60
+ /* Lets a cell be edited in place; the rows that are `locked` stay as they are. */
61
+ editable?: boolean;
62
+ /* The selected rows by key. Without it the grid keeps the selection itself. */
63
+ selectedKeys?: ReadonlySet<string>;
64
+ onSelectedKeysChange?(keys: ReadonlySet<string>): void;
65
+ /* A cell was edited, or set to NULL or DEFAULT from its menu. */
66
+ onCommit?(rowKey: string, column: number, value: EditValue): void;
67
+ /* The whole value of a cell that is only a preview, which editing needs. Resolves `undefined` when it cannot be had, which cancels the edit. */
68
+ loadValue?(rowKey: string, column: number): Promise<Value | undefined>;
69
+ /* The sorted columns by name, in order. Only shown unless `onSortsChange` is given, which is what makes the headers sort. */
70
+ sorts?: readonly GridSort[];
71
+ onSortsChange?(sorts: readonly GridSort[]): void;
72
+ /* The cell that holds the focus changed, or lost it. */
73
+ onFocusedCellChange?(cell: FocusedCell | null): void;
74
+ /* Delete or Backspace with rows selected. */
75
+ onDeleteSelected?(): void;
76
+ /* The table the rows come from, which "Copy as SQL INSERT" writes statements for. Without it that format is not offered. */
77
+ sqlTarget?: SqlTarget;
78
+ /* Items that filter or sort by the cell under the pointer, drawn under Copy as. Not drawn for a row number. */
79
+ queryMenu?(context: GridMenuContext): ReactNode;
80
+ /* Items for the end of the context menu of a cell or a row number, after the grid's own. */
81
+ menu?(context: GridMenuContext): ReactNode;
82
+ /* Mod+D on the selected rows, or on the row with the focus when none is selected. */
83
+ onDuplicateRows?(rowKeys: readonly string[]): void;
84
+ /* Draws an arrow on the cells `canFollow` allows, and runs on a click on it or a Mod+click on the cell. */
85
+ onFollow?(cell: FocusedCell): void;
86
+ /* Without it every cell can be followed. */
87
+ canFollow?(cell: FocusedCell): boolean;
88
+ /* The block of cells the range covers, or `null` when no cell has focus. Told again when the block changes. */
89
+ onRangeChange?(block: RangeBlock | null): void;
90
+ /* The widths, hidden columns and pins to start from, by column name. Read when the columns first appear. */
91
+ initialLayout?: GridLayout;
92
+ /* A person resized, hid or pinned a column. */
93
+ onLayoutChange?(layout: GridLayout): void;
94
+ /* Puts the focus on a cell, whenever the object changes. */
95
+ focusRequest?: FocusedCell | null;
96
+ /* Picks a column and brings it into view, whenever the object changes. */
97
+ columnRequest?: ColumnRequest | null;
98
+ /* Drawn under the header while there are no rows. */
99
+ empty?: ReactNode;
100
+ className?: string;
101
+ ref?: Ref<HTMLDivElement>;
102
+ }
103
+
104
+ interface Editing extends CellPosition {
105
+ draft: string;
106
+ initial: string;
107
+ loading: boolean;
108
+ }
109
+
110
+ interface MenuTarget {
111
+ readonly row: number;
112
+ /* `null` for the row number. */
113
+ readonly column: number | null;
114
+ }
115
+
116
+ /* What the person did to the columns, kept while the same columns come back with new rows. */
117
+ interface Layout {
118
+ signature: string;
119
+ rows: readonly GridRow[];
120
+ notation: NumberNotation;
121
+ estimated: readonly number[];
122
+ resized: Readonly<Record<number, number>>;
123
+ view: ColumnView;
124
+ }
125
+
126
+ const NO_KEYS: ReadonlySet<string> = new Set();
127
+ const NO_SORTS: readonly GridSort[] = [];
128
+
129
+ const signatureOf = (columns: readonly GridColumn[]): string => columns.map((column) => column.name).join('\u0000');
130
+
131
+ const textOf = (view: CellView): string => (view.suffix === undefined ? view.text : `${view.text} ${view.suffix}`);
132
+
133
+ const estimateWidths = (columns: readonly GridColumn[], rows: readonly GridRow[], notation: NumberNotation): number[] =>
134
+ columns.map((column, index) =>
135
+ estimateColumnWidth(
136
+ column.name,
137
+ rows.slice(0, 50).map((row) => textOf(cellView(row.cells[index] ?? null, column.kind, notation))),
138
+ column.primaryKey === true
139
+ )
140
+ );
141
+
142
+ const TONE: Record<CellView['tone'], string> = {
143
+ value: '',
144
+ null: 'text-text-faint',
145
+ default: 'text-text-faint italic'
146
+ };
147
+
148
+ const ROW_STATE = {
149
+ inserted: 'bg-positive/10',
150
+ deleted: 'bg-status-error/10 text-text-faint'
151
+ } as const;
152
+
153
+ /*
154
+ * The table of the table view and of the console. Only the rows in view are drawn, at a fixed
155
+ * height, inside a box as tall as all of them; the header and the row-number gutter stick to the edges
156
+ * of the scroll box. Focus stays on that box and points at the cell with `aria-activedescendant`, so
157
+ * a cell scrolled out of the window loses nothing.
158
+ */
159
+ export function DataGrid({
160
+ columns,
161
+ rows,
162
+ label,
163
+ editable = false,
164
+ selectedKeys,
165
+ onSelectedKeysChange,
166
+ onCommit,
167
+ loadValue,
168
+ sorts,
169
+ onSortsChange,
170
+ onFocusedCellChange,
171
+ onDeleteSelected,
172
+ sqlTarget,
173
+ queryMenu,
174
+ menu,
175
+ onDuplicateRows,
176
+ onFollow,
177
+ canFollow,
178
+ onRangeChange,
179
+ initialLayout,
180
+ onLayoutChange,
181
+ focusRequest,
182
+ columnRequest,
183
+ empty,
184
+ className,
185
+ ref
186
+ }: DataGridProps) {
187
+ const { t } = useTranslation('database');
188
+ const gridId = useId();
189
+ const scroller = useRef<HTMLDivElement | null>(null);
190
+ const anchor = useRef(-1);
191
+ const pressedInPopup = usePopupPress();
192
+ const notation = useNumberNotation();
193
+ /* Set once an edit commits or cancels, so the blur that follows the input's removal does not commit it again. */
194
+ const settled = useRef(true);
195
+ const dragging = useRef(false);
196
+ const dragOrigin = useRef<CellPosition | null>(null);
197
+ const reportFocus = useRef(onFocusedCellChange);
198
+ const reportRange = useRef(onRangeChange);
199
+ const reportLayout = useRef(onLayoutChange);
200
+ const [measure, size] = useContentSize();
201
+ const [scrollTop, setScrollTop] = useState(0);
202
+ const [focused, setFocused] = useState<CellPosition | null>(null);
203
+ const [rangeAnchor, setRangeAnchor] = useState<CellPosition | null>(null);
204
+ const [columnSelection, setColumnSelection] = useState<ColumnSelection>(NO_COLUMN_SELECTION);
205
+ const [menuTarget, setMenuTarget] = useState<MenuTarget | null>(null);
206
+ const [editing, setEditing] = useState<Editing | null>(null);
207
+ const [ownSelection, setOwnSelection] = useState<ReadonlySet<string>>(NO_KEYS);
208
+ const [layout, setLayout] = useState<Layout>(() => ({
209
+ signature: signatureOf(columns),
210
+ rows,
211
+ notation,
212
+ estimated: estimateWidths(columns, rows, notation),
213
+ ...restoreLayout(columns, initialLayout)
214
+ }));
215
+ const reportedLayout = useRef<GridLayout>(captureLayout(columns, layout));
216
+
217
+ const signature = signatureOf(columns);
218
+ let current = layout;
219
+ if (layout.signature !== signature || layout.rows !== rows || layout.notation !== notation) {
220
+ const same = layout.signature === signature;
221
+ const estimated = estimateWidths(columns, rows, notation);
222
+ current = {
223
+ signature,
224
+ rows,
225
+ notation,
226
+ estimated: same ? growWidths(layout.estimated, estimated) : estimated,
227
+ ...(same ? { resized: layout.resized, view: layout.view } : restoreLayout(columns, initialLayout))
228
+ };
229
+ setLayout(current);
230
+ }
231
+
232
+ const order = displayOrder(columns.length, current.view);
233
+ const pinnedShown = pinnedCount(columns.length, current.view);
234
+ const displayOf = new Map(order.map((index, position) => [index, position]));
235
+ const widths = columns.map((_, index) => current.resized[index] ?? current.estimated[index] ?? clampColumnWidth(0));
236
+ const shownWidths = order.map((index) => widths[index]!);
237
+ const offsets = columnOffsets(shownWidths);
238
+ const gutter = gutterWidth(rows.length);
239
+ const totalWidth = gutter + shownWidths.reduce((sum, width) => sum + width, 0);
240
+ const pinnedWidth = shownWidths.slice(0, pinnedShown).reduce((sum, width) => sum + width, 0);
241
+ const viewportHeight = size.height > 0 ? size.height : FALLBACK_VIEWPORT_HEIGHT;
242
+ const range = visibleRange(scrollTop - HEADER_HEIGHT, viewportHeight, ROW_HEIGHT, rows.length, OVERSCAN_ROWS);
243
+ const selection = selectedKeys ?? ownSelection;
244
+ const isShown = (position: CellPosition | null): position is CellPosition =>
245
+ position !== null && position.row < rows.length && displayOf.has(position.column);
246
+ const focusCell = isShown(focused) ? focused : null;
247
+ const anchorCell = isShown(rangeAnchor) ? rangeAnchor : null;
248
+ const toDisplay = (position: CellPosition): CellPosition => ({ row: position.row, column: displayOf.get(position.column)! });
249
+ const fromDisplay = (position: CellPosition): CellPosition => ({ row: position.row, column: order[position.column]! });
250
+ const rect = focusCell === null ? null : rectBetween(toDisplay(anchorCell ?? focusCell), toDisplay(focusCell));
251
+ const page = Math.max(1, Math.floor((viewportHeight - HEADER_HEIGHT) / ROW_HEIGHT));
252
+ const sortable = onSortsChange !== undefined;
253
+ const sortList = sorts ?? NO_SORTS;
254
+ const focusedRowKey = focusCell === null ? null : (rows[focusCell.row]?.key ?? null);
255
+ const focusedColumn = focusCell?.column ?? -1;
256
+ const pickedColumns = shownSelection(columnSelection, order);
257
+ const pickedSet = new Set(pickedColumns);
258
+ const columnBlock: RangeBlock | null = pickedColumns.length > 0 && rows.length > 0 ? { top: 0, bottom: rows.length - 1, columns: pickedColumns } : null;
259
+ const cellBlock: RangeBlock | null = rect === null ? null : { top: rect.top, bottom: rect.bottom, columns: order.slice(rect.left, rect.right + 1) };
260
+ const block = columnBlock ?? cellBlock;
261
+ const blockKey = block === null ? '' : `${block.top}:${block.bottom}:${block.columns.join(',')}`;
262
+ const headerId = (column: number): string => `${gridId}-h-${column}`;
263
+ const enumTypes = useMemo(() => columns.map((column) => parseEnumType(column.type)), [columns]);
264
+
265
+ useEffect(() => {
266
+ reportFocus.current = onFocusedCellChange;
267
+ });
268
+
269
+ useEffect(() => {
270
+ reportRange.current = onRangeChange;
271
+ reportLayout.current = onLayoutChange;
272
+ });
273
+
274
+ useEffect(() => {
275
+ const captured = captureLayout(columns, current);
276
+ if (!sameLayout(captured, reportedLayout.current)) {
277
+ reportedLayout.current = captured;
278
+ reportLayout.current?.(captured);
279
+ }
280
+ }, [columns, current]);
281
+
282
+ useEffect(() => {
283
+ reportFocus.current?.(focusedRowKey === null ? null : { rowKey: focusedRowKey, column: focusedColumn });
284
+ }, [focusedRowKey, focusedColumn]);
285
+
286
+ useEffect(() => {
287
+ if (blockKey === '') {
288
+ reportRange.current?.(null);
289
+ return;
290
+ }
291
+ const [top, bottom, list] = blockKey.split(':');
292
+ reportRange.current?.({ top: Number(top), bottom: Number(bottom), columns: list === '' ? [] : list!.split(',').map(Number) });
293
+ }, [blockKey]);
294
+
295
+ useEffect(() => {
296
+ const index = focusRequest === null || focusRequest === undefined ? -1 : rows.findIndex((row) => row.key === focusRequest.rowKey);
297
+ if (focusRequest !== null && focusRequest !== undefined && index >= 0) {
298
+ focusAt({ row: index, column: focusRequest.column });
299
+ scroller.current?.focus();
300
+ }
301
+ // Only a new request moves the focus; the rows changing under a request already handled must not.
302
+ // oxlint-disable-next-line react-hooks/exhaustive-deps
303
+ }, [focusRequest]);
304
+
305
+ useEffect(() => {
306
+ if (columnRequest !== null && columnRequest !== undefined && displayOf.has(columnRequest.column)) {
307
+ pickColumn(columnRequest.column);
308
+ revealColumn(columnRequest.column);
309
+ scroller.current?.focus();
310
+ }
311
+ // Only a new request moves the pick; the columns changing under a request already handled must not.
312
+ // oxlint-disable-next-line react-hooks/exhaustive-deps
313
+ }, [columnRequest]);
314
+
315
+ useEffect(() => {
316
+ const stop = (): void => {
317
+ dragging.current = false;
318
+ };
319
+ window.addEventListener('pointerup', stop);
320
+ window.addEventListener('pointercancel', stop);
321
+ return () => {
322
+ window.removeEventListener('pointerup', stop);
323
+ window.removeEventListener('pointercancel', stop);
324
+ };
325
+ }, []);
326
+
327
+ const attach = useCallback(
328
+ (node: HTMLDivElement | null) => {
329
+ scroller.current = node;
330
+ measure(node);
331
+ if (typeof ref === 'function') {
332
+ ref(node);
333
+ } else if (ref) {
334
+ ref.current = node;
335
+ }
336
+ },
337
+ [measure, ref]
338
+ );
339
+
340
+ const cellId = (row: number, column: number): string => `${gridId}-${row}-${column}`;
341
+
342
+ const canEdit = (position: CellPosition): boolean => {
343
+ const row = rows[position.row];
344
+ const column = columns[position.column];
345
+ if (!editable || row === undefined || column === undefined || row.locked === true || row.state === 'deleted' || column.readOnly === true) {
346
+ return false;
347
+ }
348
+ return !isPreview(row.cells[position.column] ?? null) || loadValue !== undefined;
349
+ };
350
+
351
+ const revealColumn = (column: number): void => {
352
+ const element = scroller.current;
353
+ const at = displayOf.get(column);
354
+ if (element === null || at === undefined || at < pinnedShown) {
355
+ return;
356
+ }
357
+ const left = scrollToReveal(gutter + (offsets[at] ?? 0), shownWidths[at] ?? 0, element.scrollLeft, element.clientWidth, gutter + pinnedWidth);
358
+ if (left !== element.scrollLeft) {
359
+ element.scrollLeft = left;
360
+ }
361
+ };
362
+
363
+ const reveal = (position: CellPosition): void => {
364
+ const element = scroller.current;
365
+ if (element === null) {
366
+ return;
367
+ }
368
+ const top = scrollToReveal(HEADER_HEIGHT + position.row * ROW_HEIGHT, ROW_HEIGHT, element.scrollTop, element.clientHeight, HEADER_HEIGHT);
369
+ if (top !== element.scrollTop) {
370
+ element.scrollTop = top;
371
+ }
372
+ revealColumn(position.column);
373
+ };
374
+
375
+ const pickColumn = (column: number, click = { shiftKey: false, mod: false }): void => {
376
+ setColumnSelection((now) => selectColumn(now, order, column, click));
377
+ setRangeAnchor(null);
378
+ };
379
+
380
+ /* With `extend` the range grows from where it started; otherwise the cell stands alone. */
381
+ const focusAt = (position: CellPosition, extend = false): void => {
382
+ setColumnSelection(NO_COLUMN_SELECTION);
383
+ setRangeAnchor(extend && focusCell !== null ? (anchorCell ?? focusCell) : null);
384
+ setFocused(position);
385
+ reveal(position);
386
+ };
387
+
388
+ const step = (from: CellPosition | null, input: NavigationKey): CellPosition | null => {
389
+ const next = moveFocus(isShown(from) ? toDisplay(from) : null, input, { rows: rows.length, columns: order.length, page });
390
+ return next === null ? null : fromDisplay(next);
391
+ };
392
+
393
+ const setSelection = (next: ReadonlySet<string>): void => {
394
+ if (selectedKeys === undefined) {
395
+ setOwnSelection(next);
396
+ }
397
+ onSelectedKeysChange?.(next);
398
+ };
399
+
400
+ const beginEdit = async (position: CellPosition): Promise<void> => {
401
+ const row = rows[position.row];
402
+ if (row === undefined || !canEdit(position)) {
403
+ return;
404
+ }
405
+ const cell = row.cells[position.column] ?? null;
406
+ settled.current = false;
407
+ if (!isPreview(cell)) {
408
+ const text = draftOf(cell);
409
+ setEditing({ ...position, draft: text, initial: text, loading: false });
410
+ return;
411
+ }
412
+ setEditing({ ...position, draft: '', initial: '', loading: true });
413
+ const value = await loadValue?.(row.key, position.column);
414
+ setEditing((now) => {
415
+ if (now === null || !now.loading || now.row !== position.row || now.column !== position.column) {
416
+ return now;
417
+ }
418
+ if (value === undefined) {
419
+ return null;
420
+ }
421
+ const text = draftOf(value);
422
+ return { ...now, draft: text, initial: text, loading: false };
423
+ });
424
+ };
425
+
426
+ const finishEdit = (commit: boolean, move?: EditMove): void => {
427
+ if (editing === null || settled.current) {
428
+ return;
429
+ }
430
+ settled.current = true;
431
+ const column = columns[editing.column];
432
+ const row = rows[editing.row];
433
+ if (commit && !editing.loading && editing.draft !== editing.initial && column !== undefined && row !== undefined) {
434
+ onCommit?.(row.key, editing.column, parseDraft(editing.draft, column.kind));
435
+ }
436
+ setEditing(null);
437
+ scroller.current?.focus();
438
+ if (move !== undefined) {
439
+ const next = step(editing, { key: 'Tab', shiftKey: move === 'previous', mod: false });
440
+ if (next !== null) {
441
+ focusAt(next);
442
+ }
443
+ }
444
+ };
445
+
446
+ const finishPick = (result: { readonly value: string | null } | undefined): void => {
447
+ if (editing === null || settled.current) {
448
+ return;
449
+ }
450
+ settled.current = true;
451
+ const row = rows[editing.row];
452
+ const before = row?.cells[editing.column] ?? null;
453
+ if (result !== undefined && row !== undefined && result.value !== (typeof before === 'string' ? before : null)) {
454
+ onCommit?.(row.key, editing.column, result.value);
455
+ }
456
+ setEditing(null);
457
+ scroller.current?.focus();
458
+ };
459
+
460
+ const followCell = (cell: FocusedCell): boolean => onFollow !== undefined && (canFollow?.(cell) ?? true);
461
+
462
+ const commitValue = (value: EditValue): void => {
463
+ const position = menuTarget === null || menuTarget.column === null ? null : { row: menuTarget.row, column: menuTarget.column };
464
+ const row = position === null ? undefined : rows[position.row];
465
+ if (position !== null && row !== undefined && canEdit(position)) {
466
+ onCommit?.(row.key, position.column, value);
467
+ }
468
+ };
469
+
470
+ /* The cells of the range or of the picked columns, as the columns and rows a copy writes. */
471
+ const blockOf = (): CopyInput | null => {
472
+ if (block === null) {
473
+ return null;
474
+ }
475
+ return {
476
+ columns: block.columns.map((index) => ({ name: columns[index]!.name, kind: columns[index]!.kind })),
477
+ rows: rows.slice(block.top, block.bottom + 1).map((row) => block.columns.map((index) => row.cells[index] ?? null)),
478
+ target: sqlTarget
479
+ };
480
+ };
481
+
482
+ const copyAs = (format: CopyFormat): void => {
483
+ const block = blockOf();
484
+ if (block !== null) {
485
+ copyText(formatCopy(format, block));
486
+ }
487
+ };
488
+
489
+ /* One cell copies as it reads; a block copies as TSV, which a spreadsheet pastes into cells. */
490
+ const copyRange = (): void => {
491
+ if (block !== null && block.top === block.bottom && block.columns.length === 1) {
492
+ copyText(copyTextOf(rows[block.top]?.cells[block.columns[0]!] ?? null));
493
+ } else {
494
+ copyAs('tsv');
495
+ }
496
+ };
497
+
498
+ const copyCell = (): void => {
499
+ const row = menuTarget === null ? undefined : rows[menuTarget.row];
500
+ if (row !== undefined && menuTarget?.column != null) {
501
+ copyText(copyTextOf(row.cells[menuTarget.column] ?? null));
502
+ }
503
+ };
504
+
505
+ const selectAll = (): void => {
506
+ const all = fullRect(rows.length, order.length);
507
+ if (all !== null) {
508
+ setColumnSelection(NO_COLUMN_SELECTION);
509
+ setRangeAnchor(fromDisplay({ row: all.top, column: all.left }));
510
+ setFocused(fromDisplay({ row: all.bottom, column: all.right }));
511
+ }
512
+ };
513
+
514
+ const selectRowAt = (index: number, click: { shiftKey: boolean; mod: boolean }): void => {
515
+ const result = selectRow(
516
+ rows.map((row) => row.key),
517
+ selection,
518
+ anchor.current,
519
+ index,
520
+ click
521
+ );
522
+ anchor.current = result.anchor;
523
+ setSelection(result.selected);
524
+ if (!click.mod && order.length > 0) {
525
+ setColumnSelection(NO_COLUMN_SELECTION);
526
+ setRangeAnchor({ row: result.anchor, column: order[order.length - 1]! });
527
+ setFocused({ row: index, column: order[0]! });
528
+ }
529
+ };
530
+
531
+ const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>): void => {
532
+ if (event.target !== event.currentTarget || event.altKey) {
533
+ return;
534
+ }
535
+ if (editing !== null) {
536
+ if (event.key === 'Escape') {
537
+ finishEdit(false);
538
+ }
539
+ return;
540
+ }
541
+ const mod = isModHeld(event, isApplePlatform());
542
+ const key = event.key.toLowerCase();
543
+ if (mod && key === 'c') {
544
+ event.preventDefault();
545
+ copyRange();
546
+ } else if (mod && key === 'a') {
547
+ event.preventDefault();
548
+ selectAll();
549
+ } else if (mod && key === 'd' && onDuplicateRows !== undefined) {
550
+ event.preventDefault();
551
+ const keys =
552
+ selection.size > 0 ? rows.filter((row) => selection.has(row.key)).map((row) => row.key) : focusedRowKey === null ? [] : [focusedRowKey];
553
+ if (keys.length > 0) {
554
+ onDuplicateRows(keys);
555
+ }
556
+ } else if ((event.key === 'Enter' || event.key === 'F2') && focusCell !== null) {
557
+ event.preventDefault();
558
+ void beginEdit(focusCell);
559
+ } else if (event.key === ' ' && event.shiftKey && focusCell !== null) {
560
+ event.preventDefault();
561
+ selectRowAt(focusCell.row, { shiftKey: false, mod });
562
+ } else if ((event.key === 'Delete' || event.key === 'Backspace') && onDeleteSelected !== undefined && selection.size > 0) {
563
+ event.preventDefault();
564
+ onDeleteSelected();
565
+ } else if (event.key === 'Escape' && (anchorCell !== null || columnSelection.columns.size > 0)) {
566
+ setRangeAnchor(null);
567
+ setColumnSelection(NO_COLUMN_SELECTION);
568
+ } else {
569
+ const next = step(focusCell, { key: event.key, shiftKey: event.shiftKey, mod });
570
+ if (next !== null) {
571
+ event.preventDefault();
572
+ focusAt(next, event.shiftKey && event.key !== 'Tab');
573
+ }
574
+ }
575
+ };
576
+
577
+ const pressCell = (event: PointerEvent, position: CellPosition, isEditing: boolean): void => {
578
+ if (event.button !== 0 || isEditing) {
579
+ return;
580
+ }
581
+ const row = rows[position.row];
582
+ if (row !== undefined && isModHeld(event, isApplePlatform()) && followCell({ rowKey: row.key, column: position.column })) {
583
+ focusAt(position);
584
+ onFollow?.({ rowKey: row.key, column: position.column });
585
+ return;
586
+ }
587
+ const extend = event.shiftKey && focusCell !== null;
588
+ dragOrigin.current = extend ? (anchorCell ?? focusCell) : position;
589
+ dragging.current = true;
590
+ focusAt(position, extend);
591
+ };
592
+
593
+ const dragTo = (position: CellPosition): void => {
594
+ if (dragging.current && dragOrigin.current !== null) {
595
+ setRangeAnchor(dragOrigin.current);
596
+ setFocused(position);
597
+ }
598
+ };
599
+
600
+ const openCellMenu = (position: CellPosition): void => {
601
+ if (!rectContains(rect, position.row, displayOf.get(position.column)!)) {
602
+ focusAt(position);
603
+ }
604
+ setMenuTarget(position);
605
+ };
606
+
607
+ const openRowMenu = (index: number): void => {
608
+ const row = rows[index];
609
+ if (row !== undefined && !selection.has(row.key)) {
610
+ selectRowAt(index, { shiftKey: false, mod: false });
611
+ }
612
+ setMenuTarget({ row: index, column: null });
613
+ };
614
+
615
+ const resizeColumn = (index: number, width: number): void => {
616
+ setLayout((now) => ({ ...now, resized: { ...now.resized, [index]: width } }));
617
+ };
618
+
619
+ const fitColumn = (index: number): void => {
620
+ const column = columns[index];
621
+ if (column !== undefined) {
622
+ resizeColumn(
623
+ index,
624
+ fitColumnWidth(
625
+ column.name,
626
+ rows.map((row) => textOf(cellView(row.cells[index] ?? null, column.kind, notation))),
627
+ column.primaryKey === true
628
+ )
629
+ );
630
+ }
631
+ };
632
+
633
+ const changeView = (change: (view: ColumnView) => ColumnView): void => {
634
+ setLayout((now) => ({ ...now, view: change(now.view) }));
635
+ };
636
+
637
+ const headerActions: HeaderActions = {
638
+ onSelect: (index, click) => {
639
+ if (!pressedInPopup()) {
640
+ pickColumn(index, click);
641
+ }
642
+ },
643
+ onSortDirection: (index, direction) => onSortsChange?.(sortOnly(columns[index]!.name, direction)),
644
+ onClearSort: () => onSortsChange?.([]),
645
+ onResize: resizeColumn,
646
+ onFit: fitColumn,
647
+ onHide: (index) => changeView((view) => hideColumn(view, index)),
648
+ onShowAll: () => changeView(showAllColumns),
649
+ onTogglePin: (index) => changeView((view) => togglePin(view, index))
650
+ };
651
+
652
+ const renderRow = (index: number): ReactNode => {
653
+ const row = rows[index]!;
654
+ const selected = selection.has(row.key);
655
+ const tint = row.state === undefined ? (selected ? 'bg-accent-soft/40' : '') : ROW_STATE[row.state];
656
+ return (
657
+ <div
658
+ key={row.key}
659
+ role="row"
660
+ aria-rowindex={index + 2}
661
+ aria-selected={selected}
662
+ className={clsx('group/row absolute inset-x-0 flex border-b border-border-soft', tint, tint === '' && 'hover:bg-surface-hover')}
663
+ style={{ top: index * ROW_HEIGHT, height: ROW_HEIGHT }}
664
+ >
665
+ <div
666
+ role="rowheader"
667
+ data-selected={selected ? '' : undefined}
668
+ className={clsx(
669
+ 'sticky left-0 z-10 flex shrink-0 cursor-default items-center justify-end border-r border-border bg-surface pr-2 pl-1 font-mono text-xs text-text-faint tabular-nums select-none data-[selected]:bg-accent-soft data-[selected]:text-text',
670
+ row.state === 'inserted' && 'text-positive'
671
+ )}
672
+ style={{ width: gutter }}
673
+ onClick={(event: MouseEvent) => {
674
+ if (!pressedInPopup()) {
675
+ selectRowAt(index, { shiftKey: event.shiftKey, mod: isModHeld(event, isApplePlatform()) });
676
+ }
677
+ }}
678
+ onContextMenu={() => openRowMenu(index)}
679
+ >
680
+ {row.number === null ? '+' : formatNumber(row.number)}
681
+ </div>
682
+ {order.map((columnIndex, position) => {
683
+ const column = columns[columnIndex]!;
684
+ const cell = row.cells[columnIndex] ?? null;
685
+ const view = cellView(cell, column.kind, notation);
686
+ const cellPosition = { row: index, column: columnIndex };
687
+ const isFocused = focusCell !== null && focusCell.row === index && focusCell.column === columnIndex;
688
+ const ranged = pickedSet.has(columnIndex) || (columnBlock === null && rectContains(rect, index, position));
689
+ const edited = row.edited?.has(columnIndex) === true;
690
+ const isEditing = editing !== null && editing.row === index && editing.column === columnIndex;
691
+ const pinned = position < pinnedShown;
692
+ const enumType = enumTypes[columnIndex] ?? null;
693
+ const followable = !isEditing && followCell({ rowKey: row.key, column: columnIndex });
694
+ return (
695
+ <div
696
+ key={columnIndex}
697
+ id={cellId(index, columnIndex)}
698
+ role="gridcell"
699
+ aria-colindex={position + 2}
700
+ aria-selected={ranged}
701
+ data-focused={isFocused ? '' : undefined}
702
+ data-ranged={ranged ? '' : undefined}
703
+ className={clsx(
704
+ 'group/cell flex h-full shrink-0 items-center overflow-hidden px-3 font-mono text-code whitespace-nowrap outline-0 select-none data-[focused]:outline-1 data-[focused]:-outline-offset-1 data-[focused]:outline-border-strong group-focus-within/grid:data-[focused]:outline-accent',
705
+ position === order.length - 1 ? 'border-r-0' : 'border-r',
706
+ view.align === 'end' ? 'justify-end tabular-nums' : 'justify-start',
707
+ pinned
708
+ ? [
709
+ 'sticky z-5',
710
+ position === pinnedShown - 1 ? 'border-border-strong' : 'border-border-soft',
711
+ ranged ? 'bg-accent-soft' : ['bg-surface', tint === '' && 'group-hover/row:bg-surface-hover']
712
+ ]
713
+ : ['relative border-border-soft', ranged ? 'bg-accent-soft' : edited && 'bg-accent/10']
714
+ )}
715
+ style={{ width: shownWidths[position], left: pinned ? gutter + (offsets[position] ?? 0) : undefined }}
716
+ onPointerDown={(event) => pressCell(event, cellPosition, isEditing)}
717
+ onPointerEnter={() => dragTo(cellPosition)}
718
+ onDoubleClick={() => void beginEdit(cellPosition)}
719
+ onContextMenu={() => openCellMenu(cellPosition)}
720
+ >
721
+ {pinned && !ranged && <span aria-hidden className={clsx('pointer-events-none absolute inset-0', tint, edited && 'bg-accent/10')} />}
722
+ {isEditing && editing.loading && <Spinner size={12} label={t('grid.loadingValue')} />}
723
+ {isEditing && !editing.loading && enumType !== null && (
724
+ <EnumPicker
725
+ autoOpen
726
+ type={enumType}
727
+ value={typeof cell === 'string' ? cell : null}
728
+ nullable={column.nullable !== false}
729
+ label={column.name}
730
+ onDone={finishPick}
731
+ />
732
+ )}
733
+ {isEditing && !editing.loading && enumType === null && (
734
+ <CellEditor
735
+ value={editing.draft}
736
+ label={column.name}
737
+ onValueChange={(draft) => setEditing({ ...editing, draft })}
738
+ onCommit={(move) => finishEdit(true, move)}
739
+ onCancel={() => finishEdit(false)}
740
+ />
741
+ )}
742
+ {!isEditing && (
743
+ <>
744
+ <span className={clsx('min-w-0 truncate', TONE[view.tone], row.state === 'deleted' && 'line-through')}>{view.text}</span>
745
+ {view.suffix !== undefined && <span className="ml-2 shrink-0 text-text-faint">{view.suffix}</span>}
746
+ </>
747
+ )}
748
+ {followable && (
749
+ <IconButton
750
+ icon={ArrowUpRight}
751
+ size="2xs"
752
+ label={t('grid.followReference')}
753
+ tabIndex={-1}
754
+ className="absolute top-1/2 right-1 -translate-y-1/2 bg-surface-raised opacity-0 group-hover/cell:opacity-100 group-data-[focused]/cell:opacity-100 focus-visible:opacity-100"
755
+ onPointerDown={(event) => event.stopPropagation()}
756
+ onClick={() => {
757
+ if (!pressedInPopup()) {
758
+ onFollow?.({ rowKey: row.key, column: columnIndex });
759
+ }
760
+ }}
761
+ />
762
+ )}
763
+ </div>
764
+ );
765
+ })}
766
+ </div>
767
+ );
768
+ };
769
+
770
+ const menuRow = menuTarget === null ? undefined : rows[menuTarget.row];
771
+ const menuCell: CellPosition | null = menuTarget === null || menuTarget.column === null ? null : { row: menuTarget.row, column: menuTarget.column };
772
+ const settable = menuCell !== null && canEdit(menuCell);
773
+ const nullable = menuCell !== null && columns[menuCell.column]?.nullable !== false;
774
+ const menuColumn = menuCell === null ? undefined : columns[menuCell.column];
775
+ const hasDefault = menuColumn !== undefined && ((menuColumn.defaultValue ?? null) !== null || menuColumn.autoIncrement === true);
776
+ const menuContext: GridMenuContext | null =
777
+ menuRow === undefined || menuTarget === null
778
+ ? null
779
+ : {
780
+ rowKeys: selection.has(menuRow.key) ? rows.filter((row) => selection.has(row.key)).map((row) => row.key) : [menuRow.key],
781
+ cell: menuTarget.column === null ? null : { rowKey: menuRow.key, column: menuTarget.column }
782
+ };
783
+
784
+ return (
785
+ <div
786
+ ref={attach}
787
+ role="grid"
788
+ aria-label={label}
789
+ aria-rowcount={rows.length + 1}
790
+ aria-colcount={order.length + 1}
791
+ aria-activedescendant={
792
+ pickedColumns.length > 0 ? headerId(pickedColumns[0]!) : focusCell === null ? undefined : cellId(focusCell.row, focusCell.column)
793
+ }
794
+ tabIndex={0}
795
+ className={clsx('group/grid relative min-h-0 flex-1 overflow-auto bg-surface text-text outline-none', className)}
796
+ onKeyDown={handleKeyDown}
797
+ onScroll={(event) => setScrollTop(Math.floor(event.currentTarget.scrollTop / ROW_HEIGHT) * ROW_HEIGHT)}
798
+ >
799
+ <div className="relative min-w-full" style={{ width: totalWidth }}>
800
+ <div role="row" aria-rowindex={1} className="sticky top-0 z-20 flex w-full border-b border-border bg-surface" style={{ height: HEADER_HEIGHT }}>
801
+ <div className="sticky left-0 z-10 shrink-0 border-r border-border bg-surface" style={{ width: gutter }} />
802
+ {order.map((columnIndex, position) => {
803
+ const column = columns[columnIndex]!;
804
+ return (
805
+ <GridHeaderCell
806
+ key={columnIndex}
807
+ column={column}
808
+ index={columnIndex}
809
+ position={position}
810
+ width={shownWidths[position]!}
811
+ sortable={sortable}
812
+ selected={pickedSet.has(columnIndex)}
813
+ id={headerId(columnIndex)}
814
+ sort={sortable ? sortStateOf(sortList, column.name) : null}
815
+ multipleSorts={sortList.length > 1}
816
+ hasSorts={sortList.length > 0}
817
+ pinned={position < pinnedShown}
818
+ stickyLeft={gutter + (offsets[position] ?? 0)}
819
+ lastPinned={position === pinnedShown - 1}
820
+ last={position === order.length - 1}
821
+ hasHidden={current.view.hidden.size > 0}
822
+ canHide={order.length > 1}
823
+ actions={headerActions}
824
+ />
825
+ );
826
+ })}
827
+ </div>
828
+ <ContextMenu.Root>
829
+ <ContextMenu.Trigger role="rowgroup" className="relative" style={{ height: rows.length * ROW_HEIGHT }}>
830
+ {Array.from({ length: range.end - range.start }, (_, offset) => renderRow(range.start + offset))}
831
+ </ContextMenu.Trigger>
832
+ <ContextMenu.Popup>
833
+ <ContextMenu.Item disabled={menuCell === null} onClick={copyCell}>
834
+ <Icon icon={Copy} size={14} />
835
+ {t('grid.copyValue')}
836
+ <Kbd shortcut={EDIT_SHORTCUTS.copy} />
837
+ </ContextMenu.Item>
838
+ <CopyAsMenu
839
+ disabled={block === null}
840
+ formats={sqlTarget === undefined ? COPY_FORMATS.filter((format) => format !== 'sql') : COPY_FORMATS}
841
+ onCopy={copyAs}
842
+ />
843
+ {menuContext !== null && menuCell !== null && queryMenu !== undefined && (
844
+ <>
845
+ <ContextMenu.Separator />
846
+ {queryMenu(menuContext)}
847
+ </>
848
+ )}
849
+ <ContextMenu.Separator />
850
+ {editable && menuCell !== null && (
851
+ <>
852
+ <ContextMenu.Item disabled={!settable || !nullable} onClick={() => commitValue(null)}>
853
+ <Icon icon={Ban} size={14} />
854
+ {t('grid.setNull')}
855
+ </ContextMenu.Item>
856
+ {hasDefault && (
857
+ <ContextMenu.Item disabled={!settable} onClick={() => commitValue({ kind: 'default' })}>
858
+ <Icon icon={RotateCcw} size={14} />
859
+ {t('grid.setDefault')}
860
+ </ContextMenu.Item>
861
+ )}
862
+ </>
863
+ )}
864
+ {menuContext !== null && menu?.(menuContext)}
865
+ </ContextMenu.Popup>
866
+ </ContextMenu.Root>
867
+ {rows.length === 0 && empty !== undefined && <div className="sticky left-0 w-fit px-4 py-6 text-xs text-text-muted">{empty}</div>}
868
+ </div>
869
+ </div>
870
+ );
871
+ }