@growthub/cli 0.12.2 → 0.13.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.
@@ -0,0 +1,206 @@
1
+ "use client";
2
+
3
+ /**
4
+ * Custom Folders Navigation — user-facing View surface.
5
+ *
6
+ * Renders a clean, deployed-runtime table for a single folder Item of
7
+ * type "view". The view configuration (objectId + viewConfig with
8
+ * `columns`, `filters`, `sort`) is stored on the nav-folders governed
9
+ * object via the existing PATCH allowlist. Resolving the Data Model
10
+ * object's `columns` and `rows` happens entirely from the live
11
+ * workspace config — there is no parallel store, no new schema
12
+ * namespace, and no new sandbox primitive.
13
+ *
14
+ * The page deliberately uses the canonical <WorkspaceRail /> so the
15
+ * Folders module remains the user's navigation surface; the body is a
16
+ * thin table renderer mirroring the same column/sort/filter contract
17
+ * used by the dashboard View widget.
18
+ */
19
+
20
+ import { useCallback, useEffect, useMemo, useState } from "react";
21
+ import { useParams, useRouter } from "next/navigation";
22
+ import { WorkspaceRail } from "../../workspace-rail.jsx";
23
+
24
+ function findViewItem(workspaceConfig, viewId) {
25
+ const obj = (workspaceConfig?.dataModel?.objects || []).find((o) => o?.id === "nav-folders");
26
+ const rows = Array.isArray(obj?.rows) ? obj.rows : [];
27
+ for (const row of rows) {
28
+ const items = Array.isArray(row?.items) ? row.items : [];
29
+ const hit = items.find((it) => it?.type === "view" && it?.id === viewId);
30
+ if (hit) return { folder: row, item: hit };
31
+ }
32
+ return null;
33
+ }
34
+
35
+ function findObjectById(workspaceConfig, objectId) {
36
+ const objects = Array.isArray(workspaceConfig?.dataModel?.objects) ? workspaceConfig.dataModel.objects : [];
37
+ return objects.find((o) => o?.id === objectId) || null;
38
+ }
39
+
40
+ function applyFilters(rows, filters) {
41
+ if (!Array.isArray(filters) || filters.length === 0) return rows;
42
+ return rows.filter((row) => {
43
+ return filters.every((f) => {
44
+ if (!f || typeof f.field !== "string") return true;
45
+ const value = row?.[f.field];
46
+ const cmp = f.value;
47
+ switch (f.op) {
48
+ case "eq": return String(value ?? "") === String(cmp ?? "");
49
+ case "ne": return String(value ?? "") !== String(cmp ?? "");
50
+ case "contains": return String(value ?? "").toLowerCase().includes(String(cmp ?? "").toLowerCase());
51
+ case "isEmpty": return value == null || value === "";
52
+ case "isNotEmpty": return !(value == null || value === "");
53
+ case "gt": return Number(value) > Number(cmp);
54
+ case "lt": return Number(value) < Number(cmp);
55
+ default: return true;
56
+ }
57
+ });
58
+ });
59
+ }
60
+
61
+ function applySort(rows, sort) {
62
+ if (!sort || typeof sort.field !== "string") return rows;
63
+ const dir = sort.dir === "desc" ? -1 : 1;
64
+ const sorted = rows.slice().sort((a, b) => {
65
+ const av = a?.[sort.field];
66
+ const bv = b?.[sort.field];
67
+ if (typeof av === "number" && typeof bv === "number") return (av - bv) * dir;
68
+ return String(av ?? "").localeCompare(String(bv ?? "")) * dir;
69
+ });
70
+ return sorted;
71
+ }
72
+
73
+ export default function ViewPage() {
74
+ const params = useParams();
75
+ const router = useRouter();
76
+ const viewId = decodeURIComponent(String(params?.viewId || ""));
77
+
78
+ const [workspaceConfig, setWorkspaceConfig] = useState(null);
79
+ const [authority, setAuthority] = useState(null);
80
+ const [loading, setLoading] = useState(true);
81
+ const [error, setError] = useState("");
82
+
83
+ const load = useCallback(async () => {
84
+ setLoading(true);
85
+ setError("");
86
+ try {
87
+ const res = await fetch("/api/workspace", { cache: "no-store" });
88
+ const payload = await res.json();
89
+ if (!res.ok) throw new Error(payload.error || "Failed to load workspace");
90
+ setWorkspaceConfig(payload.workspaceConfig);
91
+ setAuthority(payload.adapters?.integrations?.authority || null);
92
+ } catch (err) {
93
+ setError(err.message || "Failed to load workspace");
94
+ } finally {
95
+ setLoading(false);
96
+ }
97
+ }, []);
98
+
99
+ useEffect(() => { load(); }, [load]);
100
+
101
+ const located = useMemo(
102
+ () => (workspaceConfig ? findViewItem(workspaceConfig, viewId) : null),
103
+ [workspaceConfig, viewId]
104
+ );
105
+
106
+ const dmObject = useMemo(
107
+ () => (workspaceConfig && located ? findObjectById(workspaceConfig, located.item.objectId) : null),
108
+ [workspaceConfig, located]
109
+ );
110
+
111
+ useEffect(() => {
112
+ if (!located?.item?.objectId || !dmObject) return;
113
+ router.replace(`/data-model?object=${encodeURIComponent(located.item.objectId)}`, { scroll: false });
114
+ }, [dmObject, located, router]);
115
+
116
+ const { columns, rows } = useMemo(() => {
117
+ if (!dmObject || !located) return { columns: [], rows: [] };
118
+ const objectColumns = Array.isArray(dmObject.columns) ? dmObject.columns : [];
119
+ const objectRows = Array.isArray(dmObject.rows) ? dmObject.rows : [];
120
+ const viewConfig = located.item.viewConfig || {};
121
+ const requestedColumns = Array.isArray(viewConfig.columns) && viewConfig.columns.length
122
+ ? viewConfig.columns.filter((c) => objectColumns.includes(c))
123
+ : objectColumns;
124
+ const filtered = applyFilters(objectRows, viewConfig.filters);
125
+ const sorted = applySort(filtered, viewConfig.sort);
126
+ return { columns: requestedColumns, rows: sorted };
127
+ }, [dmObject, located]);
128
+
129
+ return (
130
+ <main className="workspace-builder">
131
+ <WorkspaceRail
132
+ workspaceConfig={workspaceConfig}
133
+ authority={authority}
134
+ helperOpen={false}
135
+ onConfigChange={(nextConfig) => setWorkspaceConfig(nextConfig)}
136
+ onOpenHelper={() => router.push("/data-model?helper=open")}
137
+ onOpenThread={(row) => router.push(`/data-model?thread=${encodeURIComponent(row.id)}`)}
138
+ />
139
+ <section className="workspace-surface">
140
+ <header className="workspace-toolbar">
141
+ <div>
142
+ <p>{located?.folder?.name || "Folder"}</p>
143
+ <h1>{located?.item?.label || dmObject?.label || "View"}</h1>
144
+ </div>
145
+ </header>
146
+ <section className="workspace-view-surface" aria-label="View table">
147
+ {loading ? (
148
+ <p className="workspace-view-empty">Loading view…</p>
149
+ ) : error ? (
150
+ <p className="workspace-view-empty workspace-view-error">{error}</p>
151
+ ) : !located ? (
152
+ <p className="workspace-view-empty">
153
+ View not found. It may have been removed from its folder.
154
+ </p>
155
+ ) : !dmObject ? (
156
+ <p className="workspace-view-empty">
157
+ The Data Model object backing this view (<code>{located.item.objectId}</code>) is missing.
158
+ </p>
159
+ ) : columns.length === 0 ? (
160
+ <p className="workspace-view-empty">This object has no columns yet.</p>
161
+ ) : (
162
+ <div className="workspace-view-table-wrap">
163
+ <table className="workspace-view-table">
164
+ <thead>
165
+ <tr>
166
+ {columns.map((col) => (<th key={col}>{col}</th>))}
167
+ </tr>
168
+ </thead>
169
+ <tbody>
170
+ {rows.length === 0 ? (
171
+ <tr>
172
+ <td colSpan={columns.length} className="workspace-view-table-empty">
173
+ No rows match this view.
174
+ </td>
175
+ </tr>
176
+ ) : (
177
+ rows.map((row, ri) => (
178
+ <tr key={ri}>
179
+ {columns.map((col) => (
180
+ <td key={col}>{formatCell(row?.[col])}</td>
181
+ ))}
182
+ </tr>
183
+ ))
184
+ )}
185
+ </tbody>
186
+ </table>
187
+ <p className="workspace-view-footer">
188
+ {rows.length} row{rows.length === 1 ? "" : "s"}
189
+ {" · "}
190
+ <span>{dmObject.label}</span>
191
+ </p>
192
+ </div>
193
+ )}
194
+ </section>
195
+ </section>
196
+ </main>
197
+ );
198
+ }
199
+
200
+ function formatCell(value) {
201
+ if (value == null) return "";
202
+ if (typeof value === "object") {
203
+ try { return JSON.stringify(value); } catch { return String(value); }
204
+ }
205
+ return String(value);
206
+ }
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import Link from "next/link";
4
+ import { useSearchParams } from "next/navigation";
4
5
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
5
6
  import {
6
7
  Activity,
@@ -3307,6 +3308,7 @@ function WorkspaceManagementPanel({ config, persistence, adapterConfig, onClose
3307
3308
  }
3308
3309
 
3309
3310
  function WorkspaceBuilder({ initialConfig, adapterConfig, integrationAdapter, integrationSettings, persistence }) {
3311
+ const searchParams = useSearchParams();
3310
3312
  const [config, setConfig] = useState(() => {
3311
3313
  const dashboards = Array.isArray(initialConfig.dashboards) && initialConfig.dashboards.length
3312
3314
  ? initialConfig.dashboards.map((dashboard, index) =>
@@ -3527,6 +3529,15 @@ function WorkspaceBuilder({ initialConfig, adapterConfig, integrationAdapter, in
3527
3529
  });
3528
3530
  }, [activeDashboardId]);
3529
3531
 
3532
+ useEffect(() => {
3533
+ const dashboardParam = searchParams?.get("dashboard");
3534
+ if (!dashboardParam || dashboards.length === 0) return;
3535
+ const targetIndex = dashboards.findIndex((dashboard) => dashboard.id === dashboardParam);
3536
+ if (targetIndex < 0) return;
3537
+ if (dashboards[targetIndex]?.id === resolvedActiveDashboardId && workspaceView === "builder") return;
3538
+ selectDashboard(targetIndex);
3539
+ }, [dashboards, resolvedActiveDashboardId, searchParams, selectDashboard, workspaceView]);
3540
+
3530
3541
  const enterDashboardTitleEdit = useCallback((dashboard) => {
3531
3542
  if (!dashboard) return;
3532
3543
  setEditingDashboardId(dashboard.id);