@solidxai/core-ui 0.1.12-beta.7 → 0.1.12-beta.9

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 (233) hide show
  1. package/dist/components/auth/AuthLayout.d.ts.map +1 -1
  2. package/dist/components/auth/AuthLayout.js +19 -17
  3. package/dist/components/auth/AuthLayout.js.map +1 -1
  4. package/dist/components/auth/AuthLayout.tsx +59 -34
  5. package/dist/components/common/BackendReconnectIndicator.d.ts +6 -0
  6. package/dist/components/common/BackendReconnectIndicator.d.ts.map +1 -0
  7. package/dist/components/common/BackendReconnectIndicator.js +25 -0
  8. package/dist/components/common/BackendReconnectIndicator.js.map +1 -0
  9. package/dist/components/common/BackendReconnectIndicator.tsx +45 -0
  10. package/dist/components/common/SettingsComponent.d.ts.map +1 -1
  11. package/dist/components/common/SettingsComponent.js +7 -3
  12. package/dist/components/common/SettingsComponent.js.map +1 -1
  13. package/dist/components/common/SettingsComponent.module.css +144 -12
  14. package/dist/components/common/SettingsComponent.tsx +22 -3
  15. package/dist/components/common/SolidAdmin.d.ts.map +1 -1
  16. package/dist/components/common/SolidAdmin.js +7 -2
  17. package/dist/components/common/SolidAdmin.js.map +1 -1
  18. package/dist/components/common/SolidAdmin.tsx +7 -2
  19. package/dist/components/common/SolidBreadcrumb.d.ts.map +1 -1
  20. package/dist/components/common/SolidBreadcrumb.js +1 -6
  21. package/dist/components/common/SolidBreadcrumb.js.map +1 -1
  22. package/dist/components/common/SolidBreadcrumb.tsx +1 -8
  23. package/dist/components/common/SolidMaterialSymbol.d.ts +10 -0
  24. package/dist/components/common/SolidMaterialSymbol.d.ts.map +1 -0
  25. package/dist/components/common/SolidMaterialSymbol.js +69 -0
  26. package/dist/components/common/SolidMaterialSymbol.js.map +1 -0
  27. package/dist/components/common/SolidMaterialSymbol.tsx +96 -0
  28. package/dist/components/common/SolidToastProvider.d.ts.map +1 -1
  29. package/dist/components/common/SolidToastProvider.js +14 -1
  30. package/dist/components/common/SolidToastProvider.js.map +1 -1
  31. package/dist/components/common/SolidToastProvider.tsx +19 -1
  32. package/dist/components/core/common/SolidAccountSettings/SolidAccountSettings.module.css +3 -1
  33. package/dist/components/core/common/SolidGlobalSearchElement.d.ts.map +1 -1
  34. package/dist/components/core/common/SolidGlobalSearchElement.js +3 -2
  35. package/dist/components/core/common/SolidGlobalSearchElement.js.map +1 -1
  36. package/dist/components/core/common/SolidGlobalSearchElement.tsx +14 -18
  37. package/dist/components/core/extension/solid-core/modelMetadata/list/DeleteModelRowAction.d.ts.map +1 -1
  38. package/dist/components/core/extension/solid-core/modelMetadata/list/DeleteModelRowAction.js +43 -16
  39. package/dist/components/core/extension/solid-core/modelMetadata/list/DeleteModelRowAction.js.map +1 -1
  40. package/dist/components/core/extension/solid-core/modelMetadata/list/DeleteModelRowAction.tsx +24 -2
  41. package/dist/components/core/form/SolidFormFooter.js +1 -1
  42. package/dist/components/core/form/SolidFormFooter.js.map +1 -1
  43. package/dist/components/core/form/SolidFormFooter.tsx +1 -1
  44. package/dist/components/core/form/SolidFormLayouts.js +1 -1
  45. package/dist/components/core/form/SolidFormLayouts.js.map +1 -1
  46. package/dist/components/core/form/SolidFormLayouts.tsx +2 -2
  47. package/dist/components/core/form/fields/SolidMediaMultipleField.d.ts.map +1 -1
  48. package/dist/components/core/form/fields/SolidMediaMultipleField.js +120 -56
  49. package/dist/components/core/form/fields/SolidMediaMultipleField.js.map +1 -1
  50. package/dist/components/core/form/fields/SolidMediaMultipleField.tsx +63 -50
  51. package/dist/components/core/form/fields/SolidMediaSingleField.d.ts.map +1 -1
  52. package/dist/components/core/form/fields/SolidMediaSingleField.js +115 -37
  53. package/dist/components/core/form/fields/SolidMediaSingleField.js.map +1 -1
  54. package/dist/components/core/form/fields/SolidMediaSingleField.tsx +36 -35
  55. package/dist/components/core/form/fields/SolidRichTextField.d.ts.map +1 -1
  56. package/dist/components/core/form/fields/SolidRichTextField.js +54 -5
  57. package/dist/components/core/form/fields/SolidRichTextField.js.map +1 -1
  58. package/dist/components/core/form/fields/SolidRichTextField.tsx +71 -7
  59. package/dist/components/core/form/fields/mediaFieldUtils.d.ts +3 -0
  60. package/dist/components/core/form/fields/mediaFieldUtils.d.ts.map +1 -0
  61. package/dist/components/core/form/fields/mediaFieldUtils.js +30 -0
  62. package/dist/components/core/form/fields/mediaFieldUtils.js.map +1 -0
  63. package/dist/components/core/form/fields/mediaFieldUtils.ts +45 -0
  64. package/dist/components/core/form/fields/relations/RolesGroupedByModuleWidget.d.ts.map +1 -1
  65. package/dist/components/core/form/fields/relations/RolesGroupedByModuleWidget.js +1 -1
  66. package/dist/components/core/form/fields/relations/RolesGroupedByModuleWidget.js.map +1 -1
  67. package/dist/components/core/form/fields/relations/RolesGroupedByModuleWidget.tsx +3 -1
  68. package/dist/components/core/form/fields/relations/SolidRelationManyToOneField.d.ts.map +1 -1
  69. package/dist/components/core/form/fields/relations/SolidRelationManyToOneField.js +22 -23
  70. package/dist/components/core/form/fields/relations/SolidRelationManyToOneField.js.map +1 -1
  71. package/dist/components/core/form/fields/relations/SolidRelationManyToOneField.tsx +13 -7
  72. package/dist/components/core/form/fields/widgets/SolidIconEditWidget.d.ts.map +1 -1
  73. package/dist/components/core/form/fields/widgets/SolidIconEditWidget.js +16 -11
  74. package/dist/components/core/form/fields/widgets/SolidIconEditWidget.js.map +1 -1
  75. package/dist/components/core/form/fields/widgets/SolidIconEditWidget.tsx +32 -18
  76. package/dist/components/core/form/fields/widgets/SolidIconViewWidget.d.ts.map +1 -1
  77. package/dist/components/core/form/fields/widgets/SolidIconViewWidget.js +3 -1
  78. package/dist/components/core/form/fields/widgets/SolidIconViewWidget.js.map +1 -1
  79. package/dist/components/core/form/fields/widgets/SolidIconViewWidget.tsx +11 -5
  80. package/dist/components/core/list/SolidDataTable.d.ts.map +1 -1
  81. package/dist/components/core/list/SolidDataTable.js +3 -2
  82. package/dist/components/core/list/SolidDataTable.js.map +1 -1
  83. package/dist/components/core/list/SolidDataTable.tsx +3 -2
  84. package/dist/components/core/list/SolidListView.d.ts.map +1 -1
  85. package/dist/components/core/list/SolidListView.js +11 -5
  86. package/dist/components/core/list/SolidListView.js.map +1 -1
  87. package/dist/components/core/list/SolidListView.tsx +15 -8
  88. package/dist/components/core/list/SolidListViewColumn.d.ts.map +1 -1
  89. package/dist/components/core/list/SolidListViewColumn.js +1 -0
  90. package/dist/components/core/list/SolidListViewColumn.js.map +1 -1
  91. package/dist/components/core/list/SolidListViewColumn.tsx +1 -0
  92. package/dist/components/core/list/columns/SolidShortTextColumn.d.ts.map +1 -1
  93. package/dist/components/core/list/columns/SolidShortTextColumn.js +7 -2
  94. package/dist/components/core/list/columns/SolidShortTextColumn.js.map +1 -1
  95. package/dist/components/core/list/columns/SolidShortTextColumn.tsx +7 -2
  96. package/dist/components/core/list/columns/relations/SolidRelationManyToManyColumn.d.ts.map +1 -1
  97. package/dist/components/core/list/columns/relations/SolidRelationManyToManyColumn.js +4 -1
  98. package/dist/components/core/list/columns/relations/SolidRelationManyToManyColumn.js.map +1 -1
  99. package/dist/components/core/list/columns/relations/SolidRelationManyToManyColumn.tsx +5 -2
  100. package/dist/components/core/list/columns/relations/SolidRelationManyToOneColumn.d.ts.map +1 -1
  101. package/dist/components/core/list/columns/relations/SolidRelationManyToOneColumn.js +2 -1
  102. package/dist/components/core/list/columns/relations/SolidRelationManyToOneColumn.js.map +1 -1
  103. package/dist/components/core/list/columns/relations/SolidRelationManyToOneColumn.tsx +2 -1
  104. package/dist/components/core/list/columns/relations/SolidRelationOneToManyColumn.d.ts.map +1 -1
  105. package/dist/components/core/list/columns/relations/SolidRelationOneToManyColumn.js +4 -1
  106. package/dist/components/core/list/columns/relations/SolidRelationOneToManyColumn.js.map +1 -1
  107. package/dist/components/core/list/columns/relations/SolidRelationOneToManyColumn.tsx +5 -2
  108. package/dist/components/core/locales/SolidLocale.d.ts.map +1 -1
  109. package/dist/components/core/locales/SolidLocale.js +1 -1
  110. package/dist/components/core/locales/SolidLocale.js.map +1 -1
  111. package/dist/components/core/locales/SolidLocale.tsx +1 -0
  112. package/dist/components/core/model/CreateModel.css +26 -0
  113. package/dist/components/core/model/FieldMetaData.d.ts.map +1 -1
  114. package/dist/components/core/model/FieldMetaData.js +21 -3
  115. package/dist/components/core/model/FieldMetaData.js.map +1 -1
  116. package/dist/components/core/model/FieldMetaData.tsx +45 -4
  117. package/dist/components/core/model/FieldMetaDataForm.css +82 -0
  118. package/dist/components/core/model/FieldMetaDataForm.d.ts +1 -0
  119. package/dist/components/core/model/FieldMetaDataForm.d.ts.map +1 -1
  120. package/dist/components/core/model/FieldMetaDataForm.js +246 -235
  121. package/dist/components/core/model/FieldMetaDataForm.js.map +1 -1
  122. package/dist/components/core/model/FieldMetaDataForm.tsx +46 -34
  123. package/dist/components/core/model/FieldSelector.js +1 -1
  124. package/dist/components/core/model/FieldSelector.js.map +1 -1
  125. package/dist/components/core/model/FieldSelector.tsx +1 -1
  126. package/dist/components/core/model/ModelMetaData.js +1 -1
  127. package/dist/components/core/model/ModelMetaData.js.map +1 -1
  128. package/dist/components/core/model/ModelMetaData.tsx +1 -1
  129. package/dist/components/core/module/CreateModule.css +55 -2
  130. package/dist/components/core/module/CreateModule.d.ts.map +1 -1
  131. package/dist/components/core/module/CreateModule.js +2 -2
  132. package/dist/components/core/module/CreateModule.js.map +1 -1
  133. package/dist/components/core/module/CreateModule.tsx +3 -2
  134. package/dist/components/core/module/ModuleMetadataExplorer.css +27 -0
  135. package/dist/components/core/module/ModuleMetadataExplorer.d.ts.map +1 -1
  136. package/dist/components/core/module/ModuleMetadataExplorer.js +1 -1
  137. package/dist/components/core/module/ModuleMetadataExplorer.js.map +1 -1
  138. package/dist/components/core/module/ModuleMetadataExplorer.tsx +1 -0
  139. package/dist/components/core/tree/SolidTreeView.d.ts.map +1 -1
  140. package/dist/components/core/tree/SolidTreeView.js +6 -1
  141. package/dist/components/core/tree/SolidTreeView.js.map +1 -1
  142. package/dist/components/core/tree/SolidTreeView.tsx +7 -1
  143. package/dist/components/layout/AdminHeaderActions.d.ts.map +1 -1
  144. package/dist/components/layout/AdminHeaderActions.js +10 -9
  145. package/dist/components/layout/AdminHeaderActions.js.map +1 -1
  146. package/dist/components/layout/AdminHeaderActions.tsx +2 -0
  147. package/dist/components/layout/AppSidebar.d.ts.map +1 -1
  148. package/dist/components/layout/AppSidebar.js +2 -1
  149. package/dist/components/layout/AppSidebar.js.map +1 -1
  150. package/dist/components/layout/AppSidebar.tsx +12 -5
  151. package/dist/components/layout/SolidMenuItemIcon.d.ts +9 -0
  152. package/dist/components/layout/SolidMenuItemIcon.d.ts.map +1 -0
  153. package/dist/components/layout/SolidMenuItemIcon.js +20 -0
  154. package/dist/components/layout/SolidMenuItemIcon.js.map +1 -0
  155. package/dist/components/layout/SolidMenuItemIcon.tsx +45 -0
  156. package/dist/components/layout/navbar-two-menu.d.ts.map +1 -1
  157. package/dist/components/layout/navbar-two-menu.js +6 -7
  158. package/dist/components/layout/navbar-two-menu.js.map +1 -1
  159. package/dist/components/layout/navbar-two-menu.tsx +4 -12
  160. package/dist/components/shad-cn-ui/SolidCodeEditor.d.ts.map +1 -1
  161. package/dist/components/shad-cn-ui/SolidCodeEditor.js +30 -2
  162. package/dist/components/shad-cn-ui/SolidCodeEditor.js.map +1 -1
  163. package/dist/components/shad-cn-ui/SolidCodeEditor.tsx +38 -1
  164. package/dist/helpers/backendHealthMonitor.d.ts +39 -0
  165. package/dist/helpers/backendHealthMonitor.d.ts.map +1 -0
  166. package/dist/helpers/backendHealthMonitor.js +255 -0
  167. package/dist/helpers/backendHealthMonitor.js.map +1 -0
  168. package/dist/helpers/backendHealthMonitor.ts +177 -0
  169. package/dist/helpers/helpers.d.ts +1 -0
  170. package/dist/helpers/helpers.d.ts.map +1 -1
  171. package/dist/helpers/helpers.js +9 -0
  172. package/dist/helpers/helpers.js.map +1 -1
  173. package/dist/helpers/helpers.ts +11 -1
  174. package/dist/helpers/menuItemIcons.d.ts +34 -0
  175. package/dist/helpers/menuItemIcons.d.ts.map +1 -0
  176. package/dist/helpers/menuItemIcons.js +144 -0
  177. package/dist/helpers/menuItemIcons.js.map +1 -0
  178. package/dist/helpers/menuItemIcons.ts +191 -0
  179. package/dist/helpers/relationDisplay.d.ts +3 -0
  180. package/dist/helpers/relationDisplay.d.ts.map +1 -0
  181. package/dist/helpers/relationDisplay.js +92 -0
  182. package/dist/helpers/relationDisplay.js.map +1 -0
  183. package/dist/helpers/relationDisplay.ts +101 -0
  184. package/dist/helpers/settingsPayload.d.ts +1 -0
  185. package/dist/helpers/settingsPayload.d.ts.map +1 -1
  186. package/dist/helpers/settingsPayload.js.map +1 -1
  187. package/dist/helpers/settingsPayload.ts +1 -0
  188. package/dist/helpers/solidIcons.d.ts +2 -0
  189. package/dist/helpers/solidIcons.d.ts.map +1 -1
  190. package/dist/helpers/solidIcons.js +8 -0
  191. package/dist/helpers/solidIcons.js.map +1 -1
  192. package/dist/helpers/solidIcons.tsx +10 -1
  193. package/dist/helpers/waitForBackendAvailability.d.ts +1 -0
  194. package/dist/helpers/waitForBackendAvailability.d.ts.map +1 -1
  195. package/dist/helpers/waitForBackendAvailability.js +35 -22
  196. package/dist/helpers/waitForBackendAvailability.js.map +1 -1
  197. package/dist/helpers/waitForBackendAvailability.ts +18 -12
  198. package/dist/http/solidHttp.d.ts.map +1 -1
  199. package/dist/http/solidHttp.js +10 -4
  200. package/dist/http/solidHttp.js.map +1 -1
  201. package/dist/http/solidHttp.ts +9 -4
  202. package/dist/redux/api/datasourceIntrospectionApi.d.ts +4 -0
  203. package/dist/redux/api/datasourceIntrospectionApi.d.ts.map +1 -1
  204. package/dist/redux/api/datasourceIntrospectionApi.js.map +1 -1
  205. package/dist/redux/api/datasourceIntrospectionApi.ts +4 -0
  206. package/dist/redux/api/fetchBaseQuery.d.ts.map +1 -1
  207. package/dist/redux/api/fetchBaseQuery.js +9 -3
  208. package/dist/redux/api/fetchBaseQuery.js.map +1 -1
  209. package/dist/redux/api/fetchBaseQuery.tsx +9 -3
  210. package/dist/redux/store/StoreProvider.d.ts.map +1 -1
  211. package/dist/redux/store/StoreProvider.js +4 -3
  212. package/dist/redux/store/StoreProvider.js.map +1 -1
  213. package/dist/redux/store/StoreProvider.tsx +4 -3
  214. package/dist/resources/globals.css +341 -49
  215. package/dist/resources/solid-custom.css +410 -12
  216. package/dist/resources/solid-responsive.css +27 -16
  217. package/dist/resources/solid-tailwind-generated.css +1 -1
  218. package/dist/routes/AppEventListener.d.ts.map +1 -1
  219. package/dist/routes/AppEventListener.js +23 -0
  220. package/dist/routes/AppEventListener.js.map +1 -1
  221. package/dist/routes/AppEventListener.tsx +29 -0
  222. package/dist/routes/pages/admin/core/DashboardPage.module.css +12 -0
  223. package/dist/routes/pages/admin/core/DatasourceIntrospectionPage.css +306 -3
  224. package/dist/routes/pages/admin/core/DatasourceIntrospectionPage.d.ts.map +1 -1
  225. package/dist/routes/pages/admin/core/DatasourceIntrospectionPage.js +745 -393
  226. package/dist/routes/pages/admin/core/DatasourceIntrospectionPage.js.map +1 -1
  227. package/dist/routes/pages/admin/core/DatasourceIntrospectionPage.tsx +1074 -420
  228. package/dist/routes/pages/admin/core/DatasourcesPage.css +15 -0
  229. package/dist/routes/pages/admin/core/DatasourcesPage.d.ts.map +1 -1
  230. package/dist/routes/pages/admin/core/DatasourcesPage.js +7 -188
  231. package/dist/routes/pages/admin/core/DatasourcesPage.js.map +1 -1
  232. package/dist/routes/pages/admin/core/DatasourcesPage.tsx +3 -393
  233. package/package.json +3 -2
