@growthub/cli 0.13.8 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/codex-sites/route.js +13 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/env-status/route.js +31 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/helper/apply/route.js +130 -5
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceActivationPanel.jsx +17 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceHelperSetupModal.jsx +5 -2
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/ApiRegistryCreationCockpit.jsx +200 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/DataModelShell.jsx +501 -5
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/HelperSidecar.jsx +75 -55
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/ReferencePicker.jsx +2 -2
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +215 -13
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/apps/codex-sites-data-model-card.jsx +81 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/apps/page.jsx +31 -14
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/apps/settings-accordion-section.jsx +50 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workflows/WorkflowSurface.jsx +176 -5
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-builder.jsx +137 -5
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/resolver-loader.js +2 -4
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/api-registry-creation-flow.js +317 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/api-response-profile.js +207 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/codex-sites-local-state.js +139 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/codex-sites-workspace-adapter.js +156 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/creation-error-recovery.js +103 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/env-status.js +100 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph.js +63 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/sandbox-serverless-flow.js +215 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/server-resolver-write.js +67 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/serverless-upgrade.js +89 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-activation.js +11 -4
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-data-model.js +8 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-helper.js +7 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-resolver-proposal.js +200 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/package.json +1 -1
- package/package.json +1 -1
|
@@ -80,9 +80,16 @@ import { SourceTestPanel } from "./SourceTestPanel.jsx";
|
|
|
80
80
|
import { SandboxToolDraftPanel } from "./SandboxToolDraftPanel.jsx";
|
|
81
81
|
import { SandboxToolConfirmModal } from "./SandboxToolConfirmModal.jsx";
|
|
82
82
|
import { OrchestrationRunTracePanel } from "./OrchestrationRunTracePanel.jsx";
|
|
83
|
+
import { ApiRegistryCreationCockpit } from "./ApiRegistryCreationCockpit.jsx";
|
|
84
|
+
import { deriveApiRegistryCreationState } from "@/lib/api-registry-creation-flow";
|
|
85
|
+
import { deriveSandboxServerlessState } from "@/lib/sandbox-serverless-flow";
|
|
86
|
+
import { profileApiResponse, recommendResolver } from "@/lib/api-response-profile";
|
|
87
|
+
import { classifyCreationError } from "@/lib/creation-error-recovery";
|
|
83
88
|
import {
|
|
84
89
|
buildSandboxRowFromApiRegistry,
|
|
85
90
|
findSandboxRowsForRegistry,
|
|
91
|
+
buildDataSourceRowFromApiRegistry,
|
|
92
|
+
findDataSourceRowsForRegistry,
|
|
86
93
|
getOrchestrationGraphUiState,
|
|
87
94
|
redactSecretsFromText
|
|
88
95
|
} from "@/lib/orchestration-graph";
|
|
@@ -95,6 +102,12 @@ import {
|
|
|
95
102
|
pluralize,
|
|
96
103
|
textColorForAccent,
|
|
97
104
|
} from "./dm-shared.jsx";
|
|
105
|
+
import {
|
|
106
|
+
CODEX_SITES_OBJECT_ID,
|
|
107
|
+
codexSiteRecordToRow,
|
|
108
|
+
isCodexSiteUrl,
|
|
109
|
+
normalizeCodexSiteRecord,
|
|
110
|
+
} from "@/lib/codex-sites-workspace-adapter";
|
|
98
111
|
|
|
99
112
|
// ─── Object type definitions for the type-picker step ────────────────────────
|
|
100
113
|
|
|
@@ -511,8 +524,11 @@ function StaticSelect({ value, options, disabled, onChange, placeholder = "Selec
|
|
|
511
524
|
);
|
|
512
525
|
}
|
|
513
526
|
|
|
514
|
-
function DrawerSection({ title, children, defaultOpen = false }) {
|
|
527
|
+
function DrawerSection({ title, children, defaultOpen = false, forceOpen = false }) {
|
|
515
528
|
const [open, setOpen] = useState(defaultOpen);
|
|
529
|
+
useEffect(() => {
|
|
530
|
+
if (forceOpen) setOpen(true);
|
|
531
|
+
}, [forceOpen]);
|
|
516
532
|
return (
|
|
517
533
|
<section className={`dm-drawer-section${open ? " open" : ""}`}>
|
|
518
534
|
<button type="button" className="dm-drawer-section-toggle" onClick={() => setOpen((current) => !current)}>
|
|
@@ -646,13 +662,30 @@ function SandboxRecordFields({
|
|
|
646
662
|
onOpenGraphSidecar,
|
|
647
663
|
onOpenTraceSidecar
|
|
648
664
|
}) {
|
|
665
|
+
const router = useRouter();
|
|
649
666
|
const [sandboxAdapters, setSandboxAdapters] = useState([]);
|
|
667
|
+
const [serverlessSignals, setServerlessSignals] = useState({ configuredEnvRefs: [], persistenceAdapters: [] });
|
|
650
668
|
useEffect(() => {
|
|
651
669
|
fetch("/api/workspace/sandbox-adapters", { cache: "no-store" })
|
|
652
670
|
.then((res) => res.json())
|
|
653
671
|
.then((payload) => setSandboxAdapters(Array.isArray(payload.adapters) ? payload.adapters : []))
|
|
654
672
|
.catch(() => setSandboxAdapters([]));
|
|
655
673
|
}, []);
|
|
674
|
+
// Real runtime truth for the serverless/persistence cockpit (env-status).
|
|
675
|
+
useEffect(() => {
|
|
676
|
+
let cancelled = false;
|
|
677
|
+
fetch("/api/workspace/env-status", { cache: "no-store" })
|
|
678
|
+
.then((res) => (res.ok ? res.json() : {}))
|
|
679
|
+
.then((payload) => {
|
|
680
|
+
if (cancelled) return;
|
|
681
|
+
setServerlessSignals({
|
|
682
|
+
configuredEnvRefs: Array.isArray(payload.configuredEnvRefs) ? payload.configuredEnvRefs : [],
|
|
683
|
+
persistenceAdapters: Array.isArray(payload.persistenceAdapters) ? payload.persistenceAdapters : [],
|
|
684
|
+
});
|
|
685
|
+
})
|
|
686
|
+
.catch(() => {});
|
|
687
|
+
return () => { cancelled = true; };
|
|
688
|
+
}, [rowIndex, table.objectId]);
|
|
656
689
|
|
|
657
690
|
const locality = String(draft.runLocality || "local").trim().toLowerCase() === "serverless" ? "serverless" : "local";
|
|
658
691
|
const savedEnvRefs = useMemo(() => listSavedEnvRefs(workspaceConfig || {}), [workspaceConfig]);
|
|
@@ -691,8 +724,30 @@ function SandboxRecordFields({
|
|
|
691
724
|
|
|
692
725
|
const netOn = ["true", "1", "on", "yes"].includes(String(draft.networkAllow || "").trim().toLowerCase());
|
|
693
726
|
|
|
727
|
+
// Same cockpit interface + mental model as the API Registry lane, driven by
|
|
728
|
+
// the serverless/scheduling/persistence derivation. Steps are status-only
|
|
729
|
+
// here (inlineEditing) — the editable fields below are the editor.
|
|
730
|
+
const serverlessState = deriveSandboxServerlessState({
|
|
731
|
+
sandboxRow: draft,
|
|
732
|
+
workspaceConfig,
|
|
733
|
+
configuredEnvRefs: serverlessSignals.configuredEnvRefs,
|
|
734
|
+
persistenceAdapters: serverlessSignals.persistenceAdapters,
|
|
735
|
+
inlineEditing: true,
|
|
736
|
+
});
|
|
737
|
+
function handleServerlessAction(action) {
|
|
738
|
+
if (!action) return;
|
|
739
|
+
if (action.id === "toggle-locality") setRunLocality(serverlessState.isServerless ? "local" : "serverless");
|
|
740
|
+
else if (action.id === "open-settings") router.push(action.href || "/settings");
|
|
741
|
+
}
|
|
742
|
+
|
|
694
743
|
return (
|
|
695
744
|
<div className="dm-sandbox-config">
|
|
745
|
+
<ApiRegistryCreationCockpit
|
|
746
|
+
state={serverlessState}
|
|
747
|
+
onAction={handleServerlessAction}
|
|
748
|
+
disabled={!table.mutable || saving}
|
|
749
|
+
eyebrow={serverlessState.isServerless ? "Serverless workflow" : "Workflow runtime"}
|
|
750
|
+
/>
|
|
696
751
|
<DrawerSection title="Identity & Mode">
|
|
697
752
|
<label className="dm-record-field">
|
|
698
753
|
<span>Name</span>
|
|
@@ -1015,6 +1070,95 @@ function SandboxRecordFields({
|
|
|
1015
1070
|
);
|
|
1016
1071
|
}
|
|
1017
1072
|
|
|
1073
|
+
function CodexSitesRecordFields({ draft, setDraft, table, saving, onSave, rowIndex }) {
|
|
1074
|
+
const [sites, setSites] = useState([]);
|
|
1075
|
+
const [loadingSites, setLoadingSites] = useState(false);
|
|
1076
|
+
const [sitesMessage, setSitesMessage] = useState("");
|
|
1077
|
+
const selectedUrl = String(draft?.url || "").trim();
|
|
1078
|
+
|
|
1079
|
+
useEffect(() => {
|
|
1080
|
+
let cancelled = false;
|
|
1081
|
+
setLoadingSites(true);
|
|
1082
|
+
setSitesMessage("");
|
|
1083
|
+
fetch("/api/workspace/codex-sites", { cache: "no-store" })
|
|
1084
|
+
.then((res) => res.json())
|
|
1085
|
+
.then((payload) => {
|
|
1086
|
+
if (cancelled) return;
|
|
1087
|
+
const nextSites = Array.isArray(payload.sites)
|
|
1088
|
+
? payload.sites.map((site) => normalizeCodexSiteRecord(site)).filter((site) => isCodexSiteUrl(site.url))
|
|
1089
|
+
: [];
|
|
1090
|
+
setSites(nextSites);
|
|
1091
|
+
setSitesMessage(nextSites.length ? "" : "No Codex Sites are available from the workspace adapter.");
|
|
1092
|
+
})
|
|
1093
|
+
.catch((error) => {
|
|
1094
|
+
if (cancelled) return;
|
|
1095
|
+
setSites([]);
|
|
1096
|
+
setSitesMessage(error?.message || "Codex Sites adapter unavailable.");
|
|
1097
|
+
})
|
|
1098
|
+
.finally(() => {
|
|
1099
|
+
if (!cancelled) setLoadingSites(false);
|
|
1100
|
+
});
|
|
1101
|
+
return () => { cancelled = true; };
|
|
1102
|
+
}, []);
|
|
1103
|
+
|
|
1104
|
+
function patchFields(fields) {
|
|
1105
|
+
setDraft((current) => ({ ...current, ...fields }));
|
|
1106
|
+
onSave((config) => Object.entries(fields).reduce(
|
|
1107
|
+
(nextConfig, [column, value]) => updateTableCell(nextConfig, table, rowIndex, column, value),
|
|
1108
|
+
config
|
|
1109
|
+
));
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
function selectSite(url) {
|
|
1113
|
+
const site = sites.find((item) => item.url === url);
|
|
1114
|
+
if (!site) return;
|
|
1115
|
+
patchFields(codexSiteRecordToRow(site));
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
return (
|
|
1119
|
+
<div className="dm-codex-sites-config">
|
|
1120
|
+
<DrawerSection title="Codex Site Binding" defaultOpen>
|
|
1121
|
+
<label className="dm-record-field">
|
|
1122
|
+
<span>Available site</span>
|
|
1123
|
+
<StaticSelect
|
|
1124
|
+
value={selectedUrl}
|
|
1125
|
+
disabled={!table.mutable || saving || loadingSites || sites.length === 0}
|
|
1126
|
+
placeholder={loadingSites ? "Loading Codex Sites..." : "Select Codex Site..."}
|
|
1127
|
+
options={sites.map((site) => ({
|
|
1128
|
+
value: site.url,
|
|
1129
|
+
label: site.Name,
|
|
1130
|
+
source: site.url,
|
|
1131
|
+
}))}
|
|
1132
|
+
onChange={selectSite}
|
|
1133
|
+
/>
|
|
1134
|
+
{sitesMessage && <span className="dm-cell-empty">{sitesMessage}</span>}
|
|
1135
|
+
</label>
|
|
1136
|
+
{selectedUrl && (
|
|
1137
|
+
<a className="dm-btn-outline dm-codex-sites-open-link" href={selectedUrl} target="_blank" rel="noreferrer">
|
|
1138
|
+
<Link2 size={13} />Open selected site
|
|
1139
|
+
</a>
|
|
1140
|
+
)}
|
|
1141
|
+
</DrawerSection>
|
|
1142
|
+
<DrawerSection title="Bound Row" defaultOpen>
|
|
1143
|
+
{["Name", "app", "client", "url", "status", "accessMode", "dashboardId", "lastRecordedAt", "notes"].map((column) => (
|
|
1144
|
+
<RecordFieldEditor
|
|
1145
|
+
key={column}
|
|
1146
|
+
table={table}
|
|
1147
|
+
tables={[]}
|
|
1148
|
+
column={column}
|
|
1149
|
+
value={String(draft?.[column] ?? "")}
|
|
1150
|
+
saving={saving}
|
|
1151
|
+
editable={false}
|
|
1152
|
+
onDraft={() => {}}
|
|
1153
|
+
onCommit={() => {}}
|
|
1154
|
+
onExpandJson={() => {}}
|
|
1155
|
+
/>
|
|
1156
|
+
))}
|
|
1157
|
+
</DrawerSection>
|
|
1158
|
+
</div>
|
|
1159
|
+
);
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1018
1162
|
function DataModelRecordDrawer({
|
|
1019
1163
|
table,
|
|
1020
1164
|
tables,
|
|
@@ -1046,9 +1190,20 @@ function DataModelRecordDrawer({
|
|
|
1046
1190
|
const [createdSandboxMeta, setCreatedSandboxMeta] = useState(null);
|
|
1047
1191
|
const [createdSandboxTesting, setCreatedSandboxTesting] = useState(false);
|
|
1048
1192
|
const [createdSandboxTestMessage, setCreatedSandboxTestMessage] = useState("");
|
|
1193
|
+
const [creatingDataSource, setCreatingDataSource] = useState(false);
|
|
1194
|
+
const [createdDataSourceMeta, setCreatedDataSourceMeta] = useState(null);
|
|
1195
|
+
const [dataSourceMessage, setDataSourceMessage] = useState("");
|
|
1196
|
+
const [cockpitBusy, setCockpitBusy] = useState("");
|
|
1197
|
+
const [cockpitCollapsed, setCockpitCollapsed] = useState(false);
|
|
1198
|
+
// Real runtime truth for the creation cockpit: which auth refs resolve in the
|
|
1199
|
+
// server runtime, and the live source-records sidecar. Fetched (never guessed)
|
|
1200
|
+
// so auth/refresh readiness reflect actual state, and refreshed after actions.
|
|
1201
|
+
const [creationSignals, setCreationSignals] = useState({ configuredEnvRefs: [], sourceRecords: {} });
|
|
1202
|
+
const [creationReceipts, setCreationReceipts] = useState([]);
|
|
1049
1203
|
const [sidecarMode, setSidecarMode] = useState(null);
|
|
1050
1204
|
const [traceField, setTraceField] = useState(null);
|
|
1051
1205
|
const [traceRunId, setTraceRunId] = useState("");
|
|
1206
|
+
const drawerScrollRef = useRef(null);
|
|
1052
1207
|
const drawerKeyRef = useRef("");
|
|
1053
1208
|
const router = useRouter();
|
|
1054
1209
|
|
|
@@ -1070,6 +1225,14 @@ function DataModelRecordDrawer({
|
|
|
1070
1225
|
setSandboxToolDraft({});
|
|
1071
1226
|
setCreatedSandboxMeta(null);
|
|
1072
1227
|
setCreatedSandboxTestMessage("");
|
|
1228
|
+
setCreatingDataSource(false);
|
|
1229
|
+
setCreatedDataSourceMeta(null);
|
|
1230
|
+
setDataSourceMessage("");
|
|
1231
|
+
setCreationReceipts([]);
|
|
1232
|
+
setCockpitCollapsed(false);
|
|
1233
|
+
requestAnimationFrame(() => {
|
|
1234
|
+
if (drawerScrollRef.current) drawerScrollRef.current.scrollTop = 0;
|
|
1235
|
+
});
|
|
1073
1236
|
}
|
|
1074
1237
|
if (initialSidecar?.mode === "trace") {
|
|
1075
1238
|
setSidecarMode("trace");
|
|
@@ -1082,10 +1245,39 @@ function DataModelRecordDrawer({
|
|
|
1082
1245
|
}
|
|
1083
1246
|
}, [row, rowIndex, initialSidecar, table.id, table.objectId, table.source, table.columns, table.fieldSettings?.hidden]);
|
|
1084
1247
|
|
|
1248
|
+
// Load real cockpit truth (configured auth refs + source-records sidecar) when
|
|
1249
|
+
// an API Registry row is open. The creation cockpit derives auth/refresh
|
|
1250
|
+
// readiness from these — never guessed, never faked.
|
|
1251
|
+
useEffect(() => {
|
|
1252
|
+
if (table.objectType !== "api-registry" || rowIndex === null || rowIndex === undefined) return;
|
|
1253
|
+
let cancelled = false;
|
|
1254
|
+
(async () => {
|
|
1255
|
+
try {
|
|
1256
|
+
const [statusRes, wsRes] = await Promise.all([
|
|
1257
|
+
fetch("/api/workspace/env-status", { cache: "no-store" }),
|
|
1258
|
+
fetch("/api/workspace", { cache: "no-store" }),
|
|
1259
|
+
]);
|
|
1260
|
+
const status = statusRes.ok ? await statusRes.json() : {};
|
|
1261
|
+
const ws = wsRes.ok ? await wsRes.json() : {};
|
|
1262
|
+
if (cancelled) return;
|
|
1263
|
+
setCreationSignals({
|
|
1264
|
+
configuredEnvRefs: Array.isArray(status.configuredEnvRefs) ? status.configuredEnvRefs : [],
|
|
1265
|
+
sourceRecords: ws.workspaceSourceRecords && typeof ws.workspaceSourceRecords === "object"
|
|
1266
|
+
? ws.workspaceSourceRecords
|
|
1267
|
+
: {},
|
|
1268
|
+
});
|
|
1269
|
+
} catch {
|
|
1270
|
+
/* leave signals as-is — cockpit degrades to pending, never fakes */
|
|
1271
|
+
}
|
|
1272
|
+
})();
|
|
1273
|
+
return () => { cancelled = true; };
|
|
1274
|
+
}, [table.objectType, table.objectId, rowIndex]);
|
|
1275
|
+
|
|
1085
1276
|
if (rowIndex === null || rowIndex === undefined || !row) return null;
|
|
1086
1277
|
|
|
1087
1278
|
const isApiRegistry = table.objectType === "api-registry";
|
|
1088
1279
|
const isSandbox = table.objectType === "sandbox-environment";
|
|
1280
|
+
const isCodexSitesObject = table.objectId === CODEX_SITES_OBJECT_ID;
|
|
1089
1281
|
const isDirty = JSON.stringify(draft || {}) !== JSON.stringify(row || {}) || JSON.stringify(pendingColumns) !== JSON.stringify(table.columns || []) || JSON.stringify(pendingHidden) !== JSON.stringify(table.fieldSettings?.hidden || []);
|
|
1090
1282
|
|
|
1091
1283
|
function updateField(column, value) {
|
|
@@ -1162,6 +1354,14 @@ function DataModelRecordDrawer({
|
|
|
1162
1354
|
});
|
|
1163
1355
|
setDraft((current) => ({ ...current, status, lastTested: new Date().toISOString(), lastResponse: responseText }));
|
|
1164
1356
|
setTestMessage(payload.ok ? "Connected" : payload.error || "Connection failed");
|
|
1357
|
+
if (isApiRegistry) {
|
|
1358
|
+
if (payload.ok) {
|
|
1359
|
+
pushReceipt({ kind: "api-test", ok: true, detail: `Tested — HTTP ${payload.status ?? 200}${payload.usedServerSecret ? " · used server secret" : ""}.` });
|
|
1360
|
+
} else {
|
|
1361
|
+
const recovery = classifyCreationError({ phase: "test", httpStatus: payload.status, detail: redactSecretsFromText(payload.error || `HTTP ${payload.status ?? ""} failed`) });
|
|
1362
|
+
pushReceipt({ kind: "api-test", ok: false, detail: `${recovery.safeDetail}. ${recovery.requiredAction}` });
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1165
1365
|
} catch (err) {
|
|
1166
1366
|
const responseText = JSON.stringify({ error: err.message || "Connection failed" }, null, 2);
|
|
1167
1367
|
onSave((config) => {
|
|
@@ -1245,6 +1445,221 @@ function DataModelRecordDrawer({
|
|
|
1245
1445
|
}
|
|
1246
1446
|
}
|
|
1247
1447
|
|
|
1448
|
+
function createDataSourceFromRegistry() {
|
|
1449
|
+
const integrationId = String(draft?.integrationId || "").trim();
|
|
1450
|
+
if (!integrationId) {
|
|
1451
|
+
setDataSourceMessage("This API Registry row needs an integrationId before a Data Source can reference it.");
|
|
1452
|
+
return;
|
|
1453
|
+
}
|
|
1454
|
+
if (findDataSourceRowsForRegistry(workspaceConfig, integrationId).length > 0) {
|
|
1455
|
+
setDataSourceMessage("A Data Source already references this API. Open it instead of creating a duplicate.");
|
|
1456
|
+
return;
|
|
1457
|
+
}
|
|
1458
|
+
setCreatingDataSource(true);
|
|
1459
|
+
setDataSourceMessage("");
|
|
1460
|
+
try {
|
|
1461
|
+
let createdMeta = null;
|
|
1462
|
+
onSave((config) => {
|
|
1463
|
+
let next = config;
|
|
1464
|
+
if (findDataSourceRowsForRegistry(next, integrationId).length > 0) return next;
|
|
1465
|
+
// Dedicated data-source object per API (one object = one live integration,
|
|
1466
|
+
// matching the refresh-sources contract which binds integrationId per object).
|
|
1467
|
+
const dsName = `${String(draft?.Name || integrationId).trim()} Source`;
|
|
1468
|
+
const beforeIds = new Set((next.dataModel?.objects || []).map((o) => o.id));
|
|
1469
|
+
next = createTypedBusinessObject(next, { name: dsName, objectType: "data-source" });
|
|
1470
|
+
const newObj = (next.dataModel?.objects || []).find((o) => o.objectType === "data-source" && !beforeIds.has(o.id));
|
|
1471
|
+
if (!newObj) return next;
|
|
1472
|
+
const sourceTable = listWorkspaceDataModelTables(next).find((t) => t.objectId === newObj.id);
|
|
1473
|
+
const profile = profileApiResponse(draft?.lastResponse);
|
|
1474
|
+
const newRow = buildDataSourceRowFromApiRegistry(next, draft, {
|
|
1475
|
+
entityType: profile?.parsed ? profile.suggestedEntityType : undefined,
|
|
1476
|
+
});
|
|
1477
|
+
if (sourceTable) next = appendRowsToTable(next, sourceTable, [newRow]);
|
|
1478
|
+
// Make the object live-backed so refresh-sources hydrates the sidecar
|
|
1479
|
+
// (keyed by object id). Without this binding, refresh skips it as
|
|
1480
|
+
// "not-live-backed" and the journey would never close.
|
|
1481
|
+
next = {
|
|
1482
|
+
...next,
|
|
1483
|
+
dataModel: {
|
|
1484
|
+
...next.dataModel,
|
|
1485
|
+
objects: (next.dataModel.objects || []).map((o) =>
|
|
1486
|
+
o.id === newObj.id
|
|
1487
|
+
? {
|
|
1488
|
+
...o,
|
|
1489
|
+
sourceId: newRow.sourceId,
|
|
1490
|
+
binding: {
|
|
1491
|
+
mode: "integration",
|
|
1492
|
+
lane: "data-source",
|
|
1493
|
+
sourceStorage: "workspace-source-records",
|
|
1494
|
+
integrationId,
|
|
1495
|
+
sourceId: newRow.sourceId,
|
|
1496
|
+
source: newRow.Name,
|
|
1497
|
+
},
|
|
1498
|
+
}
|
|
1499
|
+
: o
|
|
1500
|
+
),
|
|
1501
|
+
},
|
|
1502
|
+
};
|
|
1503
|
+
createdMeta = { objectId: newObj.id, name: newRow.Name, sourceId: newRow.sourceId };
|
|
1504
|
+
return next;
|
|
1505
|
+
});
|
|
1506
|
+
if (createdMeta) {
|
|
1507
|
+
setCreatedDataSourceMeta(createdMeta);
|
|
1508
|
+
setDataSourceMessage("Data Source created and live-backed. Use Refresh to pull records into the workspace — nothing auto-fetches.");
|
|
1509
|
+
pushReceipt({ kind: "data-source-created", ok: true, detail: `Created "${createdMeta.name}" (sourceId ${createdMeta.sourceId}), live-backed via registryId ${integrationId}.` });
|
|
1510
|
+
reloadCreationSignals();
|
|
1511
|
+
} else {
|
|
1512
|
+
setDataSourceMessage("A Data Source already references this API.");
|
|
1513
|
+
}
|
|
1514
|
+
} finally {
|
|
1515
|
+
setCreatingDataSource(false);
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
function openDataSourceRow(objectIdOverride) {
|
|
1520
|
+
const objectId = String(objectIdOverride || createdDataSourceMeta?.objectId || "").trim();
|
|
1521
|
+
if (!objectId) return;
|
|
1522
|
+
onClose();
|
|
1523
|
+
router.push(`/data-model?object=${encodeURIComponent(objectId)}`);
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
// Append a creation receipt (test / create / refresh outcomes). Secret-safe —
|
|
1527
|
+
// detail strings are caller-redacted; receipts hold no values.
|
|
1528
|
+
function pushReceipt(entry) {
|
|
1529
|
+
setCreationReceipts((cur) => [{ at: new Date().toISOString(), ...entry }, ...cur].slice(0, 12));
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
// Pull real cockpit truth: configured auth refs (server-resolved, slugs only)
|
|
1533
|
+
// + the live source-records sidecar. Safe to call repeatedly; never throws.
|
|
1534
|
+
async function reloadCreationSignals() {
|
|
1535
|
+
try {
|
|
1536
|
+
const [statusRes, wsRes] = await Promise.all([
|
|
1537
|
+
fetch("/api/workspace/env-status", { cache: "no-store" }),
|
|
1538
|
+
fetch("/api/workspace", { cache: "no-store" }),
|
|
1539
|
+
]);
|
|
1540
|
+
const status = statusRes.ok ? await statusRes.json() : {};
|
|
1541
|
+
const ws = wsRes.ok ? await wsRes.json() : {};
|
|
1542
|
+
setCreationSignals({
|
|
1543
|
+
configuredEnvRefs: Array.isArray(status.configuredEnvRefs) ? status.configuredEnvRefs : [],
|
|
1544
|
+
sourceRecords: ws.workspaceSourceRecords && typeof ws.workspaceSourceRecords === "object"
|
|
1545
|
+
? ws.workspaceSourceRecords
|
|
1546
|
+
: {},
|
|
1547
|
+
});
|
|
1548
|
+
} catch {
|
|
1549
|
+
/* signals stay at their last value — the cockpit degrades to pending, never fakes */
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
// Refresh the linked Data Source through the sidecar dispatcher
|
|
1554
|
+
// (refresh-sources, plural) keyed by the data-source OBJECT id, so the records
|
|
1555
|
+
// land in the source-records sidecar the cockpit reads — then reload signals
|
|
1556
|
+
// so the refresh step flips to complete from real state.
|
|
1557
|
+
async function refreshLinkedSource({ objectId }) {
|
|
1558
|
+
const sourceObjectId = String(objectId || "").trim();
|
|
1559
|
+
if (!sourceObjectId) {
|
|
1560
|
+
setDataSourceMessage("No linked Data Source object to refresh yet.");
|
|
1561
|
+
return;
|
|
1562
|
+
}
|
|
1563
|
+
setDataSourceMessage("");
|
|
1564
|
+
try {
|
|
1565
|
+
const res = await fetch("/api/workspace/refresh-sources", {
|
|
1566
|
+
method: "POST",
|
|
1567
|
+
headers: { "content-type": "application/json" },
|
|
1568
|
+
body: JSON.stringify({ sourceIds: [sourceObjectId] }),
|
|
1569
|
+
});
|
|
1570
|
+
const payload = await res.json();
|
|
1571
|
+
const result = Array.isArray(payload.refreshed) ? payload.refreshed.find((r) => r.sourceId === sourceObjectId) : null;
|
|
1572
|
+
if (res.ok && result) {
|
|
1573
|
+
const msg = `Refreshed — ${result.recordCount ?? 0} record(s) pulled into the sidecar.`;
|
|
1574
|
+
setDataSourceMessage("");
|
|
1575
|
+
pushReceipt({ kind: "source-refresh", ok: true, detail: msg });
|
|
1576
|
+
} else if (res.ok && Array.isArray(payload.skipped) && payload.skipped.includes(sourceObjectId)) {
|
|
1577
|
+
const detail = (payload.skippedDetail || []).find((d) => d.sourceId === sourceObjectId);
|
|
1578
|
+
const recovery = classifyCreationError({ phase: "refresh", reason: detail?.reason });
|
|
1579
|
+
setDataSourceMessage(`Refresh skipped: ${recovery.safeDetail}. ${recovery.requiredAction}`);
|
|
1580
|
+
pushReceipt({ kind: "source-refresh", ok: false, detail: `Skipped (${recovery.errorKind}). ${recovery.requiredAction}` });
|
|
1581
|
+
} else {
|
|
1582
|
+
const recovery = classifyCreationError({ phase: "refresh", httpStatus: res.status, detail: redactSecretsFromText(payload.error || "Refresh failed") });
|
|
1583
|
+
setDataSourceMessage(`${recovery.safeDetail} — ${recovery.requiredAction}`);
|
|
1584
|
+
pushReceipt({ kind: "source-refresh", ok: false, detail: `${recovery.safeDetail}. ${recovery.requiredAction}` });
|
|
1585
|
+
}
|
|
1586
|
+
await reloadCreationSignals();
|
|
1587
|
+
} catch (err) {
|
|
1588
|
+
setDataSourceMessage(redactSecretsFromText(err.message || "Refresh failed"));
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
// The creation cockpit emits a single action descriptor per step; map it to
|
|
1593
|
+
// the drawer's existing governed handlers. No new mutation paths.
|
|
1594
|
+
async function handleCockpitAction(action) {
|
|
1595
|
+
if (!action || !action.id) return;
|
|
1596
|
+
const tag = `${action.stepId}:${action.id}`;
|
|
1597
|
+
setCockpitBusy(tag);
|
|
1598
|
+
try {
|
|
1599
|
+
switch (action.id) {
|
|
1600
|
+
case "edit":
|
|
1601
|
+
setEditMode(true);
|
|
1602
|
+
setCockpitCollapsed(true);
|
|
1603
|
+
break;
|
|
1604
|
+
case "open-settings":
|
|
1605
|
+
onClose();
|
|
1606
|
+
router.push(action.href || "/settings");
|
|
1607
|
+
break;
|
|
1608
|
+
case "open-resolver":
|
|
1609
|
+
// Hand off to the governed helper widget — the resolver proposal lane.
|
|
1610
|
+
// Carries the integrationId so the helper can scope a resolver proposal.
|
|
1611
|
+
onClose();
|
|
1612
|
+
router.push(`/data-model?helper=open&resolverFor=${encodeURIComponent(String(draft?.integrationId || "").trim())}`);
|
|
1613
|
+
break;
|
|
1614
|
+
case "test":
|
|
1615
|
+
await testApiRecord();
|
|
1616
|
+
await reloadCreationSignals();
|
|
1617
|
+
break;
|
|
1618
|
+
case "create-data-source":
|
|
1619
|
+
createDataSourceFromRegistry();
|
|
1620
|
+
break;
|
|
1621
|
+
case "open-data-source":
|
|
1622
|
+
openDataSourceRow(action.objectId);
|
|
1623
|
+
break;
|
|
1624
|
+
case "create-sandbox-tool":
|
|
1625
|
+
setSandboxToolFlow("draft");
|
|
1626
|
+
break;
|
|
1627
|
+
case "refresh-source":
|
|
1628
|
+
await refreshLinkedSource({ objectId: action.objectId });
|
|
1629
|
+
break;
|
|
1630
|
+
default:
|
|
1631
|
+
break;
|
|
1632
|
+
}
|
|
1633
|
+
} finally {
|
|
1634
|
+
setCockpitBusy("");
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
const creationState = isApiRegistry
|
|
1639
|
+
? deriveApiRegistryCreationState({
|
|
1640
|
+
workspaceConfig,
|
|
1641
|
+
registryRow: draft,
|
|
1642
|
+
sourceRecords: creationSignals.sourceRecords,
|
|
1643
|
+
runtime: { configuredEnvRefs: creationSignals.configuredEnvRefs },
|
|
1644
|
+
})
|
|
1645
|
+
: null;
|
|
1646
|
+
// Shape analysis from the tested response — drives the resolver recommendation
|
|
1647
|
+
// and the field candidates the operator sees before creating a Data Source.
|
|
1648
|
+
const creationProfile = isApiRegistry && creationState?.tested
|
|
1649
|
+
? profileApiResponse(draft?.lastResponse)
|
|
1650
|
+
: null;
|
|
1651
|
+
const creationResolverRec = creationProfile ? recommendResolver(creationProfile) : null;
|
|
1652
|
+
// Preview the exact Data Source that "Create Data Source" will produce, before
|
|
1653
|
+
// any mutation — shown once tested and while no source is linked yet.
|
|
1654
|
+
const creationDataSourcePreview = isApiRegistry && creationState?.tested && !creationState.sourceExists
|
|
1655
|
+
? {
|
|
1656
|
+
row: buildDataSourceRowFromApiRegistry(workspaceConfig, draft, {
|
|
1657
|
+
entityType: creationProfile?.suggestedEntityType,
|
|
1658
|
+
}),
|
|
1659
|
+
fields: creationProfile?.fields || [],
|
|
1660
|
+
}
|
|
1661
|
+
: null;
|
|
1662
|
+
|
|
1248
1663
|
async function runSandboxToolByName({ objectId, name }) {
|
|
1249
1664
|
const rowName = String(name || "").trim();
|
|
1250
1665
|
const objectIdValue = String(objectId || "").trim();
|
|
@@ -1461,6 +1876,7 @@ function DataModelRecordDrawer({
|
|
|
1461
1876
|
onTest={testApiRecord}
|
|
1462
1877
|
/>
|
|
1463
1878
|
)}
|
|
1879
|
+
<div className="dm-record-scroll" ref={drawerScrollRef}>
|
|
1464
1880
|
{isApiRegistry && sandboxToolFlow === "created" && createdSandboxMeta && (
|
|
1465
1881
|
<section className="dm-api-action-card dm-api-action-card-success" aria-label="Sandbox tool created">
|
|
1466
1882
|
<div className="dm-api-action-card-body">
|
|
@@ -1489,6 +1905,24 @@ function DataModelRecordDrawer({
|
|
|
1489
1905
|
</div>
|
|
1490
1906
|
</section>
|
|
1491
1907
|
)}
|
|
1908
|
+
{isApiRegistry && sandboxToolFlow !== "draft" && sandboxToolFlow !== "confirm" && creationState && (
|
|
1909
|
+
<>
|
|
1910
|
+
<ApiRegistryCreationCockpit
|
|
1911
|
+
state={creationState}
|
|
1912
|
+
onAction={handleCockpitAction}
|
|
1913
|
+
busyAction={cockpitBusy}
|
|
1914
|
+
disabled={saving || creatingDataSource || testing}
|
|
1915
|
+
profile={creationProfile}
|
|
1916
|
+
resolverRec={creationResolverRec}
|
|
1917
|
+
receipts={creationReceipts}
|
|
1918
|
+
dataSourcePreview={creationDataSourcePreview}
|
|
1919
|
+
defaultCollapsed={cockpitCollapsed}
|
|
1920
|
+
hideWhenComplete
|
|
1921
|
+
onCollapsedChange={setCockpitCollapsed}
|
|
1922
|
+
/>
|
|
1923
|
+
{dataSourceMessage ? <p className="dm-sandbox-tool-test-msg">{dataSourceMessage}</p> : null}
|
|
1924
|
+
</>
|
|
1925
|
+
)}
|
|
1492
1926
|
{isApiRegistry && sandboxToolFlow === "draft" && (
|
|
1493
1927
|
<SandboxToolDraftPanel
|
|
1494
1928
|
registryRow={draft}
|
|
@@ -1561,8 +1995,21 @@ function DataModelRecordDrawer({
|
|
|
1561
1995
|
onOpenGraphSidecar={openWorkflowView}
|
|
1562
1996
|
onOpenTraceSidecar={openTraceSidecar}
|
|
1563
1997
|
/>
|
|
1998
|
+
) : isCodexSitesObject ? (
|
|
1999
|
+
<CodexSitesRecordFields
|
|
2000
|
+
draft={draft}
|
|
2001
|
+
setDraft={setDraft}
|
|
2002
|
+
table={table}
|
|
2003
|
+
saving={saving}
|
|
2004
|
+
onSave={onSave}
|
|
2005
|
+
rowIndex={rowIndex}
|
|
2006
|
+
/>
|
|
1564
2007
|
) : groupRecordColumns(table.columns || []).map((section) => (
|
|
1565
|
-
<DrawerSection
|
|
2008
|
+
<DrawerSection
|
|
2009
|
+
key={section.title}
|
|
2010
|
+
title={section.title}
|
|
2011
|
+
forceOpen={isApiRegistry && editMode}
|
|
2012
|
+
>
|
|
1566
2013
|
{section.columns.map((column) => (
|
|
1567
2014
|
<RecordFieldEditor
|
|
1568
2015
|
key={column}
|
|
@@ -1580,7 +2027,7 @@ function DataModelRecordDrawer({
|
|
|
1580
2027
|
</DrawerSection>
|
|
1581
2028
|
))}
|
|
1582
2029
|
{!isSandbox && editMode && (
|
|
1583
|
-
<DrawerSection title="Fields"
|
|
2030
|
+
<DrawerSection title="Fields">
|
|
1584
2031
|
<div className="dm-drawer-field-editor">
|
|
1585
2032
|
{pendingColumns.map((column, index) => (
|
|
1586
2033
|
<div key={`${column}-${index}`} className="dm-drawer-field-row">
|
|
@@ -1609,6 +2056,7 @@ function DataModelRecordDrawer({
|
|
|
1609
2056
|
</DrawerSection>
|
|
1610
2057
|
)}
|
|
1611
2058
|
</div>
|
|
2059
|
+
</div>
|
|
1612
2060
|
{!isSandbox && editMode && (
|
|
1613
2061
|
<footer className="dm-record-drawer-foot">
|
|
1614
2062
|
<button type="button" className="dm-btn-outline" onClick={cancelEdits}>Cancel</button>
|
|
@@ -1658,7 +2106,9 @@ function DataModelTableSurface({
|
|
|
1658
2106
|
onSave,
|
|
1659
2107
|
onOpenThread,
|
|
1660
2108
|
focusSandboxRowName,
|
|
2109
|
+
focusRecordValue,
|
|
1661
2110
|
onFocusSandboxRowConsumed,
|
|
2111
|
+
onFocusRecordConsumed,
|
|
1662
2112
|
onFocusSandboxRow,
|
|
1663
2113
|
selectedRecordIndex,
|
|
1664
2114
|
onSelectedRecordIndexChange,
|
|
@@ -1756,6 +2206,24 @@ function DataModelTableSurface({
|
|
|
1756
2206
|
onFocusSandboxRowConsumed?.();
|
|
1757
2207
|
}, [focusSandboxRowName, table.id, table.objectType, table.rows, rowEntries, pageSize, onFocusSandboxRowConsumed]);
|
|
1758
2208
|
|
|
2209
|
+
useEffect(() => {
|
|
2210
|
+
if (!focusRecordValue) return;
|
|
2211
|
+
const wanted = String(focusRecordValue).trim();
|
|
2212
|
+
if (!wanted) return;
|
|
2213
|
+
const originalIndex = (table.rows || []).findIndex((r) => (
|
|
2214
|
+
String(r?.integrationId || "").trim() === wanted
|
|
2215
|
+
|| String(r?.Name || r?.name || r?.slug || r?.id || "").trim() === wanted
|
|
2216
|
+
));
|
|
2217
|
+
if (originalIndex < 0) return;
|
|
2218
|
+
const visibleIndex = rowEntries.findIndex((entry) => entry.originalIndex === originalIndex);
|
|
2219
|
+
if (visibleIndex < 0) return;
|
|
2220
|
+
const pageForRow = Math.floor(visibleIndex / pageSize);
|
|
2221
|
+
setPageIndex(pageForRow);
|
|
2222
|
+
setSelectedRow(visibleIndex);
|
|
2223
|
+
selectOriginalIndex(originalIndex);
|
|
2224
|
+
onFocusRecordConsumed?.();
|
|
2225
|
+
}, [focusRecordValue, table.id, table.rows, rowEntries, pageSize, onFocusRecordConsumed]);
|
|
2226
|
+
|
|
1759
2227
|
useEffect(() => {
|
|
1760
2228
|
setPageIndex((current) => Math.min(current, pageCount - 1));
|
|
1761
2229
|
}, [pageCount]);
|
|
@@ -2543,9 +3011,11 @@ export default function DataModelShell() {
|
|
|
2543
3011
|
const [helperInitialThread, setHelperInitialThread] = useState(null);
|
|
2544
3012
|
const [commandPaletteOpen, setCommandPaletteOpen] = useState(false);
|
|
2545
3013
|
const [focusSandboxRowName, setFocusSandboxRowName] = useState(null);
|
|
3014
|
+
const [focusRecordValue, setFocusRecordValue] = useState(null);
|
|
2546
3015
|
const [selectedRecordByTable, setSelectedRecordByTable] = useState({});
|
|
2547
3016
|
const pendingPatchRef = useRef({});
|
|
2548
3017
|
const saveTimerRef = useRef(null);
|
|
3018
|
+
const consumedHelperRouteRef = useRef("");
|
|
2549
3019
|
|
|
2550
3020
|
// Cross-page rail entrypoints. Settings / integrations pages render
|
|
2551
3021
|
// <WorkspaceRail> without an in-process helper handler — clicking the
|
|
@@ -2558,7 +3028,11 @@ export default function DataModelShell() {
|
|
|
2558
3028
|
if (!workspaceConfig) return;
|
|
2559
3029
|
const helperParam = searchParams?.get("helper");
|
|
2560
3030
|
const threadParam = searchParams?.get("thread");
|
|
2561
|
-
|
|
3031
|
+
const resolverForParam = searchParams?.get("resolverFor");
|
|
3032
|
+
if (!helperParam && !threadParam && !resolverForParam) return;
|
|
3033
|
+
const routeKey = `${helperParam || ""}:${threadParam || ""}:${resolverForParam || ""}`;
|
|
3034
|
+
if (routeKey === consumedHelperRouteRef.current) return;
|
|
3035
|
+
consumedHelperRouteRef.current = routeKey;
|
|
2562
3036
|
if (threadParam) {
|
|
2563
3037
|
const ht = (workspaceConfig?.dataModel?.objects || []).find((o) => o?.id === "helper-threads");
|
|
2564
3038
|
const row = (ht?.rows || []).find((r) => r?.id === threadParam);
|
|
@@ -2566,6 +3040,12 @@ export default function DataModelShell() {
|
|
|
2566
3040
|
setHelperInitialThread(row);
|
|
2567
3041
|
setHelperOpen(true);
|
|
2568
3042
|
}
|
|
3043
|
+
} else if (resolverForParam) {
|
|
3044
|
+
setHelperIntent("register_api");
|
|
3045
|
+
setHelperInitialThread(null);
|
|
3046
|
+
setHelperInitialPrompt(`Create the response resolver for API Registry integration "${resolverForParam}". Use the tested lastResponse from that registry row, extract the records from the response into governed Data Source rows, and keep the resolver scoped to this integrationId.`);
|
|
3047
|
+
setHelperOpen(true);
|
|
3048
|
+
return;
|
|
2569
3049
|
} else if (helperParam === "open") {
|
|
2570
3050
|
setHelperInitialThread(null);
|
|
2571
3051
|
setHelperOpen(true);
|
|
@@ -2573,6 +3053,7 @@ export default function DataModelShell() {
|
|
|
2573
3053
|
const next = new URLSearchParams(searchParams.toString());
|
|
2574
3054
|
next.delete("helper");
|
|
2575
3055
|
next.delete("thread");
|
|
3056
|
+
next.delete("resolverFor");
|
|
2576
3057
|
const query = next.toString();
|
|
2577
3058
|
router.replace(query ? `/data-model?${query}` : "/data-model", { scroll: false });
|
|
2578
3059
|
}, [workspaceConfig, searchParams, router]);
|
|
@@ -2673,7 +3154,20 @@ export default function DataModelShell() {
|
|
|
2673
3154
|
useEffect(() => {
|
|
2674
3155
|
const rowParam = searchParams?.get("row");
|
|
2675
3156
|
if (!rowParam || !tables.length) return;
|
|
2676
|
-
|
|
3157
|
+
const objectParam = searchParams?.get("object");
|
|
3158
|
+
const target = objectParam
|
|
3159
|
+
? tables.find((table) => (
|
|
3160
|
+
table.objectId === objectParam
|
|
3161
|
+
|| table.id === objectParam
|
|
3162
|
+
|| table.source === objectParam
|
|
3163
|
+
|| table.label === objectParam
|
|
3164
|
+
))
|
|
3165
|
+
: null;
|
|
3166
|
+
if (target?.objectType === "sandbox-environment" || (!target && rowParam)) {
|
|
3167
|
+
focusSandboxEnvironmentRow({ rowName: rowParam, deferOpen: true });
|
|
3168
|
+
return;
|
|
3169
|
+
}
|
|
3170
|
+
requestAnimationFrame(() => setFocusRecordValue(rowParam));
|
|
2677
3171
|
}, [focusSandboxEnvironmentRow, searchParams, tables]);
|
|
2678
3172
|
|
|
2679
3173
|
// Flush any accumulated patch keys to the server. Called by the debounce
|
|
@@ -3100,7 +3594,9 @@ export default function DataModelShell() {
|
|
|
3100
3594
|
onSave={save}
|
|
3101
3595
|
onOpenThread={openHelperThreadFromRow}
|
|
3102
3596
|
focusSandboxRowName={focusSandboxRowName}
|
|
3597
|
+
focusRecordValue={focusRecordValue}
|
|
3103
3598
|
onFocusSandboxRowConsumed={() => setFocusSandboxRowName(null)}
|
|
3599
|
+
onFocusRecordConsumed={() => setFocusRecordValue(null)}
|
|
3104
3600
|
onFocusSandboxRow={focusSandboxEnvironmentRow}
|
|
3105
3601
|
selectedRecordIndex={selectedTableKey ? selectedRecordByTable[selectedTableKey] ?? null : null}
|
|
3106
3602
|
onSelectedRecordIndexChange={(index) => {
|