@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,880 @@
1
+ import { useEffect, useMemo, useRef, useState, type KeyboardEvent, type ReactNode, type Ref } from 'react';
2
+ import clsx from 'clsx';
3
+ import { ArrowDownWideNarrow, ArrowUpRight, Check, CircleAlert, CopyPlus, Download, Filter, FilterX, RefreshCw, Trash2, Undo2 } from 'lucide-react';
4
+ import { useTranslation } from 'react-i18next';
5
+ import {
6
+ Banner,
7
+ Button,
8
+ ContextMenu,
9
+ Icon,
10
+ isApplePlatform,
11
+ Kbd,
12
+ KEY_SHORTCUTS,
13
+ matchesShortcut,
14
+ messageOf,
15
+ PanelEmpty,
16
+ PromptDialog,
17
+ shortcut,
18
+ Spinner
19
+ } from '@adecore/ui';
20
+ import { formatNumber } from '@adecore/ui/format';
21
+ import { DatabaseRequestError, type Connection } from '../client/types.ts';
22
+ import { useDatabaseAction, useDatabaseClient, useDatabaseFiles, useDatabaseStorage } from '../client-context.ts';
23
+ import { aggregateBlock, blockSize, type RangeBlock } from '../grid/aggregates.ts';
24
+ import { DataGrid } from '../grid/DataGrid.tsx';
25
+ import { wholeValueOf } from '../grid/focused-value.ts';
26
+ import type { GridSort } from '../grid/sort.ts';
27
+ import type { ColumnRequest, FocusedCell, GridMenuContext } from '../grid/types.ts';
28
+ import { RecordView } from '../grid/RecordView.tsx';
29
+ import { ValueDock } from '../grid/ValueDock.tsx';
30
+ import { valueOfCell, type EditValue, type RowChange, type Value } from '../protocol/index.ts';
31
+ import type { SqlTarget } from '../sql.ts';
32
+ import { useSchemaChange } from '../use-schema-change.ts';
33
+ import { ValuePanel } from '../value/ValuePanel.tsx';
34
+ import {
35
+ addChip,
36
+ cellChip,
37
+ chipsFromQuery,
38
+ chipsToOrderBy,
39
+ chipsToWhere,
40
+ shorten,
41
+ sortsOf,
42
+ toggleSort,
43
+ withSorts,
44
+ type Chip,
45
+ type CommandColumn,
46
+ type FilterChip
47
+ } from './command-field.ts';
48
+ import { buildGridColumns, buildGridRows, cloneValues, parseRowKey } from './grid-rows.ts';
49
+ import { importableColumns } from './import-mapping.ts';
50
+ import { ImportDialog } from './ImportDialog.tsx';
51
+ import { layoutStorageKey } from './layout-store.ts';
52
+ import { DEFAULT_PAGE_SIZE, lastPageOffset, pageBounds } from './paging.ts';
53
+ import {
54
+ addInsertWith,
55
+ describeKey,
56
+ emptyPending,
57
+ hasRowChanges,
58
+ isPendingEmpty,
59
+ markDeleted,
60
+ pendingCount,
61
+ removeInsert,
62
+ revertRows,
63
+ rowKeyOf,
64
+ setEdit,
65
+ setInsertValue,
66
+ toRowChanges,
67
+ type PendingChanges,
68
+ type RowSelectionRefs
69
+ } from './pending.ts';
70
+ import { foreignKeyOf, referenceOf, type Reference } from './references.ts';
71
+ import { TableStatusBar } from './TableStatusBar.tsx';
72
+ import { TableToolbar } from './TableToolbar.tsx';
73
+ import { useLoaded } from './useLoaded.ts';
74
+ import { useStoredLayout } from './useStoredLayout.ts';
75
+ import { useTableTransfer } from './useTableTransfer.ts';
76
+
77
+ export interface TableViewProps {
78
+ connection: Connection;
79
+ schema: string;
80
+ table: string;
81
+ /* The filters the view starts with, such as the condition of a jump along a foreign key. They win over the ones remembered. */
82
+ defaultWhere?: string;
83
+ defaultOrderBy?: string;
84
+ /* Told whether the view holds changes that are not submitted, whenever that changes. */
85
+ onDirtyChange?(dirty: boolean): void;
86
+ className?: string;
87
+ ref?: Ref<HTMLDivElement>;
88
+ }
89
+
90
+ interface Filters {
91
+ where: string;
92
+ orderBy: string;
93
+ }
94
+
95
+ const NO_KEYS: ReadonlySet<string> = new Set();
96
+
97
+ /* How much of a value a menu item spells out before it cuts it. */
98
+ const MENU_VALUE_LENGTH = 40;
99
+
100
+ const FOCUS_COMMAND = shortcut('Mod+F');
101
+ const DUPLICATE_ROWS = shortcut('Mod+D');
102
+
103
+ /*
104
+ * The rows of one table: read a page at a time, filtered and sorted with SQL a person types, and
105
+ * editable in place. Changes pile up as pending and go to the server together on Submit.
106
+ */
107
+ export function TableView({ connection, schema, table, defaultWhere, defaultOrderBy, onDirtyChange, className, ref }: TableViewProps) {
108
+ // A new table, or the same one asked for with other filters, starts from nothing: no page, no pending change.
109
+ return (
110
+ <TableBody
111
+ key={[connection.id, schema, table, defaultWhere ?? '', defaultOrderBy ?? ''].join('\u0000')}
112
+ connection={connection}
113
+ schema={schema}
114
+ table={table}
115
+ defaultWhere={defaultWhere}
116
+ defaultOrderBy={defaultOrderBy}
117
+ onDirtyChange={onDirtyChange}
118
+ className={className}
119
+ ref={ref}
120
+ />
121
+ );
122
+ }
123
+
124
+ function TableBody({ connection, schema, table, defaultWhere, defaultOrderBy, onDirtyChange, className, ref }: TableViewProps) {
125
+ const { t } = useTranslation('database');
126
+ const client = useDatabaseClient();
127
+ const onAction = useDatabaseAction();
128
+ const files = useDatabaseFiles();
129
+ const { stored, remember } = useStoredLayout(useDatabaseStorage(), layoutStorageKey(connection.id, schema, table));
130
+ const session = useMemo(() => client.session(connection), [client, connection]);
131
+ const grid = useRef<HTMLDivElement>(null);
132
+ const command = useRef<HTMLInputElement>(null);
133
+ const lifetime = useRef<AbortController | null>(null);
134
+ const counter = useRef<AbortController | null>(null);
135
+ const insertedBefore = useRef(0);
136
+ const engine = connection.config.engine;
137
+ const [chips, setChips] = useState<Chip[]>(() =>
138
+ chipsFromQuery(engine, {
139
+ where: defaultWhere ?? stored?.where ?? '',
140
+ orderBy: defaultOrderBy ?? stored?.orderBy ?? '',
141
+ remembered: defaultWhere === undefined ? stored?.filters : undefined
142
+ })
143
+ );
144
+ const [offset, setOffset] = useState(0);
145
+ const [pageSize, setPageSize] = useState(stored?.pageSize ?? DEFAULT_PAGE_SIZE);
146
+ const [pending, setPending] = useState<PendingChanges>(emptyPending);
147
+ const [selected, setSelected] = useState<ReadonlySet<string>>(NO_KEYS);
148
+ const [counted, setCounted] = useState<number | null>(null);
149
+ const [counting, setCounting] = useState(false);
150
+ const [submitting, setSubmitting] = useState(false);
151
+ const [failure, setFailure] = useState<string | null>(null);
152
+ const [shapeChanged, setShapeChanged] = useState(false);
153
+ const [discard, setDiscard] = useState<{ run(): void } | null>(null);
154
+ const [panelOpen, setPanelOpen] = useState(false);
155
+ const [focus, setFocus] = useState<FocusedCell | null>(null);
156
+ const [recordOpen, setRecordOpen] = useState(false);
157
+ const [recordKey, setRecordKey] = useState<string | null>(null);
158
+ const [focusRequest, setFocusRequest] = useState<FocusedCell | null>(null);
159
+ const [columnRequest, setColumnRequest] = useState<ColumnRequest | null>(null);
160
+ const [range, setRange] = useState<RangeBlock | null>(null);
161
+ const [fetched, setFetched] = useState<{ id: string; rows: unknown; value: Value | undefined } | null>(null);
162
+
163
+ const applied = useMemo((): Filters => ({ where: chipsToWhere(chips), orderBy: chipsToOrderBy(engine, chips) }), [chips, engine]);
164
+ const target = `${connection.id}|${schema}|${table}`;
165
+ const structureLoad = useLoaded((signal) => session.structure(schema, table, { signal }), target);
166
+ // The offset travels with the page it belongs to, since the page on screen lags behind the one asked for.
167
+ const rowsLoad = useLoaded(
168
+ async (signal) => ({
169
+ result: await session.rows(
170
+ schema,
171
+ table,
172
+ { where: applied.where || undefined, orderBy: applied.orderBy || undefined, offset, limit: pageSize },
173
+ { signal }
174
+ ),
175
+ offset
176
+ }),
177
+ JSON.stringify([target, applied, offset, pageSize])
178
+ );
179
+ const structure = structureLoad.value;
180
+ const loaded = rowsLoad.value?.result ?? null;
181
+ const loadedOffset = rowsLoad.value?.offset ?? 0;
182
+
183
+ const readOnlyReason =
184
+ connection.config.readOnly === true
185
+ ? t('table.readOnly.connection')
186
+ : structure === null
187
+ ? null
188
+ : structure.kind === 'view'
189
+ ? t('table.readOnly.view')
190
+ : structure.rowKey === null
191
+ ? t('table.readOnly.noKey')
192
+ : null;
193
+ const editable = readOnlyReason === null && structure !== null && loaded !== null && !rowsLoad.loading;
194
+
195
+ const columns = useMemo(() => (loaded === null ? [] : buildGridColumns(loaded, structure)), [loaded, structure]);
196
+ const rows = useMemo(
197
+ () => (loaded === null ? [] : buildGridRows({ structure, loaded, pending, offset: loadedOffset })),
198
+ [loaded, structure, pending, loadedOffset]
199
+ );
200
+ const transfer = useTableTransfer({
201
+ client,
202
+ session,
203
+ files,
204
+ schema,
205
+ table,
206
+ structure,
207
+ where: applied.where,
208
+ orderBy: applied.orderBy,
209
+ onImported: () => {
210
+ setCounted(null);
211
+ rowsLoad.reload();
212
+ }
213
+ });
214
+ const changeCount = pendingCount(pending);
215
+ const dirty = changeCount > 0;
216
+ const reportDirty = useRef(onDirtyChange);
217
+ const reportedDirty = useRef(false);
218
+ const sorts = useMemo(() => sortsOf(chips), [chips]);
219
+ const commandColumns = useMemo((): CommandColumn[] => columns.map((column) => ({ name: column.name, type: column.type })), [columns]);
220
+ const rowByKey = useMemo(() => new Map(rows.map((row) => [row.key, row])), [rows]);
221
+ const recordIndex = Math.max(
222
+ 0,
223
+ rows.findIndex((row) => row.key === recordKey)
224
+ );
225
+ const selectionFigures = useMemo(
226
+ () =>
227
+ recordOpen || range === null || blockSize(range) < 2
228
+ ? null
229
+ : {
230
+ columns: range.columns.map((index) => columns[index]?.name ?? ''),
231
+ aggregates: aggregateBlock(
232
+ rows.map((row) => row.cells),
233
+ columns.map((column) => column.kind),
234
+ range
235
+ )
236
+ },
237
+ [recordOpen, range, rows, columns]
238
+ );
239
+ const sqlTarget = useMemo((): SqlTarget => ({ engine, schema, table }), [engine, schema, table]);
240
+ const focusedRow = panelOpen && focus !== null ? rows.find((row) => row.key === focus.rowKey) : undefined;
241
+ const focusedCell = focus === null ? undefined : focusedRow?.cells[focus.column];
242
+ const needsFetch =
243
+ focusedCell !== undefined &&
244
+ focus !== null &&
245
+ wholeValueOf(focusedCell) === undefined &&
246
+ !(focusedCell !== null && typeof focusedCell === 'object' && focusedCell.kind === 'default');
247
+ const focusedRef = focus === null ? null : parseRowKey(focus.rowKey);
248
+ const canFetch = focusedRef?.kind === 'loaded' && structure !== null && loaded !== null && rowKeyOf(structure, loaded, focusedRef.index) !== null;
249
+ const fetchId = needsFetch && canFetch ? `${focus.rowKey}:${focus.column}` : null;
250
+
251
+ useSchemaChange(connection.id, schema, () => {
252
+ if (isPendingEmpty(pending)) {
253
+ setCounted(null);
254
+ structureLoad.reload();
255
+ rowsLoad.reload();
256
+ } else {
257
+ setShapeChanged(true);
258
+ }
259
+ });
260
+
261
+ useEffect(() => {
262
+ reportDirty.current = onDirtyChange;
263
+ });
264
+
265
+ useEffect(() => {
266
+ if (dirty !== reportedDirty.current) {
267
+ reportedDirty.current = dirty;
268
+ reportDirty.current?.(dirty);
269
+ }
270
+ }, [dirty]);
271
+
272
+ useEffect(() => {
273
+ const controller = new AbortController();
274
+ lifetime.current = controller;
275
+ return () => {
276
+ controller.abort();
277
+ counter.current?.abort();
278
+ };
279
+ }, []);
280
+
281
+ useEffect(() => {
282
+ if (pending.inserts.length > insertedBefore.current && grid.current !== null) {
283
+ grid.current.scrollTop = grid.current.scrollHeight;
284
+ }
285
+ insertedBefore.current = pending.inserts.length;
286
+ }, [pending.inserts.length]);
287
+
288
+ useEffect(() => {
289
+ if (fetchId === null || focus === null || structure === null || loaded === null) {
290
+ return;
291
+ }
292
+ const row = parseRowKey(focus.rowKey);
293
+ const key = row.kind === 'loaded' ? rowKeyOf(structure, loaded, row.index) : null;
294
+ const name = loaded.columns[focus.column]?.name;
295
+ if (key === null || name === undefined) {
296
+ return;
297
+ }
298
+ const controller = new AbortController();
299
+ session.cell(schema, table, key, name, { signal: controller.signal }).then(
300
+ (value) => {
301
+ if (!controller.signal.aborted) {
302
+ setFetched({ id: fetchId, rows: loaded, value });
303
+ }
304
+ },
305
+ (error: unknown) => {
306
+ if (!controller.signal.aborted) {
307
+ setFetched({ id: fetchId, rows: loaded, value: undefined });
308
+ setFailure(messageOf(error));
309
+ }
310
+ }
311
+ );
312
+ return () => controller.abort();
313
+ }, [fetchId, focus, structure, loaded, session, schema, table]);
314
+
315
+ const discardPending = (): void => {
316
+ setPending(emptyPending);
317
+ setSelected(NO_KEYS);
318
+ };
319
+
320
+ /* Anything that replaces the page asks first when it would throw pending changes away. */
321
+ const guard = (action: () => void): void => {
322
+ if (isPendingEmpty(pending)) {
323
+ action();
324
+ } else {
325
+ setDiscard({
326
+ run: () => {
327
+ discardPending();
328
+ action();
329
+ }
330
+ });
331
+ }
332
+ };
333
+
334
+ /* Every change of the chips applies at once; one that changes the page asks first when it would throw pending changes away. */
335
+ const applyChips = (next: readonly Chip[]): void => {
336
+ const where = chipsToWhere(next);
337
+ const orderBy = chipsToOrderBy(engine, next);
338
+ const filters = next.filter((chip): chip is FilterChip => chip.kind === 'filter').map(({ text, sql }) => ({ text, sql }));
339
+ if (where === applied.where && orderBy === applied.orderBy) {
340
+ remember({ filters });
341
+ setChips([...next]);
342
+ return;
343
+ }
344
+ guard(() => {
345
+ remember({ where, orderBy, filters });
346
+ counter.current?.abort();
347
+ setCounting(false);
348
+ setCounted(null);
349
+ setOffset(0);
350
+ setSelected(NO_KEYS);
351
+ setFailure(null);
352
+ setChips([...next]);
353
+ });
354
+ };
355
+
356
+ const changeSorts = (next: readonly GridSort[]): void => {
357
+ applyChips(withSorts(chips, next));
358
+ };
359
+
360
+ const cellChipAt = (cell: FocusedCell, exclude: boolean): FilterChip | null => {
361
+ const row = parseRowKey(cell.rowKey);
362
+ const column = loaded?.columns[cell.column];
363
+ const value = loaded === null || row.kind !== 'loaded' ? undefined : valueOfCell(loaded.rows[row.index]?.[cell.column] ?? null);
364
+ return column === undefined || value === undefined ? null : cellChip(engine, column.name, value, column.kind, exclude);
365
+ };
366
+
367
+ const sortByColumn = (cell: FocusedCell): void => {
368
+ const name = columns[cell.column]?.name;
369
+ if (name !== undefined) {
370
+ applyChips(toggleSort(chips, name));
371
+ }
372
+ };
373
+
374
+ const jumpToColumn = (name: string): void => {
375
+ const index = columns.findIndex((column) => column.name === name);
376
+ if (index >= 0) {
377
+ setRecordOpen(false);
378
+ setColumnRequest({ column: index });
379
+ }
380
+ };
381
+
382
+ const refresh = (): void => {
383
+ guard(() => {
384
+ setCounted(null);
385
+ setFailure(null);
386
+ setShapeChanged(false);
387
+ structureLoad.reload();
388
+ rowsLoad.reload();
389
+ });
390
+ };
391
+
392
+ const goToPage = (nextOffset: number): void => {
393
+ guard(() => {
394
+ setOffset(Math.max(0, nextOffset));
395
+ setSelected(NO_KEYS);
396
+ });
397
+ };
398
+
399
+ const changePageSize = (size: number): void => {
400
+ guard(() => {
401
+ remember({ pageSize: size });
402
+ setPageSize(size);
403
+ setOffset(0);
404
+ });
405
+ };
406
+
407
+ const countRows = async (): Promise<number | null> => {
408
+ const controller = new AbortController();
409
+ counter.current?.abort();
410
+ counter.current = controller;
411
+ setCounting(true);
412
+ try {
413
+ const total = await session.count(schema, table, applied.where || undefined, { signal: controller.signal });
414
+ if (!controller.signal.aborted) {
415
+ setCounted(total);
416
+ return total;
417
+ }
418
+ } catch (error) {
419
+ if (!controller.signal.aborted) {
420
+ setFailure(messageOf(error));
421
+ }
422
+ } finally {
423
+ if (!controller.signal.aborted) {
424
+ setCounting(false);
425
+ }
426
+ }
427
+ return null;
428
+ };
429
+
430
+ const goToLast = async (): Promise<void> => {
431
+ const total = counted ?? (await countRows());
432
+ if (total !== null) {
433
+ goToPage(lastPageOffset(total, pageSize));
434
+ }
435
+ };
436
+
437
+ const commit = (rowKey: string, column: number, value: EditValue): void => {
438
+ const name = loaded?.columns[column]?.name;
439
+ const row = parseRowKey(rowKey);
440
+ if (loaded === null || name === undefined) {
441
+ return;
442
+ }
443
+ if (row.kind === 'inserted') {
444
+ setPending((now) => setInsertValue(now, row.id, name, value));
445
+ } else {
446
+ setPending((now) => setEdit(now, row.index, name, value, loaded.rows[row.index]![column] ?? null));
447
+ }
448
+ };
449
+
450
+ const loadValue = async (rowKey: string, column: number): Promise<Value | undefined> => {
451
+ const row = parseRowKey(rowKey);
452
+ const name = loaded?.columns[column]?.name;
453
+ const key = row.kind === 'loaded' && structure !== null && loaded !== null ? rowKeyOf(structure, loaded, row.index) : null;
454
+ if (key === null || name === undefined) {
455
+ return undefined;
456
+ }
457
+ try {
458
+ return await session.cell(schema, table, key, name, { signal: lifetime.current?.signal });
459
+ } catch (error) {
460
+ setFailure(messageOf(error));
461
+ return undefined;
462
+ }
463
+ };
464
+
465
+ const refsOf = (keys: Iterable<string>): RowSelectionRefs => {
466
+ const loadedRows: number[] = [];
467
+ const inserted: number[] = [];
468
+ for (const key of keys) {
469
+ const row = parseRowKey(key);
470
+ if (row.kind === 'inserted') {
471
+ inserted.push(row.id);
472
+ } else {
473
+ loadedRows.push(row.index);
474
+ }
475
+ }
476
+ return { loaded: loadedRows, inserted };
477
+ };
478
+
479
+ const deleteRows = (keys: Iterable<string>): void => {
480
+ setPending((now) => {
481
+ const refs = refsOf(keys);
482
+ const next = refs.inserted.reduce(removeInsert, now);
483
+ return markDeleted(
484
+ next,
485
+ refs.loaded.filter((index) => loaded !== null && structure !== null && rowKeyOf(structure, loaded, index) !== null)
486
+ );
487
+ });
488
+ setSelected(NO_KEYS);
489
+ };
490
+
491
+ const deleteSelected = (): void => deleteRows(selected);
492
+
493
+ const cloneRows = (keys: readonly string[]): void => {
494
+ const wanted = new Set(keys);
495
+ const copies = rows.filter((row) => wanted.has(row.key)).map((row) => cloneValues(columns, row.cells));
496
+ setPending((now) => copies.reduce(addInsertWith, now));
497
+ };
498
+
499
+ const revertRowKeys = (keys: Iterable<string>): void => {
500
+ setPending((now) => revertRows(now, refsOf(keys)));
501
+ };
502
+
503
+ const canRevert = (keys: Iterable<string>): boolean => hasRowChanges(pending, refsOf(keys));
504
+
505
+ const referenceAt = (cell: FocusedCell): Reference | null => {
506
+ const column = columns[cell.column];
507
+ const key = column === undefined || structure === null ? undefined : foreignKeyOf(structure.foreignKeys, column.name);
508
+ const row = rowByKey.get(cell.rowKey);
509
+ return key === undefined || row === undefined ? null : referenceOf(engine, key, columns, row.cells);
510
+ };
511
+
512
+ const isReferenceColumn = (column: number): boolean => {
513
+ const name = columns[column]?.name;
514
+ return name !== undefined && structure !== null && foreignKeyOf(structure.foreignKeys, name) !== undefined;
515
+ };
516
+
517
+ const follow = (cell: FocusedCell): void => {
518
+ const reference = referenceAt(cell);
519
+ if (reference !== null) {
520
+ onAction?.({
521
+ kind: 'open-table',
522
+ ref: { connectionId: connection.id, schema: reference.schema, table: reference.table },
523
+ view: 'data',
524
+ where: reference.where
525
+ });
526
+ }
527
+ };
528
+
529
+ const toggleRecordView = (): void => {
530
+ if (recordOpen) {
531
+ const row = rows[recordIndex];
532
+ setRecordOpen(false);
533
+ if (row !== undefined) {
534
+ setFocusRequest({ rowKey: row.key, column: focus?.column ?? 0 });
535
+ }
536
+ return;
537
+ }
538
+ const focused = focus === null ? undefined : rows.find((row) => row.key === focus.rowKey);
539
+ setRecordKey((focused ?? rows[0])?.key ?? null);
540
+ setRecordOpen(true);
541
+ };
542
+
543
+ const changeRecordIndex = (index: number): void => {
544
+ const row = rows[index];
545
+ if (row !== undefined) {
546
+ setRecordKey(row.key);
547
+ setFocus((now) => (now === null ? now : { rowKey: row.key, column: now.column }));
548
+ }
549
+ };
550
+
551
+ const queryMenu = (context: GridMenuContext): ReactNode => {
552
+ const cell = context.cell;
553
+ const name = cell === null ? undefined : columns[cell.column]?.name;
554
+ if (cell === null || name === undefined) {
555
+ return null;
556
+ }
557
+ const include = cellChipAt(cell, false);
558
+ const exclude = cellChipAt(cell, true);
559
+ return (
560
+ <>
561
+ <ContextMenu.Item disabled={include === null} onClick={() => include !== null && applyChips(addChip(chips, include))}>
562
+ <Icon icon={Filter} size={14} />
563
+ {t('table.menu.filterOn')} <span className="font-mono">{shorten(include?.text ?? name, MENU_VALUE_LENGTH)}</span>
564
+ </ContextMenu.Item>
565
+ <ContextMenu.Item disabled={exclude === null} onClick={() => exclude !== null && applyChips(addChip(chips, exclude))}>
566
+ <Icon icon={FilterX} size={14} />
567
+ {t('table.menu.exclude')} <span className="font-mono">{shorten(exclude?.text ?? name, MENU_VALUE_LENGTH)}</span>
568
+ </ContextMenu.Item>
569
+ <ContextMenu.Item onClick={() => sortByColumn(cell)}>
570
+ <Icon icon={ArrowDownWideNarrow} size={14} />
571
+ {t('table.menu.sortBy')} <span className="font-mono">{name}</span>
572
+ </ContextMenu.Item>
573
+ {onAction !== undefined && isReferenceColumn(cell.column) && (
574
+ <ContextMenu.Item disabled={referenceAt(cell) === null} onClick={() => follow(cell)}>
575
+ <Icon icon={ArrowUpRight} size={14} />
576
+ {t('table.goToReferenced')}
577
+ </ContextMenu.Item>
578
+ )}
579
+ </>
580
+ );
581
+ };
582
+
583
+ const gridMenu = (context: GridMenuContext): ReactNode => {
584
+ const count = context.rowKeys.length;
585
+ return (
586
+ <>
587
+ <ContextMenu.Item disabled={!editable} onClick={() => cloneRows(context.rowKeys)}>
588
+ <Icon icon={CopyPlus} size={14} />
589
+ {t('table.menu.duplicateRows', { count, formatted: formatNumber(count) })}
590
+ <Kbd shortcut={DUPLICATE_ROWS} />
591
+ </ContextMenu.Item>
592
+ {canRevert(context.rowKeys) && (
593
+ <ContextMenu.Item onClick={() => revertRowKeys(context.rowKeys)}>
594
+ <Icon icon={Undo2} size={14} />
595
+ {t('table.menu.revertRows', { count, formatted: formatNumber(count) })}
596
+ </ContextMenu.Item>
597
+ )}
598
+ <ContextMenu.Item className="text-status-error" disabled={!editable} onClick={() => deleteRows(context.rowKeys)}>
599
+ <Icon icon={Trash2} size={14} />
600
+ {t('table.menu.deleteRows', { count, formatted: formatNumber(count) })}
601
+ <Kbd shortcut={KEY_SHORTCUTS.backspace} />
602
+ </ContextMenu.Item>
603
+ </>
604
+ );
605
+ };
606
+
607
+ const conflictMessage = (error: DatabaseRequestError, changes: readonly RowChange[]): string => {
608
+ const change = error.change === undefined ? undefined : changes[error.change];
609
+ if (error.change === undefined || change === undefined || change.kind === 'insert') {
610
+ return error.message;
611
+ }
612
+ return t('table.conflict', { number: formatNumber(error.change + 1), row: describeKey(change.key) });
613
+ };
614
+
615
+ const submit = async (): Promise<void> => {
616
+ if (structure === null || loaded === null) {
617
+ return;
618
+ }
619
+ const changes = toRowChanges(structure, loaded, pending);
620
+ setSubmitting(true);
621
+ setFailure(null);
622
+ try {
623
+ await session.apply(schema, table, changes);
624
+ discardPending();
625
+ setCounted(null);
626
+ rowsLoad.reload();
627
+ } catch (error) {
628
+ setFailure(error instanceof DatabaseRequestError && error.code === 'conflict' ? conflictMessage(error, changes) : messageOf(error));
629
+ } finally {
630
+ setSubmitting(false);
631
+ }
632
+ };
633
+
634
+ const handleKeyDown = (event: KeyboardEvent<HTMLDivElement>): void => {
635
+ // A dialog opened from here is a portal: its events reach this handler through React, not through the DOM.
636
+ if (
637
+ !event.nativeEvent.isComposing &&
638
+ event.currentTarget.contains(event.target as Node) &&
639
+ matchesShortcut(FOCUS_COMMAND, event.nativeEvent, isApplePlatform())
640
+ ) {
641
+ event.preventDefault();
642
+ command.current?.focus();
643
+ command.current?.select();
644
+ }
645
+ };
646
+
647
+ const bounds = pageBounds(loadedOffset, loaded?.rows.length ?? 0, loaded?.hasMore ?? false, counted);
648
+ const focusedColumn = focus === null ? null : (columns[focus.column] ?? null);
649
+ const panelEditable =
650
+ editable && focusedRow !== undefined && focusedRow.locked !== true && focusedRow.state !== 'deleted' && focusedColumn?.readOnly !== true;
651
+ const fetchedNow = fetched !== null && fetched.id === fetchId && fetched.rows === loaded;
652
+ const panelValue = ((): Value | undefined => {
653
+ if (focusedCell === undefined || focusedRow === undefined || focus === null) {
654
+ return undefined;
655
+ }
656
+ const whole = wholeValueOf(focusedCell);
657
+ if (whole !== undefined) {
658
+ return whole;
659
+ }
660
+ if (focusedCell !== null && typeof focusedCell === 'object' && focusedCell.kind === 'default') {
661
+ // A pending DEFAULT holds no value of its own, so the panel keeps showing what the row had.
662
+ const row = parseRowKey(focusedRow.key);
663
+ return row.kind === 'loaded' && loaded !== null ? wholeValueOf(loaded.rows[row.index]?.[focus.column] ?? null) : null;
664
+ }
665
+ return fetchedNow ? fetched.value : undefined;
666
+ })();
667
+ const notice = failure ?? rowsLoad.error ?? structureLoad.error;
668
+ const noticeText = (found: NonNullable<typeof transfer.notice>): string => {
669
+ switch (found.kind) {
670
+ case 'exporting':
671
+ return t('table.export.running', { format: found.format.toUpperCase() });
672
+ case 'exported':
673
+ return t('table.export.done', { count: found.rows, rows: formatNumber(found.rows) });
674
+ case 'imported':
675
+ return t('table.import.done', { count: found.rows, rows: formatNumber(found.rows) });
676
+ case 'failed':
677
+ return found.message;
678
+ }
679
+ };
680
+
681
+ return (
682
+ <div ref={ref} className={clsx('flex min-h-0 flex-col bg-surface text-text', className)} onKeyDown={handleKeyDown}>
683
+ <TableToolbar
684
+ refreshing={rowsLoad.loading}
685
+ readOnlyReason={readOnlyReason}
686
+ hasSelection={selected.size > 0}
687
+ canRevertSelection={selected.size > 0 && canRevert(selected)}
688
+ valuePanelOpen={panelOpen}
689
+ recordViewOpen={recordOpen}
690
+ transfer={
691
+ files === undefined
692
+ ? undefined
693
+ : {
694
+ busy: transfer.notice?.kind === 'exporting',
695
+ onExport: transfer.exportAs,
696
+ onImport: readOnlyReason === null && structure !== null ? () => guard(transfer.chooseImportFile) : undefined
697
+ }
698
+ }
699
+ pendingCount={changeCount}
700
+ submitting={submitting}
701
+ engine={engine}
702
+ chips={chips}
703
+ columns={commandColumns}
704
+ pageSize={pageSize}
705
+ bounds={bounds}
706
+ commandRef={command}
707
+ onChipsChange={applyChips}
708
+ onJumpToColumn={jumpToColumn}
709
+ onRefresh={refresh}
710
+ onAddRow={() => setPending((now) => addInsertWith(now, {}))}
711
+ onDeleteRows={deleteSelected}
712
+ onCloneRows={() => cloneRows([...selected])}
713
+ onRevertRows={() => revertRowKeys(selected)}
714
+ onToggleValuePanel={() => setPanelOpen((now) => !now)}
715
+ onToggleRecordView={toggleRecordView}
716
+ onPageSizeChange={changePageSize}
717
+ onFirstPage={() => goToPage(0)}
718
+ onLastPage={() => void goToLast()}
719
+ onSubmit={() => void submit()}
720
+ onRevert={discardPending}
721
+ />
722
+ {notice !== null && (
723
+ <Banner icon={CircleAlert} tone="error" message={notice} className="shrink-0 pt-2">
724
+ {failure === null ? (
725
+ <Button size="xs" onClick={refresh}>
726
+ {t('table.retry')}
727
+ </Button>
728
+ ) : (
729
+ <Button size="xs" onClick={() => setFailure(null)}>
730
+ {t('table.dismiss')}
731
+ </Button>
732
+ )}
733
+ </Banner>
734
+ )}
735
+ {shapeChanged && (
736
+ <Banner icon={RefreshCw} tone="neutral" message={t('table.changed')} className="shrink-0 pt-2">
737
+ <Button size="xs" onClick={refresh}>
738
+ {t('table.reload')}
739
+ </Button>
740
+ </Banner>
741
+ )}
742
+ {transfer.notice !== null && (
743
+ <Banner
744
+ icon={transfer.notice.kind === 'failed' ? CircleAlert : transfer.notice.kind === 'exporting' ? Download : Check}
745
+ tone={transfer.notice.kind === 'failed' ? 'error' : 'neutral'}
746
+ message={noticeText(transfer.notice)}
747
+ className="shrink-0 pt-2"
748
+ >
749
+ {transfer.notice.kind === 'exporting' ? (
750
+ <>
751
+ <Spinner size={12} label={t('table.export.running', { format: transfer.notice.format.toUpperCase() })} />
752
+ <Button size="xs" onClick={transfer.cancelExport}>
753
+ {t('table.export.cancel')}
754
+ </Button>
755
+ </>
756
+ ) : (
757
+ <Button size="xs" onClick={transfer.dismiss}>
758
+ {t('table.dismiss')}
759
+ </Button>
760
+ )}
761
+ </Banner>
762
+ )}
763
+ {loaded === null ? (
764
+ <PanelEmpty busy={notice === null}>{notice === null ? t('table.loading') : t('table.notLoaded')}</PanelEmpty>
765
+ ) : (
766
+ <>
767
+ <ValueDock
768
+ open={panelOpen}
769
+ panel={
770
+ <ValuePanel
771
+ column={focusedColumn}
772
+ value={panelValue}
773
+ loading={fetchId !== null && !fetchedNow}
774
+ editable={panelEditable}
775
+ onCommit={(value) => focus !== null && commit(focus.rowKey, focus.column, value)}
776
+ onClose={() => setPanelOpen(false)}
777
+ className="min-w-0 flex-1"
778
+ />
779
+ }
780
+ >
781
+ <div
782
+ aria-busy={rowsLoad.loading}
783
+ className={clsx(
784
+ 'relative min-h-0 flex-1 flex-col',
785
+ recordOpen ? 'hidden' : 'flex',
786
+ rowsLoad.loading && 'pointer-events-none opacity-60'
787
+ )}
788
+ >
789
+ <DataGrid
790
+ ref={grid}
791
+ label={t('table.grid', { table })}
792
+ columns={columns}
793
+ rows={rows}
794
+ editable={editable}
795
+ empty={t('table.empty')}
796
+ selectedKeys={selected}
797
+ onSelectedKeysChange={setSelected}
798
+ onCommit={commit}
799
+ loadValue={loadValue}
800
+ sorts={sorts}
801
+ onSortsChange={changeSorts}
802
+ onFocusedCellChange={setFocus}
803
+ onDeleteSelected={editable ? deleteSelected : undefined}
804
+ sqlTarget={sqlTarget}
805
+ queryMenu={queryMenu}
806
+ menu={gridMenu}
807
+ onDuplicateRows={editable ? cloneRows : undefined}
808
+ onFollow={onAction === undefined ? undefined : follow}
809
+ canFollow={(cell) => referenceAt(cell) !== null}
810
+ onRangeChange={setRange}
811
+ initialLayout={stored ?? undefined}
812
+ onLayoutChange={remember}
813
+ focusRequest={focusRequest}
814
+ columnRequest={columnRequest}
815
+ />
816
+ </div>
817
+ {recordOpen && (
818
+ <RecordView
819
+ label={t('table.recordFields', { table })}
820
+ columns={columns}
821
+ rows={rows}
822
+ index={recordIndex}
823
+ onIndexChange={changeRecordIndex}
824
+ editable={editable}
825
+ onCommit={commit}
826
+ loadValue={loadValue}
827
+ onFocusedCellChange={setFocus}
828
+ onFollow={onAction === undefined ? undefined : follow}
829
+ canFollow={(cell) => referenceAt(cell) !== null}
830
+ empty={t('table.empty')}
831
+ />
832
+ )}
833
+ </ValueDock>
834
+ <TableStatusBar
835
+ elapsedMs={loaded.elapsedMs}
836
+ bounds={bounds}
837
+ counting={counting}
838
+ selection={selectionFigures}
839
+ onCount={() => void countRows()}
840
+ onPrevious={() => goToPage(offset - pageSize)}
841
+ onNext={() => goToPage(offset + pageSize)}
842
+ />
843
+ </>
844
+ )}
845
+ {transfer.importDraft !== null && structure !== null && (
846
+ <ImportDialog
847
+ open
848
+ path={transfer.importDraft.path}
849
+ tableColumns={importableColumns(structure.columns)}
850
+ sample={transfer.importDraft.sample}
851
+ header={transfer.importDraft.header}
852
+ mapping={transfer.importDraft.mapping}
853
+ busy={transfer.importDraft.busy}
854
+ error={transfer.importDraft.error}
855
+ onHeaderChange={transfer.changeHeader}
856
+ onMappingChange={transfer.changeMapping}
857
+ onImport={transfer.runImport}
858
+ onCancel={transfer.closeImport}
859
+ onOpenChange={(open) => !open && transfer.closeImport()}
860
+ />
861
+ )}
862
+ <PromptDialog
863
+ open={discard !== null}
864
+ danger
865
+ title={t('table.discard.title')}
866
+ description={t('table.discard.description')}
867
+ confirmLabel={t('table.discard.confirm')}
868
+ onConfirm={() => {
869
+ discard?.run();
870
+ setDiscard(null);
871
+ }}
872
+ onOpenChange={(open) => {
873
+ if (!open) {
874
+ setDiscard(null);
875
+ }
876
+ }}
877
+ />
878
+ </div>
879
+ );
880
+ }