@solidxai/core-ui 0.1.12-beta.3 → 0.1.12-beta.6

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 (86) hide show
  1. package/dist/components/auth/AuthLayout.d.ts.map +1 -1
  2. package/dist/components/auth/AuthLayout.js +12 -5
  3. package/dist/components/auth/AuthLayout.js.map +1 -1
  4. package/dist/components/auth/AuthLayout.tsx +15 -4
  5. package/dist/components/auth/AuthSettingsContext.js +1 -1
  6. package/dist/components/auth/AuthSettingsContext.js.map +1 -1
  7. package/dist/components/auth/AuthSettingsContext.tsx +1 -1
  8. package/dist/components/core/chatter/SolidChatterHeader.d.ts.map +1 -1
  9. package/dist/components/core/chatter/SolidChatterHeader.js +18 -4
  10. package/dist/components/core/chatter/SolidChatterHeader.js.map +1 -1
  11. package/dist/components/core/chatter/SolidChatterHeader.tsx +17 -3
  12. package/dist/components/core/common/SolidGenericImport/SolidImportTransaction.d.ts.map +1 -1
  13. package/dist/components/core/common/SolidGenericImport/SolidImportTransaction.js +12 -2
  14. package/dist/components/core/common/SolidGenericImport/SolidImportTransaction.js.map +1 -1
  15. package/dist/components/core/common/SolidGenericImport/SolidImportTransaction.tsx +12 -1
  16. package/dist/components/core/extension/solid-core/metadata/MetadataExplorerFormWidget.d.ts +2 -0
  17. package/dist/components/core/extension/solid-core/metadata/MetadataExplorerFormWidget.d.ts.map +1 -1
  18. package/dist/components/core/extension/solid-core/metadata/MetadataExplorerFormWidget.js +4 -2
  19. package/dist/components/core/extension/solid-core/metadata/MetadataExplorerFormWidget.js.map +1 -1
  20. package/dist/components/core/extension/solid-core/metadata/MetadataExplorerFormWidget.tsx +11 -3
  21. package/dist/components/core/form/SolidFormView.js +1 -1
  22. package/dist/components/core/form/SolidFormView.js.map +1 -1
  23. package/dist/components/core/form/SolidFormView.tsx +1 -1
  24. package/dist/components/core/form/fields/relations/RolesGroupedByModuleWidget.d.ts.map +1 -1
  25. package/dist/components/core/form/fields/relations/RolesGroupedByModuleWidget.js +11 -1
  26. package/dist/components/core/form/fields/relations/RolesGroupedByModuleWidget.js.map +1 -1
  27. package/dist/components/core/form/fields/relations/RolesGroupedByModuleWidget.tsx +26 -19
  28. package/dist/components/core/form/fields/relations/SolidRelationManyToOneField.d.ts.map +1 -1
  29. package/dist/components/core/form/fields/relations/SolidRelationManyToOneField.js +13 -5
  30. package/dist/components/core/form/fields/relations/SolidRelationManyToOneField.js.map +1 -1
  31. package/dist/components/core/form/fields/relations/SolidRelationManyToOneField.tsx +16 -9
  32. package/dist/components/core/json/SolidJsonEditor.css +7 -0
  33. package/dist/components/core/list/SolidDataTable.d.ts.map +1 -1
  34. package/dist/components/core/list/SolidDataTable.js +2 -2
  35. package/dist/components/core/list/SolidDataTable.js.map +1 -1
  36. package/dist/components/core/list/SolidDataTable.tsx +2 -1
  37. package/dist/components/core/list/SolidListView.d.ts.map +1 -1
  38. package/dist/components/core/list/SolidListView.js +58 -58
  39. package/dist/components/core/list/SolidListView.js.map +1 -1
  40. package/dist/components/core/list/SolidListView.tsx +5 -4
  41. package/dist/components/core/model/FieldMetaDataForm.d.ts +1 -1
  42. package/dist/components/core/model/FieldMetaDataForm.d.ts.map +1 -1
  43. package/dist/components/core/model/FieldMetaDataForm.js +119 -102
  44. package/dist/components/core/model/FieldMetaDataForm.js.map +1 -1
  45. package/dist/components/core/model/FieldMetaDataForm.tsx +82 -39
  46. package/dist/components/core/model/FieldSelector.d.ts +5 -1
  47. package/dist/components/core/model/FieldSelector.d.ts.map +1 -1
  48. package/dist/components/core/model/FieldSelector.js +25 -6
  49. package/dist/components/core/model/FieldSelector.js.map +1 -1
  50. package/dist/components/core/model/FieldSelector.tsx +50 -5
  51. package/dist/components/core/module/CreateModule.css +60 -6
  52. package/dist/components/core/module/CreateModule.d.ts.map +1 -1
  53. package/dist/components/core/module/CreateModule.js +2 -2
  54. package/dist/components/core/module/CreateModule.js.map +1 -1
  55. package/dist/components/core/module/CreateModule.tsx +56 -37
  56. package/dist/components/layout/AdminTopHeader.js +1 -1
  57. package/dist/components/layout/AdminTopHeader.js.map +1 -1
  58. package/dist/components/layout/AdminTopHeader.tsx +2 -2
  59. package/dist/components/shad-cn-ui/SolidSelect.d.ts +2 -1
  60. package/dist/components/shad-cn-ui/SolidSelect.d.ts.map +1 -1
  61. package/dist/components/shad-cn-ui/SolidSelect.js +12 -4
  62. package/dist/components/shad-cn-ui/SolidSelect.js.map +1 -1
  63. package/dist/components/shad-cn-ui/SolidSelect.tsx +13 -1
  64. package/dist/helpers/modelViewPersistence.d.ts.map +1 -1
  65. package/dist/helpers/modelViewPersistence.js +3 -2
  66. package/dist/helpers/modelViewPersistence.js.map +1 -1
  67. package/dist/helpers/modelViewPersistence.ts +3 -2
  68. package/dist/redux/api/datasourceIntrospectionApi.d.ts +43 -1
  69. package/dist/redux/api/datasourceIntrospectionApi.d.ts.map +1 -1
  70. package/dist/redux/api/datasourceIntrospectionApi.js +34 -1
  71. package/dist/redux/api/datasourceIntrospectionApi.js.map +1 -1
  72. package/dist/redux/api/datasourceIntrospectionApi.ts +61 -0
  73. package/dist/redux/store/defaultStoreConfig.d.ts +11 -0
  74. package/dist/redux/store/defaultStoreConfig.d.ts.map +1 -1
  75. package/dist/resources/solid-custom.css +3 -86
  76. package/dist/resources/solid-tailwind-generated.css +1 -1
  77. package/dist/resources/themes/solid-dark-purple/theme-variables.css +226 -0
  78. package/dist/resources/themes/solid-dark-purple/theme.css +1 -185
  79. package/dist/resources/themes/solid-light-purple/theme-variables.css +237 -0
  80. package/dist/resources/themes/solid-light-purple/theme.css +1 -206
  81. package/dist/routes/pages/admin/core/DatasourceIntrospectionPage.css +396 -31
  82. package/dist/routes/pages/admin/core/DatasourceIntrospectionPage.d.ts.map +1 -1
  83. package/dist/routes/pages/admin/core/DatasourceIntrospectionPage.js +680 -152
  84. package/dist/routes/pages/admin/core/DatasourceIntrospectionPage.js.map +1 -1
  85. package/dist/routes/pages/admin/core/DatasourceIntrospectionPage.tsx +1358 -441
  86. package/package.json +1 -1
