@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.
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/DataModelShell.jsx +34 -213
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/dm-shared.jsx +8 -2
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +911 -8
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/views/[viewId]/page.jsx +206 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-builder.jsx +11 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-rail.jsx +1275 -4
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-data-model.js +11 -4
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-helper-apply.js +96 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-schema.js +102 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/kit.json +1 -0
- package/package.json +1 -1
|
@@ -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);
|