@@ -2,6 +2,7 @@ import {
2
2
  ArrowLeft,
3
3
  Check,
4
4
  Cog,
5
+ Copy,
5
6
  Database,
6
7
  ExternalLink,
7
8
  RefreshCw,
@@ -44,16 +45,16 @@ import {
44
45
  type DatasourceIntrospectionTableRecord,
45
46
  type DatasourceIntrospectionApplyResponse,
46
47
  type DatasourceIntrospectionCreateMigrationResponse,
48
+ datasourceIntrospectionApi,
47
49
  useApplyDatasourceIntrospectionMappingMutation,
48
50
  useCreateDatasourceIntrospectionMigrationArtifactsMutation,
49
51
  useGenerateDatasourceIntrospectionCodeMutation,
50
52
  useGetDatasourceIntrospectionBootstrapQuery,
51
- useGetDatasourceIntrospectionTableDetailQuery,
52
53
  useGetDatasourceIntrospectionTablesQuery,
53
54
  usePreviewDatasourceIntrospectionMappingMutation,
54
55
  useRunDatasourceIntrospectionMigrationMutation,
55
56
  } from "../../../../redux/api/datasourceIntrospectionApi";
56
- import { useGetmodelByIdQuery } from "../../../../redux/api/modelApi";
57
+ import { modelsApi } from "../../../../redux/api/modelApi";
57
58
  import "./DatasourceIntrospectionPage.css";
58
59
 
59
60
  type SessionSettings = {
@@ -61,6 +62,13 @@ type SessionSettings = {
61
62
  prioritizeUnmappedTables: boolean;
62
63
  };
63
64
 
65
+ type BatchWorkspaceItemState = "idle" | "loading" | "ready" | "success" | "error";
66
+
67
+ type BatchWorkspaceItemStatus = {
68
+ state: BatchWorkspaceItemState;
69
+ message: string;
70
+ };
71
+
64
72
  type WorkspaceStep =
65
73
  | "configure"
66
74
  | "review-json"
@@ -111,10 +119,34 @@ type DrawerDraft = {
111
119
  enableSoftDelete: boolean;
112
120
  draftPublishWorkflow: boolean;
113
121
  internationalisation: boolean;
122
+ hasPrimaryKey: boolean;
123
+ primaryKeyColumnCount: number;
124
+ primaryKeyColumnNames: string[];
125
+ mappingBlockedReason: string | null;
114
126
  columns: DrawerColumnDraft[];
115
127
  preservedFields: any[];
116
128
  };
117
129
 
130
+ type WorkspaceBatchItem = {
131
+ key: string;
132
+ record: DatasourceIntrospectionTableRecord;
133
+ detail: DatasourceIntrospectionTableDetail | null;
134
+ mappedModelData: any | null;
135
+ draft: DrawerDraft | null;
136
+ loadStatus: BatchWorkspaceItemStatus;
137
+ reviewStatus: BatchWorkspaceItemStatus;
138
+ saveStatus: BatchWorkspaceItemStatus;
139
+ migrationStatus: BatchWorkspaceItemStatus;
140
+ mappingPreview: DatasourceIntrospectionMappingPreview | null;
141
+ persistedMappingResult: DatasourceIntrospectionApplyResponse | null;
142
+ migrationArtifactsResult: DatasourceIntrospectionCreateMigrationResponse | null;
143
+ reviewedMetadataJsonValue: any | null;
144
+ metadataJsonText: string;
145
+ metadataJsonValidationError: string | null;
146
+ metadataJsonResetToken: string;
147
+ error: string | null;
148
+ };
149
+
118
150
  function providerLabel(type: string) {
119
151
  if (type === "postgres") return "PostgreSQL";
120
152
  if (type === "mysql") return "MySQL";
@@ -139,7 +171,9 @@ const scalarFieldTypeOptions = [
139
171
  { value: "password", label: "Password" },
140
172
  ];
141
173
 
142
- const scalarFieldTypeValueSet = new Set(scalarFieldTypeOptions.map((option) => option.value));
174
+ function getFieldTypeLabel(type: string) {
175
+ return scalarFieldTypeOptions.find((option) => option.value === type)?.label ?? startCase(type);
176
+ }
143
177
  const mappingStrategyOptions = [
144
178
  { value: "generated_id", label: "Legacy generated id" },
145
179
  { value: "existing_id", label: "Legacy existing id" },
@@ -242,6 +276,10 @@ function getDefaultOrmTypeForSolidFieldType(dataSourceType: string, solidFieldTy
242
276
  }
243
277
 
244
278
  function statusLabel(record: DatasourceIntrospectionTableRecord) {
279
+ if (!record.hasPrimaryKey) {
280
+ return "Blocked";
281
+ }
282
+
245
283
  return record.mapped ? "Mapped" : "Ready to map";
246
284
  }
247
285
 
@@ -351,7 +389,13 @@ function buildDrawerDraft(
351
389
  };
352
390
  });
353
391
 
354
- const draftUserKeyField = modelUserKeyField || tableDetail.suggestedMetadata.userKeyField;
392
+ const requestedUserKeyField = modelUserKeyField || tableDetail.suggestedMetadata.userKeyField;
393
+ const resolvedUserKeyField = (
394
+ columns.find((column) => column.fieldName === requestedUserKeyField)
395
+ || columns.find((column) => column.columnName === requestedUserKeyField)
396
+ || columns.find((column) => !column.handledBySuperclass && column.isPrimaryKey)
397
+ || columns.find((column) => !column.handledBySuperclass && column.include)
398
+ )?.fieldName ?? requestedUserKeyField;
355
399
 
356
400
  return {
357
401
  mapped: selectedTable.mapped,
@@ -367,11 +411,15 @@ function buildDrawerDraft(
367
411
  dataSourceType: modelData?.dataSourceType ?? tableDetail.suggestedMetadata.dataSourceType,
368
412
  legacyTableType: modelData?.legacyTableType ?? tableDetail.suggestedMetadata.legacyTableType,
369
413
  baseClassName: tableDetail.suggestedMetadata.baseClassName,
370
- userKeyField: draftUserKeyField,
414
+ userKeyField: resolvedUserKeyField,
371
415
  enableAuditTracking: modelData?.enableAuditTracking ?? true,
372
416
  enableSoftDelete: modelData?.enableSoftDelete ?? false,
373
417
  draftPublishWorkflow: modelData?.draftPublishWorkflow ?? false,
374
418
  internationalisation: modelData?.internationalisation ?? false,
419
+ hasPrimaryKey: selectedTable.hasPrimaryKey,
420
+ primaryKeyColumnCount: selectedTable.primaryKeyColumnCount,
421
+ primaryKeyColumnNames: selectedTable.primaryKeyColumnNames,
422
+ mappingBlockedReason: selectedTable.mappingBlockedReason,
375
423
  columns,
376
424
  preservedFields,
377
425
  };
@@ -395,6 +443,13 @@ function getDrawerValidationErrors(draft: DrawerDraft) {
395
443
  errors.push("Select at least one physical column to include in the mapping.");
396
444
  }
397
445
 
446
+ if (!draft.hasPrimaryKey) {
447
+ errors.push(
448
+ draft.mappingBlockedReason
449
+ || `Table "${draft.tableName}" cannot be mapped because the datasource table does not define any primary key.`,
450
+ );
451
+ }
452
+
398
453
  for (const column of includedColumns) {
399
454
  const normalizedFieldName = column.fieldName.trim();
400
455
  if (!normalizedFieldName) {
@@ -407,8 +462,8 @@ function getDrawerValidationErrors(draft: DrawerDraft) {
407
462
  continue;
408
463
  }
409
464
 
410
- if (!scalarFieldTypeValueSet.has(column.solidFieldType)) {
411
- errors.push(`SolidX type "${column.solidFieldType}" is not allowed in step 1 for column "${column.columnName}".`);
465
+ if (!column.solidFieldType.trim()) {
466
+ errors.push(`SolidX type is required for column "${column.columnName}".`);
412
467
  continue;
413
468
  }
414
469
 
@@ -423,17 +478,26 @@ function getDrawerValidationErrors(draft: DrawerDraft) {
423
478
  if (!draft.userKeyField.trim()) {
424
479
  errors.push("Choose a user key field before saving the mapping.");
425
480
  } else {
426
- const userKeyExists = includedColumns.some((column) => column.fieldName === draft.userKeyField);
481
+ const userKeyExists = draft.columns.some((column) => (
482
+ column.fieldName === draft.userKeyField
483
+ && (column.handledBySuperclass || column.include)
484
+ ));
427
485
  if (!userKeyExists) {
428
486
  errors.push("The chosen user key field must remain part of the mapped columns.");
429
487
  }
430
488
  }
431
489
 
432
- if (draft.legacyTableType === "existing_id") {
433
- const hasPrimaryKey = includedColumns.some((column) => column.isPrimaryKey);
434
- if (!hasPrimaryKey) {
435
- errors.push("Legacy existing-id mappings must keep the generated id column mapped to field name \"id\".");
436
- }
490
+ const includedPrimaryKeyColumns = includedColumns.filter((column) => column.isPrimaryKey);
491
+ if (draft.hasPrimaryKey && !includedPrimaryKeyColumns.length) {
492
+ errors.push("Keep the datasource primary key columns included in the mapping.");
493
+ }
494
+
495
+ const missingDetectedPrimaryKeyColumns = draft.columns
496
+ .filter((column) => column.isDetectedPrimaryKey && !column.handledBySuperclass)
497
+ .filter((column) => !column.include || !column.isPrimaryKey)
498
+ .map((column) => column.columnName);
499
+ if (missingDetectedPrimaryKeyColumns.length) {
500
+ errors.push(`Keep all datasource primary key columns mapped and marked as primary key: ${missingDetectedPrimaryKeyColumns.join(", ")}.`);
437
501
  }
438
502
 
439
503
  return errors;
@@ -616,23 +680,90 @@ function parseAndValidateReviewedModelJson(
616
680
  seenFieldNames.add(normalizedFieldName);
617
681
  }
618
682
 
619
- if (!parsed.fields.some((field: any) => field?.name === parsed.userKeyFieldUserKey && !field?.isMarkedForRemoval)) {
683
+ const expectedUserKeyFieldName = `${parsed.userKeyFieldUserKey ?? draft.userKeyField ?? ""}`.trim();
684
+ const resolvedUserKeyFieldName = resolveReviewedUserKeyFieldName(
685
+ parsed.fields,
686
+ expectedUserKeyFieldName,
687
+ draft.userKeyField,
688
+ );
689
+ const inheritedGeneratedIdUserKey =
690
+ draft.legacyTableType === "generated_id" && expectedUserKeyFieldName.toLowerCase() === "id";
691
+
692
+ if (!resolvedUserKeyFieldName && !inheritedGeneratedIdUserKey) {
620
693
  return {
621
694
  error: "Metadata JSON must keep userKeyFieldUserKey present in the fields array.",
622
695
  parsed: null,
623
696
  };
624
697
  }
625
698
 
699
+ if (draft.hasPrimaryKey) {
700
+ const activeFields = parsed.fields.filter((field: any) => !field?.isMarkedForRemoval);
701
+ const missingPrimaryKeyColumns = draft.columns
702
+ .filter((column) => column.isDetectedPrimaryKey && !column.handledBySuperclass)
703
+ .filter((column) => {
704
+ const matchingField = activeFields.find((field: any) =>
705
+ `${field?.columnName ?? ""}`.trim().toLowerCase() === column.columnName.trim().toLowerCase(),
706
+ );
707
+
708
+ return !matchingField || matchingField.isPrimaryKey !== true;
709
+ })
710
+ .map((column) => column.columnName);
711
+
712
+ if (missingPrimaryKeyColumns.length) {
713
+ return {
714
+ error: `Metadata JSON must keep all datasource primary key columns marked with isPrimaryKey: true: ${missingPrimaryKeyColumns.join(", ")}.`,
715
+ parsed: null,
716
+ };
717
+ }
718
+ }
719
+
720
+ parsed.userKeyFieldUserKey = resolvedUserKeyFieldName ?? expectedUserKeyFieldName;
721
+
626
722
  return {
627
723
  error: null,
628
724
  parsed,
629
725
  };
630
726
  }
631
727
 
728
+ function resolveReviewedUserKeyFieldName(
729
+ reviewedFields: Array<Record<string, any>>,
730
+ requestedUserKeyFieldName: string,
731
+ fallbackUserKeyFieldName?: string,
732
+ ) {
733
+ const activeFields = reviewedFields.filter((field) => !field?.isMarkedForRemoval);
734
+ const normalizedRequested = `${requestedUserKeyFieldName ?? ""}`.trim().toLowerCase();
735
+ const normalizedFallback = `${fallbackUserKeyFieldName ?? ""}`.trim().toLowerCase();
736
+
737
+ const byExactRequested = activeFields.find((field) => field?.name?.toLowerCase?.() === normalizedRequested);
738
+ if (byExactRequested?.name) {
739
+ return byExactRequested.name;
740
+ }
741
+
742
+ const byExistingUserKeyFlag = activeFields.find((field) => field?.isUserKey);
743
+ if (byExistingUserKeyFlag?.name) {
744
+ return byExistingUserKeyFlag.name;
745
+ }
746
+
747
+ const byPrimaryKeyFallback = activeFields.find((field) => {
748
+ if (!field?.isPrimaryKey) {
749
+ return false;
750
+ }
751
+
752
+ const normalizedName = `${field?.name ?? ""}`.trim().toLowerCase();
753
+ return normalizedName === normalizedFallback || normalizedName === "id" || normalizedName === "legacyid";
754
+ });
755
+ if (byPrimaryKeyFallback?.name) {
756
+ return byPrimaryKeyFallback.name;
757
+ }
758
+
759
+ const byFallbackFieldName = activeFields.find((field) => field?.name?.toLowerCase?.() === normalizedFallback);
760
+ return byFallbackFieldName?.name ?? null;
761
+ }
762
+
632
763
  export function DatasourceIntrospectionPage() {
633
764
  const params = useParams();
634
765
  const navigate = useNavigate();
635
- const dispatch = useDispatch();
766
+ const dispatch = useDispatch<any>();
636
767
  const moduleId = Number(params.moduleId ?? 0);
637
768
  const isValidModuleId = Number.isFinite(moduleId) && moduleId > 0;
638
769
 
@@ -648,36 +779,30 @@ export function DatasourceIntrospectionPage() {
648
779
 
649
780
  const [selectedDatasource, setSelectedDatasource] = useState("");
650
781
  const [selectedRows, setSelectedRows] = useState<DatasourceIntrospectionTableRecord[]>([]);
651
- const [workspaceTableKey, setWorkspaceTableKey] = useState("");
782
+ const [workspaceTableKeys, setWorkspaceTableKeys] = useState<string[]>([]);
783
+ const [activeWorkspaceTableKey, setActiveWorkspaceTableKey] = useState("");
784
+ const [workspaceItems, setWorkspaceItems] = useState<Record<string, WorkspaceBatchItem>>({});
785
+ const workspaceRailItemRefs = useRef<Record<string, HTMLButtonElement | null>>({});
652
786
  const [tableSearch, setTableSearch] = useState("");
653
787
  const [settingsOpen, setSettingsOpen] = useState(false);
654
788
  const [settingsTab, setSettingsTab] = useState("datasource-info");
655
- const [drawerDraft, setDrawerDraft] = useState<DrawerDraft | null>(null);
656
789
  const [drawerTab, setDrawerTab] = useState("general-info");
657
790
  const [workspaceStep, setWorkspaceStep] = useState<WorkspaceStep>("configure");
658
- const [mappingPreview, setMappingPreview] = useState<DatasourceIntrospectionMappingPreview | null>(null);
659
- const [persistedMappingResult, setPersistedMappingResult] = useState<DatasourceIntrospectionApplyResponse | null>(null);
660
- const [migrationArtifactsResult, setMigrationArtifactsResult] = useState<DatasourceIntrospectionCreateMigrationResponse | null>(null);
661
791
  const [generateCodeOutput, setGenerateCodeOutput] = useState("");
662
792
  const [isGeneratingCodeWorkflow, setIsGeneratingCodeWorkflow] = useState(false);
663
793
  const [generateCodeStatusMessage, setGenerateCodeStatusMessage] = useState("");
664
794
  const [generateCodeTimeoutMessage, setGenerateCodeTimeoutMessage] = useState<string | null>(null);
665
795
  const [generateCodeReady, setGenerateCodeReady] = useState(false);
666
796
  const [hasAttemptedGenerateCode, setHasAttemptedGenerateCode] = useState(false);
797
+ const [migrationPreviewCopied, setMigrationPreviewCopied] = useState(false);
667
798
  const [runMigrationOutput, setRunMigrationOutput] = useState("");
668
799
  const [runMigrationCompleted, setRunMigrationCompleted] = useState(false);
669
- const [reviewedMetadataJsonValue, setReviewedMetadataJsonValue] = useState<any>(null);
670
- const [metadataJsonText, setMetadataJsonText] = useState("");
671
- const [metadataJsonValidationError, setMetadataJsonValidationError] = useState<string | null>(null);
672
- const [metadataJsonResetToken, setMetadataJsonResetToken] = useState("initial");
673
- const [drawerBootstrapping, setDrawerBootstrapping] = useState(false);
674
800
  const [fieldConfigColumnName, setFieldConfigColumnName] = useState<string | null>(null);
675
801
  const [sessionSettings, setSessionSettings] = useState<SessionSettings>({
676
802
  relationInferenceEnabled: false,
677
803
  prioritizeUnmappedTables: true,
678
804
  });
679
805
  const [draftSessionSettings, setDraftSessionSettings] = useState<SessionSettings>(sessionSettings);
680
- const drawerBootstrapTimerRef = useRef<number | null>(null);
681
806
 
682
807
  const [
683
808
  previewMapping,
@@ -725,14 +850,12 @@ export function DatasourceIntrospectionPage() {
725
850
 
726
851
  useEffect(() => {
727
852
  setSelectedRows([]);
728
- setWorkspaceTableKey("");
729
- setDrawerDraft(null);
853
+ setWorkspaceTableKeys([]);
854
+ setActiveWorkspaceTableKey("");
855
+ setWorkspaceItems({});
730
856
  setFieldConfigColumnName(null);
731
857
  setTableSearch("");
732
858
  setWorkspaceStep("configure");
733
- setMappingPreview(null);
734
- setPersistedMappingResult(null);
735
- setMigrationArtifactsResult(null);
736
859
  setGenerateCodeOutput("");
737
860
  setIsGeneratingCodeWorkflow(false);
738
861
  setGenerateCodeStatusMessage("");
@@ -741,17 +864,35 @@ export function DatasourceIntrospectionPage() {
741
864
  setHasAttemptedGenerateCode(false);
742
865
  setRunMigrationOutput("");
743
866
  setRunMigrationCompleted(false);
744
- setReviewedMetadataJsonValue(null);
745
- setMetadataJsonText("");
746
- setMetadataJsonValidationError(null);
747
- setMetadataJsonResetToken(`datasource-${Date.now()}`);
748
867
  }, [selectedDatasource]);
749
868
 
750
- useEffect(() => () => {
751
- if (drawerBootstrapTimerRef.current) {
752
- window.clearTimeout(drawerBootstrapTimerRef.current);
753
- }
754
- }, []);
869
+ const activeWorkspaceProcessingKey = useMemo(
870
+ () =>
871
+ workspaceTableKeys.find((key) => {
872
+ const item = workspaceItems[key];
873
+ if (!item) return false;
874
+
875
+ return [
876
+ item.loadStatus.state,
877
+ item.reviewStatus.state,
878
+ item.saveStatus.state,
879
+ item.migrationStatus.state,
880
+ ].includes("loading");
881
+ }) ?? null,
882
+ [workspaceItems, workspaceTableKeys],
883
+ );
884
+
885
+ useEffect(() => {
886
+ const targetKey = activeWorkspaceProcessingKey ?? activeWorkspaceTableKey;
887
+ if (!targetKey) return;
888
+
889
+ const targetNode = workspaceRailItemRefs.current[targetKey];
890
+ targetNode?.scrollIntoView({
891
+ block: "nearest",
892
+ inline: "nearest",
893
+ behavior: "smooth",
894
+ });
895
+ }, [activeWorkspaceProcessingKey, activeWorkspaceTableKey]);
755
896
 
756
897
  const datasourceOptions = useMemo(
757
898
  () => (bootstrap?.datasources ?? []).map((datasource) => ({
@@ -773,45 +914,27 @@ export function DatasourceIntrospectionPage() {
773
914
  skip: !isValidModuleId || !selectedDatasource,
774
915
  },
775
916
  );
776
-
777
- const selectedListRow = selectedRows[0] ?? null;
778
- const selectedListRowKey = selectedListRow ? `${selectedListRow.schema ?? ""}::${selectedListRow.tableName}` : "";
779
- const isWorkspaceMode = Boolean(workspaceTableKey);
780
-
781
- const selectedTable = useMemo(
782
- () => tablesResponse?.records?.find((record) => `${record.schema ?? ""}::${record.tableName}` === workspaceTableKey) ?? null,
783
- [tablesResponse, workspaceTableKey],
917
+ const selectedDatasourceRecord = bootstrap?.datasources?.find((datasource) => datasource.name === selectedDatasource) ?? null;
918
+ const isSynchronizeBlocked = Boolean(selectedDatasource && tablesResponse?.synchronizeBlocked);
919
+ const makeTableKey = (schema: string | null | undefined, tableName: string) => `${schema ?? ""}::${tableName}`;
920
+ const selectedListRowKeySet = useMemo(
921
+ () => new Set(selectedRows.map((row) => makeTableKey(row.schema, row.tableName))),
922
+ [selectedRows],
784
923
  );
785
-
786
- const {
787
- data: tableDetail,
788
- isLoading: isTableDetailLoading,
789
- isFetching: isTableDetailFetching,
790
- error: tableDetailError,
791
- refetch: refetchTableDetail,
792
- } = useGetDatasourceIntrospectionTableDetailQuery(
793
- {
794
- moduleId,
795
- datasource: selectedDatasource,
796
- table: selectedTable?.tableName ?? "",
797
- schema: selectedTable?.schema ?? undefined,
798
- },
799
- {
800
- skip: !isWorkspaceMode || !isValidModuleId || !selectedDatasource || !selectedTable || !!tablesResponse?.synchronizeBlocked,
801
- },
924
+ const isWorkspaceMode = workspaceTableKeys.length > 0 && Boolean(activeWorkspaceTableKey);
925
+ const selectedTable = useMemo(
926
+ () => workspaceItems[activeWorkspaceTableKey]?.record ?? null,
927
+ [activeWorkspaceTableKey, workspaceItems],
802
928
  );
803
-
804
- const {
805
- data: mappedModelResponse,
806
- isLoading: isMappedModelLoading,
807
- isFetching: isMappedModelFetching,
808
- error: mappedModelError,
809
- } = useGetmodelByIdQuery(selectedTable?.mappedModelId ?? 0, {
810
- skip: !isWorkspaceMode || !selectedTable?.mapped || !selectedTable?.mappedModelId,
811
- });
812
-
813
- const mappedModelData = mappedModelResponse?.data;
814
- const selectedDatasourceRecord = bootstrap?.datasources?.find((datasource) => datasource.name === selectedDatasource) ?? null;
929
+ const activeWorkspaceItem = activeWorkspaceTableKey ? workspaceItems[activeWorkspaceTableKey] ?? null : null;
930
+ const drawerDraft = activeWorkspaceItem?.draft ?? null;
931
+ const mappingPreview = activeWorkspaceItem?.mappingPreview ?? null;
932
+ const persistedMappingResult = activeWorkspaceItem?.persistedMappingResult ?? null;
933
+ const migrationArtifactsResult = activeWorkspaceItem?.migrationArtifactsResult ?? null;
934
+ const reviewedMetadataJsonValue = activeWorkspaceItem?.reviewedMetadataJsonValue ?? null;
935
+ const metadataJsonText = activeWorkspaceItem?.metadataJsonText ?? "";
936
+ const metadataJsonValidationError = activeWorkspaceItem?.metadataJsonValidationError ?? null;
937
+ const metadataJsonResetToken = activeWorkspaceItem?.metadataJsonResetToken ?? "initial";
815
938
  const activeFieldConfigColumn = drawerDraft?.columns.find((column) => column.columnName === fieldConfigColumnName) ?? null;
816
939
  const activeFieldConfigMetaData = useMemo(() => {
817
940
  if (!drawerDraft || !activeFieldConfigColumn) {
@@ -864,76 +987,12 @@ export function DatasourceIntrospectionPage() {
864
987
  columnName: column.columnName,
865
988
  }));
866
989
  }, [activeFieldConfigColumn, drawerDraft]);
867
- const drawerLoadingError = selectedTable?.mapped ? (tableDetailError || mappedModelError) : tableDetailError;
868
- const isDrawerBlockingLoading = Boolean(
869
- selectedTable
870
- && (
871
- drawerBootstrapping
872
- || !drawerDraft
873
- || isTableDetailLoading
874
- || isTableDetailFetching
875
- || (selectedTable.mapped && (isMappedModelLoading || isMappedModelFetching))
876
- ),
877
- );
878
-
879
- useEffect(() => {
880
- if (!isWorkspaceMode) {
881
- setDrawerDraft(null);
882
- setFieldConfigColumnName(null);
883
- return;
884
- }
885
-
886
- if (drawerBootstrapTimerRef.current) {
887
- window.clearTimeout(drawerBootstrapTimerRef.current);
888
- }
889
-
890
- setDrawerDraft(null);
891
- setFieldConfigColumnName(null);
892
- setDrawerTab("general-info");
893
- setWorkspaceStep("configure");
894
- setMappingPreview(null);
895
- setPersistedMappingResult(null);
896
- setMigrationArtifactsResult(null);
897
- setGenerateCodeOutput("");
898
- setIsGeneratingCodeWorkflow(false);
899
- setGenerateCodeStatusMessage("");
900
- setGenerateCodeTimeoutMessage(null);
901
- setGenerateCodeReady(false);
902
- setHasAttemptedGenerateCode(false);
903
- setRunMigrationOutput("");
904
- setRunMigrationCompleted(false);
905
- setReviewedMetadataJsonValue(null);
906
- setMetadataJsonText("");
907
- setMetadataJsonValidationError(null);
908
- setMetadataJsonResetToken(`table-${workspaceTableKey}-${Date.now()}`);
909
- setDrawerBootstrapping(true);
910
- }, [isWorkspaceMode, workspaceTableKey]);
911
-
912
- useEffect(() => {
913
- if (!isWorkspaceMode || !selectedTable) return;
914
- setSelectedRows([selectedTable]);
915
- }, [isWorkspaceMode, selectedTable]);
916
-
917
- useEffect(() => {
918
- if (!isWorkspaceMode || !selectedTable || !tableDetail) return;
919
- if (selectedTable.mapped && (!mappedModelData || mappedModelData.id !== selectedTable.mappedModelId)) return;
920
-
921
- if (drawerBootstrapTimerRef.current) {
922
- window.clearTimeout(drawerBootstrapTimerRef.current);
923
- }
924
-
925
- setDrawerDraft(buildDrawerDraft(
926
- tableDetail,
927
- selectedTable,
928
- selectedTable.mapped ? mappedModelResponse : undefined,
929
- ));
930
- drawerBootstrapTimerRef.current = window.setTimeout(() => {
931
- setDrawerBootstrapping(false);
932
- }, 150);
933
- }, [isWorkspaceMode, selectedTable, tableDetail, mappedModelData, mappedModelResponse]);
990
+ const drawerLoadingError = activeWorkspaceItem?.error ?? null;
991
+ const isDrawerBlockingLoading = activeWorkspaceItem?.loadStatus.state === "loading" || !drawerDraft;
934
992
 
935
993
  const filteredRecords = useMemo(() => {
936
- const records = [...(tablesResponse?.records ?? [])];
994
+ const records = [...(tablesResponse?.records ?? [])]
995
+ .filter((record) => !record.tableName.toLowerCase().startsWith("ss_"));
937
996
  const searchNeedle = tableSearch.trim().toLowerCase();
938
997
 
939
998
  const filtered = searchNeedle
@@ -958,10 +1017,183 @@ export function DatasourceIntrospectionPage() {
958
1017
  return filtered;
959
1018
  }, [sessionSettings.prioritizeUnmappedTables, tableSearch, tablesResponse?.records]);
960
1019
 
1020
+ const workspaceRecords = useMemo(
1021
+ () => workspaceTableKeys
1022
+ .map((key) => workspaceItems[key]?.record)
1023
+ .filter(Boolean) as DatasourceIntrospectionTableRecord[],
1024
+ [workspaceItems, workspaceTableKeys],
1025
+ );
1026
+ const selectedRowCount = selectedRows.length;
1027
+ const visibleRowKeySet = useMemo(
1028
+ () => new Set(filteredRecords.map((row) => makeTableKey(row.schema, row.tableName))),
1029
+ [filteredRecords],
1030
+ );
1031
+ const visibleSelectedRowCount = useMemo(
1032
+ () => selectedRows.filter((row) => row.hasPrimaryKey && visibleRowKeySet.has(makeTableKey(row.schema, row.tableName))).length,
1033
+ [selectedRows, visibleRowKeySet],
1034
+ );
1035
+ const visibleSelectableRowCount = useMemo(
1036
+ () => filteredRecords.filter((row) => row.hasPrimaryKey).length,
1037
+ [filteredRecords],
1038
+ );
1039
+ const hasVisibleRows = filteredRecords.length > 0;
1040
+ const allVisibleRowsSelected = visibleSelectableRowCount > 0 && visibleSelectedRowCount === visibleSelectableRowCount;
1041
+ const totalWorkspaceCount = workspaceTableKeys.length;
1042
+ const activeWorkspacePosition = activeWorkspaceTableKey
1043
+ ? Math.max(0, workspaceTableKeys.indexOf(activeWorkspaceTableKey)) + 1
1044
+ : 0;
1045
+
1046
+ const allWorkspaceItemsReady = useMemo(
1047
+ () => workspaceTableKeys.length > 0 && workspaceTableKeys.every((key) => workspaceItems[key]?.draft),
1048
+ [workspaceItems, workspaceTableKeys],
1049
+ );
1050
+ const allWorkspaceItemsPreviewed = useMemo(
1051
+ () => workspaceTableKeys.length > 0 && workspaceTableKeys.every((key) => workspaceItems[key]?.mappingPreview),
1052
+ [workspaceItems, workspaceTableKeys],
1053
+ );
1054
+ const allWorkspaceItemsPersisted = useMemo(
1055
+ () => workspaceTableKeys.length > 0 && workspaceTableKeys.every((key) => workspaceItems[key]?.persistedMappingResult),
1056
+ [workspaceItems, workspaceTableKeys],
1057
+ );
1058
+ const allWorkspaceItemsMigrated = useMemo(
1059
+ () => workspaceTableKeys.length > 0 && workspaceTableKeys.every((key) => workspaceItems[key]?.migrationArtifactsResult),
1060
+ [workspaceItems, workspaceTableKeys],
1061
+ );
1062
+ const hasMigrationGenerationWork = useMemo(
1063
+ () => workspaceTableKeys.some((key) => Boolean(workspaceItems[key]?.mappingPreview?.migration.willGenerate)),
1064
+ [workspaceItems, workspaceTableKeys],
1065
+ );
1066
+ const reviewedWorkspaceCount = useMemo(
1067
+ () => workspaceTableKeys.filter((key) => workspaceItems[key]?.mappingPreview).length,
1068
+ [workspaceItems, workspaceTableKeys],
1069
+ );
1070
+ const savedWorkspaceCount = useMemo(
1071
+ () => workspaceTableKeys.filter((key) => workspaceItems[key]?.persistedMappingResult).length,
1072
+ [workspaceItems, workspaceTableKeys],
1073
+ );
1074
+ const migrationReadyWorkspaceCount = useMemo(
1075
+ () => workspaceTableKeys.filter((key) => workspaceItems[key]?.migrationArtifactsResult).length,
1076
+ [workspaceItems, workspaceTableKeys],
1077
+ );
1078
+ const reviewErrorWorkspaceCount = useMemo(
1079
+ () => workspaceTableKeys.filter((key) => workspaceItems[key]?.reviewStatus.state === "error").length,
1080
+ [workspaceItems, workspaceTableKeys],
1081
+ );
1082
+ const saveErrorWorkspaceCount = useMemo(
1083
+ () => workspaceTableKeys.filter((key) => workspaceItems[key]?.saveStatus.state === "error").length,
1084
+ [workspaceItems, workspaceTableKeys],
1085
+ );
1086
+ const migrationErrorWorkspaceCount = useMemo(
1087
+ () => workspaceTableKeys.filter((key) => workspaceItems[key]?.migrationStatus.state === "error").length,
1088
+ [workspaceItems, workspaceTableKeys],
1089
+ );
1090
+ const totalIncludedMappedColumns = useMemo(
1091
+ () => workspaceTableKeys.reduce((count, key) => {
1092
+ const preview = workspaceItems[key]?.mappingPreview;
1093
+ return count + (preview?.summary.includedColumnNames.length ?? 0);
1094
+ }, 0),
1095
+ [workspaceItems, workspaceTableKeys],
1096
+ );
1097
+ const totalGeneratedSystemColumns = useMemo(
1098
+ () => workspaceTableKeys.reduce((count, key) => {
1099
+ const preview = workspaceItems[key]?.mappingPreview;
1100
+ return count + (preview?.summary.generatedSystemColumnCount ?? 0);
1101
+ }, 0),
1102
+ [workspaceItems, workspaceTableKeys],
1103
+ );
1104
+ const totalGeneratedMigrations = useMemo(
1105
+ () => workspaceTableKeys.reduce((count, key) => {
1106
+ return count + (workspaceItems[key]?.mappingPreview?.migration.willGenerate ? 1 : 0);
1107
+ }, 0),
1108
+ [workspaceItems, workspaceTableKeys],
1109
+ );
1110
+
1111
+ const updateWorkspaceItem = (key: string, updater: (current: WorkspaceBatchItem) => WorkspaceBatchItem) => {
1112
+ setWorkspaceItems((current) => {
1113
+ const item = current[key];
1114
+ if (!item) {
1115
+ return current;
1116
+ }
1117
+
1118
+ return {
1119
+ ...current,
1120
+ [key]: updater(item),
1121
+ };
1122
+ });
1123
+ };
1124
+
1125
+ const buildWorkspacePlaceholder = (record: DatasourceIntrospectionTableRecord): WorkspaceBatchItem => ({
1126
+ key: makeTableKey(record.schema, record.tableName),
1127
+ record,
1128
+ detail: null,
1129
+ mappedModelData: null,
1130
+ draft: null,
1131
+ loadStatus: { state: "loading", message: "Loading mapping workspace..." },
1132
+ reviewStatus: { state: "idle", message: "Not reviewed yet." },
1133
+ saveStatus: { state: "idle", message: "Not saved yet." },
1134
+ migrationStatus: { state: "idle", message: "Migration artifacts not created yet." },
1135
+ mappingPreview: null,
1136
+ persistedMappingResult: null,
1137
+ migrationArtifactsResult: null,
1138
+ reviewedMetadataJsonValue: null,
1139
+ metadataJsonText: "",
1140
+ metadataJsonValidationError: null,
1141
+ metadataJsonResetToken: `workspace-${record.tableName}-${Date.now()}`,
1142
+ error: null,
1143
+ });
1144
+
1145
+ const loadWorkspaceItem = async (record: DatasourceIntrospectionTableRecord) => {
1146
+ const key = makeTableKey(record.schema, record.tableName);
1147
+
1148
+ try {
1149
+ const tableDetailRequest = dispatch(
1150
+ datasourceIntrospectionApi.endpoints.getDatasourceIntrospectionTableDetail.initiate({
1151
+ moduleId,
1152
+ datasource: selectedDatasource,
1153
+ table: record.tableName,
1154
+ schema: record.schema ?? undefined,
1155
+ }),
1156
+ ) as any;
1157
+ const tableDetailData = await tableDetailRequest.unwrap();
1158
+ tableDetailRequest.unsubscribe?.();
1159
+
1160
+ let mappedModelData: any = null;
1161
+ if (record.mapped && record.mappedModelId) {
1162
+ const modelRequest = dispatch(
1163
+ modelsApi.endpoints.getmodelById.initiate(record.mappedModelId),
1164
+ ) as any;
1165
+ const modelResponse = await modelRequest.unwrap();
1166
+ modelRequest.unsubscribe?.();
1167
+ mappedModelData = modelResponse?.data ?? modelResponse ?? null;
1168
+ }
1169
+
1170
+ const draft = buildDrawerDraft(
1171
+ tableDetailData,
1172
+ record,
1173
+ mappedModelData ? { data: mappedModelData } : undefined,
1174
+ );
1175
+
1176
+ updateWorkspaceItem(key, (current) => ({
1177
+ ...current,
1178
+ detail: tableDetailData,
1179
+ mappedModelData,
1180
+ draft,
1181
+ loadStatus: { state: "ready", message: "Ready to configure." },
1182
+ error: null,
1183
+ }));
1184
+ } catch (error: any) {
1185
+ updateWorkspaceItem(key, (current) => ({
1186
+ ...current,
1187
+ loadStatus: { state: "error", message: readQueryError(error) },
1188
+ error: readQueryError(error),
1189
+ }));
1190
+ }
1191
+ };
1192
+
961
1193
  const drawerUserKeyOptions = useMemo(() => {
962
1194
  const options = new Map<string, { label: string; value: string }>();
963
1195
 
964
- tableDetail?.userKeyCandidates?.forEach((candidate) => {
1196
+ activeWorkspaceItem?.detail?.userKeyCandidates?.forEach((candidate) => {
965
1197
  options.set(candidate.name, {
966
1198
  label: `${candidate.label} · ${candidate.reason}`,
967
1199
  value: candidate.name,
@@ -980,7 +1212,7 @@ export function DatasourceIntrospectionPage() {
980
1212
  });
981
1213
 
982
1214
  return Array.from(options.values());
983
- }, [drawerDraft?.columns, tableDetail?.userKeyCandidates]);
1215
+ }, [activeWorkspaceItem?.detail?.userKeyCandidates, drawerDraft?.columns]);
984
1216
 
985
1217
  const settingsTabs = [
986
1218
  {
@@ -1063,33 +1295,109 @@ export function DatasourceIntrospectionPage() {
1063
1295
  if (selectedDatasource) {
1064
1296
  await refetchTables();
1065
1297
  }
1066
- if (isWorkspaceMode && selectedTable) {
1067
- await refetchTableDetail();
1298
+ if (isWorkspaceMode) {
1299
+ workspaceRecords.forEach((record) => {
1300
+ void loadWorkspaceItem(record);
1301
+ });
1068
1302
  }
1069
1303
  };
1070
1304
 
1071
- const startMappingForRecord = (record: DatasourceIntrospectionTableRecord) => {
1072
- if (drawerBootstrapTimerRef.current) {
1073
- window.clearTimeout(drawerBootstrapTimerRef.current);
1305
+ const toggleSelectedRow = (row: DatasourceIntrospectionTableRecord, checked?: boolean) => {
1306
+ if (!row.hasPrimaryKey) {
1307
+ return;
1074
1308
  }
1075
1309
 
1076
- setSelectedRows([record]);
1077
- setWorkspaceTableKey(`${record.schema ?? ""}::${record.tableName}`);
1310
+ const rowKey = makeTableKey(row.schema, row.tableName);
1311
+ setSelectedRows((current) => {
1312
+ const exists = current.some((item) => makeTableKey(item.schema, item.tableName) === rowKey);
1313
+ const shouldInclude = checked ?? !exists;
1314
+
1315
+ if (shouldInclude) {
1316
+ if (exists) return current;
1317
+ return [...current, row];
1318
+ }
1319
+
1320
+ return current.filter((item) => makeTableKey(item.schema, item.tableName) !== rowKey);
1321
+ });
1078
1322
  };
1079
1323
 
1080
- const leaveWorkspace = () => {
1081
- if (drawerBootstrapTimerRef.current) {
1082
- window.clearTimeout(drawerBootstrapTimerRef.current);
1324
+ const toggleAllVisibleRows = (checked: boolean) => {
1325
+ setSelectedRows((current) => {
1326
+ if (checked) {
1327
+ const merged = [...current];
1328
+ const currentKeySet = new Set(current.map((row) => makeTableKey(row.schema, row.tableName)));
1329
+
1330
+ filteredRecords.forEach((row) => {
1331
+ if (!row.hasPrimaryKey) {
1332
+ return;
1333
+ }
1334
+
1335
+ const rowKey = makeTableKey(row.schema, row.tableName);
1336
+ if (!currentKeySet.has(rowKey)) {
1337
+ merged.push(row);
1338
+ }
1339
+ });
1340
+
1341
+ return merged;
1342
+ }
1343
+
1344
+ return current.filter((row) => !visibleRowKeySet.has(makeTableKey(row.schema, row.tableName)));
1345
+ });
1346
+ };
1347
+
1348
+ const startMappingForRows = (records: DatasourceIntrospectionTableRecord[]) => {
1349
+ if (!records.length) return;
1350
+
1351
+ const eligibleRecords = records.filter((record) => record.hasPrimaryKey);
1352
+ const skippedRecords = records.filter((record) => !record.hasPrimaryKey);
1353
+
1354
+ if (skippedRecords.length) {
1355
+ dispatch(showToast({
1356
+ severity: "warn",
1357
+ summary: "Skipped blocked tables",
1358
+ detail: skippedRecords.length === 1
1359
+ ? skippedRecords[0].mappingBlockedReason || `Table "${skippedRecords[0].tableName}" was skipped because it has no primary key.`
1360
+ : `${skippedRecords.length} tables were skipped because they do not define a datasource primary key.`,
1361
+ }));
1083
1362
  }
1084
1363
 
1085
- setWorkspaceTableKey("");
1086
- setDrawerDraft(null);
1364
+ if (!eligibleRecords.length) {
1365
+ return;
1366
+ }
1367
+
1368
+ const keys = eligibleRecords.map((record) => makeTableKey(record.schema, record.tableName));
1369
+ const nextWorkspaceItems = Object.fromEntries(
1370
+ eligibleRecords.map((record) => [makeTableKey(record.schema, record.tableName), buildWorkspacePlaceholder(record)]),
1371
+ );
1372
+
1373
+ setSelectedRows(eligibleRecords);
1374
+ setWorkspaceTableKeys(keys);
1375
+ setActiveWorkspaceTableKey(keys[0]);
1376
+ setWorkspaceItems(nextWorkspaceItems);
1377
+ setFieldConfigColumnName(null);
1378
+ setDrawerTab("general-info");
1379
+ setWorkspaceStep("configure");
1380
+ setGenerateCodeOutput("");
1381
+ setIsGeneratingCodeWorkflow(false);
1382
+ setGenerateCodeStatusMessage("");
1383
+ setGenerateCodeTimeoutMessage(null);
1384
+ setGenerateCodeReady(false);
1385
+ setHasAttemptedGenerateCode(false);
1386
+ setRunMigrationOutput("");
1387
+ setRunMigrationCompleted(false);
1388
+
1389
+ eligibleRecords.forEach((record) => {
1390
+ void loadWorkspaceItem(record);
1391
+ });
1392
+ };
1393
+
1394
+ const leaveWorkspace = () => {
1395
+ setWorkspaceTableKeys([]);
1396
+ setActiveWorkspaceTableKey("");
1397
+ setWorkspaceItems({});
1087
1398
  setFieldConfigColumnName(null);
1088
1399
  setWorkspaceStep("configure");
1089
1400
  setDrawerTab("general-info");
1090
- setMappingPreview(null);
1091
- setPersistedMappingResult(null);
1092
- setMigrationArtifactsResult(null);
1093
1401
  setGenerateCodeOutput("");
1094
1402
  setIsGeneratingCodeWorkflow(false);
1095
1403
  setGenerateCodeStatusMessage("");
@@ -1098,11 +1406,6 @@ export function DatasourceIntrospectionPage() {
1098
1406
  setHasAttemptedGenerateCode(false);
1099
1407
  setRunMigrationOutput("");
1100
1408
  setRunMigrationCompleted(false);
1101
- setReviewedMetadataJsonValue(null);
1102
- setMetadataJsonText("");
1103
- setMetadataJsonValidationError(null);
1104
- setMetadataJsonResetToken(`workspace-closed-${Date.now()}`);
1105
- setDrawerBootstrapping(false);
1106
1409
  };
1107
1410
 
1108
1411
  const applyDraftSettings = () => {
@@ -1110,18 +1413,54 @@ export function DatasourceIntrospectionPage() {
1110
1413
  setSettingsOpen(false);
1111
1414
  };
1112
1415
 
1416
+ const updateActiveWorkspaceItem = (updater: (current: WorkspaceBatchItem) => WorkspaceBatchItem) => {
1417
+ if (!activeWorkspaceTableKey) return;
1418
+ updateWorkspaceItem(activeWorkspaceTableKey, updater);
1419
+ };
1420
+
1113
1421
  const updateDrawerDraft = (updater: (current: DrawerDraft) => DrawerDraft) => {
1114
- setMappingPreview(null);
1115
- setPersistedMappingResult(null);
1116
- setMigrationArtifactsResult(null);
1117
1422
  setGenerateCodeOutput("");
1118
1423
  setIsGeneratingCodeWorkflow(false);
1119
1424
  setGenerateCodeStatusMessage("");
1120
1425
  setGenerateCodeTimeoutMessage(null);
1121
1426
  setGenerateCodeReady(false);
1122
1427
  setHasAttemptedGenerateCode(false);
1428
+ setMigrationPreviewCopied(false);
1123
1429
  setRunMigrationOutput("");
1124
- setDrawerDraft((current) => (current ? updater(current) : current));
1430
+ updateActiveWorkspaceItem((current) => ({
1431
+ ...current,
1432
+ mappingPreview: null,
1433
+ persistedMappingResult: null,
1434
+ migrationArtifactsResult: null,
1435
+ reviewStatus: { state: "idle", message: "Not reviewed yet." },
1436
+ saveStatus: { state: "idle", message: "Not saved yet." },
1437
+ migrationStatus: { state: "idle", message: "Migration artifacts not created yet." },
1438
+ draft: current.draft ? updater(current.draft) : current.draft,
1439
+ }));
1440
+ };
1441
+
1442
+ const handleCopyMigrationPreview = async () => {
1443
+ const migrationContent = mappingPreview?.migration.content;
1444
+ if (!migrationContent) {
1445
+ return;
1446
+ }
1447
+
1448
+ try {
1449
+ await navigator.clipboard.writeText(migrationContent);
1450
+ setMigrationPreviewCopied(true);
1451
+ window.setTimeout(() => setMigrationPreviewCopied(false), 2000);
1452
+ dispatch(showToast({
1453
+ severity: "success",
1454
+ summary: "Migration copied",
1455
+ detail: "The migration preview was copied to the clipboard.",
1456
+ }));
1457
+ } catch (error) {
1458
+ dispatch(showToast({
1459
+ severity: "error",
1460
+ summary: "Copy failed",
1461
+ detail: "Unable to copy the migration preview to the clipboard.",
1462
+ }));
1463
+ }
1125
1464
  };
1126
1465
 
1127
1466
  const updateGeneralField = (field: keyof DrawerDraft, value: any) => {
@@ -1182,29 +1521,6 @@ export function DatasourceIntrospectionPage() {
1182
1521
  }));
1183
1522
  };
1184
1523
 
1185
- const updateColumnSolidFieldType = (columnName: string, nextSolidFieldType: string) => {
1186
- updateDrawerDraft((current) => ({
1187
- ...current,
1188
- columns: current.columns.map((column) => {
1189
- if (column.columnName !== columnName) return column;
1190
- if (column.handledBySuperclass) return column;
1191
-
1192
- return {
1193
- ...column,
1194
- solidFieldType: nextSolidFieldType,
1195
- ormType: getDefaultOrmTypeForSolidFieldType(current.dataSourceType, nextSolidFieldType),
1196
- fieldConfig: column.fieldConfig
1197
- ? {
1198
- ...column.fieldConfig,
1199
- type: nextSolidFieldType,
1200
- ormType: getDefaultOrmTypeForSolidFieldType(current.dataSourceType, nextSolidFieldType),
1201
- }
1202
- : column.fieldConfig,
1203
- };
1204
- }),
1205
- }));
1206
- };
1207
-
1208
1524
  const openFieldConfigEditor = (columnName: string) => {
1209
1525
  setFieldConfigColumnName(columnName);
1210
1526
  };
@@ -1379,86 +1695,167 @@ export function DatasourceIntrospectionPage() {
1379
1695
  };
1380
1696
 
1381
1697
  const reviewMappingJson = async () => {
1382
- if (!drawerDraft || !bootstrap?.module?.id) return;
1383
-
1384
- const validationErrors = getDrawerValidationErrors(drawerDraft);
1385
- if (validationErrors.length) {
1386
- validationErrors.forEach((detail) => {
1387
- dispatch(showToast({ severity: "error", summary: "Validation", detail }));
1698
+ if (!bootstrap?.module?.id || !workspaceTableKeys.length) return;
1699
+
1700
+ const tablesWithErrors = workspaceTableKeys
1701
+ .map((key) => ({ key, item: workspaceItems[key] }))
1702
+ .filter((entry) => entry.item?.draft)
1703
+ .flatMap(({ item }) => getDrawerValidationErrors(item.draft as DrawerDraft).map((detail) => ({
1704
+ tableName: item?.record.tableName,
1705
+ detail,
1706
+ })));
1707
+
1708
+ if (tablesWithErrors.length) {
1709
+ tablesWithErrors.forEach(({ tableName, detail }) => {
1710
+ dispatch(showToast({ severity: "error", summary: tableName || "Validation", detail }));
1388
1711
  });
1389
1712
  return;
1390
1713
  }
1391
1714
 
1392
- try {
1393
- const preview = await previewMapping({
1394
- moduleId: bootstrap.module.id,
1395
- payload: buildMappingRequest(drawerDraft),
1396
- }).unwrap();
1715
+ let firstSuccessKey = "";
1716
+
1717
+ for (const key of workspaceTableKeys) {
1718
+ const item = workspaceItems[key];
1719
+ if (!item?.draft) continue;
1720
+
1721
+ updateWorkspaceItem(key, (current) => ({
1722
+ ...current,
1723
+ reviewStatus: { state: "loading", message: "Generating mapping preview..." },
1724
+ }));
1725
+
1726
+ try {
1727
+ const preview = await previewMapping({
1728
+ moduleId: bootstrap.module.id,
1729
+ payload: buildMappingRequest(item.draft),
1730
+ }).unwrap();
1731
+
1732
+ if (!firstSuccessKey) {
1733
+ firstSuccessKey = key;
1734
+ }
1735
+
1736
+ updateWorkspaceItem(key, (current) => ({
1737
+ ...current,
1738
+ mappingPreview: preview,
1739
+ reviewedMetadataJsonValue: preview.metadataJson.model,
1740
+ metadataJsonText: JSON.stringify(preview.metadataJson.model, null, 2),
1741
+ metadataJsonValidationError: null,
1742
+ metadataJsonResetToken: `preview-${preview.metadataJson.filePath}-${Date.now()}`,
1743
+ persistedMappingResult: null,
1744
+ migrationArtifactsResult: null,
1745
+ reviewStatus: { state: "success", message: "Preview ready." },
1746
+ saveStatus: { state: "idle", message: "Not saved yet." },
1747
+ migrationStatus: { state: "idle", message: "Migration artifacts not created yet." },
1748
+ }));
1749
+ } catch (error: any) {
1750
+ updateWorkspaceItem(key, (current) => ({
1751
+ ...current,
1752
+ reviewStatus: { state: "error", message: readQueryError(error) },
1753
+ }));
1754
+ dispatch(showToast({
1755
+ severity: "error",
1756
+ summary: `Preview failed for ${item.record.tableName}`,
1757
+ detail: readQueryError(error),
1758
+ }));
1759
+ }
1760
+ }
1397
1761
 
1398
- setMappingPreview(preview);
1399
- setReviewedMetadataJsonValue(preview.metadataJson.model);
1400
- setMetadataJsonText(JSON.stringify(preview.metadataJson.model, null, 2));
1401
- setMetadataJsonValidationError(null);
1402
- setMetadataJsonResetToken(`preview-${preview.metadataJson.filePath}-${Date.now()}`);
1403
- setPersistedMappingResult(null);
1404
- setMigrationArtifactsResult(null);
1762
+ if (firstSuccessKey) {
1763
+ setActiveWorkspaceTableKey(firstSuccessKey);
1764
+ setWorkspaceStep("review-json");
1405
1765
  setGenerateCodeOutput("");
1406
1766
  setRunMigrationOutput("");
1407
- setWorkspaceStep("review-json");
1408
- } catch (error: any) {
1409
- dispatch(showToast({
1410
- severity: "error",
1411
- summary: "Preview failed",
1412
- detail: readQueryError(error),
1413
- }));
1414
1767
  }
1415
1768
  };
1416
1769
 
1417
1770
  const persistMappingArtifacts = async () => {
1418
- if (!drawerDraft || !bootstrap?.module?.id) return;
1771
+ if (!bootstrap?.module?.id || !workspaceTableKeys.length) return;
1419
1772
 
1420
- const validationErrors = getDrawerValidationErrors(drawerDraft);
1421
- if (validationErrors.length) {
1422
- validationErrors.forEach((detail) => {
1423
- dispatch(showToast({ severity: "error", summary: "Validation", detail }));
1424
- });
1425
- setWorkspaceStep("configure");
1426
- return;
1427
- }
1773
+ let firstSavedKey = "";
1428
1774
 
1429
- const reviewedModelValidation = parseAndValidateReviewedModelJson(metadataJsonText, drawerDraft);
1430
- if (reviewedModelValidation.error || !reviewedModelValidation.parsed) {
1431
- setMetadataJsonValidationError(reviewedModelValidation.error ?? "Metadata JSON is invalid.");
1432
- setWorkspaceStep("review-json");
1433
- dispatch(showToast({
1434
- severity: "error",
1435
- summary: "Validation",
1436
- detail: reviewedModelValidation.error ?? "Metadata JSON is invalid.",
1437
- }));
1438
- return;
1439
- }
1775
+ for (const key of workspaceTableKeys) {
1776
+ const item = workspaceItems[key];
1777
+ if (!item?.draft || !item.mappingPreview) continue;
1440
1778
 
1441
- setMetadataJsonValidationError(null);
1779
+ const validationErrors = getDrawerValidationErrors(item.draft);
1780
+ if (validationErrors.length) {
1781
+ updateWorkspaceItem(key, (current) => ({
1782
+ ...current,
1783
+ saveStatus: { state: "error", message: validationErrors[0] },
1784
+ }));
1785
+ validationErrors.forEach((detail) => {
1786
+ dispatch(showToast({ severity: "error", summary: item.record.tableName, detail }));
1787
+ });
1788
+ continue;
1789
+ }
1442
1790
 
1443
- try {
1444
- const response = await applyMapping({
1445
- moduleId: bootstrap.module.id,
1446
- payload: {
1447
- ...buildMappingRequest(drawerDraft, mappingPreview?.migration.timestamp ?? null),
1448
- reviewedModel: reviewedModelValidation.parsed,
1449
- },
1450
- }).unwrap();
1791
+ const reviewedModelValidation = parseAndValidateReviewedModelJson(item.metadataJsonText, item.draft);
1792
+ if (reviewedModelValidation.error || !reviewedModelValidation.parsed) {
1793
+ updateWorkspaceItem(key, (current) => ({
1794
+ ...current,
1795
+ metadataJsonValidationError: reviewedModelValidation.error ?? "Metadata JSON is invalid.",
1796
+ saveStatus: { state: "error", message: reviewedModelValidation.error ?? "Metadata JSON is invalid." },
1797
+ }));
1798
+ dispatch(showToast({
1799
+ severity: "error",
1800
+ summary: item.record.tableName,
1801
+ detail: reviewedModelValidation.error ?? "Metadata JSON is invalid.",
1802
+ }));
1803
+ continue;
1804
+ }
1451
1805
 
1452
- dispatch(showToast({
1453
- severity: "success",
1454
- summary: "Mapping saved",
1455
- detail: drawerDraft.mapped
1456
- ? "The model mapping metadata was updated successfully."
1457
- : "The model mapping metadata was created successfully.",
1806
+ updateWorkspaceItem(key, (current) => ({
1807
+ ...current,
1808
+ metadataJsonValidationError: null,
1809
+ saveStatus: { state: "loading", message: "Saving mapping..." },
1458
1810
  }));
1459
1811
 
1460
- setPersistedMappingResult(response);
1461
- setMigrationArtifactsResult(null);
1812
+ try {
1813
+ const response = await applyMapping({
1814
+ moduleId: bootstrap.module.id,
1815
+ payload: {
1816
+ ...buildMappingRequest(item.draft, item.mappingPreview.migration.timestamp ?? null),
1817
+ reviewedModel: reviewedModelValidation.parsed,
1818
+ },
1819
+ }).unwrap();
1820
+
1821
+ if (!firstSavedKey) {
1822
+ firstSavedKey = key;
1823
+ }
1824
+
1825
+ updateWorkspaceItem(key, (current) => ({
1826
+ ...current,
1827
+ persistedMappingResult: response,
1828
+ migrationArtifactsResult: null,
1829
+ saveStatus: { state: "success", message: "Mapping saved." },
1830
+ migrationStatus: { state: "idle", message: "Migration artifacts not created yet." },
1831
+ draft: current.draft
1832
+ ? {
1833
+ ...current.draft,
1834
+ mapped: true,
1835
+ modelId: response.modelId,
1836
+ }
1837
+ : current.draft,
1838
+ record: {
1839
+ ...current.record,
1840
+ mapped: true,
1841
+ mappedModelId: response.modelId,
1842
+ },
1843
+ }));
1844
+ } catch (error: any) {
1845
+ updateWorkspaceItem(key, (current) => ({
1846
+ ...current,
1847
+ saveStatus: { state: "error", message: readQueryError(error) },
1848
+ }));
1849
+ dispatch(showToast({
1850
+ severity: "error",
1851
+ summary: `Save failed for ${item.record.tableName}`,
1852
+ detail: readQueryError(error),
1853
+ }));
1854
+ }
1855
+ }
1856
+
1857
+ if (firstSavedKey) {
1858
+ setActiveWorkspaceTableKey(firstSavedKey);
1462
1859
  setGenerateCodeOutput("");
1463
1860
  setIsGeneratingCodeWorkflow(false);
1464
1861
  setGenerateCodeStatusMessage("");
@@ -1467,21 +1864,11 @@ export function DatasourceIntrospectionPage() {
1467
1864
  setHasAttemptedGenerateCode(false);
1468
1865
  setRunMigrationOutput("");
1469
1866
  setRunMigrationCompleted(false);
1470
- setDrawerDraft((current) => (
1471
- current
1472
- ? {
1473
- ...current,
1474
- mapped: true,
1475
- modelId: response.modelId,
1476
- }
1477
- : current
1478
- ));
1479
1867
  setWorkspaceStep("generate-code");
1480
- } catch (error: any) {
1481
1868
  dispatch(showToast({
1482
- severity: "error",
1483
- summary: "Save failed",
1484
- detail: readQueryError(error),
1869
+ severity: "success",
1870
+ summary: "Mappings saved",
1871
+ detail: "The selected table mappings were saved successfully.",
1485
1872
  }));
1486
1873
  }
1487
1874
  };
@@ -1543,7 +1930,6 @@ export function DatasourceIntrospectionPage() {
1543
1930
 
1544
1931
  setGenerateCodeStatusMessage("Backend is back online. You can continue.");
1545
1932
  setGenerateCodeReady(true);
1546
- setMigrationArtifactsResult(null);
1547
1933
  setRunMigrationOutput("");
1548
1934
  dispatch(showToast({
1549
1935
  severity: "success",
@@ -1553,50 +1939,83 @@ export function DatasourceIntrospectionPage() {
1553
1939
  };
1554
1940
 
1555
1941
  const handleCreateMigrationArtifacts = async () => {
1556
- if (!drawerDraft || !bootstrap?.module?.id || !persistedMappingResult?.modelId) return;
1942
+ if (!bootstrap?.module?.id || !workspaceTableKeys.length) return;
1557
1943
 
1558
- const reviewedModelValidation = parseAndValidateReviewedModelJson(metadataJsonText, drawerDraft);
1559
- if (reviewedModelValidation.error || !reviewedModelValidation.parsed) {
1560
- setMetadataJsonValidationError(reviewedModelValidation.error ?? "Metadata JSON is invalid.");
1561
- setWorkspaceStep("review-json");
1562
- dispatch(showToast({
1563
- severity: "error",
1564
- summary: "Validation",
1565
- detail: reviewedModelValidation.error ?? "Metadata JSON is invalid.",
1944
+ let firstMigrationKey = "";
1945
+
1946
+ for (const key of workspaceTableKeys) {
1947
+ const item = workspaceItems[key];
1948
+ if (!item?.draft || !item.persistedMappingResult?.modelId || !item.mappingPreview) continue;
1949
+
1950
+ const reviewedModelValidation = parseAndValidateReviewedModelJson(item.metadataJsonText, item.draft);
1951
+ if (reviewedModelValidation.error || !reviewedModelValidation.parsed) {
1952
+ updateWorkspaceItem(key, (current) => ({
1953
+ ...current,
1954
+ metadataJsonValidationError: reviewedModelValidation.error ?? "Metadata JSON is invalid.",
1955
+ migrationStatus: { state: "error", message: reviewedModelValidation.error ?? "Metadata JSON is invalid." },
1956
+ }));
1957
+ dispatch(showToast({
1958
+ severity: "error",
1959
+ summary: item.record.tableName,
1960
+ detail: reviewedModelValidation.error ?? "Metadata JSON is invalid.",
1961
+ }));
1962
+ continue;
1963
+ }
1964
+
1965
+ updateWorkspaceItem(key, (current) => ({
1966
+ ...current,
1967
+ metadataJsonValidationError: null,
1968
+ migrationStatus: { state: "loading", message: "Creating migration artifacts..." },
1566
1969
  }));
1567
- return;
1568
- }
1569
1970
 
1570
- try {
1571
- const response = await createMigrationArtifacts({
1572
- moduleId: bootstrap.module.id,
1573
- payload: {
1574
- ...buildMappingRequest(
1575
- {
1576
- ...drawerDraft,
1577
- mapped: true,
1578
- modelId: persistedMappingResult.modelId,
1579
- },
1580
- mappingPreview?.migration.timestamp ?? null,
1581
- ),
1582
- reviewedModel: reviewedModelValidation.parsed,
1583
- },
1584
- }).unwrap();
1971
+ try {
1972
+ const response = await createMigrationArtifacts({
1973
+ moduleId: bootstrap.module.id,
1974
+ payload: {
1975
+ ...buildMappingRequest(
1976
+ {
1977
+ ...item.draft,
1978
+ mapped: true,
1979
+ modelId: item.persistedMappingResult.modelId,
1980
+ },
1981
+ item.mappingPreview.migration.timestamp ?? null,
1982
+ ),
1983
+ reviewedModel: reviewedModelValidation.parsed,
1984
+ },
1985
+ }).unwrap();
1986
+
1987
+ if (!firstMigrationKey) {
1988
+ firstMigrationKey = key;
1989
+ }
1990
+
1991
+ updateWorkspaceItem(key, (current) => ({
1992
+ ...current,
1993
+ migrationArtifactsResult: response,
1994
+ migrationStatus: {
1995
+ state: "success",
1996
+ message: response.migration.written ? "Migration artifacts created." : "No migration file required.",
1997
+ },
1998
+ }));
1999
+ } catch (error: any) {
2000
+ updateWorkspaceItem(key, (current) => ({
2001
+ ...current,
2002
+ migrationStatus: { state: "error", message: readQueryError(error) },
2003
+ }));
2004
+ dispatch(showToast({
2005
+ severity: "error",
2006
+ summary: `Create migration artifacts failed for ${item.record.tableName}`,
2007
+ detail: readQueryError(error),
2008
+ }));
2009
+ }
2010
+ }
1585
2011
 
1586
- setMigrationArtifactsResult(response);
2012
+ if (firstMigrationKey) {
2013
+ setActiveWorkspaceTableKey(firstMigrationKey);
1587
2014
  setWorkspaceStep("run-migration");
1588
2015
  dispatch(showToast({
1589
2016
  severity: "success",
1590
- summary: "Migration artifacts created",
1591
- detail: response.migration.written
1592
- ? "The migration file and datasource registration were written successfully."
1593
- : "No migration file was required, but the datasource registration was updated successfully.",
1594
- }));
1595
- } catch (error: any) {
1596
- dispatch(showToast({
1597
- severity: "error",
1598
- summary: "Create migration artifacts failed",
1599
- detail: readQueryError(error),
2017
+ summary: "Migration artifacts ready",
2018
+ detail: "Datasource registration and migration artifacts were prepared for the selected tables.",
1600
2019
  }));
1601
2020
  }
1602
2021
  };
@@ -1664,19 +2083,19 @@ export function DatasourceIntrospectionPage() {
1664
2083
  label: "Review Mapping JSON",
1665
2084
  complete: currentStepIndex > 1,
1666
2085
  active: workspaceStep === "review-json",
1667
- disabled: !mappingPreview,
2086
+ disabled: !allWorkspaceItemsPreviewed,
1668
2087
  onClick: () => {
1669
- if (mappingPreview) setWorkspaceStep("review-json");
2088
+ if (allWorkspaceItemsPreviewed) setWorkspaceStep("review-json");
1670
2089
  },
1671
2090
  },
1672
2091
  {
1673
2092
  key: "generate-code" as const,
1674
2093
  label: "Generate Code",
1675
- complete: currentStepIndex > 2,
2094
+ complete: currentStepIndex > 2 || generateCodeReady,
1676
2095
  active: workspaceStep === "generate-code",
1677
- disabled: !persistedMappingResult,
2096
+ disabled: !allWorkspaceItemsPersisted,
1678
2097
  onClick: () => {
1679
- if (persistedMappingResult) setWorkspaceStep("generate-code");
2098
+ if (allWorkspaceItemsPersisted) setWorkspaceStep("generate-code");
1680
2099
  },
1681
2100
  },
1682
2101
  {
@@ -1691,12 +2110,12 @@ export function DatasourceIntrospectionPage() {
1691
2110
  },
1692
2111
  {
1693
2112
  key: "run-migration" as const,
1694
- label: "Run Migration",
2113
+ label: hasMigrationGenerationWork ? "Run Migration" : "Finish",
1695
2114
  complete: false,
1696
2115
  active: workspaceStep === "run-migration",
1697
- disabled: !migrationArtifactsResult,
2116
+ disabled: !allWorkspaceItemsMigrated,
1698
2117
  onClick: () => {
1699
- if (migrationArtifactsResult) setWorkspaceStep("run-migration");
2118
+ if (allWorkspaceItemsMigrated) setWorkspaceStep("run-migration");
1700
2119
  },
1701
2120
  },
1702
2121
  ];
@@ -1706,7 +2125,7 @@ export function DatasourceIntrospectionPage() {
1706
2125
  ) ?? [];
1707
2126
  const allSelectableUnmappedIncluded = selectableUnmappedColumns.length > 0
1708
2127
  && selectableUnmappedColumns.every((column) => column.include);
1709
- const drawerRawDdl = tableDetail?.rawDdl ?? null;
2128
+ const drawerRawDdl = activeWorkspaceItem?.detail?.rawDdl ?? null;
1710
2129
  const drawerTabs = drawerDraft ? [
1711
2130
  {
1712
2131
  value: "general-info",
@@ -1845,7 +2264,7 @@ export function DatasourceIntrospectionPage() {
1845
2264
  <span>Database</span>
1846
2265
  <span>SolidX</span>
1847
2266
  <span>Status</span>
1848
- <span>Action</span>
2267
+ <span>Mapping</span>
1849
2268
  </div>
1850
2269
  {drawerDraft.columns.map((column) => {
1851
2270
  const isUserKey = drawerDraft.userKeyField === column.fieldName;
@@ -1911,33 +2330,27 @@ export function DatasourceIntrospectionPage() {
1911
2330
  <span className="sdix-column-meta">
1912
2331
  <strong>{column.dataType}</strong>
1913
2332
  </span>
1914
- <span>
1915
- {column.handledBySuperclass ? (
1916
- <span>{column.solidFieldType}</span>
1917
- ) : (
1918
- <SolidSelect
1919
- value={column.solidFieldType}
1920
- options={scalarFieldTypeOptions}
1921
- onChange={(event) => updateColumnSolidFieldType(column.columnName, event.value)}
1922
- />
1923
- )}
2333
+ <span className="sdix-column-type-cell">
2334
+ {!column.handledBySuperclass && (column.include || column.matched) ? (
2335
+ <SolidButton
2336
+ size="small"
2337
+ variant="outline"
2338
+ className="sdix-column-action-icon"
2339
+ onClick={() => openFieldConfigEditor(column.columnName)}
2340
+ aria-label={`Configure ${column.fieldName || column.columnName} field`}
2341
+ >
2342
+ <Cog size={14} />
2343
+ </SolidButton>
2344
+ ) : null}
2345
+ <span className={`sdix-status-badge ${column.handledBySuperclass ? "is-superclass" : "is-muted"}`}>
2346
+ {getFieldTypeLabel(column.solidFieldType)}
2347
+ </span>
1924
2348
  </span>
1925
2349
  <span>
1926
2350
  <span className={`sdix-status-badge ${statusClassName}`}>{statusText}</span>
1927
2351
  </span>
1928
2352
  <span className="sdix-column-action">
1929
2353
  <div className="sdix-column-action-stack">
1930
- {!column.handledBySuperclass && (column.include || column.matched) ? (
1931
- <SolidButton
1932
- size="small"
1933
- variant="outline"
1934
- className="sdix-column-action-icon"
1935
- onClick={() => openFieldConfigEditor(column.columnName)}
1936
- aria-label={`Configure ${column.fieldName || column.columnName} field`}
1937
- >
1938
- <Cog size={14} />
1939
- </SolidButton>
1940
- ) : null}
1941
2354
  {column.handledBySuperclass ? (
1942
2355
  <SolidButton size="small" variant="outline" disabled className="sdix-column-action-chip sdix-column-action-chip--managed">
1943
2356
  Managed
@@ -1982,6 +2395,24 @@ export function DatasourceIntrospectionPage() {
1982
2395
 
1983
2396
  const reviewJsonPanel = mappingPreview ? (
1984
2397
  <div className="sdix-review-panel sdix-review-panel--metadata">
2398
+ <div className="sdix-workspace-summary">
2399
+ <div className="sdix-workspace-summary__item">
2400
+ <span>Selected tables</span>
2401
+ <strong>{totalWorkspaceCount}</strong>
2402
+ </div>
2403
+ <div className="sdix-workspace-summary__item">
2404
+ <span>Previews ready</span>
2405
+ <strong>{reviewedWorkspaceCount}/{totalWorkspaceCount}</strong>
2406
+ </div>
2407
+ <div className="sdix-workspace-summary__item">
2408
+ <span>Current table</span>
2409
+ <strong>{activeWorkspacePosition}/{totalWorkspaceCount}</strong>
2410
+ </div>
2411
+ <div className="sdix-workspace-summary__item">
2412
+ <span>Review errors</span>
2413
+ <strong>{reviewErrorWorkspaceCount}</strong>
2414
+ </div>
2415
+ </div>
1985
2416
  <div className="sdix-review-meta sdix-review-meta--compact">
1986
2417
  <span>Target file</span>
1987
2418
  <strong>{mappingPreview.metadataJson.filePath}</strong>
@@ -1991,15 +2422,24 @@ export function DatasourceIntrospectionPage() {
1991
2422
  value={reviewedMetadataJsonValue ?? mappingPreview.metadataJson.model}
1992
2423
  resetToken={metadataJsonResetToken}
1993
2424
  onValueChange={(nextValue) => {
1994
- setReviewedMetadataJsonValue(nextValue);
1995
- if (metadataJsonValidationError) {
1996
- setMetadataJsonValidationError(null);
1997
- }
2425
+ updateActiveWorkspaceItem((current) => ({
2426
+ ...current,
2427
+ reviewedMetadataJsonValue: nextValue,
2428
+ metadataJsonValidationError: null,
2429
+ }));
1998
2430
  }}
1999
2431
  onTextChange={(nextText) => {
2000
- setMetadataJsonText(nextText);
2432
+ updateActiveWorkspaceItem((current) => ({
2433
+ ...current,
2434
+ metadataJsonText: nextText,
2435
+ }));
2436
+ }}
2437
+ onErrorChange={(nextError) => {
2438
+ updateActiveWorkspaceItem((current) => ({
2439
+ ...current,
2440
+ metadataJsonValidationError: nextError,
2441
+ }));
2001
2442
  }}
2002
- onErrorChange={setMetadataJsonValidationError}
2003
2443
  />
2004
2444
  {metadataJsonValidationError ? (
2005
2445
  <div className="sdix-json-validation-error">
@@ -2011,11 +2451,42 @@ export function DatasourceIntrospectionPage() {
2011
2451
 
2012
2452
  const createMigrationsPanel = mappingPreview ? (
2013
2453
  <div className="sdix-review-panel sdix-review-panel--migration">
2454
+ <div className="sdix-workspace-summary">
2455
+ <div className="sdix-workspace-summary__item">
2456
+ <span>Selected tables</span>
2457
+ <strong>{totalWorkspaceCount}</strong>
2458
+ </div>
2459
+ <div className="sdix-workspace-summary__item">
2460
+ <span>Migration previews ready</span>
2461
+ <strong>{migrationReadyWorkspaceCount}/{totalWorkspaceCount}</strong>
2462
+ </div>
2463
+ <div className="sdix-workspace-summary__item">
2464
+ <span>Migration files to write</span>
2465
+ <strong>{totalGeneratedMigrations}</strong>
2466
+ </div>
2467
+ <div className="sdix-workspace-summary__item">
2468
+ <span>System columns to add</span>
2469
+ <strong>{totalGeneratedSystemColumns}</strong>
2470
+ </div>
2471
+ </div>
2014
2472
  {mappingPreview.migration.willGenerate && mappingPreview.migration.filePath ? (
2015
2473
  <>
2016
- <div className="sdix-review-meta sdix-review-meta--compact">
2017
- <span>Target file</span>
2018
- <strong>{mappingPreview.migration.filePath}</strong>
2474
+ <div className="sdix-review-code-header">
2475
+ <div className="sdix-review-meta sdix-review-meta--compact">
2476
+ <span>Target file</span>
2477
+ <strong>{mappingPreview.migration.filePath}</strong>
2478
+ </div>
2479
+ {mappingPreview.migration.content ? (
2480
+ <SolidButton
2481
+ size="small"
2482
+ variant="outline"
2483
+ className="sdix-review-copy-button"
2484
+ leftIcon={migrationPreviewCopied ? <Check size={14} /> : <Copy size={14} />}
2485
+ onClick={() => void handleCopyMigrationPreview()}
2486
+ >
2487
+ {migrationPreviewCopied ? "Copied" : "Copy"}
2488
+ </SolidButton>
2489
+ ) : null}
2019
2490
  </div>
2020
2491
  <p className="sdix-review-note">
2021
2492
  This migration preview is generated from the current mapping and stays read-only here. Moving ahead writes the migration file to disk and updates the datasource registration now that the entity code already exists.
@@ -2038,15 +2509,33 @@ export function DatasourceIntrospectionPage() {
2038
2509
  <div className="sdix-stage-panel__header">
2039
2510
  <span className="sdix-stage-panel__eyebrow">Step 3</span>
2040
2511
  <h3>Generate module code</h3>
2041
- <p>The reviewed metadata JSON is now persisted. Run the normal SolidX module generation flow next so the entity, DTOs, repository, service, controller, and UI code are refreshed before we register the entity on the datasource and write the migration.</p>
2512
+ <p>The reviewed metadata JSON is now persisted for the selected batch. Run the normal SolidX module generation flow next so every mapped entity, DTO, repository, service, controller, and UI asset is refreshed before datasource registration and migration files are written.</p>
2042
2513
  </div>
2043
2514
  <div className="sdix-stage-panel__body">
2515
+ <div className="sdix-workspace-summary">
2516
+ <div className="sdix-workspace-summary__item">
2517
+ <span>Selected tables</span>
2518
+ <strong>{totalWorkspaceCount}</strong>
2519
+ </div>
2520
+ <div className="sdix-workspace-summary__item">
2521
+ <span>Saved mappings</span>
2522
+ <strong>{savedWorkspaceCount}/{totalWorkspaceCount}</strong>
2523
+ </div>
2524
+ <div className="sdix-workspace-summary__item">
2525
+ <span>Columns queued</span>
2526
+ <strong>{totalIncludedMappedColumns}</strong>
2527
+ </div>
2528
+ <div className="sdix-workspace-summary__item">
2529
+ <span>System columns pending</span>
2530
+ <strong>{totalGeneratedSystemColumns}</strong>
2531
+ </div>
2532
+ </div>
2044
2533
  <div className="sdix-stage-panel__card">
2045
2534
  <div className="sdix-stage-panel__card-title">What happens here</div>
2046
2535
  <ul className="sdix-stage-panel__list">
2047
- <li>The module generate-code pipeline runs for <strong>{bootstrap?.module?.displayName}</strong>.</li>
2048
- <li>The newly mapped model entity is generated inside the module source folder.</li>
2049
- <li>Any existing generated assets for this module may be refreshed.</li>
2536
+ <li>The module generate-code pipeline runs once for <strong>{bootstrap?.module?.displayName}</strong>.</li>
2537
+ <li>All saved mappings in this batch contribute to the generated entities inside the module source folder.</li>
2538
+ <li>Any existing generated assets for this module may be refreshed in the same pass.</li>
2050
2539
  </ul>
2051
2540
  </div>
2052
2541
  <div className="sdix-stage-panel__actions">
@@ -2059,6 +2548,13 @@ export function DatasourceIntrospectionPage() {
2059
2548
  Generate Code
2060
2549
  </SolidButton>
2061
2550
  </div>
2551
+ {generateCodeReady && !isGeneratingCodeWorkflow ? (
2552
+ <div className="sdix-empty-state is-inline sdix-stage-progress sdix-stage-progress--success">
2553
+ <Check size={24} />
2554
+ <h3>Code generation finished</h3>
2555
+ <p>The module generation workflow completed and the backend is back online. You can continue to migrations, or run generate code again if you want to retry.</p>
2556
+ </div>
2557
+ ) : null}
2062
2558
  {isGeneratingCodeWorkflow ? (
2063
2559
  <div className="sdix-empty-state is-inline sdix-stage-progress">
2064
2560
  <SolidSpinner size={30} />
@@ -2085,11 +2581,33 @@ export function DatasourceIntrospectionPage() {
2085
2581
  {!runMigrationCompleted ? (
2086
2582
  <div className="sdix-stage-panel__header">
2087
2583
  <span className="sdix-stage-panel__eyebrow">Step 5</span>
2088
- <h3>Run migration, build, and seed</h3>
2089
- <p>With the datasource file updated and module code generated, SolidX can now run the TypeORM migration for the selected datasource, rebuild the project, and reseed the mapped module metadata.</p>
2584
+ <h3>{hasMigrationGenerationWork ? "Run migration, build, and seed" : "Finish mapping workflow"}</h3>
2585
+ <p>
2586
+ {hasMigrationGenerationWork
2587
+ ? "With datasource registration updated and module code generated, SolidX can now run the TypeORM migration, rebuild the project, and reseed metadata for the full selected batch."
2588
+ : "SolidX will now run the final wrap-up workflow for the full selected batch: execute the datasource migration command, rebuild the project, and reseed the mapped module metadata."}
2589
+ </p>
2090
2590
  </div>
2091
2591
  ) : null}
2092
2592
  <div className="sdix-stage-panel__body">
2593
+ <div className="sdix-workspace-summary">
2594
+ <div className="sdix-workspace-summary__item">
2595
+ <span>Selected tables</span>
2596
+ <strong>{totalWorkspaceCount}</strong>
2597
+ </div>
2598
+ <div className="sdix-workspace-summary__item">
2599
+ <span>Generated code ready</span>
2600
+ <strong>{hasAttemptedGenerateCode ? "Yes" : "No"}</strong>
2601
+ </div>
2602
+ <div className="sdix-workspace-summary__item">
2603
+ <span>Migration artifacts ready</span>
2604
+ <strong>{migrationReadyWorkspaceCount}/{totalWorkspaceCount}</strong>
2605
+ </div>
2606
+ <div className="sdix-workspace-summary__item">
2607
+ <span>Datasource</span>
2608
+ <strong>{selectedDatasource}</strong>
2609
+ </div>
2610
+ </div>
2093
2611
  {!runMigrationCompleted ? (
2094
2612
  <div className="sdix-stage-panel__card">
2095
2613
  <div className="sdix-stage-panel__card-title">Commands</div>
@@ -2141,7 +2659,16 @@ npx @solidxai/solidctl@latest seed --modules-to-seed ${bootstrap?.module?.name |
2141
2659
  case "review-json":
2142
2660
  return (
2143
2661
  <span>
2144
- Review the generated metadata JSON before it is written to the module metadata file.
2662
+ Reviewing
2663
+ {" "}
2664
+ <strong>{activeWorkspacePosition} of {totalWorkspaceCount}</strong>
2665
+ {" "}
2666
+ selected tables. Preview generation is ready for
2667
+ {" "}
2668
+ <strong>{reviewedWorkspaceCount}</strong>
2669
+ {" "}
2670
+ table(s)
2671
+ {reviewErrorWorkspaceCount ? `, with ${reviewErrorWorkspaceCount} table(s) still needing review fixes.` : "."}
2145
2672
  </span>
2146
2673
  );
2147
2674
  case "generate-code":
@@ -2149,24 +2676,37 @@ npx @solidxai/solidctl@latest seed --modules-to-seed ${bootstrap?.module?.name |
2149
2676
  <span>
2150
2677
  Metadata JSON has been saved for
2151
2678
  {" "}
2152
- <strong>{drawerDraft.displayName || drawerDraft.singularName}</strong>
2153
- . Generate the module code next so the entity file exists before datasource registration is updated. Continue stays disabled until you attempt code generation at least once.
2679
+ <strong>{savedWorkspaceCount}</strong>
2680
+ {" "}
2681
+ of
2682
+ {" "}
2683
+ <strong>{totalWorkspaceCount}</strong>
2684
+ {" "}
2685
+ selected tables. Generate the module code next so every entity file exists before datasource registration is updated. Continue stays disabled until you attempt code generation at least once.
2154
2686
  </span>
2155
2687
  ) : (
2156
- <span>Save the metadata JSON before generating code.</span>
2688
+ <span>Save the reviewed metadata JSON for the selected tables before generating code.</span>
2157
2689
  );
2158
2690
  case "create-migrations":
2159
2691
  return mappingPreview ? (
2160
2692
  <span>
2161
- Create the migration artifacts for
2693
+ Create migration artifacts for
2694
+ {" "}
2695
+ <strong>{migrationReadyWorkspaceCount}</strong>
2696
+ {" "}
2697
+ of
2162
2698
  {" "}
2163
- <strong>{mappingPreview.summary.includedColumnNames.length}</strong>
2699
+ <strong>{totalWorkspaceCount}</strong>
2164
2700
  {" "}
2165
- mapped columns
2166
- {mappingPreview.summary.generatedSystemColumnCount
2167
- ? ` and ${mappingPreview.summary.generatedSystemColumnCount} SolidX system column migration changes`
2168
- : ""}
2169
- .
2701
+ selected tables. This batch currently includes
2702
+ {" "}
2703
+ <strong>{totalIncludedMappedColumns}</strong>
2704
+ {" "}
2705
+ mapped columns and
2706
+ {" "}
2707
+ <strong>{totalGeneratedSystemColumns}</strong>
2708
+ {" "}
2709
+ SolidX system column changes.
2170
2710
  </span>
2171
2711
  ) : null;
2172
2712
  case "run-migration":
@@ -2175,28 +2715,54 @@ npx @solidxai/solidctl@latest seed --modules-to-seed ${bootstrap?.module?.name |
2175
2715
  The migration, build, and seed workflow has completed for
2176
2716
  {" "}
2177
2717
  <strong>{selectedDatasource}</strong>
2178
- . Review the output below and click Finish when you are ready to return to the model list.
2718
+ {" "}
2719
+ across
2720
+ {" "}
2721
+ <strong>{totalWorkspaceCount}</strong>
2722
+ {" "}
2723
+ selected table mappings. Review the output below and click Finish when you are ready to return to the model list.
2179
2724
  </span>
2180
2725
  ) : (
2181
2726
  <span>
2182
- Run the migration, build, and seed workflow for
2727
+ {hasMigrationGenerationWork ? "Run the migration, build, and seed workflow for" : "Finish the mapping workflow for"}
2183
2728
  {" "}
2184
2729
  <strong>{selectedDatasource}</strong>
2185
2730
  {" "}
2186
- to apply the SolidX system columns on the legacy table and refresh the module metadata state.
2731
+ across
2732
+ {" "}
2733
+ <strong>{totalWorkspaceCount}</strong>
2734
+ {" "}
2735
+ selected table mappings.
2736
+ {" "}
2737
+ {hasMigrationGenerationWork
2738
+ ? "to apply the SolidX system columns on the legacy table and refresh the module metadata state."
2739
+ : "to execute the final rebuild and reseed flow after the mapping changes are already in place."}
2187
2740
  </span>
2188
2741
  );
2189
2742
  case "configure":
2190
2743
  default:
2191
2744
  return drawerDraft.mapped ? (
2192
2745
  <span>
2193
- Editing mapped model
2746
+ Editing
2747
+ {" "}
2748
+ <strong>{activeWorkspacePosition} of {totalWorkspaceCount}</strong>
2749
+ {" "}
2750
+ selected tables.
2751
+ {" "}
2752
+ Current mapped model:
2194
2753
  {" "}
2195
2754
  <strong>{selectedTable?.mappedModelDisplayName || selectedTable?.mappedModelSingularName}</strong>
2196
2755
  {drawerDraft.mappedModuleLabel ? ` in ${drawerDraft.mappedModuleLabel}` : ""}
2197
2756
  </span>
2198
2757
  ) : (
2199
- <span>Configure the field mapping draft before generating the metadata JSON preview.</span>
2758
+ <span>
2759
+ Configure
2760
+ {" "}
2761
+ <strong>{activeWorkspacePosition} of {totalWorkspaceCount}</strong>
2762
+ {" "}
2763
+ selected tables before generating the batch metadata JSON previews.
2764
+ {allWorkspaceItemsReady ? " All selected mapping drafts are loaded." : ""}
2765
+ </span>
2200
2766
  );
2201
2767
  }
2202
2768
  })() : null;
@@ -2241,12 +2807,8 @@ npx @solidxai/solidctl@latest seed --modules-to-seed ${bootstrap?.module?.name |
2241
2807
  <SolidButton
2242
2808
  size="small"
2243
2809
  leftIcon={<Sparkles size={14} />}
2244
- onClick={() => {
2245
- if (selectedListRow) {
2246
- startMappingForRecord(selectedListRow);
2247
- }
2248
- }}
2249
- disabled={!selectedListRow}
2810
+ onClick={() => startMappingForRows(selectedRows)}
2811
+ disabled={!selectedRowCount}
2250
2812
  >
2251
2813
  Start Mapping
2252
2814
  </SolidButton>
@@ -2281,12 +2843,18 @@ npx @solidxai/solidctl@latest seed --modules-to-seed ${bootstrap?.module?.name |
2281
2843
  </div>
2282
2844
  ) : null}
2283
2845
 
2284
- {selectedDatasource && tablesResponse?.synchronizeBlocked ? (
2846
+ {isSynchronizeBlocked ? (
2285
2847
  <div className="sdix-blocker">
2286
- <ShieldAlert size={20} />
2287
- <div>
2288
- <h3>Synchronize must be disabled before introspection</h3>
2289
- <p>{tablesResponse.synchronizeMessage}</p>
2848
+ <div className="sdix-blocker__icon">
2849
+ <ShieldAlert size={22} />
2850
+ </div>
2851
+ <div className="sdix-blocker__content">
2852
+ <span className="sdix-blocker__eyebrow">Configuration blocker</span>
2853
+ <h3>Disable TypeORM synchronize before running datasource introspection</h3>
2854
+ <p>{tablesResponse?.synchronizeMessage}</p>
2855
+ <div className="sdix-blocker__note">
2856
+ Introspection is paused here to avoid schema-sync side effects while reviewing legacy datasource tables.
2857
+ </div>
2290
2858
  </div>
2291
2859
  </div>
2292
2860
  ) : null}
@@ -2311,7 +2879,7 @@ npx @solidxai/solidctl@latest seed --modules-to-seed ${bootstrap?.module?.name |
2311
2879
  Retry
2312
2880
  </SolidButton>
2313
2881
  </div>
2314
- ) : !filteredRecords.length ? (
2882
+ ) : isSynchronizeBlocked ? null : !filteredRecords.length ? (
2315
2883
  <div className="sdix-empty-state is-inline">
2316
2884
  <Database size={22} />
2317
2885
  <p>{tableSearch ? "No tables match the current quick filter." : "No database tables were discovered for this datasource."}</p>
@@ -2323,19 +2891,33 @@ npx @solidxai/solidctl@latest seed --modules-to-seed ${bootstrap?.module?.name |
2323
2891
  dataKey="tableName"
2324
2892
  viewportHeight="calc(100vh - 162px)"
2325
2893
  size="small"
2326
- onRowClick={({ data }) => setSelectedRows([data])}
2327
- rowClassName={(rowData) => `${rowData.schema ?? ""}::${rowData.tableName}` === selectedListRowKey ? "sdix-table-row-selected" : ""}
2894
+ onRowClick={({ data }) => toggleSelectedRow(data)}
2895
+ rowClassName={(rowData) => selectedListRowKeySet.has(makeTableKey(rowData.schema, rowData.tableName)) ? "sdix-table-row-selected" : ""}
2328
2896
  emptyMessage="No datasource tables found"
2329
2897
  >
2330
- <Column
2331
- header=""
2332
- body={(rowData: DatasourceIntrospectionTableRecord) => {
2333
- const rowKey = `${rowData.schema ?? ""}::${rowData.tableName}`;
2334
- return (
2898
+ <Column
2899
+ header={
2900
+ <div className="sdix-selection-cell" data-no-row-click="true">
2901
+ <SolidCheckbox
2902
+ checked={allVisibleRowsSelected}
2903
+ disabled={!hasVisibleRows || !filteredRecords.some((row) => row.hasPrimaryKey)}
2904
+ onChange={(event) => toggleAllVisibleRows(event.target.checked)}
2905
+ aria-label={
2906
+ allVisibleRowsSelected
2907
+ ? "Deselect all visible tables"
2908
+ : "Select all visible tables"
2909
+ }
2910
+ />
2911
+ </div>
2912
+ }
2913
+ body={(rowData: DatasourceIntrospectionTableRecord) => {
2914
+ const rowKey = `${rowData.schema ?? ""}::${rowData.tableName}`;
2915
+ return (
2335
2916
  <div className="sdix-selection-cell" data-no-row-click="true">
2336
2917
  <SolidCheckbox
2337
- checked={rowKey === selectedListRowKey}
2338
- onChange={(event) => setSelectedRows(event.target.checked ? [rowData] : [])}
2918
+ checked={selectedListRowKeySet.has(rowKey)}
2919
+ disabled={!rowData.hasPrimaryKey}
2920
+ onChange={(event) => toggleSelectedRow(rowData, event.target.checked)}
2339
2921
  />
2340
2922
  </div>
2341
2923
  );
@@ -2357,13 +2939,24 @@ npx @solidxai/solidctl@latest seed --modules-to-seed ${bootstrap?.module?.name |
2357
2939
  header="Status"
2358
2940
  body={(rowData: DatasourceIntrospectionTableRecord) => (
2359
2941
  <span
2360
- className={`sdix-status-badge ${rowData.mapped ? "is-mapped" : "is-unmapped"}`}
2942
+ className={`sdix-status-badge ${!rowData.hasPrimaryKey ? "is-blocked" : rowData.mapped ? "is-mapped" : "is-unmapped"}`}
2361
2943
  >
2362
2944
  {statusLabel(rowData)}
2363
2945
  </span>
2364
2946
  )}
2365
2947
  style={{ minWidth: 150 }}
2366
2948
  />
2949
+ <Column
2950
+ header="Primary Key"
2951
+ body={(rowData: DatasourceIntrospectionTableRecord) => (
2952
+ rowData.hasPrimaryKey
2953
+ ? rowData.primaryKeyColumnCount > 1
2954
+ ? `${rowData.primaryKeyColumnCount} columns`
2955
+ : rowData.primaryKeyColumnNames[0] ?? "1 column"
2956
+ : <span className="sdix-muted-cell">Missing</span>
2957
+ )}
2958
+ style={{ minWidth: 160 }}
2959
+ />
2367
2960
  <Column
2368
2961
  header="Superclass"
2369
2962
  body={(rowData: DatasourceIntrospectionTableRecord) => rowData.suggestedBaseClassName}
@@ -2427,7 +3020,7 @@ npx @solidxai/solidctl@latest seed --modules-to-seed ${bootstrap?.module?.name |
2427
3020
  ) : drawerLoadingError ? (
2428
3021
  <div className="sdix-error">
2429
3022
  <span>{readQueryError(drawerLoadingError)}</span>
2430
- <SolidButton size="small" variant="outline" onClick={() => void refetchTableDetail()}>
3023
+ <SolidButton size="small" variant="outline" onClick={() => selectedTable && void loadWorkspaceItem(selectedTable)}>
2431
3024
  Retry
2432
3025
  </SolidButton>
2433
3026
  </div>
@@ -2438,6 +3031,59 @@ npx @solidxai/solidctl@latest seed --modules-to-seed ${bootstrap?.module?.name |
2438
3031
  </div>
2439
3032
  ) : (
2440
3033
  <div className="sdix-wizard-shell">
3034
+ <div className="sdix-workspace-body">
3035
+ <aside className="sdix-workspace-rail">
3036
+ <div className="sdix-workspace-rail__header">
3037
+ <strong>Selected tables</strong>
3038
+ <span>{workspaceRecords.length}</span>
3039
+ </div>
3040
+ <div className="sdix-workspace-rail__list">
3041
+ {workspaceTableKeys.map((key) => {
3042
+ const item = workspaceItems[key];
3043
+ if (!item) return null;
3044
+ const isActive = key === activeWorkspaceTableKey;
3045
+ const status =
3046
+ item.migrationStatus.state !== "idle" ? item.migrationStatus
3047
+ : item.saveStatus.state !== "idle" ? item.saveStatus
3048
+ : item.reviewStatus.state !== "idle" ? item.reviewStatus
3049
+ : item.loadStatus;
3050
+
3051
+ return (
3052
+ <button
3053
+ key={key}
3054
+ ref={(node) => {
3055
+ workspaceRailItemRefs.current[key] = node;
3056
+ }}
3057
+ type="button"
3058
+ className={`sdix-workspace-rail__item${isActive ? " is-active" : ""}`}
3059
+ onClick={() => {
3060
+ setActiveWorkspaceTableKey(key);
3061
+ if (workspaceStep === "configure") {
3062
+ setDrawerTab("general-info");
3063
+ }
3064
+ }}
3065
+ >
3066
+ <div className="sdix-workspace-rail__item-main">
3067
+ <strong>{item.record.tableName}</strong>
3068
+ <span>{item.record.schema || "dbo"}</span>
3069
+ </div>
3070
+ <div className="sdix-workspace-rail__item-status">
3071
+ {status.state === "loading" ? <SolidSpinner size={12} /> : null}
3072
+ <span className={`sdix-workspace-rail__status sdix-workspace-rail__status--${status.state}`}>
3073
+ {status.state === "success"
3074
+ ? "Ready"
3075
+ : status.state === "error"
3076
+ ? "Needs attention"
3077
+ : status.message}
3078
+ </span>
3079
+ </div>
3080
+ </button>
3081
+ );
3082
+ })}
3083
+ </div>
3084
+ </aside>
3085
+
3086
+ <div className="sdix-workspace-main">
2441
3087
  <div className="sdix-wizard-stepper">
2442
3088
  {stepperItems.map((step, index) => (
2443
3089
  <button
@@ -2465,6 +3111,8 @@ npx @solidxai/solidctl@latest seed --modules-to-seed ${bootstrap?.module?.name |
2465
3111
  {workspaceContent}
2466
3112
  </div>
2467
3113
  </div>
3114
+ </div>
3115
+ </div>
2468
3116
 
2469
3117
  {drawerDraft ? (
2470
3118
  <div className="sdix-wizard-footer">
@@ -2554,45 +3202,51 @@ npx @solidxai/solidctl@latest seed --modules-to-seed ${bootstrap?.module?.name |
2554
3202
  setFieldConfigColumnName(null);
2555
3203
  }
2556
3204
  }}
2557
- style={{ width: "min(1080px, calc(100vw - 48px))" }}
3205
+ style={{ width: "min(760px, calc(100vw - 64px))" }}
2558
3206
  >
2559
3207
  <SolidDialogHeader>
2560
3208
  <div>
2561
3209
  <SolidDialogTitle>Field configuration</SolidDialogTitle>
2562
3210
  <SolidDialogDescription>
2563
- Reuse the standard field form here to fine-tune the mapped SolidX field before saving the introspection draft.
3211
+ Configure how this mapped field should work in SolidX before saving it to the mapping draft.
2564
3212
  </SolidDialogDescription>
2565
3213
  </div>
2566
3214
  <SolidDialogClose />
2567
3215
  </SolidDialogHeader>
2568
3216
  <SolidDialogSeparator />
2569
3217
  <SolidDialogBody>
2570
- {activeFieldConfigColumn && activeFieldConfigMetaData && drawerDraft ? (
2571
- <FieldMetaDataForm
2572
- modelMetaData={{
2573
- singularName: drawerDraft.singularName,
2574
- pluralName: drawerDraft.pluralName,
2575
- }}
2576
- fieldMetaData={activeFieldConfigMetaData}
2577
- setFieldMetaData={() => undefined}
2578
- setVisiblePopup={() => undefined}
2579
- setIsDirty={() => undefined}
2580
- allFields={fieldDraftAllFields}
2581
- deleteModelFunction={() => undefined}
2582
- params={params}
2583
- setIsRequiredPopUp={() => undefined}
2584
- showToaster={showFieldDraftToaster}
2585
- onDraftSubmit={(nextFieldConfig: Record<string, any>) => applyFieldConfigDraft(activeFieldConfigColumn.columnName, nextFieldConfig)}
2586
- onClose={() => setFieldConfigColumnName(null)}
2587
- availableFieldTypes={scalarFieldTypeOptions.map((option) => option.value)}
2588
- forceShowTypeSelector
2589
- selectorRequireContinue
2590
- selectorContinueLabel="Next"
2591
- selectorInitialFieldType={activeFieldConfigColumn.solidFieldType}
2592
- disableIdentityEditingForExisting={false}
2593
- submitLabel="Save field config"
2594
- />
2595
- ) : null}
3218
+ <div className="sdix-field-config-dialog">
3219
+ {activeFieldConfigColumn && activeFieldConfigMetaData && drawerDraft ? (
3220
+ <FieldMetaDataForm
3221
+ modelMetaData={{
3222
+ singularName: drawerDraft.singularName,
3223
+ pluralName: drawerDraft.pluralName,
3224
+ dataSourceType: drawerDraft.dataSourceType,
3225
+ module: {
3226
+ name: bootstrap?.module?.name,
3227
+ displayName: bootstrap?.module?.displayName,
3228
+ },
3229
+ }}
3230
+ fieldMetaData={activeFieldConfigMetaData}
3231
+ setFieldMetaData={() => undefined}
3232
+ setVisiblePopup={() => undefined}
3233
+ setIsDirty={() => undefined}
3234
+ allFields={fieldDraftAllFields}
3235
+ deleteModelFunction={() => undefined}
3236
+ params={params}
3237
+ setIsRequiredPopUp={() => undefined}
3238
+ showToaster={showFieldDraftToaster}
3239
+ onDraftSubmit={(nextFieldConfig: Record<string, any>) => applyFieldConfigDraft(activeFieldConfigColumn.columnName, nextFieldConfig)}
3240
+ onClose={() => setFieldConfigColumnName(null)}
3241
+ forceShowTypeSelector
3242
+ selectorRequireContinue
3243
+ selectorContinueLabel="Next"
3244
+ selectorInitialFieldType={activeFieldConfigColumn.solidFieldType}
3245
+ disableIdentityEditingForExisting={false}
3246
+ submitLabel="Save field config"
3247
+ />
3248
+ ) : null}
3249
+ </div>
2596
3250
  </SolidDialogBody>
2597
3251
  </SolidDialog>
2598
3252