@@ -1,4 +1,6 @@
1
1
  import {
2
+ ArrowLeft,
3
+ Check,
2
4
  Cog,
3
5
  Database,
4
6
  ExternalLink,
@@ -11,10 +13,12 @@ import {
11
13
  import { camelCase, startCase } from "lodash";
12
14
  import { useEffect, useMemo, useRef, useState } from "react";
13
15
  import { useDispatch } from "react-redux";
14
- import { useParams } from "react-router-dom";
16
+ import { useNavigate, useParams } from "react-router-dom";
15
17
  import { showToast } from "../../../../redux/features/toastSlice";
16
18
  import { SolidJsonEditor } from "../../../../components/core/json/SolidJsonEditor";
17
19
  import { Column, SolidDataTable } from "../../../../components/core/list/SolidDataTable";
20
+ import FieldMetaDataForm from "../../../../components/core/model/FieldMetaDataForm";
21
+ import { waitForBackendAvailability } from "../../../../helpers/waitForBackendAvailability";
18
22
  import {
19
23
  SolidButton,
20
24
  SolidCheckbox,
@@ -38,11 +42,16 @@ import {
38
42
  type DatasourceIntrospectionMappingPreview,
39
43
  type DatasourceIntrospectionMappingRequest,
40
44
  type DatasourceIntrospectionTableRecord,
45
+ type DatasourceIntrospectionApplyResponse,
46
+ type DatasourceIntrospectionCreateMigrationResponse,
41
47
  useApplyDatasourceIntrospectionMappingMutation,
48
+ useCreateDatasourceIntrospectionMigrationArtifactsMutation,
49
+ useGenerateDatasourceIntrospectionCodeMutation,
42
50
  useGetDatasourceIntrospectionBootstrapQuery,
43
51
  useGetDatasourceIntrospectionTableDetailQuery,
44
52
  useGetDatasourceIntrospectionTablesQuery,
45
53
  usePreviewDatasourceIntrospectionMappingMutation,
54
+ useRunDatasourceIntrospectionMigrationMutation,
46
55
  } from "../../../../redux/api/datasourceIntrospectionApi";
47
56
  import { useGetmodelByIdQuery } from "../../../../redux/api/modelApi";
48
57
  import "./DatasourceIntrospectionPage.css";
@@ -52,9 +61,17 @@ type SessionSettings = {
52
61
  prioritizeUnmappedTables: boolean;
53
62
  };
54
63
 
64
+ type WorkspaceStep =
65
+ | "configure"
66
+ | "review-json"
67
+ | "generate-code"
68
+ | "create-migrations"
69
+ | "run-migration";
70
+
55
71
  type DrawerColumnDraft = {
56
72
  columnName: string;
57
73
  fieldName: string;
74
+ displayName: string;
58
75
  dataType: string;
59
76
  solidFieldType: string;
60
77
  ormType: string;
@@ -65,12 +82,14 @@ type DrawerColumnDraft = {
65
82
  numericScale: number | null;
66
83
  ordinalPosition: number | null;
67
84
  isAutoGenerated: boolean;
85
+ isDetectedPrimaryKey: boolean;
68
86
  handledBySuperclass: boolean;
69
87
  superclassFieldName: string | null;
70
88
  fieldId: number | null;
71
89
  include: boolean;
72
90
  matched: boolean;
73
91
  isPrimaryKey: boolean;
92
+ fieldConfig: Record<string, any> | null;
74
93
  };
75
94
 
76
95
  type DrawerDraft = {
@@ -121,6 +140,49 @@ const scalarFieldTypeOptions = [
121
140
  ];
122
141
 
123
142
  const scalarFieldTypeValueSet = new Set(scalarFieldTypeOptions.map((option) => option.value));
143
+ const mappingStrategyOptions = [
144
+ { value: "generated_id", label: "Legacy generated id" },
145
+ { value: "existing_id", label: "Legacy existing id" },
146
+ ];
147
+
148
+ function getBaseClassNameForLegacyType(value: "none" | "existing_id" | "generated_id") {
149
+ return value === "existing_id"
150
+ ? "LegacyCommonEntityWithExistingId"
151
+ : "LegacyCommonEntityWithGeneratedId";
152
+ }
153
+
154
+ function getSuperclassFieldNameForLegacyType(columnName: string, legacyTableType: "none" | "existing_id" | "generated_id") {
155
+ const normalized = columnName.toLowerCase();
156
+ const legacyFieldMap: Record<string, string> = {
157
+ ss_created_at: "createdAt",
158
+ ss_updated_at: "updatedAt",
159
+ ss_deleted_at: "deletedAt",
160
+ ss_deleted_tracker: "deletedTracker",
161
+ ss_published_at: "publishedAt",
162
+ ss_locale_name: "localeName",
163
+ ss_default_entity_locale_id: "defaultEntityLocaleId",
164
+ ss_created_by_id: "createdBy",
165
+ ss_updated_by_id: "updatedBy",
166
+ };
167
+
168
+ if (normalized === "ss_id" && legacyTableType === "generated_id") {
169
+ return "id";
170
+ }
171
+
172
+ return legacyFieldMap[normalized] ?? null;
173
+ }
174
+
175
+ function getSuggestedFieldNameForLegacyType(column: Pick<DrawerColumnDraft, "columnName" | "isAutoGenerated">, legacyTableType: "none" | "existing_id" | "generated_id") {
176
+ if (legacyTableType === "existing_id" && column.isAutoGenerated) {
177
+ return "id";
178
+ }
179
+
180
+ if (legacyTableType === "generated_id" && column.columnName.toLowerCase() === "id") {
181
+ return "legacyId";
182
+ }
183
+
184
+ return camelCase(column.columnName.replace(/^ss_/i, ""));
185
+ }
124
186
 
125
187
  function getDefaultOrmTypeForSolidFieldType(dataSourceType: string, solidFieldType: string) {
126
188
  const normalizedDatasourceType = dataSourceType.toLowerCase();
@@ -179,12 +241,6 @@ function getDefaultOrmTypeForSolidFieldType(dataSourceType: string, solidFieldTy
179
241
  return ormTypesByDatasource[normalizedDatasourceType]?.[solidFieldType] ?? "varchar";
180
242
  }
181
243
 
182
- function legacyTypeLabel(value: string) {
183
- if (value === "generated_id") return "Legacy generated id";
184
- if (value === "existing_id") return "Legacy existing id";
185
- return "Legacy generated id";
186
- }
187
-
188
244
  function statusLabel(record: DatasourceIntrospectionTableRecord) {
189
245
  return record.mapped ? "Mapped" : "Ready to map";
190
246
  }
@@ -198,6 +254,30 @@ function readQueryError(error: any) {
198
254
  );
199
255
  }
200
256
 
257
+ function isRecoverableGenerateCodeError(error: any) {
258
+ const status = error?.status ?? error?.originalStatus;
259
+ const message = [
260
+ error?.error,
261
+ error?.message,
262
+ error?.data?.message,
263
+ error?.data?.error,
264
+ ]
265
+ .filter(Boolean)
266
+ .join(" ")
267
+ .toLowerCase();
268
+
269
+ if (status === "FETCH_ERROR" || status === "TIMEOUT_ERROR" || status === "PARSING_ERROR") {
270
+ return true;
271
+ }
272
+
273
+ return (
274
+ message.includes("failed to fetch")
275
+ || message.includes("network")
276
+ || message.includes("load failed")
277
+ || message.includes("fetch")
278
+ );
279
+ }
280
+
201
281
  function normalizeName(value: string) {
202
282
  return value.trim().toLowerCase();
203
283
  }
@@ -249,6 +329,7 @@ function buildDrawerDraft(
249
329
  return {
250
330
  columnName: column.columnName,
251
331
  fieldName: matchingField?.name ?? column.fieldName,
332
+ displayName: matchingField?.displayName ?? column.displayName,
252
333
  dataType: column.dataType,
253
334
  solidFieldType: matchingField?.type ?? column.solidFieldType,
254
335
  ormType: matchingField?.ormType ?? column.ormType,
@@ -259,12 +340,14 @@ function buildDrawerDraft(
259
340
  numericScale: column.numericScale,
260
341
  ordinalPosition: column.ordinalPosition,
261
342
  isAutoGenerated: column.isAutoGenerated,
343
+ isDetectedPrimaryKey: column.isPrimaryKey,
262
344
  handledBySuperclass: column.handledBySuperclass,
263
345
  superclassFieldName: column.superclassFieldName,
264
346
  fieldId: matchingField?.id ?? null,
265
347
  include: column.handledBySuperclass ? false : Boolean(matchingField) || !selectedTable.mapped,
266
348
  matched: Boolean(matchingField),
267
- isPrimaryKey: Boolean(matchingField?.isPrimaryKey) || (column.isAutoGenerated && column.fieldName === "id"),
349
+ isPrimaryKey: Boolean(matchingField?.isPrimaryKey) || column.isPrimaryKey || (column.isAutoGenerated && column.fieldName === "id"),
350
+ fieldConfig: matchingField ? sanitizeExistingFieldPayload(matchingField) : null,
268
351
  };
269
352
  });
270
353
 
@@ -319,6 +402,11 @@ function getDrawerValidationErrors(draft: DrawerDraft) {
319
402
  continue;
320
403
  }
321
404
 
405
+ if (!column.displayName.trim()) {
406
+ errors.push(`Field label is required for column "${column.columnName}".`);
407
+ continue;
408
+ }
409
+
322
410
  if (!scalarFieldTypeValueSet.has(column.solidFieldType)) {
323
411
  errors.push(`SolidX type "${column.solidFieldType}" is not allowed in step 1 for column "${column.columnName}".`);
324
412
  continue;
@@ -377,6 +465,7 @@ function buildMappingRequest(
377
465
  columns: draft.columns.map((column) => ({
378
466
  columnName: column.columnName,
379
467
  fieldName: column.fieldName,
468
+ displayName: column.displayName,
380
469
  dataType: column.dataType,
381
470
  solidFieldType: column.solidFieldType,
382
471
  ormType: column.ormType,
@@ -393,6 +482,7 @@ function buildMappingRequest(
393
482
  include: column.include,
394
483
  matched: column.matched,
395
484
  isPrimaryKey: column.isPrimaryKey,
485
+ fieldConfig: column.fieldConfig,
396
486
  })),
397
487
  };
398
488
  }
@@ -541,6 +631,7 @@ function parseAndValidateReviewedModelJson(
541
631
 
542
632
  export function DatasourceIntrospectionPage() {
543
633
  const params = useParams();
634
+ const navigate = useNavigate();
544
635
  const dispatch = useDispatch();
545
636
  const moduleId = Number(params.moduleId ?? 0);
546
637
  const isValidModuleId = Number.isFinite(moduleId) && moduleId > 0;
@@ -556,21 +647,31 @@ export function DatasourceIntrospectionPage() {
556
647
  });
557
648
 
558
649
  const [selectedDatasource, setSelectedDatasource] = useState("");
559
- const [selectedTableKey, setSelectedTableKey] = useState("");
650
+ const [selectedRows, setSelectedRows] = useState<DatasourceIntrospectionTableRecord[]>([]);
651
+ const [workspaceTableKey, setWorkspaceTableKey] = useState("");
560
652
  const [tableSearch, setTableSearch] = useState("");
561
653
  const [settingsOpen, setSettingsOpen] = useState(false);
562
654
  const [settingsTab, setSettingsTab] = useState("datasource-info");
563
- const [drawerOpen, setDrawerOpen] = useState(false);
564
655
  const [drawerDraft, setDrawerDraft] = useState<DrawerDraft | null>(null);
565
656
  const [drawerTab, setDrawerTab] = useState("general-info");
566
- const [drawerStep, setDrawerStep] = useState<"edit" | "review">("edit");
567
- const [reviewTab, setReviewTab] = useState("metadata-json");
657
+ const [workspaceStep, setWorkspaceStep] = useState<WorkspaceStep>("configure");
568
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
+ const [generateCodeOutput, setGenerateCodeOutput] = useState("");
662
+ const [isGeneratingCodeWorkflow, setIsGeneratingCodeWorkflow] = useState(false);
663
+ const [generateCodeStatusMessage, setGenerateCodeStatusMessage] = useState("");
664
+ const [generateCodeTimeoutMessage, setGenerateCodeTimeoutMessage] = useState<string | null>(null);
665
+ const [generateCodeReady, setGenerateCodeReady] = useState(false);
666
+ const [hasAttemptedGenerateCode, setHasAttemptedGenerateCode] = useState(false);
667
+ const [runMigrationOutput, setRunMigrationOutput] = useState("");
668
+ const [runMigrationCompleted, setRunMigrationCompleted] = useState(false);
569
669
  const [reviewedMetadataJsonValue, setReviewedMetadataJsonValue] = useState<any>(null);
570
670
  const [metadataJsonText, setMetadataJsonText] = useState("");
571
671
  const [metadataJsonValidationError, setMetadataJsonValidationError] = useState<string | null>(null);
572
672
  const [metadataJsonResetToken, setMetadataJsonResetToken] = useState("initial");
573
673
  const [drawerBootstrapping, setDrawerBootstrapping] = useState(false);
674
+ const [fieldConfigColumnName, setFieldConfigColumnName] = useState<string | null>(null);
574
675
  const [sessionSettings, setSessionSettings] = useState<SessionSettings>({
575
676
  relationInferenceEnabled: false,
576
677
  prioritizeUnmappedTables: true,
@@ -578,15 +679,6 @@ export function DatasourceIntrospectionPage() {
578
679
  const [draftSessionSettings, setDraftSessionSettings] = useState<SessionSettings>(sessionSettings);
579
680
  const drawerBootstrapTimerRef = useRef<number | null>(null);
580
681
 
581
- const scrollDrawerBodyToTop = () => {
582
- if (typeof document === "undefined") return;
583
-
584
- window.requestAnimationFrame(() => {
585
- const drawerBody = document.querySelector<HTMLDivElement>(".sdix-drawer-body");
586
- drawerBody?.scrollTo({ top: 0, left: 0 });
587
- });
588
- };
589
-
590
682
  const [
591
683
  previewMapping,
592
684
  { isLoading: isPreviewMappingLoading },
@@ -596,8 +688,32 @@ export function DatasourceIntrospectionPage() {
596
688
  applyMapping,
597
689
  { isLoading: isApplyMappingLoading },
598
690
  ] = useApplyDatasourceIntrospectionMappingMutation();
691
+ const [
692
+ createMigrationArtifacts,
693
+ { isLoading: isCreateMigrationArtifactsLoading },
694
+ ] = useCreateDatasourceIntrospectionMigrationArtifactsMutation();
695
+ const [
696
+ generateCode,
697
+ { isLoading: isGenerateCodeLoading },
698
+ ] = useGenerateDatasourceIntrospectionCodeMutation();
699
+ const [
700
+ runMigration,
701
+ { isLoading: isRunMigrationLoading },
702
+ ] = useRunDatasourceIntrospectionMigrationMutation();
599
703
 
600
704
  const isSavingMapping = isApplyMappingLoading;
705
+ const isCreatingMigrationArtifacts = isCreateMigrationArtifactsLoading;
706
+
707
+ const showFieldDraftToaster = (message: any, severity: any) => {
708
+ const errorMessages = Object.values(message ?? {});
709
+ if (!errorMessages.length) return;
710
+ dispatch(showToast({
711
+ severity,
712
+ summary: "Datasource introspection",
713
+ detail: errorMessages.map(String).join(", "),
714
+ life: 3000,
715
+ }));
716
+ };
601
717
 
602
718
  useEffect(() => {
603
719
  if (!bootstrap) return;
@@ -608,13 +724,23 @@ export function DatasourceIntrospectionPage() {
608
724
  }, [bootstrap, selectedDatasource]);
609
725
 
610
726
  useEffect(() => {
611
- setSelectedTableKey("");
727
+ setSelectedRows([]);
728
+ setWorkspaceTableKey("");
612
729
  setDrawerDraft(null);
613
- setDrawerOpen(false);
730
+ setFieldConfigColumnName(null);
614
731
  setTableSearch("");
615
- setDrawerStep("edit");
616
- setReviewTab("metadata-json");
732
+ setWorkspaceStep("configure");
617
733
  setMappingPreview(null);
734
+ setPersistedMappingResult(null);
735
+ setMigrationArtifactsResult(null);
736
+ setGenerateCodeOutput("");
737
+ setIsGeneratingCodeWorkflow(false);
738
+ setGenerateCodeStatusMessage("");
739
+ setGenerateCodeTimeoutMessage(null);
740
+ setGenerateCodeReady(false);
741
+ setHasAttemptedGenerateCode(false);
742
+ setRunMigrationOutput("");
743
+ setRunMigrationCompleted(false);
618
744
  setReviewedMetadataJsonValue(null);
619
745
  setMetadataJsonText("");
620
746
  setMetadataJsonValidationError(null);
@@ -627,11 +753,6 @@ export function DatasourceIntrospectionPage() {
627
753
  }
628
754
  }, []);
629
755
 
630
- useEffect(() => {
631
- if (!drawerOpen) return;
632
- scrollDrawerBodyToTop();
633
- }, [drawerOpen, drawerStep, drawerTab, reviewTab, selectedTableKey]);
634
-
635
756
  const datasourceOptions = useMemo(
636
757
  () => (bootstrap?.datasources ?? []).map((datasource) => ({
637
758
  label: `${datasource.displayName} (${providerLabel(datasource.type)})`,
@@ -653,9 +774,13 @@ export function DatasourceIntrospectionPage() {
653
774
  },
654
775
  );
655
776
 
777
+ const selectedListRow = selectedRows[0] ?? null;
778
+ const selectedListRowKey = selectedListRow ? `${selectedListRow.schema ?? ""}::${selectedListRow.tableName}` : "";
779
+ const isWorkspaceMode = Boolean(workspaceTableKey);
780
+
656
781
  const selectedTable = useMemo(
657
- () => tablesResponse?.records?.find((record) => `${record.schema ?? ""}::${record.tableName}` === selectedTableKey) ?? null,
658
- [tablesResponse, selectedTableKey],
782
+ () => tablesResponse?.records?.find((record) => `${record.schema ?? ""}::${record.tableName}` === workspaceTableKey) ?? null,
783
+ [tablesResponse, workspaceTableKey],
659
784
  );
660
785
 
661
786
  const {
@@ -672,7 +797,7 @@ export function DatasourceIntrospectionPage() {
672
797
  schema: selectedTable?.schema ?? undefined,
673
798
  },
674
799
  {
675
- skip: !drawerOpen || !isValidModuleId || !selectedDatasource || !selectedTable || !!tablesResponse?.synchronizeBlocked,
800
+ skip: !isWorkspaceMode || !isValidModuleId || !selectedDatasource || !selectedTable || !!tablesResponse?.synchronizeBlocked,
676
801
  },
677
802
  );
678
803
 
@@ -682,11 +807,63 @@ export function DatasourceIntrospectionPage() {
682
807
  isFetching: isMappedModelFetching,
683
808
  error: mappedModelError,
684
809
  } = useGetmodelByIdQuery(selectedTable?.mappedModelId ?? 0, {
685
- skip: !drawerOpen || !selectedTable?.mapped || !selectedTable?.mappedModelId,
810
+ skip: !isWorkspaceMode || !selectedTable?.mapped || !selectedTable?.mappedModelId,
686
811
  });
687
812
 
688
813
  const mappedModelData = mappedModelResponse?.data;
689
814
  const selectedDatasourceRecord = bootstrap?.datasources?.find((datasource) => datasource.name === selectedDatasource) ?? null;
815
+ const activeFieldConfigColumn = drawerDraft?.columns.find((column) => column.columnName === fieldConfigColumnName) ?? null;
816
+ const activeFieldConfigMetaData = useMemo(() => {
817
+ if (!drawerDraft || !activeFieldConfigColumn) {
818
+ return null;
819
+ }
820
+
821
+ const configured = activeFieldConfigColumn.fieldConfig ?? {};
822
+ const suggestedMax =
823
+ activeFieldConfigColumn.characterMaximumLength && ["shortText", "longText"].includes(activeFieldConfigColumn.solidFieldType)
824
+ ? activeFieldConfigColumn.characterMaximumLength
825
+ : configured.max;
826
+
827
+ return {
828
+ ...configured,
829
+ ...(activeFieldConfigColumn.fieldId ? { id: activeFieldConfigColumn.fieldId } : {}),
830
+ name: activeFieldConfigColumn.fieldName,
831
+ displayName: activeFieldConfigColumn.displayName,
832
+ description: configured.description ?? activeFieldConfigColumn.displayName,
833
+ type: activeFieldConfigColumn.solidFieldType,
834
+ ormType: activeFieldConfigColumn.ormType,
835
+ columnName: activeFieldConfigColumn.columnName,
836
+ defaultValue: configured.defaultValue ?? activeFieldConfigColumn.defaultValue ?? undefined,
837
+ required: configured.required ?? (!activeFieldConfigColumn.nullable || activeFieldConfigColumn.isPrimaryKey),
838
+ unique: configured.unique ?? false,
839
+ encrypt: configured.encrypt ?? false,
840
+ index: configured.index ?? false,
841
+ max: suggestedMax ?? undefined,
842
+ min: configured.min ?? undefined,
843
+ private: configured.private ?? false,
844
+ isSystem: false,
845
+ isPrimaryKey: activeFieldConfigColumn.isPrimaryKey,
846
+ isUserKey: drawerDraft.userKeyField === activeFieldConfigColumn.fieldName,
847
+ enableAuditTracking: configured.enableAuditTracking ?? drawerDraft.enableAuditTracking,
848
+ isMultiSelect: configured.isMultiSelect ?? false,
849
+ length: configured.length ?? activeFieldConfigColumn.characterMaximumLength ?? undefined,
850
+ };
851
+ }, [activeFieldConfigColumn, drawerDraft]);
852
+ const fieldDraftAllFields = useMemo(() => {
853
+ if (!drawerDraft || !activeFieldConfigColumn) {
854
+ return [];
855
+ }
856
+
857
+ return drawerDraft.columns
858
+ .filter((column) => column.columnName !== activeFieldConfigColumn.columnName)
859
+ .map((column) => ({
860
+ id: column.fieldId ?? undefined,
861
+ name: column.fieldName,
862
+ displayName: column.displayName,
863
+ type: column.solidFieldType,
864
+ columnName: column.columnName,
865
+ }));
866
+ }, [activeFieldConfigColumn, drawerDraft]);
690
867
  const drawerLoadingError = selectedTable?.mapped ? (tableDetailError || mappedModelError) : tableDetailError;
691
868
  const isDrawerBlockingLoading = Boolean(
692
869
  selectedTable
@@ -700,7 +877,45 @@ export function DatasourceIntrospectionPage() {
700
877
  );
701
878
 
702
879
  useEffect(() => {
703
- if (!drawerOpen || !selectedTable || !tableDetail) return;
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;
704
919
  if (selectedTable.mapped && (!mappedModelData || mappedModelData.id !== selectedTable.mappedModelId)) return;
705
920
 
706
921
  if (drawerBootstrapTimerRef.current) {
@@ -715,7 +930,7 @@ export function DatasourceIntrospectionPage() {
715
930
  drawerBootstrapTimerRef.current = window.setTimeout(() => {
716
931
  setDrawerBootstrapping(false);
717
932
  }, 150);
718
- }, [drawerOpen, selectedTable, tableDetail, mappedModelData, mappedModelResponse]);
933
+ }, [isWorkspaceMode, selectedTable, tableDetail, mappedModelData, mappedModelResponse]);
719
934
 
720
935
  const filteredRecords = useMemo(() => {
721
936
  const records = [...(tablesResponse?.records ?? [])];
@@ -723,7 +938,7 @@ export function DatasourceIntrospectionPage() {
723
938
 
724
939
  const filtered = searchNeedle
725
940
  ? records.filter((record) => {
726
- const haystack = `${record.tableName} ${record.schema ?? ""} ${record.mappedModelSingularName ?? ""} ${record.mappedModelDisplayName ?? ""}`.toLowerCase();
941
+ const haystack = `${record.tableName} ${record.schema ?? ""} ${record.mappedModelSingularName ?? ""} ${record.mappedModelDisplayName ?? ""} ${record.mappedModuleName ?? ""} ${record.mappedModuleDisplayName ?? ""}`.toLowerCase();
727
942
  return haystack.includes(searchNeedle);
728
943
  })
729
944
  : records;
@@ -848,28 +1063,46 @@ export function DatasourceIntrospectionPage() {
848
1063
  if (selectedDatasource) {
849
1064
  await refetchTables();
850
1065
  }
851
- if (selectedTable) {
1066
+ if (isWorkspaceMode && selectedTable) {
852
1067
  await refetchTableDetail();
853
1068
  }
854
1069
  };
855
1070
 
856
- const openTableDrawer = (record: DatasourceIntrospectionTableRecord) => {
1071
+ const startMappingForRecord = (record: DatasourceIntrospectionTableRecord) => {
857
1072
  if (drawerBootstrapTimerRef.current) {
858
1073
  window.clearTimeout(drawerBootstrapTimerRef.current);
859
1074
  }
860
1075
 
861
- setSelectedTableKey(`${record.schema ?? ""}::${record.tableName}`);
1076
+ setSelectedRows([record]);
1077
+ setWorkspaceTableKey(`${record.schema ?? ""}::${record.tableName}`);
1078
+ };
1079
+
1080
+ const leaveWorkspace = () => {
1081
+ if (drawerBootstrapTimerRef.current) {
1082
+ window.clearTimeout(drawerBootstrapTimerRef.current);
1083
+ }
1084
+
1085
+ setWorkspaceTableKey("");
862
1086
  setDrawerDraft(null);
1087
+ setFieldConfigColumnName(null);
1088
+ setWorkspaceStep("configure");
863
1089
  setDrawerTab("general-info");
864
- setDrawerStep("edit");
865
- setReviewTab("metadata-json");
866
1090
  setMappingPreview(null);
1091
+ setPersistedMappingResult(null);
1092
+ setMigrationArtifactsResult(null);
1093
+ setGenerateCodeOutput("");
1094
+ setIsGeneratingCodeWorkflow(false);
1095
+ setGenerateCodeStatusMessage("");
1096
+ setGenerateCodeTimeoutMessage(null);
1097
+ setGenerateCodeReady(false);
1098
+ setHasAttemptedGenerateCode(false);
1099
+ setRunMigrationOutput("");
1100
+ setRunMigrationCompleted(false);
867
1101
  setReviewedMetadataJsonValue(null);
868
1102
  setMetadataJsonText("");
869
1103
  setMetadataJsonValidationError(null);
870
- setMetadataJsonResetToken(`table-${record.schema ?? "default"}-${record.tableName}-${Date.now()}`);
871
- setDrawerBootstrapping(true);
872
- setDrawerOpen(true);
1104
+ setMetadataJsonResetToken(`workspace-closed-${Date.now()}`);
1105
+ setDrawerBootstrapping(false);
873
1106
  };
874
1107
 
875
1108
  const applyDraftSettings = () => {
@@ -879,6 +1112,15 @@ export function DatasourceIntrospectionPage() {
879
1112
 
880
1113
  const updateDrawerDraft = (updater: (current: DrawerDraft) => DrawerDraft) => {
881
1114
  setMappingPreview(null);
1115
+ setPersistedMappingResult(null);
1116
+ setMigrationArtifactsResult(null);
1117
+ setGenerateCodeOutput("");
1118
+ setIsGeneratingCodeWorkflow(false);
1119
+ setGenerateCodeStatusMessage("");
1120
+ setGenerateCodeTimeoutMessage(null);
1121
+ setGenerateCodeReady(false);
1122
+ setHasAttemptedGenerateCode(false);
1123
+ setRunMigrationOutput("");
882
1124
  setDrawerDraft((current) => (current ? updater(current) : current));
883
1125
  };
884
1126
 
@@ -891,7 +1133,28 @@ export function DatasourceIntrospectionPage() {
891
1133
  ...current,
892
1134
  columns: current.columns.map((column) => {
893
1135
  if (column.columnName !== columnName) return column;
894
- return { ...column, fieldName: camelCase(nextFieldName) };
1136
+
1137
+ const previousSuggestedLabel = startCase(column.fieldName);
1138
+ const nextNormalizedFieldName = camelCase(nextFieldName);
1139
+ const nextFieldConfig = column.fieldConfig
1140
+ ? {
1141
+ ...column.fieldConfig,
1142
+ name: nextNormalizedFieldName,
1143
+ displayName: !column.displayName.trim() || column.displayName === previousSuggestedLabel
1144
+ ? startCase(nextNormalizedFieldName)
1145
+ : column.displayName,
1146
+ isUserKey: current.userKeyField === column.fieldName,
1147
+ isPrimaryKey: column.isPrimaryKey,
1148
+ }
1149
+ : column.fieldConfig;
1150
+ return {
1151
+ ...column,
1152
+ fieldName: nextNormalizedFieldName,
1153
+ displayName: !column.displayName.trim() || column.displayName === previousSuggestedLabel
1154
+ ? startCase(nextNormalizedFieldName)
1155
+ : column.displayName,
1156
+ fieldConfig: nextFieldConfig,
1157
+ };
895
1158
  }),
896
1159
  userKeyField: current.userKeyField === current.columns.find((column) => column.columnName === columnName)?.fieldName
897
1160
  ? camelCase(nextFieldName)
@@ -899,6 +1162,26 @@ export function DatasourceIntrospectionPage() {
899
1162
  }));
900
1163
  };
901
1164
 
1165
+ const updateColumnDisplayName = (columnName: string, nextDisplayName: string) => {
1166
+ updateDrawerDraft((current) => ({
1167
+ ...current,
1168
+ columns: current.columns.map((column) => (
1169
+ column.columnName === columnName
1170
+ ? {
1171
+ ...column,
1172
+ displayName: nextDisplayName,
1173
+ fieldConfig: column.fieldConfig
1174
+ ? {
1175
+ ...column.fieldConfig,
1176
+ displayName: nextDisplayName,
1177
+ }
1178
+ : column.fieldConfig,
1179
+ }
1180
+ : column
1181
+ )),
1182
+ }));
1183
+ };
1184
+
902
1185
  const updateColumnSolidFieldType = (columnName: string, nextSolidFieldType: string) => {
903
1186
  updateDrawerDraft((current) => ({
904
1187
  ...current,
@@ -910,11 +1193,142 @@ export function DatasourceIntrospectionPage() {
910
1193
  ...column,
911
1194
  solidFieldType: nextSolidFieldType,
912
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,
913
1203
  };
914
1204
  }),
915
1205
  }));
916
1206
  };
917
1207
 
1208
+ const openFieldConfigEditor = (columnName: string) => {
1209
+ setFieldConfigColumnName(columnName);
1210
+ };
1211
+
1212
+ const applyFieldConfigDraft = (columnName: string, nextFieldConfig: Record<string, any>) => {
1213
+ const sanitized = sanitizeExistingFieldPayload(nextFieldConfig);
1214
+ updateDrawerDraft((current) => {
1215
+ const currentColumn = current.columns.find((column) => column.columnName === columnName);
1216
+ if (!currentColumn) {
1217
+ return current;
1218
+ }
1219
+
1220
+ const nextFieldName = typeof sanitized.name === "string" && sanitized.name.trim()
1221
+ ? sanitized.name.trim()
1222
+ : currentColumn.fieldName;
1223
+ const nextDisplayName = typeof sanitized.displayName === "string" && sanitized.displayName.trim()
1224
+ ? sanitized.displayName.trim()
1225
+ : currentColumn.displayName;
1226
+ const nextSolidFieldType = typeof sanitized.type === "string" && sanitized.type.trim()
1227
+ ? sanitized.type.trim()
1228
+ : currentColumn.solidFieldType;
1229
+ const nextOrmType = typeof sanitized.ormType === "string" && sanitized.ormType.trim()
1230
+ ? sanitized.ormType.trim()
1231
+ : getDefaultOrmTypeForSolidFieldType(current.dataSourceType, nextSolidFieldType);
1232
+ const nextUserKeyField = current.userKeyField === currentColumn.fieldName
1233
+ ? nextFieldName
1234
+ : current.userKeyField;
1235
+
1236
+ return {
1237
+ ...current,
1238
+ userKeyField: nextUserKeyField,
1239
+ columns: current.columns.map((column) => {
1240
+ if (column.columnName !== columnName) {
1241
+ return column.fieldConfig
1242
+ ? {
1243
+ ...column,
1244
+ fieldConfig: {
1245
+ ...column.fieldConfig,
1246
+ isUserKey: column.fieldName === nextUserKeyField,
1247
+ },
1248
+ }
1249
+ : column;
1250
+ }
1251
+
1252
+ return {
1253
+ ...column,
1254
+ fieldName: nextFieldName,
1255
+ displayName: nextDisplayName,
1256
+ solidFieldType: nextSolidFieldType,
1257
+ ormType: nextOrmType,
1258
+ fieldConfig: {
1259
+ ...sanitized,
1260
+ name: nextFieldName,
1261
+ displayName: nextDisplayName,
1262
+ type: nextSolidFieldType,
1263
+ ormType: nextOrmType,
1264
+ columnName: column.columnName,
1265
+ isPrimaryKey: column.isPrimaryKey,
1266
+ isUserKey: nextUserKeyField === nextFieldName,
1267
+ },
1268
+ };
1269
+ }),
1270
+ };
1271
+ });
1272
+ setFieldConfigColumnName(null);
1273
+ };
1274
+
1275
+ const updateLegacyTableType = (legacyTableType: "none" | "existing_id" | "generated_id") => {
1276
+ updateDrawerDraft((current) => {
1277
+ const renamedUserKeyColumns = new Map<string, string>();
1278
+
1279
+ const columns = current.columns.map((column) => {
1280
+ const nextSuperclassFieldName = getSuperclassFieldNameForLegacyType(column.columnName, legacyTableType);
1281
+ const nextHandledBySuperclass = Boolean(nextSuperclassFieldName);
1282
+ const nextSuggestedFieldName = getSuggestedFieldNameForLegacyType(column, legacyTableType);
1283
+ const shouldRebuildFieldName = column.handledBySuperclass !== nextHandledBySuperclass
1284
+ || (legacyTableType === "existing_id" && column.isAutoGenerated && (column.fieldName === "id" || column.fieldName === "legacyId"))
1285
+ || (legacyTableType === "generated_id" && column.columnName.toLowerCase() === "id");
1286
+ const nextFieldName = shouldRebuildFieldName ? nextSuggestedFieldName : column.fieldName;
1287
+ const nextDisplayName = !column.displayName.trim() || column.displayName === startCase(column.fieldName)
1288
+ ? startCase(nextFieldName)
1289
+ : column.displayName;
1290
+ const nextInclude = nextHandledBySuperclass
1291
+ ? false
1292
+ : (legacyTableType === "existing_id" && column.isAutoGenerated ? true : column.include);
1293
+ const nextIsPrimaryKey = (
1294
+ legacyTableType === "existing_id" && column.isAutoGenerated && nextFieldName === "id"
1295
+ ) || (
1296
+ legacyTableType === "generated_id" && column.isDetectedPrimaryKey
1297
+ );
1298
+
1299
+ if (current.userKeyField === column.fieldName && current.userKeyField !== nextFieldName) {
1300
+ renamedUserKeyColumns.set(column.fieldName, nextFieldName);
1301
+ }
1302
+
1303
+ return {
1304
+ ...column,
1305
+ fieldName: nextFieldName,
1306
+ displayName: nextDisplayName,
1307
+ handledBySuperclass: nextHandledBySuperclass,
1308
+ superclassFieldName: nextSuperclassFieldName,
1309
+ include: nextInclude,
1310
+ isPrimaryKey: nextIsPrimaryKey,
1311
+ fieldConfig: column.fieldConfig
1312
+ ? {
1313
+ ...column.fieldConfig,
1314
+ name: nextFieldName,
1315
+ displayName: nextDisplayName,
1316
+ isPrimaryKey: nextIsPrimaryKey,
1317
+ }
1318
+ : column.fieldConfig,
1319
+ };
1320
+ });
1321
+
1322
+ return {
1323
+ ...current,
1324
+ legacyTableType,
1325
+ baseClassName: getBaseClassNameForLegacyType(legacyTableType),
1326
+ columns,
1327
+ userKeyField: renamedUserKeyColumns.get(current.userKeyField) ?? current.userKeyField,
1328
+ };
1329
+ });
1330
+ };
1331
+
918
1332
  const setColumnIncluded = (columnName: string, include: boolean) => {
919
1333
  updateDrawerDraft((current) => ({
920
1334
  ...current,
@@ -930,11 +1344,19 @@ export function DatasourceIntrospectionPage() {
930
1344
  updateDrawerDraft((current) => ({
931
1345
  ...current,
932
1346
  userKeyField: fieldName,
933
- columns: current.columns.map((column) => (
934
- column.fieldName === fieldName
935
- ? { ...column, include: column.handledBySuperclass ? false : true }
936
- : column
937
- )),
1347
+ columns: current.columns.map((column) => {
1348
+ const isUserKey = column.fieldName === fieldName;
1349
+ return {
1350
+ ...column,
1351
+ include: isUserKey ? (column.handledBySuperclass ? false : true) : column.include,
1352
+ fieldConfig: column.fieldConfig
1353
+ ? {
1354
+ ...column.fieldConfig,
1355
+ isUserKey,
1356
+ }
1357
+ : column.fieldConfig,
1358
+ };
1359
+ }),
938
1360
  }));
939
1361
  };
940
1362
 
@@ -956,7 +1378,7 @@ export function DatasourceIntrospectionPage() {
956
1378
  window.open(`/admin/core/solid-core/model-metadata/form/${targetModelId}`, "_blank", "noopener,noreferrer");
957
1379
  };
958
1380
 
959
- const viewChanges = async () => {
1381
+ const reviewMappingJson = async () => {
960
1382
  if (!drawerDraft || !bootstrap?.module?.id) return;
961
1383
 
962
1384
  const validationErrors = getDrawerValidationErrors(drawerDraft);
@@ -978,8 +1400,11 @@ export function DatasourceIntrospectionPage() {
978
1400
  setMetadataJsonText(JSON.stringify(preview.metadataJson.model, null, 2));
979
1401
  setMetadataJsonValidationError(null);
980
1402
  setMetadataJsonResetToken(`preview-${preview.metadataJson.filePath}-${Date.now()}`);
981
- setReviewTab("metadata-json");
982
- setDrawerStep("review");
1403
+ setPersistedMappingResult(null);
1404
+ setMigrationArtifactsResult(null);
1405
+ setGenerateCodeOutput("");
1406
+ setRunMigrationOutput("");
1407
+ setWorkspaceStep("review-json");
983
1408
  } catch (error: any) {
984
1409
  dispatch(showToast({
985
1410
  severity: "error",
@@ -989,7 +1414,7 @@ export function DatasourceIntrospectionPage() {
989
1414
  }
990
1415
  };
991
1416
 
992
- const saveMapping = async () => {
1417
+ const persistMappingArtifacts = async () => {
993
1418
  if (!drawerDraft || !bootstrap?.module?.id) return;
994
1419
 
995
1420
  const validationErrors = getDrawerValidationErrors(drawerDraft);
@@ -997,15 +1422,14 @@ export function DatasourceIntrospectionPage() {
997
1422
  validationErrors.forEach((detail) => {
998
1423
  dispatch(showToast({ severity: "error", summary: "Validation", detail }));
999
1424
  });
1000
- setDrawerStep("edit");
1425
+ setWorkspaceStep("configure");
1001
1426
  return;
1002
1427
  }
1003
1428
 
1004
1429
  const reviewedModelValidation = parseAndValidateReviewedModelJson(metadataJsonText, drawerDraft);
1005
1430
  if (reviewedModelValidation.error || !reviewedModelValidation.parsed) {
1006
1431
  setMetadataJsonValidationError(reviewedModelValidation.error ?? "Metadata JSON is invalid.");
1007
- setReviewTab("metadata-json");
1008
- setDrawerStep("review");
1432
+ setWorkspaceStep("review-json");
1009
1433
  dispatch(showToast({
1010
1434
  severity: "error",
1011
1435
  summary: "Validation",
@@ -1027,28 +1451,180 @@ export function DatasourceIntrospectionPage() {
1027
1451
 
1028
1452
  dispatch(showToast({
1029
1453
  severity: "success",
1030
- summary: "Saved",
1454
+ summary: "Mapping saved",
1455
+ detail: drawerDraft.mapped
1456
+ ? "The model mapping metadata was updated successfully."
1457
+ : "The model mapping metadata was created successfully.",
1458
+ }));
1459
+
1460
+ setPersistedMappingResult(response);
1461
+ setMigrationArtifactsResult(null);
1462
+ setGenerateCodeOutput("");
1463
+ setIsGeneratingCodeWorkflow(false);
1464
+ setGenerateCodeStatusMessage("");
1465
+ setGenerateCodeTimeoutMessage(null);
1466
+ setGenerateCodeReady(false);
1467
+ setHasAttemptedGenerateCode(false);
1468
+ setRunMigrationOutput("");
1469
+ setRunMigrationCompleted(false);
1470
+ setDrawerDraft((current) => (
1471
+ current
1472
+ ? {
1473
+ ...current,
1474
+ mapped: true,
1475
+ modelId: response.modelId,
1476
+ }
1477
+ : current
1478
+ ));
1479
+ setWorkspaceStep("generate-code");
1480
+ } catch (error: any) {
1481
+ dispatch(showToast({
1482
+ severity: "error",
1483
+ summary: "Save failed",
1484
+ detail: readQueryError(error),
1485
+ }));
1486
+ }
1487
+ };
1488
+
1489
+ const handleGenerateCode = async () => {
1490
+ if (!bootstrap?.module?.id) return;
1491
+
1492
+ setHasAttemptedGenerateCode(true);
1493
+ setRunMigrationCompleted(false);
1494
+ setIsGeneratingCodeWorkflow(true);
1495
+ setGenerateCodeStatusMessage("Submitting module generate-code request...");
1496
+ setGenerateCodeTimeoutMessage(null);
1497
+ setGenerateCodeReady(false);
1498
+ let responseMessage = "";
1499
+
1500
+ try {
1501
+ const response = await generateCode({ moduleId: bootstrap.module.id }).unwrap();
1502
+ responseMessage = response.output || response.message || "";
1503
+ setGenerateCodeOutput(responseMessage);
1504
+ setGenerateCodeStatusMessage("Waiting briefly for the backend restart window...");
1505
+ } catch (error: any) {
1506
+ if (!isRecoverableGenerateCodeError(error)) {
1507
+ setIsGeneratingCodeWorkflow(false);
1508
+ setGenerateCodeStatusMessage("");
1509
+ setGenerateCodeTimeoutMessage(null);
1510
+ setGenerateCodeReady(false);
1511
+ dispatch(showToast({
1512
+ severity: "error",
1513
+ summary: "Generate code failed",
1514
+ detail: readQueryError(error),
1515
+ }));
1516
+ return;
1517
+ }
1518
+
1519
+ setGenerateCodeStatusMessage("The API connection dropped during restart. Waiting for the backend to come back online...");
1520
+ }
1521
+
1522
+ await new Promise((resolve) => window.setTimeout(resolve, 5000));
1523
+ setGenerateCodeStatusMessage("Checking if the backend is back online...");
1524
+
1525
+ const isBackendAvailable = await waitForBackendAvailability({
1526
+ retries: 40,
1527
+ delayMs: 1500,
1528
+ });
1529
+
1530
+ setIsGeneratingCodeWorkflow(false);
1531
+
1532
+ if (!isBackendAvailable) {
1533
+ setGenerateCodeTimeoutMessage(
1534
+ "Code generation took longer than expected. The backend did not come back within 60 seconds, so there may be a compilation failure or another startup issue. Please review the generated code manually and check the API logs before continuing.",
1535
+ );
1536
+ dispatch(showToast({
1537
+ severity: "warn",
1538
+ summary: "Backend restart taking too long",
1539
+ detail: "The generate-code request completed, but the backend did not come back within 60 seconds.",
1540
+ }));
1541
+ return;
1542
+ }
1543
+
1544
+ setGenerateCodeStatusMessage("Backend is back online. You can continue.");
1545
+ setGenerateCodeReady(true);
1546
+ setMigrationArtifactsResult(null);
1547
+ setRunMigrationOutput("");
1548
+ dispatch(showToast({
1549
+ severity: "success",
1550
+ summary: "Code generated",
1551
+ detail: responseMessage || "The backend is back online. You can continue to the migration artifacts step.",
1552
+ }));
1553
+ };
1554
+
1555
+ const handleCreateMigrationArtifacts = async () => {
1556
+ if (!drawerDraft || !bootstrap?.module?.id || !persistedMappingResult?.modelId) return;
1557
+
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.",
1566
+ }));
1567
+ return;
1568
+ }
1569
+
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();
1585
+
1586
+ setMigrationArtifactsResult(response);
1587
+ setWorkspaceStep("run-migration");
1588
+ dispatch(showToast({
1589
+ severity: "success",
1590
+ summary: "Migration artifacts created",
1031
1591
  detail: response.migration.written
1032
- ? "The mapping, metadata JSON, and migration template were written successfully."
1033
- : drawerDraft.mapped
1034
- ? "The model mapping was updated and written back to metadata JSON."
1035
- : "The model mapping was created and written to metadata JSON.",
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),
1600
+ }));
1601
+ }
1602
+ };
1603
+
1604
+ const handleRunMigration = async () => {
1605
+ if (!bootstrap?.module?.id || !selectedDatasource) return;
1606
+
1607
+ try {
1608
+ const response = await runMigration({
1609
+ moduleId: bootstrap.module.id,
1610
+ datasource: selectedDatasource,
1611
+ }).unwrap();
1612
+
1613
+ setRunMigrationOutput(response.output || response.message || "");
1614
+ setRunMigrationCompleted(true);
1615
+ dispatch(showToast({
1616
+ severity: "success",
1617
+ summary: "Migration workflow complete",
1618
+ detail: response.message || `Migration, build, and seed ran successfully for datasource "${selectedDatasource}".`,
1036
1619
  }));
1037
1620
 
1038
- setDrawerOpen(false);
1039
- setDrawerDraft(null);
1040
- setMappingPreview(null);
1041
- setReviewedMetadataJsonValue(null);
1042
- setMetadataJsonText("");
1043
- setMetadataJsonValidationError(null);
1044
- setMetadataJsonResetToken(`saved-${Date.now()}`);
1045
- setDrawerStep("edit");
1046
1621
  await refetchTables();
1047
1622
  await refetchBootstrap();
1048
1623
  } catch (error: any) {
1624
+ setRunMigrationOutput(readQueryError(error));
1049
1625
  dispatch(showToast({
1050
1626
  severity: "error",
1051
- summary: "Save failed",
1627
+ summary: "Run migration failed",
1052
1628
  detail: readQueryError(error),
1053
1629
  }));
1054
1630
  }
@@ -1066,6 +1642,65 @@ export function DatasourceIntrospectionPage() {
1066
1642
  );
1067
1643
  }
1068
1644
 
1645
+ const stepSequence: WorkspaceStep[] = [
1646
+ "configure",
1647
+ "review-json",
1648
+ "generate-code",
1649
+ "create-migrations",
1650
+ "run-migration",
1651
+ ];
1652
+ const currentStepIndex = stepSequence.indexOf(workspaceStep);
1653
+ const stepperItems = [
1654
+ {
1655
+ key: "configure" as const,
1656
+ label: "Configure Mapping",
1657
+ complete: currentStepIndex > 0,
1658
+ active: workspaceStep === "configure",
1659
+ disabled: false,
1660
+ onClick: () => setWorkspaceStep("configure"),
1661
+ },
1662
+ {
1663
+ key: "review-json" as const,
1664
+ label: "Review Mapping JSON",
1665
+ complete: currentStepIndex > 1,
1666
+ active: workspaceStep === "review-json",
1667
+ disabled: !mappingPreview,
1668
+ onClick: () => {
1669
+ if (mappingPreview) setWorkspaceStep("review-json");
1670
+ },
1671
+ },
1672
+ {
1673
+ key: "generate-code" as const,
1674
+ label: "Generate Code",
1675
+ complete: currentStepIndex > 2,
1676
+ active: workspaceStep === "generate-code",
1677
+ disabled: !persistedMappingResult,
1678
+ onClick: () => {
1679
+ if (persistedMappingResult) setWorkspaceStep("generate-code");
1680
+ },
1681
+ },
1682
+ {
1683
+ key: "create-migrations" as const,
1684
+ label: "Create Migrations",
1685
+ complete: currentStepIndex > 3,
1686
+ active: workspaceStep === "create-migrations",
1687
+ disabled: !hasAttemptedGenerateCode,
1688
+ onClick: () => {
1689
+ if (hasAttemptedGenerateCode) setWorkspaceStep("create-migrations");
1690
+ },
1691
+ },
1692
+ {
1693
+ key: "run-migration" as const,
1694
+ label: "Run Migration",
1695
+ complete: false,
1696
+ active: workspaceStep === "run-migration",
1697
+ disabled: !migrationArtifactsResult,
1698
+ onClick: () => {
1699
+ if (migrationArtifactsResult) setWorkspaceStep("run-migration");
1700
+ },
1701
+ },
1702
+ ];
1703
+
1069
1704
  const selectableUnmappedColumns = drawerDraft?.columns.filter(
1070
1705
  (column) => !column.handledBySuperclass && !column.matched && !column.isPrimaryKey,
1071
1706
  ) ?? [];
@@ -1107,19 +1742,20 @@ export function DatasourceIntrospectionPage() {
1107
1742
  </div>
1108
1743
  <div className="sdix-form-field">
1109
1744
  <label htmlFor="sdix-user-key-field">User Key Field</label>
1110
- <SolidSelect
1111
- id="sdix-user-key-field"
1112
- value={drawerDraft.userKeyField}
1113
- options={drawerUserKeyOptions}
1114
- placeholder="Select user key field"
1745
+ <SolidSelect
1746
+ id="sdix-user-key-field"
1747
+ value={drawerDraft.userKeyField}
1748
+ options={drawerUserKeyOptions}
1749
+ placeholder="Select user key field"
1115
1750
  onChange={(event) => changeUserKeyField(event.value)}
1116
1751
  />
1117
1752
  </div>
1118
1753
  <div className="sdix-form-field sdix-form-field--full">
1119
1754
  <label htmlFor="sdix-description">Description</label>
1120
1755
  <SolidTextarea
1756
+ className="sdix-description-textarea"
1121
1757
  id="sdix-description"
1122
- rows={3}
1758
+ rows={2}
1123
1759
  value={drawerDraft.description}
1124
1760
  onChange={(event) => updateGeneralField("description", event.target.value)}
1125
1761
  />
@@ -1163,20 +1799,16 @@ export function DatasourceIntrospectionPage() {
1163
1799
  />
1164
1800
  </div>
1165
1801
  <div className="sdix-form-field">
1166
- <label htmlFor="sdix-legacy-type">Legacy Strategy</label>
1167
- <SolidInput
1802
+ <label htmlFor="sdix-legacy-type">Mapping Strategy</label>
1803
+ <SolidSelect
1168
1804
  id="sdix-legacy-type"
1169
- value={legacyTypeLabel(drawerDraft.legacyTableType)}
1170
- disabled
1171
- />
1172
- </div>
1173
- <div className="sdix-form-field">
1174
- <label htmlFor="sdix-base-class">Superclass</label>
1175
- <SolidInput
1176
- id="sdix-base-class"
1177
- value={drawerDraft.baseClassName}
1178
- disabled
1805
+ value={drawerDraft.legacyTableType}
1806
+ options={mappingStrategyOptions}
1807
+ onChange={(event) => updateLegacyTableType(event.value)}
1179
1808
  />
1809
+ <small className="sdix-inline-note">
1810
+ This will use <strong>{drawerDraft.baseClassName}</strong> for the generated entity.
1811
+ </small>
1180
1812
  </div>
1181
1813
  </div>
1182
1814
  </section>
@@ -1196,10 +1828,6 @@ export function DatasourceIntrospectionPage() {
1196
1828
  </div>
1197
1829
  <div className="sdix-column-table">
1198
1830
  <div className="sdix-column-row sdix-column-row--head sdix-column-row--mapping">
1199
- <span>Column</span>
1200
- <span>Field</span>
1201
- <span>Database</span>
1202
- <span>SolidX</span>
1203
1831
  <span className="sdix-column-select-header">
1204
1832
  {selectableUnmappedColumns.length ? (
1205
1833
  <SolidCheckbox
@@ -1211,11 +1839,17 @@ export function DatasourceIntrospectionPage() {
1211
1839
  "Include"
1212
1840
  )}
1213
1841
  </span>
1842
+ <span>Column</span>
1843
+ <span>Field</span>
1844
+ <span>Label</span>
1845
+ <span>Database</span>
1846
+ <span>SolidX</span>
1214
1847
  <span>Status</span>
1215
1848
  <span>Action</span>
1216
1849
  </div>
1217
1850
  {drawerDraft.columns.map((column) => {
1218
1851
  const isUserKey = drawerDraft.userKeyField === column.fieldName;
1852
+ const isPrimaryKeyField = column.isPrimaryKey;
1219
1853
  const statusClassName = column.handledBySuperclass
1220
1854
  ? "is-superclass"
1221
1855
  : column.include
@@ -1236,6 +1870,20 @@ export function DatasourceIntrospectionPage() {
1236
1870
 
1237
1871
  return (
1238
1872
  <div key={column.columnName} className="sdix-column-row sdix-column-row--mapping">
1873
+ <span className="sdix-column-include">
1874
+ {column.handledBySuperclass ? (
1875
+ <span className="sdix-column-placeholder">-</span>
1876
+ ) : column.matched ? (
1877
+ <span className="sdix-column-placeholder">-</span>
1878
+ ) : column.isPrimaryKey ? (
1879
+ <SolidCheckbox checked disabled aria-label="Required included column" />
1880
+ ) : (
1881
+ <SolidCheckbox
1882
+ checked={column.include}
1883
+ onChange={(event) => setColumnIncluded(column.columnName, event.target.checked)}
1884
+ />
1885
+ )}
1886
+ </span>
1239
1887
  <span className="sdix-column-meta">
1240
1888
  <strong>{column.columnName}</strong>
1241
1889
  {column.isAutoGenerated ? <small>Generated id column</small> : null}
@@ -1246,7 +1894,19 @@ export function DatasourceIntrospectionPage() {
1246
1894
  disabled={column.handledBySuperclass}
1247
1895
  onChange={(event) => updateColumnFieldName(column.columnName, event.target.value)}
1248
1896
  />
1249
- {isUserKey ? <small className="sdix-inline-note">User key</small> : null}
1897
+ {isUserKey || isPrimaryKeyField ? (
1898
+ <div className="sdix-inline-note-stack">
1899
+ {isUserKey ? <small className="sdix-inline-note">User key</small> : null}
1900
+ {isPrimaryKeyField ? <small className="sdix-inline-note">Primary key</small> : null}
1901
+ </div>
1902
+ ) : null}
1903
+ </span>
1904
+ <span>
1905
+ <SolidInput
1906
+ value={column.displayName}
1907
+ disabled={column.handledBySuperclass}
1908
+ onChange={(event) => updateColumnDisplayName(column.columnName, event.target.value)}
1909
+ />
1250
1910
  </span>
1251
1911
  <span className="sdix-column-meta">
1252
1912
  <strong>{column.dataType}</strong>
@@ -1262,39 +1922,38 @@ export function DatasourceIntrospectionPage() {
1262
1922
  />
1263
1923
  )}
1264
1924
  </span>
1265
- <span className="sdix-column-include">
1266
- {column.handledBySuperclass ? (
1267
- <span className="sdix-column-placeholder">-</span>
1268
- ) : column.matched ? (
1269
- <span className="sdix-column-placeholder">-</span>
1270
- ) : column.isPrimaryKey ? (
1271
- <SolidCheckbox checked disabled label="Required" />
1272
- ) : (
1273
- <SolidCheckbox
1274
- checked={column.include}
1275
- onChange={(event) => setColumnIncluded(column.columnName, event.target.checked)}
1276
- />
1277
- )}
1278
- </span>
1279
1925
  <span>
1280
1926
  <span className={`sdix-status-badge ${statusClassName}`}>{statusText}</span>
1281
1927
  </span>
1282
1928
  <span className="sdix-column-action">
1283
- {column.handledBySuperclass ? (
1284
- <SolidButton size="small" variant="outline" disabled>
1285
- Managed
1286
- </SolidButton>
1287
- ) : column.isPrimaryKey ? (
1288
- <SolidButton size="small" variant="outline" disabled>
1289
- Required
1290
- </SolidButton>
1291
- ) : column.matched ? (
1292
- <SolidButton size="small" variant="outline" onClick={() => setColumnIncluded(column.columnName, false)}>
1293
- Remove mapping
1294
- </SolidButton>
1295
- ) : (
1296
- <span className="sdix-column-placeholder">-</span>
1297
- )}
1929
+ <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
+ {column.handledBySuperclass ? (
1942
+ <SolidButton size="small" variant="outline" disabled className="sdix-column-action-chip sdix-column-action-chip--managed">
1943
+ Managed
1944
+ </SolidButton>
1945
+ ) : column.isPrimaryKey ? (
1946
+ <SolidButton size="small" variant="outline" disabled className="sdix-column-action-chip sdix-column-action-chip--required">
1947
+ Required
1948
+ </SolidButton>
1949
+ ) : column.matched ? (
1950
+ <SolidButton size="small" variant="outline" onClick={() => setColumnIncluded(column.columnName, false)}>
1951
+ Remove mapping
1952
+ </SolidButton>
1953
+ ) : (
1954
+ <span className="sdix-column-placeholder">-</span>
1955
+ )}
1956
+ </div>
1298
1957
  </span>
1299
1958
  </div>
1300
1959
  );
@@ -1321,120 +1980,229 @@ export function DatasourceIntrospectionPage() {
1321
1980
  },
1322
1981
  ] : [];
1323
1982
 
1324
- const reviewTabs = mappingPreview ? [
1325
- {
1326
- value: "metadata-json",
1327
- label: "Metadata JSON",
1328
- content: (
1329
- <div className="sdix-review-panel sdix-review-panel--metadata">
1983
+ const reviewJsonPanel = mappingPreview ? (
1984
+ <div className="sdix-review-panel sdix-review-panel--metadata">
1985
+ <div className="sdix-review-meta sdix-review-meta--compact">
1986
+ <span>Target file</span>
1987
+ <strong>{mappingPreview.metadataJson.filePath}</strong>
1988
+ </div>
1989
+ <SolidJsonEditor
1990
+ className="sdix-json-editor"
1991
+ value={reviewedMetadataJsonValue ?? mappingPreview.metadataJson.model}
1992
+ resetToken={metadataJsonResetToken}
1993
+ onValueChange={(nextValue) => {
1994
+ setReviewedMetadataJsonValue(nextValue);
1995
+ if (metadataJsonValidationError) {
1996
+ setMetadataJsonValidationError(null);
1997
+ }
1998
+ }}
1999
+ onTextChange={(nextText) => {
2000
+ setMetadataJsonText(nextText);
2001
+ }}
2002
+ onErrorChange={setMetadataJsonValidationError}
2003
+ />
2004
+ {metadataJsonValidationError ? (
2005
+ <div className="sdix-json-validation-error">
2006
+ <span>{metadataJsonValidationError}</span>
2007
+ </div>
2008
+ ) : null}
2009
+ </div>
2010
+ ) : null;
2011
+
2012
+ const createMigrationsPanel = mappingPreview ? (
2013
+ <div className="sdix-review-panel sdix-review-panel--migration">
2014
+ {mappingPreview.migration.willGenerate && mappingPreview.migration.filePath ? (
2015
+ <>
1330
2016
  <div className="sdix-review-meta sdix-review-meta--compact">
1331
2017
  <span>Target file</span>
1332
- <strong>{mappingPreview.metadataJson.filePath}</strong>
2018
+ <strong>{mappingPreview.migration.filePath}</strong>
1333
2019
  </div>
1334
- <SolidJsonEditor
1335
- className="sdix-json-editor"
1336
- value={reviewedMetadataJsonValue ?? mappingPreview.metadataJson.model}
1337
- resetToken={metadataJsonResetToken}
1338
- onValueChange={(nextValue) => {
1339
- setReviewedMetadataJsonValue(nextValue);
1340
- if (metadataJsonValidationError) {
1341
- setMetadataJsonValidationError(null);
1342
- }
1343
- }}
1344
- onTextChange={(nextText) => {
1345
- setMetadataJsonText(nextText);
1346
- }}
1347
- onErrorChange={setMetadataJsonValidationError}
1348
- />
1349
- {metadataJsonValidationError ? (
1350
- <div className="sdix-json-validation-error">
1351
- <span>{metadataJsonValidationError}</span>
1352
- </div>
2020
+ <p className="sdix-review-note">
2021
+ 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.
2022
+ </p>
2023
+ {mappingPreview.migration.content ? (
2024
+ <pre className="sdix-ddl-code sdix-ddl-code--fill"><code>{mappingPreview.migration.content}</code></pre>
1353
2025
  ) : null}
2026
+ </>
2027
+ ) : (
2028
+ <div className="sdix-empty-state is-inline sdix-ddl-empty">
2029
+ <Database size={18} />
2030
+ <p>{mappingPreview.migration.note}</p>
1354
2031
  </div>
1355
- ),
1356
- },
1357
- {
1358
- value: "migration",
1359
- label: "Migration",
1360
- content: (
1361
- <div className="sdix-review-panel sdix-review-panel--migration">
1362
- {mappingPreview.migration.willGenerate && mappingPreview.migration.filePath ? (
1363
- <>
1364
- <div className="sdix-review-meta sdix-review-meta--compact">
1365
- <span>Target file</span>
1366
- <strong>{mappingPreview.migration.filePath}</strong>
1367
- </div>
1368
- <p className="sdix-review-note">
1369
- This migration preview is generated from the current mapping and is read-only here. Saving the mapping will write a new template migration file to the target location.
1370
- </p>
1371
- {mappingPreview.migration.content ? (
1372
- <pre className="sdix-ddl-code sdix-ddl-code--fill"><code>{mappingPreview.migration.content}</code></pre>
1373
- ) : null}
1374
- </>
1375
- ) : (
1376
- <div className="sdix-empty-state is-inline sdix-ddl-empty">
1377
- <Database size={18} />
1378
- <p>{mappingPreview.migration.note}</p>
1379
- </div>
1380
- )}
2032
+ )}
2033
+ </div>
2034
+ ) : null;
2035
+
2036
+ const generateCodePanel = (
2037
+ <div className="sdix-stage-panel">
2038
+ <div className="sdix-stage-panel__header">
2039
+ <span className="sdix-stage-panel__eyebrow">Step 3</span>
2040
+ <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>
2042
+ </div>
2043
+ <div className="sdix-stage-panel__body">
2044
+ <div className="sdix-stage-panel__card">
2045
+ <div className="sdix-stage-panel__card-title">What happens here</div>
2046
+ <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>
2050
+ </ul>
1381
2051
  </div>
1382
- ),
1383
- },
1384
- ] : [];
1385
-
1386
- return (
1387
- <div className="sdix-page">
1388
- <div className="sdix-toolbar">
1389
- <div className="sdix-toolbar__left">
1390
- <div className="sdix-toolbar-search">
1391
- <Search size={15} />
1392
- <SolidInput
1393
- value={tableSearch}
1394
- onChange={(event) => setTableSearch(event.target.value)}
1395
- placeholder="Search tables"
1396
- disabled={!selectedDatasource}
1397
- />
1398
- </div>
2052
+ <div className="sdix-stage-panel__actions">
2053
+ <SolidButton
2054
+ leftIcon={<Sparkles size={14} />}
2055
+ loading={isGenerateCodeLoading || isGeneratingCodeWorkflow}
2056
+ disabled={isGenerateCodeLoading || isGeneratingCodeWorkflow}
2057
+ onClick={() => void handleGenerateCode()}
2058
+ >
2059
+ Generate Code
2060
+ </SolidButton>
1399
2061
  </div>
2062
+ {isGeneratingCodeWorkflow ? (
2063
+ <div className="sdix-empty-state is-inline sdix-stage-progress">
2064
+ <SolidSpinner size={30} />
2065
+ <h3>Generating code</h3>
2066
+ <p>{generateCodeStatusMessage || "Waiting for the backend to finish restarting..."}</p>
2067
+ </div>
2068
+ ) : null}
2069
+ {!isGeneratingCodeWorkflow && generateCodeTimeoutMessage ? (
2070
+ <div className="sdix-error is-inline sdix-stage-progress sdix-stage-progress--warning">
2071
+ <ShieldAlert size={22} />
2072
+ <h3>Manual review recommended</h3>
2073
+ <p>{generateCodeTimeoutMessage}</p>
2074
+ </div>
2075
+ ) : null}
2076
+ {generateCodeOutput ? (
2077
+ <pre className="sdix-ddl-code sdix-ddl-code--fill"><code>{generateCodeOutput}</code></pre>
2078
+ ) : null}
2079
+ </div>
2080
+ </div>
2081
+ );
1400
2082
 
1401
- <div className="sdix-toolbar__actions">
1402
- {isBootstrapFetching || isTablesFetching || isTableDetailFetching || isMappedModelFetching ? (
1403
- <div className="sdix-inline-status">
1404
- <SolidSpinner size={16} />
1405
- <span>Refreshing</span>
1406
- </div>
1407
- ) : null}
1408
- <SolidSelect
1409
- value={selectedDatasource}
1410
- options={datasourceOptions}
1411
- placeholder="Datasource"
1412
- className="sdix-toolbar-select sdix-toolbar-select--compact"
1413
- onChange={(event) => setSelectedDatasource(event.value)}
1414
- />
1415
- <SolidButton
1416
- size="small"
1417
- className="sdix-icon-button"
1418
- leftIcon={<RefreshCw size={16} />}
1419
- onClick={() => void handleRefresh()}
1420
- tooltip="Refresh"
1421
- aria-label="Refresh"
1422
- />
1423
- <SolidButton
1424
- size="small"
1425
- className="sdix-icon-button"
1426
- leftIcon={<Cog size={16} />}
1427
- onClick={() => {
1428
- setDraftSessionSettings(sessionSettings);
1429
- setSettingsTab("datasource-info");
1430
- setSettingsOpen(true);
1431
- }}
1432
- tooltip="Session settings"
1433
- aria-label="Session settings"
1434
- />
2083
+ const runMigrationPanel = (
2084
+ <div className="sdix-stage-panel">
2085
+ {!runMigrationCompleted ? (
2086
+ <div className="sdix-stage-panel__header">
2087
+ <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>
1435
2090
  </div>
2091
+ ) : null}
2092
+ <div className="sdix-stage-panel__body">
2093
+ {!runMigrationCompleted ? (
2094
+ <div className="sdix-stage-panel__card">
2095
+ <div className="sdix-stage-panel__card-title">Commands</div>
2096
+ <pre className="sdix-stage-panel__command"><code>{`npx @solidxai/solidctl@latest migration -d ${selectedDatasource} run
2097
+ npx @solidxai/solidctl@latest build
2098
+ npx @solidxai/solidctl@latest seed --modules-to-seed ${bootstrap?.module?.name || ""}`}</code></pre>
2099
+ </div>
2100
+ ) : null}
2101
+ {runMigrationCompleted ? (
2102
+ <div className="sdix-empty-state is-inline sdix-stage-progress">
2103
+ <Check size={24} />
2104
+ <h3>Migration workflow finished</h3>
2105
+ <p>Review the command output below. When you are satisfied, click Finish to return to the model list.</p>
2106
+ </div>
2107
+ ) : null}
2108
+ {runMigrationOutput ? (
2109
+ <pre className="sdix-ddl-code sdix-ddl-code--fill"><code>{runMigrationOutput}</code></pre>
2110
+ ) : null}
1436
2111
  </div>
2112
+ </div>
2113
+ );
2114
+
2115
+ const workspaceContent = drawerDraft ? (() => {
2116
+ switch (workspaceStep) {
2117
+ case "review-json":
2118
+ return reviewJsonPanel;
2119
+ case "create-migrations":
2120
+ return createMigrationsPanel;
2121
+ case "generate-code":
2122
+ return generateCodePanel;
2123
+ case "run-migration":
2124
+ return runMigrationPanel;
2125
+ case "configure":
2126
+ default:
2127
+ return (
2128
+ <SolidTabGroup
2129
+ tabs={drawerTabs}
2130
+ value={drawerTab}
2131
+ onValueChange={setDrawerTab}
2132
+ className="sdix-drawer-tabs sdix-wizard-tabs"
2133
+ panelClassName="sdix-drawer-tab-panel sdix-wizard-tab-panel"
2134
+ />
2135
+ );
2136
+ }
2137
+ })() : null;
2138
+
2139
+ const workspaceMeta = drawerDraft ? (() => {
2140
+ switch (workspaceStep) {
2141
+ case "review-json":
2142
+ return (
2143
+ <span>
2144
+ Review the generated metadata JSON before it is written to the module metadata file.
2145
+ </span>
2146
+ );
2147
+ case "generate-code":
2148
+ return persistedMappingResult ? (
2149
+ <span>
2150
+ Metadata JSON has been saved for
2151
+ {" "}
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.
2154
+ </span>
2155
+ ) : (
2156
+ <span>Save the metadata JSON before generating code.</span>
2157
+ );
2158
+ case "create-migrations":
2159
+ return mappingPreview ? (
2160
+ <span>
2161
+ Create the migration artifacts for
2162
+ {" "}
2163
+ <strong>{mappingPreview.summary.includedColumnNames.length}</strong>
2164
+ {" "}
2165
+ mapped columns
2166
+ {mappingPreview.summary.generatedSystemColumnCount
2167
+ ? ` and ${mappingPreview.summary.generatedSystemColumnCount} SolidX system column migration changes`
2168
+ : ""}
2169
+ .
2170
+ </span>
2171
+ ) : null;
2172
+ case "run-migration":
2173
+ return runMigrationCompleted ? (
2174
+ <span>
2175
+ The migration, build, and seed workflow has completed for
2176
+ {" "}
2177
+ <strong>{selectedDatasource}</strong>
2178
+ . Review the output below and click Finish when you are ready to return to the model list.
2179
+ </span>
2180
+ ) : (
2181
+ <span>
2182
+ Run the migration, build, and seed workflow for
2183
+ {" "}
2184
+ <strong>{selectedDatasource}</strong>
2185
+ {" "}
2186
+ to apply the SolidX system columns on the legacy table and refresh the module metadata state.
2187
+ </span>
2188
+ );
2189
+ case "configure":
2190
+ default:
2191
+ return drawerDraft.mapped ? (
2192
+ <span>
2193
+ Editing mapped model
2194
+ {" "}
2195
+ <strong>{selectedTable?.mappedModelDisplayName || selectedTable?.mappedModelSingularName}</strong>
2196
+ {drawerDraft.mappedModuleLabel ? ` in ${drawerDraft.mappedModuleLabel}` : ""}
2197
+ </span>
2198
+ ) : (
2199
+ <span>Configure the field mapping draft before generating the metadata JSON preview.</span>
2200
+ );
2201
+ }
2202
+ })() : null;
1437
2203
 
2204
+ return (
2205
+ <div className="sdix-page">
1438
2206
  {isBootstrapLoading ? (
1439
2207
  <div className="sdix-loading">
1440
2208
  <SolidSpinner size={28} />
@@ -1449,6 +2217,70 @@ export function DatasourceIntrospectionPage() {
1449
2217
  </div>
1450
2218
  ) : (
1451
2219
  <>
2220
+ {!isWorkspaceMode ? (
2221
+ <div className="sdix-toolbar">
2222
+ <div className="sdix-toolbar__left">
2223
+ <div className="sdix-toolbar-search">
2224
+ <Search size={15} />
2225
+ <SolidInput
2226
+ value={tableSearch}
2227
+ onChange={(event) => setTableSearch(event.target.value)}
2228
+ placeholder="Search tables"
2229
+ disabled={!selectedDatasource}
2230
+ />
2231
+ </div>
2232
+ </div>
2233
+
2234
+ <div className="sdix-toolbar__actions">
2235
+ {isBootstrapFetching || isTablesFetching ? (
2236
+ <div className="sdix-inline-status">
2237
+ <SolidSpinner size={16} />
2238
+ <span>Refreshing</span>
2239
+ </div>
2240
+ ) : null}
2241
+ <SolidButton
2242
+ size="small"
2243
+ leftIcon={<Sparkles size={14} />}
2244
+ onClick={() => {
2245
+ if (selectedListRow) {
2246
+ startMappingForRecord(selectedListRow);
2247
+ }
2248
+ }}
2249
+ disabled={!selectedListRow}
2250
+ >
2251
+ Start Mapping
2252
+ </SolidButton>
2253
+ <SolidSelect
2254
+ value={selectedDatasource}
2255
+ options={datasourceOptions}
2256
+ placeholder="Datasource"
2257
+ className="sdix-toolbar-select sdix-toolbar-select--compact"
2258
+ onChange={(event) => setSelectedDatasource(event.value)}
2259
+ />
2260
+ <SolidButton
2261
+ size="small"
2262
+ className="sdix-icon-button"
2263
+ leftIcon={<RefreshCw size={16} />}
2264
+ onClick={() => void handleRefresh()}
2265
+ tooltip="Refresh"
2266
+ aria-label="Refresh"
2267
+ />
2268
+ <SolidButton
2269
+ size="small"
2270
+ className="sdix-icon-button"
2271
+ leftIcon={<Cog size={16} />}
2272
+ onClick={() => {
2273
+ setDraftSessionSettings(sessionSettings);
2274
+ setSettingsTab("datasource-info");
2275
+ setSettingsOpen(true);
2276
+ }}
2277
+ tooltip="Session settings"
2278
+ aria-label="Session settings"
2279
+ />
2280
+ </div>
2281
+ </div>
2282
+ ) : null}
2283
+
1452
2284
  {selectedDatasource && tablesResponse?.synchronizeBlocked ? (
1453
2285
  <div className="sdix-blocker">
1454
2286
  <ShieldAlert size={20} />
@@ -1459,93 +2291,310 @@ export function DatasourceIntrospectionPage() {
1459
2291
  </div>
1460
2292
  ) : null}
1461
2293
 
1462
- <div className="sdix-workbench">
1463
- <div className="sdix-table-shell">
1464
- {!selectedDatasource ? (
1465
- <div className="sdix-empty-state is-inline">
1466
- <Database size={22} />
1467
- <p>Select a datasource to load its tables.</p>
1468
- </div>
1469
- ) : isTablesLoading ? (
1470
- <div className="sdix-loading is-inline">
1471
- <SolidSpinner size={24} />
1472
- <p>Inspecting datasource tables...</p>
2294
+ {!isWorkspaceMode ? (
2295
+ <div className="sdix-workbench">
2296
+ <div className="sdix-table-shell">
2297
+ {!selectedDatasource ? (
2298
+ <div className="sdix-empty-state is-inline">
2299
+ <Database size={22} />
2300
+ <p>Select a datasource to load its tables.</p>
2301
+ </div>
2302
+ ) : isTablesLoading ? (
2303
+ <div className="sdix-loading is-inline">
2304
+ <SolidSpinner size={24} />
2305
+ <p>Inspecting datasource tables...</p>
2306
+ </div>
2307
+ ) : tablesError ? (
2308
+ <div className="sdix-error is-inline">
2309
+ <span>{readQueryError(tablesError)}</span>
2310
+ <SolidButton size="small" variant="outline" onClick={() => void refetchTables()}>
2311
+ Retry
2312
+ </SolidButton>
2313
+ </div>
2314
+ ) : !filteredRecords.length ? (
2315
+ <div className="sdix-empty-state is-inline">
2316
+ <Database size={22} />
2317
+ <p>{tableSearch ? "No tables match the current quick filter." : "No database tables were discovered for this datasource."}</p>
2318
+ </div>
2319
+ ) : (
2320
+ <div className="sdix-table-area">
2321
+ <SolidDataTable
2322
+ value={filteredRecords}
2323
+ dataKey="tableName"
2324
+ viewportHeight="calc(100vh - 162px)"
2325
+ size="small"
2326
+ onRowClick={({ data }) => setSelectedRows([data])}
2327
+ rowClassName={(rowData) => `${rowData.schema ?? ""}::${rowData.tableName}` === selectedListRowKey ? "sdix-table-row-selected" : ""}
2328
+ emptyMessage="No datasource tables found"
2329
+ >
2330
+ <Column
2331
+ header=""
2332
+ body={(rowData: DatasourceIntrospectionTableRecord) => {
2333
+ const rowKey = `${rowData.schema ?? ""}::${rowData.tableName}`;
2334
+ return (
2335
+ <div className="sdix-selection-cell" data-no-row-click="true">
2336
+ <SolidCheckbox
2337
+ checked={rowKey === selectedListRowKey}
2338
+ onChange={(event) => setSelectedRows(event.target.checked ? [rowData] : [])}
2339
+ />
2340
+ </div>
2341
+ );
2342
+ }}
2343
+ style={{ width: 52, minWidth: 52 }}
2344
+ />
2345
+ <Column
2346
+ field="tableName"
2347
+ header="Table"
2348
+ body={(rowData: DatasourceIntrospectionTableRecord) => (
2349
+ <div className="sdix-table-primary-cell">
2350
+ <strong>{rowData.tableName}</strong>
2351
+ </div>
2352
+ )}
2353
+ style={{ minWidth: 240 }}
2354
+ />
2355
+ <Column field="schema" header="Schema" body={(rowData: DatasourceIntrospectionTableRecord) => rowData.schema || "dbo/default"} style={{ minWidth: 120 }} />
2356
+ <Column
2357
+ header="Status"
2358
+ body={(rowData: DatasourceIntrospectionTableRecord) => (
2359
+ <span
2360
+ className={`sdix-status-badge ${rowData.mapped ? "is-mapped" : "is-unmapped"}`}
2361
+ >
2362
+ {statusLabel(rowData)}
2363
+ </span>
2364
+ )}
2365
+ style={{ minWidth: 150 }}
2366
+ />
2367
+ <Column
2368
+ header="Superclass"
2369
+ body={(rowData: DatasourceIntrospectionTableRecord) => rowData.suggestedBaseClassName}
2370
+ style={{ minWidth: 220 }}
2371
+ />
2372
+ <Column field="suggestedUserKeyField" header="User Key" style={{ minWidth: 160 }} />
2373
+ <Column
2374
+ header="Mapped Model"
2375
+ body={(rowData: DatasourceIntrospectionTableRecord) => (
2376
+ rowData.mapped
2377
+ ? (
2378
+ <div className="sdix-table-primary-cell">
2379
+ <strong>{rowData.mappedModelDisplayName || rowData.mappedModelSingularName || "-"}</strong>
2380
+ {rowData.mappedModuleDisplayName || rowData.mappedModuleName ? (
2381
+ <span>{rowData.mappedModuleDisplayName || rowData.mappedModuleName}</span>
2382
+ ) : null}
2383
+ </div>
2384
+ )
2385
+ : <span className="sdix-muted-cell">Not mapped</span>
2386
+ )}
2387
+ style={{ minWidth: 200 }}
2388
+ />
2389
+ <Column
2390
+ header="System Columns"
2391
+ body={(rowData: DatasourceIntrospectionTableRecord) => Math.max(rowData.columnCount - rowData.physicalColumnCount, 0)}
2392
+ style={{ minWidth: 130 }}
2393
+ />
2394
+ <Column
2395
+ header="Columns"
2396
+ body={(rowData: DatasourceIntrospectionTableRecord) => rowData.physicalColumnCount}
2397
+ style={{ minWidth: 110 }}
2398
+ />
2399
+ </SolidDataTable>
2400
+ </div>
2401
+ )}
1473
2402
  </div>
1474
- ) : tablesError ? (
1475
- <div className="sdix-error is-inline">
1476
- <span>{readQueryError(tablesError)}</span>
1477
- <SolidButton size="small" variant="outline" onClick={() => void refetchTables()}>
1478
- Retry
2403
+ </div>
2404
+ ) : (
2405
+ <div className="sdix-workspace-screen">
2406
+ <div className="sdix-workspace-header">
2407
+ <SolidButton size="small" variant="outline" leftIcon={<ArrowLeft size={14} />} onClick={leaveWorkspace}>
2408
+ Back
1479
2409
  </SolidButton>
2410
+ <div className="sdix-workspace-header__identity sdix-workspace-header__identity--right">
2411
+ <h2>{selectedTable?.tableName || "Mapping workspace"}</h2>
2412
+ <p>
2413
+ {selectedTable
2414
+ ? `${selectedTable.schema || "dbo"} · ${selectedDatasourceRecord?.database || selectedDatasourceRecord?.displayName || "Datasource"}${selectedDatasourceRecord ? ` · ${providerLabel(selectedDatasourceRecord.type)}` : ""}`
2415
+ : "Complete the legacy table mapping in a dedicated workspace."}
2416
+ </p>
2417
+ </div>
1480
2418
  </div>
1481
- ) : !filteredRecords.length ? (
1482
- <div className="sdix-empty-state is-inline">
1483
- <Database size={22} />
1484
- <p>{tableSearch ? "No tables match the current quick filter." : "No database tables were discovered for this datasource."}</p>
1485
- </div>
1486
- ) : (
1487
- <div className="sdix-table-area">
1488
- <SolidDataTable
1489
- value={filteredRecords}
1490
- dataKey="tableName"
1491
- viewportHeight="calc(100vh - 162px)"
1492
- size="small"
1493
- onRowClick={({ data }) => openTableDrawer(data)}
1494
- rowClassName={(rowData) => `${rowData.schema ?? ""}::${rowData.tableName}` === selectedTableKey && drawerOpen ? "sdix-table-row-selected" : ""}
1495
- emptyMessage="No datasource tables found"
1496
- >
1497
- <Column
1498
- field="tableName"
1499
- header="Table"
1500
- body={(rowData: DatasourceIntrospectionTableRecord) => (
1501
- <div className="sdix-table-primary-cell">
1502
- <strong>{rowData.tableName}</strong>
1503
- </div>
1504
- )}
1505
- style={{ minWidth: 240 }}
1506
- />
1507
- <Column field="schema" header="Schema" body={(rowData: DatasourceIntrospectionTableRecord) => rowData.schema || "dbo/default"} style={{ minWidth: 120 }} />
1508
- <Column
1509
- header="Status"
1510
- body={(rowData: DatasourceIntrospectionTableRecord) => (
1511
- <span
1512
- className={`sdix-status-badge ${rowData.mapped ? "is-mapped" : "is-unmapped"}`}
2419
+
2420
+ {!selectedTable && !isTablesLoading ? (
2421
+ <div className="sdix-error">
2422
+ <span>The selected table is no longer available for this datasource.</span>
2423
+ <SolidButton size="small" variant="outline" onClick={leaveWorkspace}>
2424
+ Back to table list
2425
+ </SolidButton>
2426
+ </div>
2427
+ ) : drawerLoadingError ? (
2428
+ <div className="sdix-error">
2429
+ <span>{readQueryError(drawerLoadingError)}</span>
2430
+ <SolidButton size="small" variant="outline" onClick={() => void refetchTableDetail()}>
2431
+ Retry
2432
+ </SolidButton>
2433
+ </div>
2434
+ ) : isDrawerBlockingLoading ? (
2435
+ <div className="sdix-loading sdix-workspace-loading">
2436
+ <SolidSpinner size={24} />
2437
+ <p>Preparing column mapping preview...</p>
2438
+ </div>
2439
+ ) : (
2440
+ <div className="sdix-wizard-shell">
2441
+ <div className="sdix-wizard-stepper">
2442
+ {stepperItems.map((step, index) => (
2443
+ <button
2444
+ key={step.key}
2445
+ type="button"
2446
+ className={[
2447
+ "sdix-wizard-stepper-item",
2448
+ step.active ? "is-active" : "",
2449
+ step.complete ? "is-complete" : "",
2450
+ step.disabled ? "is-disabled" : "",
2451
+ ].filter(Boolean).join(" ")}
2452
+ onClick={step.onClick}
2453
+ disabled={step.disabled}
1513
2454
  >
1514
- {statusLabel(rowData)}
1515
- </span>
1516
- )}
1517
- style={{ minWidth: 150 }}
1518
- />
1519
- <Column
1520
- header="Superclass"
1521
- body={(rowData: DatasourceIntrospectionTableRecord) => rowData.suggestedBaseClassName}
1522
- style={{ minWidth: 220 }}
1523
- />
1524
- <Column field="suggestedUserKeyField" header="User Key" style={{ minWidth: 160 }} />
1525
- <Column
1526
- header="Mapped Model"
1527
- body={(rowData: DatasourceIntrospectionTableRecord) => (
1528
- rowData.mapped
1529
- ? (rowData.mappedModelDisplayName || rowData.mappedModelSingularName || "-")
1530
- : <span className="sdix-muted-cell">Not mapped</span>
1531
- )}
1532
- style={{ minWidth: 200 }}
1533
- />
1534
- <Column
1535
- header="System Columns"
1536
- body={(rowData: DatasourceIntrospectionTableRecord) => Math.max(rowData.columnCount - rowData.physicalColumnCount, 0)}
1537
- style={{ minWidth: 130 }}
1538
- />
1539
- <Column
1540
- header="Columns"
1541
- body={(rowData: DatasourceIntrospectionTableRecord) => rowData.physicalColumnCount}
1542
- style={{ minWidth: 110 }}
1543
- />
1544
- </SolidDataTable>
1545
- </div>
1546
- )}
2455
+ <span className="sdix-wizard-stepper-badge">
2456
+ {step.complete ? <Check size={12} /> : index + 1}
2457
+ </span>
2458
+ <span className="sdix-wizard-stepper-label">{step.label}</span>
2459
+ </button>
2460
+ ))}
2461
+ </div>
2462
+
2463
+ <div className="sdix-wizard-panel">
2464
+ <div className="sdix-wizard-panel__body">
2465
+ {workspaceContent}
2466
+ </div>
2467
+ </div>
2468
+
2469
+ {drawerDraft ? (
2470
+ <div className="sdix-wizard-footer">
2471
+ <div className="sdix-wizard-footer__meta">{workspaceMeta}</div>
2472
+ <div className="sdix-wizard-footer__actions">
2473
+ {drawerDraft.mapped && drawerDraft.modelId ? (
2474
+ <SolidButton size="small" variant="outline" leftIcon={<ExternalLink size={14} />} onClick={openMappedModelForm}>
2475
+ Open model form
2476
+ </SolidButton>
2477
+ ) : null}
2478
+ {workspaceStep === "configure" ? (
2479
+ <SolidButton
2480
+ size="small"
2481
+ leftIcon={<Save size={14} />}
2482
+ loading={isPreviewMappingLoading}
2483
+ onClick={() => void reviewMappingJson()}
2484
+ >
2485
+ Review Mapping
2486
+ </SolidButton>
2487
+ ) : null}
2488
+ {workspaceStep === "review-json" ? (
2489
+ <>
2490
+ <SolidButton size="small" variant="outline" onClick={() => setWorkspaceStep("configure")}>
2491
+ Back
2492
+ </SolidButton>
2493
+ <SolidButton size="small" leftIcon={<Save size={14} />} loading={isSavingMapping} onClick={() => void persistMappingArtifacts()}>
2494
+ Continue
2495
+ </SolidButton>
2496
+ </>
2497
+ ) : null}
2498
+ {workspaceStep === "generate-code" ? (
2499
+ <>
2500
+ <SolidButton size="small" variant="outline" onClick={() => setWorkspaceStep("review-json")}>
2501
+ Back
2502
+ </SolidButton>
2503
+ <SolidButton
2504
+ size="small"
2505
+ disabled={!hasAttemptedGenerateCode || isGenerateCodeLoading || isGeneratingCodeWorkflow}
2506
+ onClick={() => setWorkspaceStep("create-migrations")}
2507
+ >
2508
+ Continue
2509
+ </SolidButton>
2510
+ </>
2511
+ ) : null}
2512
+ {workspaceStep === "create-migrations" ? (
2513
+ <>
2514
+ <SolidButton size="small" variant="outline" onClick={() => setWorkspaceStep("generate-code")}>
2515
+ Back
2516
+ </SolidButton>
2517
+ <SolidButton size="small" leftIcon={<Save size={14} />} loading={isCreatingMigrationArtifacts} onClick={() => void handleCreateMigrationArtifacts()}>
2518
+ Continue
2519
+ </SolidButton>
2520
+ </>
2521
+ ) : null}
2522
+ {workspaceStep === "run-migration" ? (
2523
+ runMigrationCompleted ? (
2524
+ <SolidButton
2525
+ size="small"
2526
+ leftIcon={<Check size={14} />}
2527
+ onClick={() => navigate("/admin/core/solid-core/model-metadata/list")}
2528
+ >
2529
+ Finish
2530
+ </SolidButton>
2531
+ ) : (
2532
+ <>
2533
+ <SolidButton size="small" variant="outline" onClick={() => setWorkspaceStep("create-migrations")}>
2534
+ Back
2535
+ </SolidButton>
2536
+ <SolidButton size="small" leftIcon={<Database size={14} />} loading={isRunMigrationLoading} onClick={() => void handleRunMigration()}>
2537
+ Continue
2538
+ </SolidButton>
2539
+ </>
2540
+ )
2541
+ ) : null}
2542
+ </div>
2543
+ </div>
2544
+ ) : null}
2545
+ </div>
2546
+ )}
1547
2547
  </div>
1548
- </div>
2548
+ )}
2549
+
2550
+ <SolidDialog
2551
+ open={Boolean(activeFieldConfigColumn)}
2552
+ onOpenChange={(open) => {
2553
+ if (!open) {
2554
+ setFieldConfigColumnName(null);
2555
+ }
2556
+ }}
2557
+ style={{ width: "min(1080px, calc(100vw - 48px))" }}
2558
+ >
2559
+ <SolidDialogHeader>
2560
+ <div>
2561
+ <SolidDialogTitle>Field configuration</SolidDialogTitle>
2562
+ <SolidDialogDescription>
2563
+ Reuse the standard field form here to fine-tune the mapped SolidX field before saving the introspection draft.
2564
+ </SolidDialogDescription>
2565
+ </div>
2566
+ <SolidDialogClose />
2567
+ </SolidDialogHeader>
2568
+ <SolidDialogSeparator />
2569
+ <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}
2596
+ </SolidDialogBody>
2597
+ </SolidDialog>
1549
2598
 
1550
2599
  <SolidDialog
1551
2600
  open={settingsOpen}
@@ -1580,138 +2629,6 @@ export function DatasourceIntrospectionPage() {
1580
2629
  </SolidButton>
1581
2630
  </SolidDialogFooter>
1582
2631
  </SolidDialog>
1583
-
1584
- <SolidDialog
1585
- open={drawerOpen}
1586
- onOpenChange={setDrawerOpen}
1587
- className="sdix-drawer"
1588
- overlayClassName="sdix-drawer-overlay"
1589
- dismissible
1590
- >
1591
- <SolidDialogHeader className="sdix-drawer-header">
1592
- <div className="sdix-drawer-header__identity">
1593
- <SolidDialogTitle>{selectedTable?.tableName || "Table details"}</SolidDialogTitle>
1594
- <SolidDialogDescription className="sdix-drawer-header__meta">
1595
- {selectedTable
1596
- ? `${selectedTable.schema || "default"} · ${selectedDatasourceRecord?.database || selectedDatasourceRecord?.displayName || "Datasource"}${selectedDatasourceRecord ? ` · ${providerLabel(selectedDatasourceRecord.type)}` : ""}`
1597
- : "Review mapping details"}
1598
- </SolidDialogDescription>
1599
- </div>
1600
- <SolidDialogClose />
1601
- </SolidDialogHeader>
1602
- <SolidDialogSeparator />
1603
- <SolidDialogBody className="sdix-drawer-body">
1604
- {!selectedTable ? (
1605
- <div className="sdix-empty-state is-inline">
1606
- <Sparkles size={22} />
1607
- <p>Select a table row to review its structure.</p>
1608
- </div>
1609
- ) : drawerLoadingError ? (
1610
- <div className="sdix-error is-inline">
1611
- <span>{readQueryError(drawerLoadingError)}</span>
1612
- <SolidButton size="small" variant="outline" onClick={() => void refetchTableDetail()}>
1613
- Retry
1614
- </SolidButton>
1615
- </div>
1616
- ) : isDrawerBlockingLoading ? (
1617
- <div className="sdix-loading is-inline">
1618
- <SolidSpinner size={24} />
1619
- <p>Preparing column mapping preview...</p>
1620
- </div>
1621
- ) : (
1622
- <div className="sdix-drawer-stack">
1623
- {drawerDraft && !drawerDraft.mapped && drawerStep === "edit" ? (
1624
- <div className="sdix-detail-callout sdix-system-fields-callout">
1625
- <h3>SolidX system fields are added later</h3>
1626
- <p>
1627
- This preview only lists the legacy table columns that already exist today.
1628
- {" "}
1629
- Framework system fields from the selected superclass, including the namespaced
1630
- {" "}
1631
- <code>ss_*</code>
1632
- {" "}
1633
- columns, are introduced when you save the mapping and then generate/apply the follow-up migrations.
1634
- </p>
1635
- </div>
1636
- ) : null}
1637
- {drawerStep === "edit" ? (
1638
- <SolidTabGroup
1639
- tabs={drawerTabs}
1640
- value={drawerTab}
1641
- onValueChange={setDrawerTab}
1642
- className="sdix-drawer-tabs"
1643
- panelClassName="sdix-drawer-tab-panel"
1644
- />
1645
- ) : (
1646
- <SolidTabGroup
1647
- tabs={reviewTabs}
1648
- value={reviewTab}
1649
- onValueChange={setReviewTab}
1650
- className="sdix-drawer-tabs"
1651
- panelClassName="sdix-drawer-tab-panel"
1652
- />
1653
- )}
1654
- </div>
1655
- )}
1656
- </SolidDialogBody>
1657
- {drawerDraft && !isDrawerBlockingLoading ? (
1658
- <>
1659
- <SolidDialogSeparator />
1660
- <SolidDialogFooter className="sdix-drawer-footer">
1661
- <div className="sdix-drawer-footer__meta">
1662
- {drawerStep === "review" && mappingPreview ? (
1663
- <span>
1664
- Review the metadata update first, then save to apply
1665
- {" "}
1666
- <strong>{mappingPreview.summary.includedColumnNames.length}</strong>
1667
- {" "}
1668
- mapped columns
1669
- {mappingPreview.summary.generatedSystemColumnCount
1670
- ? ` and ${mappingPreview.summary.generatedSystemColumnCount} SolidX system column migration changes`
1671
- : ""}
1672
- .
1673
- </span>
1674
- ) : drawerDraft.mapped ? (
1675
- <span>
1676
- Editing mapped model
1677
- {" "}
1678
- <strong>{selectedTable?.mappedModelDisplayName || selectedTable?.mappedModelSingularName}</strong>
1679
- {drawerDraft.mappedModuleLabel ? ` in ${drawerDraft.mappedModuleLabel}` : ""}
1680
- </span>
1681
- ) : (
1682
- <span>Saving will create the model metadata and write it to the module JSON.</span>
1683
- )}
1684
- </div>
1685
- <div className="sdix-drawer-footer__actions">
1686
- {drawerDraft.mapped && drawerDraft.modelId ? (
1687
- <SolidButton size="small" variant="outline" leftIcon={<ExternalLink size={14} />} onClick={openMappedModelForm}>
1688
- Open model form
1689
- </SolidButton>
1690
- ) : null}
1691
- {drawerStep === "review" ? (
1692
- <>
1693
- <SolidButton size="small" variant="outline" onClick={() => setDrawerStep("edit")}>
1694
- Back to editing
1695
- </SolidButton>
1696
- <SolidButton size="small" leftIcon={<Save size={14} />} loading={isSavingMapping} onClick={() => void saveMapping()}>
1697
- Save mapping
1698
- </SolidButton>
1699
- </>
1700
- ) : (
1701
- <SolidButton
1702
- size="small"
1703
- leftIcon={<Save size={14} />}
1704
- loading={isPreviewMappingLoading}
1705
- onClick={() => void viewChanges()}
1706
- >
1707
- View Changes
1708
- </SolidButton>
1709
- )}
1710
- </div>
1711
- </SolidDialogFooter>
1712
- </>
1713
- ) : null}
1714
- </SolidDialog>
1715
2632
  </>
1716
2633
  )}
1717
2634
  </div>