@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,184 @@
1
+ import type { KeyboardEvent, Ref } from 'react';
2
+ import clsx from 'clsx';
3
+ import { CircleAlert, Lock, Undo2 } from 'lucide-react';
4
+ import { useTranslation } from 'react-i18next';
5
+ import { Banner, Button, DisabledReason, Field, Icon, Input, PromptDialog, Tabs, isApplePlatform, matchesShortcut, shortcut } from '@adecore/ui';
6
+ import { formatNumber } from '@adecore/ui/format';
7
+ import type { Dialect, TableDraft } from '../ddl/index.ts';
8
+ import { ColumnsEditor } from './ColumnsEditor.tsx';
9
+ import { ForeignKeysEditor } from './ForeignKeysEditor.tsx';
10
+ import { IndexesEditor } from './IndexesEditor.tsx';
11
+ import { OptionsEditor } from './OptionsEditor.tsx';
12
+ import { renameTable } from './edit.ts';
13
+ import { scriptOf, type Plan } from './plan.ts';
14
+ import { SqlPreview } from './SqlPreview.tsx';
15
+
16
+ export interface DesignerViewProps {
17
+ dialect: Dialect;
18
+ schema: string;
19
+ draft: TableDraft;
20
+ /* The table does not exist yet. */
21
+ isNew: boolean;
22
+ /* The draft differs from what was loaded. */
23
+ changed: boolean;
24
+ /* The tables a foreign key can point at. */
25
+ tables: readonly string[];
26
+ plan: Plan;
27
+ /* Why nothing can be edited, or `null` when everything can. */
28
+ readOnlyReason: string | null;
29
+ /* What the last Apply answered with, kept until the next one. */
30
+ failure: string | null;
31
+ /* The confirmation of the statements is open. */
32
+ confirming: boolean;
33
+ /* The columns of a table a foreign key may point at, or `null` while they load. */
34
+ referenceColumns(table: string): readonly string[] | null;
35
+ onChange(draft: TableDraft): void;
36
+ /* Opens the confirmation. */
37
+ onRequestApply(): void;
38
+ onConfirm(): Promise<void>;
39
+ onCancelConfirm(): void;
40
+ onRevert(): void;
41
+ onDismissFailure(): void;
42
+ className?: string;
43
+ ref?: Ref<HTMLDivElement>;
44
+ }
45
+
46
+ const APPLY_SHORTCUT = /* @__PURE__ */ shortcut('Mod+S');
47
+
48
+ /* The designer with everything it needs handed in: the name, the four tabs, what Apply would run and the confirmation before it does. */
49
+ export function DesignerView({
50
+ dialect,
51
+ schema,
52
+ draft,
53
+ isNew,
54
+ changed,
55
+ tables,
56
+ plan,
57
+ readOnlyReason,
58
+ failure,
59
+ confirming,
60
+ referenceColumns,
61
+ onChange,
62
+ onRequestApply,
63
+ onConfirm,
64
+ onCancelConfirm,
65
+ onRevert,
66
+ onDismissFailure,
67
+ className,
68
+ ref
69
+ }: DesignerViewProps) {
70
+ const { t } = useTranslation('database');
71
+ const disabled = readOnlyReason !== null;
72
+ const applyReason =
73
+ readOnlyReason ?? (plan.problems.length > 0 ? t('designer.fixProblems') : plan.statements.length === 0 ? t('designer.noChanges') : null);
74
+
75
+ function handleKeyDown(event: KeyboardEvent<HTMLDivElement>): void {
76
+ if (matchesShortcut(APPLY_SHORTCUT, event.nativeEvent, isApplePlatform())) {
77
+ event.preventDefault();
78
+ if (applyReason === null) {
79
+ onRequestApply();
80
+ }
81
+ }
82
+ }
83
+
84
+ return (
85
+ <div ref={ref} className={clsx('flex min-h-0 min-w-0 flex-col', className)} onKeyDown={handleKeyDown}>
86
+ <div className="flex shrink-0 items-end gap-3 border-b border-border-soft px-3 py-2">
87
+ <Field label={t('designer.tableName')} className="w-80">
88
+ <Input
89
+ mono
90
+ spellCheck={false}
91
+ placeholder={t('designer.tableNamePlaceholder')}
92
+ value={draft.name}
93
+ disabled={disabled}
94
+ onChange={(event) => onChange(renameTable(draft, event.target.value))}
95
+ />
96
+ </Field>
97
+ <div className="ml-auto flex items-center gap-2">
98
+ <Button variant="secondary" disabled={!changed} onClick={onRevert}>
99
+ <Icon icon={Undo2} size={12} />
100
+ {t('designer.revert')}
101
+ </Button>
102
+ <DisabledReason reason={applyReason}>
103
+ <span className="inline-flex">
104
+ <Button variant="primary" disabled={applyReason !== null} onClick={onRequestApply}>
105
+ {isNew ? t('designer.create') : t('designer.apply')}
106
+ </Button>
107
+ </span>
108
+ </DisabledReason>
109
+ </div>
110
+ </div>
111
+ {readOnlyReason !== null && (
112
+ <div className="shrink-0 px-3 pt-3">
113
+ <Banner icon={Lock} tone="neutral" className="w-full" message={readOnlyReason} />
114
+ </div>
115
+ )}
116
+ {failure !== null && (
117
+ <div className="shrink-0 px-3 pt-3">
118
+ <Banner icon={CircleAlert} tone="error" className="w-full" message={failure}>
119
+ <Button variant="secondary" size="sm" onClick={onDismissFailure}>
120
+ {t('designer.dismiss')}
121
+ </Button>
122
+ </Banner>
123
+ </div>
124
+ )}
125
+ <Tabs.Root defaultValue="columns" className="flex min-h-0 grow flex-col">
126
+ <Tabs.List className="shrink-0 px-3">
127
+ <Tabs.Tab value="columns">
128
+ {t('designer.tabs.columns')}
129
+ <Tabs.Count value={draft.columns.length} />
130
+ </Tabs.Tab>
131
+ <Tabs.Tab value="indexes">
132
+ {t('designer.tabs.indexes')}
133
+ <Tabs.Count value={draft.indexes.length} />
134
+ </Tabs.Tab>
135
+ <Tabs.Tab value="foreignKeys">
136
+ {t('designer.tabs.foreignKeys')}
137
+ <Tabs.Count value={draft.foreignKeys.length} />
138
+ </Tabs.Tab>
139
+ <Tabs.Tab value="options">{t('designer.tabs.options')}</Tabs.Tab>
140
+ </Tabs.List>
141
+ <Tabs.Panel value="columns" className="min-h-0 grow overflow-auto">
142
+ <ColumnsEditor draft={draft} dialect={dialect} disabled={disabled} onChange={onChange} />
143
+ </Tabs.Panel>
144
+ <Tabs.Panel value="indexes" className="min-h-0 grow overflow-auto">
145
+ <IndexesEditor draft={draft} dialect={dialect} disabled={disabled} onChange={onChange} />
146
+ </Tabs.Panel>
147
+ <Tabs.Panel value="foreignKeys" className="min-h-0 grow overflow-auto">
148
+ <ForeignKeysEditor
149
+ draft={draft}
150
+ dialect={dialect}
151
+ disabled={disabled}
152
+ schema={schema}
153
+ tables={tables}
154
+ referenceColumns={referenceColumns}
155
+ onChange={onChange}
156
+ />
157
+ </Tabs.Panel>
158
+ <Tabs.Panel value="options" className="min-h-0 grow overflow-auto">
159
+ <OptionsEditor draft={draft} dialect={dialect} disabled={disabled} onChange={onChange} />
160
+ </Tabs.Panel>
161
+ </Tabs.Root>
162
+ <SqlPreview plan={plan} className="shrink-0" />
163
+ <PromptDialog
164
+ open={confirming}
165
+ danger={plan.destructive}
166
+ title={t('designer.confirm.title', { count: plan.statements.length, formatted: formatNumber(plan.statements.length) })}
167
+ description={isNew ? t('designer.confirm.create', { table: draft.name }) : t('designer.confirm.alter', { table: draft.originalName })}
168
+ confirmLabel={isNew ? t('designer.create') : t('designer.apply')}
169
+ confirmBusyLabel={t('designer.applying')}
170
+ onConfirm={onConfirm}
171
+ onOpenChange={(open) => {
172
+ if (!open) {
173
+ onCancelConfirm();
174
+ }
175
+ }}
176
+ >
177
+ {plan.destructive && <p className="mt-3 text-xs text-status-error">{t('designer.confirm.destructive')}</p>}
178
+ <pre className="mt-3 max-h-64 overflow-auto rounded-md bg-surface-sunken p-2 font-mono text-code whitespace-pre text-text select-text">
179
+ {scriptOf(plan.statements)}
180
+ </pre>
181
+ </PromptDialog>
182
+ </div>
183
+ );
184
+ }
@@ -0,0 +1,132 @@
1
+ import clsx from 'clsx';
2
+ import { Plus, Trash2 } from 'lucide-react';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { Button, EmptyState, Field, Icon, IconButton, Input, Select, Spinner } from '@adecore/ui';
5
+ import { FOREIGN_KEY_ACTIONS } from '../ddl/index.ts';
6
+ import type { EditorProps } from './editor-props.ts';
7
+ import { NamePicker } from './NamePicker.tsx';
8
+ import { addForeignKey, patchForeignKey, removeForeignKey, setReferencedTable, toggleForeignKeyColumn } from './edit.ts';
9
+
10
+ export interface ForeignKeysEditorProps extends EditorProps {
11
+ schema: string;
12
+ /* The tables a key can point at. */
13
+ tables: readonly string[];
14
+ /* The columns of a table, or `null` while they load. */
15
+ referenceColumns(table: string): readonly string[] | null;
16
+ }
17
+
18
+ /* The engine's own behavior, which a key has without a clause. */
19
+ const DEFAULT_ACTION = 'default';
20
+
21
+ /* The foreign keys of the table. Each points at a table of the same schema, picked from the list, and at columns of that table. */
22
+ export function ForeignKeysEditor({ draft, schema, tables, referenceColumns, disabled, onChange, className, ref }: ForeignKeysEditorProps) {
23
+ const { t } = useTranslation('database');
24
+ const columns = draft.columns.map((column) => column.name);
25
+ const actions = [
26
+ { value: DEFAULT_ACTION, label: t('designer.foreignKeys.engineDefault') },
27
+ ...FOREIGN_KEY_ACTIONS.map((action) => ({ value: action, label: action }))
28
+ ];
29
+ return (
30
+ <div ref={ref} className={clsx('flex min-h-0 flex-col', className)}>
31
+ <div>
32
+ {draft.foreignKeys.length === 0 && <EmptyState>{t('designer.foreignKeys.empty')}</EmptyState>}
33
+ {draft.foreignKeys.map((foreignKey) => {
34
+ const referenced = foreignKey.referencedTable === '' ? [] : referenceColumns(foreignKey.referencedTable);
35
+ const choices =
36
+ tables.includes(foreignKey.referencedTable) || foreignKey.referencedTable === '' ? tables : [...tables, foreignKey.referencedTable];
37
+ return (
38
+ <div key={foreignKey.key} className="flex flex-col gap-3 border-b border-border-soft px-3 py-3">
39
+ <div className="flex items-end gap-3">
40
+ <Field label={t('designer.foreignKeys.name')} className="w-56">
41
+ <Input
42
+ size="sm"
43
+ spellCheck={false}
44
+ value={foreignKey.name}
45
+ disabled={disabled}
46
+ onChange={(event) => onChange(patchForeignKey(draft, foreignKey.key, { name: event.target.value }))}
47
+ />
48
+ </Field>
49
+ <Field label={t('designer.foreignKeys.referencedTable')} group className="w-56">
50
+ <Select
51
+ size="sm"
52
+ className="w-full"
53
+ label={t('designer.foreignKeys.referencedTable')}
54
+ placeholder={t('designer.foreignKeys.pickTable')}
55
+ value={foreignKey.referencedTable === '' ? null : foreignKey.referencedTable}
56
+ disabled={disabled}
57
+ items={choices.map((table) => ({ value: table, label: table }))}
58
+ onValueChange={(table) => onChange(setReferencedTable(draft, foreignKey.key, schema, table))}
59
+ />
60
+ </Field>
61
+ <Field label={t('designer.foreignKeys.onUpdate')} group className="w-40">
62
+ <Select
63
+ size="sm"
64
+ className="w-full"
65
+ label={t('designer.foreignKeys.onUpdate')}
66
+ value={foreignKey.onUpdate ?? DEFAULT_ACTION}
67
+ disabled={disabled}
68
+ items={actions}
69
+ onValueChange={(action) =>
70
+ onChange(patchForeignKey(draft, foreignKey.key, { onUpdate: action === DEFAULT_ACTION ? null : action }))
71
+ }
72
+ />
73
+ </Field>
74
+ <Field label={t('designer.foreignKeys.onDelete')} group className="w-40">
75
+ <Select
76
+ size="sm"
77
+ className="w-full"
78
+ label={t('designer.foreignKeys.onDelete')}
79
+ value={foreignKey.onDelete ?? DEFAULT_ACTION}
80
+ disabled={disabled}
81
+ items={actions}
82
+ onValueChange={(action) =>
83
+ onChange(patchForeignKey(draft, foreignKey.key, { onDelete: action === DEFAULT_ACTION ? null : action }))
84
+ }
85
+ />
86
+ </Field>
87
+ <IconButton
88
+ icon={Trash2}
89
+ size="xs"
90
+ className="mb-1"
91
+ label={t('designer.foreignKeys.remove')}
92
+ disabled={disabled}
93
+ onClick={() => onChange(removeForeignKey(draft, foreignKey.key))}
94
+ />
95
+ </div>
96
+ <div className="grid grid-cols-2 gap-3">
97
+ <Field label={t('designer.foreignKeys.columns')} group>
98
+ <NamePicker
99
+ label={t('designer.foreignKeys.columns')}
100
+ options={columns}
101
+ value={foreignKey.columns}
102
+ disabled={disabled}
103
+ onToggle={(name) => onChange(toggleForeignKeyColumn(draft, foreignKey.key, 'columns', name))}
104
+ />
105
+ </Field>
106
+ <Field label={t('designer.foreignKeys.referencedColumns')} group>
107
+ {referenced === null ? (
108
+ <Spinner size={14} label={t('designer.foreignKeys.loadingColumns')} />
109
+ ) : (
110
+ <NamePicker
111
+ label={t('designer.foreignKeys.referencedColumns')}
112
+ options={referenced}
113
+ value={foreignKey.referencedColumns}
114
+ disabled={disabled}
115
+ onToggle={(name) => onChange(toggleForeignKeyColumn(draft, foreignKey.key, 'referencedColumns', name))}
116
+ />
117
+ )}
118
+ </Field>
119
+ </div>
120
+ </div>
121
+ );
122
+ })}
123
+ </div>
124
+ <div className="shrink-0 px-3 py-2">
125
+ <Button variant="secondary" size="sm" disabled={disabled} onClick={() => onChange(addForeignKey(draft, schema))}>
126
+ <Icon icon={Plus} size={12} />
127
+ {t('designer.foreignKeys.add')}
128
+ </Button>
129
+ </div>
130
+ </div>
131
+ );
132
+ }
@@ -0,0 +1,63 @@
1
+ import clsx from 'clsx';
2
+ import { Plus, Trash2 } from 'lucide-react';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { Button, Checkbox, EmptyState, Icon, IconButton, Input } from '@adecore/ui';
5
+ import type { EditorProps } from './editor-props.ts';
6
+ import { NamePicker } from './NamePicker.tsx';
7
+ import { addIndex, patchIndex, removeIndex, toggleIndexColumn } from './edit.ts';
8
+
9
+ /* The indexes of the table other than its primary key, each over columns picked from the draft. */
10
+ export function IndexesEditor({ draft, disabled, onChange, className, ref }: EditorProps) {
11
+ const { t } = useTranslation('database');
12
+ const columns = draft.columns.map((column) => column.name);
13
+ return (
14
+ <div ref={ref} className={clsx('flex min-h-0 flex-col', className)}>
15
+ <div>
16
+ {draft.indexes.length === 0 && <EmptyState>{t('designer.indexes.empty')}</EmptyState>}
17
+ {draft.indexes.map((index) => (
18
+ <div key={index.key} className="flex flex-wrap items-center gap-x-3 gap-y-2 border-b border-border-soft px-3 py-2">
19
+ <Input
20
+ size="sm"
21
+ className="w-56"
22
+ aria-label={t('designer.indexes.name')}
23
+ spellCheck={false}
24
+ value={index.name}
25
+ disabled={disabled}
26
+ onChange={(event) => onChange(patchIndex(draft, index.key, { name: event.target.value }))}
27
+ />
28
+ <label className="flex items-center gap-2 text-xs text-text-muted">
29
+ <Checkbox
30
+ label={t('designer.indexes.unique')}
31
+ checked={index.unique}
32
+ disabled={disabled}
33
+ onCheckedChange={(checked) => onChange(patchIndex(draft, index.key, { unique: checked }))}
34
+ />
35
+ {t('designer.indexes.unique')}
36
+ </label>
37
+ <NamePicker
38
+ className="min-w-0 grow"
39
+ label={t('designer.indexes.columns')}
40
+ options={columns}
41
+ value={index.columns}
42
+ disabled={disabled}
43
+ onToggle={(name) => onChange(toggleIndexColumn(draft, index.key, name))}
44
+ />
45
+ <IconButton
46
+ icon={Trash2}
47
+ size="xs"
48
+ label={t('designer.indexes.remove')}
49
+ disabled={disabled}
50
+ onClick={() => onChange(removeIndex(draft, index.key))}
51
+ />
52
+ </div>
53
+ ))}
54
+ </div>
55
+ <div className="shrink-0 px-3 py-2">
56
+ <Button variant="secondary" size="sm" disabled={disabled} onClick={() => onChange(addIndex(draft))}>
57
+ <Icon icon={Plus} size={12} />
58
+ {t('designer.indexes.add')}
59
+ </Button>
60
+ </div>
61
+ </div>
62
+ );
63
+ }
@@ -0,0 +1,44 @@
1
+ import type { Ref } from 'react';
2
+ import clsx from 'clsx';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { Button } from '@adecore/ui';
5
+ import { formatNumber } from '@adecore/ui/format';
6
+
7
+ export interface NamePickerProps {
8
+ /* Every name that can be picked, in the order to show them. */
9
+ options: readonly string[];
10
+ /* The names picked, in the order they were picked: for a key that order is the order of its columns. */
11
+ value: readonly string[];
12
+ onToggle(name: string): void;
13
+ /* The accessible name of the group. */
14
+ label: string;
15
+ disabled?: boolean;
16
+ className?: string;
17
+ ref?: Ref<HTMLDivElement>;
18
+ }
19
+
20
+ /* Picks some of a few names, such as the columns of an index, and numbers the picks once there is more than one. */
21
+ export function NamePicker({ options, value, onToggle, label, disabled = false, className, ref }: NamePickerProps) {
22
+ const { t } = useTranslation('database');
23
+ return (
24
+ <div ref={ref} role="group" aria-label={label} className={clsx('flex flex-wrap items-center gap-1', className)}>
25
+ {options.length === 0 && <span className="text-xs text-text-faint">{t('designer.noColumnsToPick')}</span>}
26
+ {options.map((name) => {
27
+ const position = value.indexOf(name);
28
+ return (
29
+ <Button
30
+ key={name}
31
+ size="xs"
32
+ variant={position < 0 ? 'ghost' : 'secondary'}
33
+ aria-pressed={position >= 0}
34
+ disabled={disabled}
35
+ onClick={() => onToggle(name)}
36
+ >
37
+ {position >= 0 && value.length > 1 && <span className="text-text-faint">{formatNumber(position + 1)}</span>}
38
+ {name}
39
+ </Button>
40
+ );
41
+ })}
42
+ </div>
43
+ );
44
+ }
@@ -0,0 +1,101 @@
1
+ import { useId } from 'react';
2
+ import clsx from 'clsx';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { Field, Input, Switch, TextArea } from '@adecore/ui';
5
+ import { MYSQL_CHARSETS, MYSQL_ENGINES, collationSuggestionsOf } from '../ddl/index.ts';
6
+ import type { EditorProps } from './editor-props.ts';
7
+ import { patchOptions } from './edit.ts';
8
+
9
+ /* The options of a table, which differ per engine: a storage engine and character set on MySQL, rowid and strict typing on SQLite. */
10
+ export function OptionsEditor({ draft, dialect, disabled, onChange, className, ref }: EditorProps) {
11
+ const { t } = useTranslation('database');
12
+ const id = useId();
13
+ const { options } = draft;
14
+ if (dialect.engine === 'sqlite') {
15
+ return (
16
+ <div ref={ref} className={clsx('flex flex-col gap-4 p-3', className)}>
17
+ <label className="flex items-start gap-3">
18
+ <Switch
19
+ label={t('designer.options.withoutRowid')}
20
+ checked={options.withoutRowid}
21
+ disabled={disabled}
22
+ onCheckedChange={(checked) => onChange(patchOptions(draft, { withoutRowid: checked }))}
23
+ />
24
+ <span className="flex flex-col">
25
+ <span className="text-sm text-text">{t('designer.options.withoutRowid')}</span>
26
+ <span className="text-xs text-text-muted">{t('designer.options.withoutRowidHint')}</span>
27
+ </span>
28
+ </label>
29
+ <label className="flex items-start gap-3">
30
+ <Switch
31
+ label={t('designer.options.strict')}
32
+ checked={options.strict}
33
+ disabled={disabled}
34
+ onCheckedChange={(checked) => onChange(patchOptions(draft, { strict: checked }))}
35
+ />
36
+ <span className="flex flex-col">
37
+ <span className="text-sm text-text">{t('designer.options.strict')}</span>
38
+ <span className="text-xs text-text-muted">{t('designer.options.strictHint')}</span>
39
+ </span>
40
+ </label>
41
+ </div>
42
+ );
43
+ }
44
+ return (
45
+ <div ref={ref} className={clsx('flex max-w-xl flex-col gap-4 p-3', className)}>
46
+ <datalist id={`${id}-engines`}>
47
+ {MYSQL_ENGINES.map((engine) => (
48
+ <option key={engine} value={engine} />
49
+ ))}
50
+ </datalist>
51
+ <datalist id={`${id}-charsets`}>
52
+ {MYSQL_CHARSETS.map((charset) => (
53
+ <option key={charset} value={charset} />
54
+ ))}
55
+ </datalist>
56
+ <datalist id={`${id}-collations`}>
57
+ {collationSuggestionsOf(options.charset).map((collation) => (
58
+ <option key={collation} value={collation} />
59
+ ))}
60
+ </datalist>
61
+ <Field label={t('designer.options.engine')} hint={t('designer.options.serverDefault')}>
62
+ <Input
63
+ mono
64
+ list={`${id}-engines`}
65
+ spellCheck={false}
66
+ value={options.engine}
67
+ disabled={disabled}
68
+ onChange={(event) => onChange(patchOptions(draft, { engine: event.target.value }))}
69
+ />
70
+ </Field>
71
+ <Field label={t('designer.options.charset')} hint={t('designer.options.serverDefault')}>
72
+ <Input
73
+ mono
74
+ list={`${id}-charsets`}
75
+ spellCheck={false}
76
+ value={options.charset}
77
+ disabled={disabled}
78
+ onChange={(event) => onChange(patchOptions(draft, { charset: event.target.value }))}
79
+ />
80
+ </Field>
81
+ <Field label={t('designer.options.collation')} hint={t('designer.options.serverDefault')}>
82
+ <Input
83
+ mono
84
+ list={`${id}-collations`}
85
+ spellCheck={false}
86
+ value={options.collation}
87
+ disabled={disabled}
88
+ onChange={(event) => onChange(patchOptions(draft, { collation: event.target.value }))}
89
+ />
90
+ </Field>
91
+ <Field label={t('designer.options.comment')}>
92
+ <TextArea
93
+ rows={3}
94
+ value={options.comment}
95
+ disabled={disabled}
96
+ onChange={(event) => onChange(patchOptions(draft, { comment: event.target.value }))}
97
+ />
98
+ </Field>
99
+ </div>
100
+ );
101
+ }
@@ -0,0 +1,56 @@
1
+ import type { Ref } from 'react';
2
+ import clsx from 'clsx';
3
+ import { Copy } from 'lucide-react';
4
+ import { useTranslation } from 'react-i18next';
5
+ import { Button, Icon, copyText } from '@adecore/ui';
6
+ import type { DraftProblem } from '../ddl/index.ts';
7
+ import type { Plan } from './plan.ts';
8
+ import { scriptOf } from './plan.ts';
9
+
10
+ export interface SqlPreviewProps {
11
+ plan: Plan;
12
+ className?: string;
13
+ ref?: Ref<HTMLDivElement>;
14
+ }
15
+
16
+ /* The sentence a problem of the draft is read as. */
17
+ export function ProblemText({ problem }: { problem: DraftProblem }) {
18
+ const { t } = useTranslation('database');
19
+ return <>{t(`ddl.problem.${problem.code}`, { subject: problem.subject })}</>;
20
+ }
21
+
22
+ /* What Apply would run, as a person reads it: monospace, read only, and copyable. */
23
+ export function SqlPreview({ plan, className, ref }: SqlPreviewProps) {
24
+ const { t } = useTranslation('database');
25
+ const script = scriptOf(plan.statements);
26
+ return (
27
+ <section
28
+ ref={ref}
29
+ aria-label={t('designer.sql')}
30
+ className={clsx('relative flex min-h-0 flex-col border-t border-border-soft bg-surface-sunken', className)}
31
+ >
32
+ <div className="flex h-8 shrink-0 items-center justify-between pr-2 pl-3">
33
+ <span className="text-xs font-medium text-text-faint">{t('designer.sql')}</span>
34
+ <Button variant="ghost" size="xs" disabled={script === ''} onClick={() => copyText(script)}>
35
+ <Icon icon={Copy} size={12} />
36
+ {t('designer.copy')}
37
+ </Button>
38
+ </div>
39
+ <div className="max-h-48 min-h-12 overflow-auto px-3 pb-3 select-text">
40
+ {plan.problems.length > 0 ? (
41
+ <ul className="flex flex-col gap-1 text-xs text-status-error">
42
+ {plan.problems.map((problem, i) => (
43
+ <li key={`${problem.code}:${problem.subject}:${i}`}>
44
+ <ProblemText problem={problem} />
45
+ </li>
46
+ ))}
47
+ </ul>
48
+ ) : script === '' ? (
49
+ <p className="text-xs text-text-faint">{t('designer.noChanges')}</p>
50
+ ) : (
51
+ <pre className="font-mono text-code whitespace-pre text-text">{script}</pre>
52
+ )}
53
+ </div>
54
+ </section>
55
+ );
56
+ }