@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.
@@ -25,12 +25,10 @@ import {
25
25
  Layers,
26
26
  Link2,
27
27
  Lock,
28
- List,
29
28
  Mail,
30
29
  Maximize2,
31
30
  MoreHorizontal,
32
31
  Plus,
33
- Pin,
34
32
  Pencil,
35
33
  Search,
36
34
  ShoppingCart,
@@ -62,7 +60,6 @@ import {
62
60
  parseSandboxAllowList,
63
61
  parseSandboxEnvRefs,
64
62
  replaceTableContent,
65
- snapshotTableViewState,
66
63
  transformTableSchema,
67
64
  updateTableFieldSettings,
68
65
  updateTableCell,
@@ -199,88 +196,20 @@ function applyRowsView(rows, settings) {
199
196
  });
200
197
  }
201
198
 
202
- function ObjectViewPicker({ tables, selectedTable, saving, onSelectSource, onSave }) {
199
+ function ObjectViewPicker({ tables, selectedTable, onSelectSource }) {
203
200
  const pickerRef = useRef(null);
204
201
  const [open, setOpen] = useState(false);
205
- const [mode, setMode] = useState("all");
206
- const [newViewName, setNewViewName] = useState("");
207
- const [viewMenuId, setViewMenuId] = useState("");
208
- const currentViews = selectedTable?.fieldSettings?.views || [];
209
- const favoriteObjects = tables.filter((table) => table.fieldSettings?.favorite);
210
202
 
211
203
  useEffect(() => {
212
204
  function handlePointer(event) {
213
205
  if (!pickerRef.current?.contains(event.target)) {
214
- setViewMenuId("");
206
+ setOpen(false);
215
207
  }
216
208
  }
217
209
  document.addEventListener("pointerdown", handlePointer);
218
210
  return () => document.removeEventListener("pointerdown", handlePointer);
219
211
  }, []);
220
212
 
221
- function applyView(view) {
222
- if (!selectedTable) return;
223
- const nextState = view
224
- ? { ...snapshotTableViewState(view), activeViewId: view.id }
225
- : { activeViewId: "", hidden: [], order: selectedTable.columns, sort: [], filter: null };
226
- onSave((config) => updateTableFieldSettings(config, selectedTable, (settings) => ({
227
- ...settings,
228
- ...nextState
229
- })));
230
- setOpen(false);
231
- }
232
-
233
- function createView() {
234
- const name = newViewName.trim();
235
- if (!selectedTable || !name) return;
236
- const viewId = `view_${Date.now().toString(36)}`;
237
- onSave((config) => updateTableFieldSettings(config, selectedTable, (settings) => ({
238
- ...settings,
239
- activeViewId: viewId,
240
- views: [...(settings.views || []), {
241
- id: viewId,
242
- name,
243
- favorite: false,
244
- locked: false,
245
- ...snapshotTableViewState(settings)
246
- }]
247
- })));
248
- setNewViewName("");
249
- }
250
-
251
- function toggleViewFavorite(viewId) {
252
- if (!selectedTable) return;
253
- onSave((config) => updateTableFieldSettings(config, selectedTable, (settings) => ({
254
- ...settings,
255
- views: (settings.views || []).map((view) => view.id === viewId ? { ...view, favorite: !view.favorite } : view)
256
- })));
257
- }
258
-
259
- function deleteView(viewId) {
260
- if (!selectedTable) return;
261
- onSave((config) => updateTableFieldSettings(config, selectedTable, (settings) => ({
262
- ...settings,
263
- activeViewId: settings.activeViewId === viewId ? "" : settings.activeViewId,
264
- views: (settings.views || []).filter((view) => view.id !== viewId)
265
- })));
266
- setViewMenuId("");
267
- }
268
-
269
- function renameView(view) {
270
- if (!selectedTable) return;
271
- const nextName = window.prompt("Rename view", view.name);
272
- if (!nextName?.trim()) return;
273
- onSave((config) => updateTableFieldSettings(config, selectedTable, (settings) => ({
274
- ...settings,
275
- views: (settings.views || []).map((candidate) => candidate.id === view.id ? { ...candidate, name: nextName.trim() } : candidate)
276
- })));
277
- setViewMenuId("");
278
- }
279
-
280
- const activeView = currentViews.find((view) => view.id === selectedTable?.fieldSettings?.activeViewId) || null;
281
- const objects = mode === "views" ? [] : tables;
282
- const views = mode === "objects" ? [] : currentViews;
283
-
284
213
  return (
285
214
  <div
286
215
  ref={pickerRef}
@@ -288,125 +217,36 @@ function ObjectViewPicker({ tables, selectedTable, saving, onSelectSource, onSav
288
217
  onBlur={(event) => {
289
218
  if (!event.currentTarget.contains(event.relatedTarget)) {
290
219
  setOpen(false);
291
- setViewMenuId("");
292
220
  }
293
221
  }}
294
222
  >
295
223
  <button type="button" className="dm-picker-trigger" onClick={() => setOpen((current) => !current)}>
296
224
  <LucideIcon name={selectedTable?.icon || OBJECT_TYPE_PRESETS[selectedTable?.objectType]?.icon || "Database"} size={14} />
297
225
  <span className="dm-picker-trigger-copy">
298
- <strong>{activeView?.name || selectedTable?.label || "Object"}</strong>
226
+ <strong>{selectedTable?.label || "Object"}</strong>
299
227
  <em>{pluralize(selectedTable?.columns?.length || 0, "field")} · {pluralize(selectedTable?.rows?.length || 0, "record")}</em>
300
228
  </span>
301
229
  <ChevronDown size={14} />
302
230
  </button>
303
231
  {open && (
304
232
  <div className="dm-picker-popover">
305
- {favoriteObjects.length > 0 && (
306
- <div className="dm-picker-section">
307
- <p>Favorites</p>
308
- {favoriteObjects.map((table, favIdx) => (
309
- <button key={`favorite-${table.id || table.source}-${favIdx}`} type="button" className="dm-picker-row" onClick={() => onSelectSource(table.source)}>
310
- <Pin size={14} />
311
- <span>{table.label}</span>
312
- </button>
233
+ <div className="dm-picker-section">
234
+ <p>Objects</p>
235
+ <div className="dm-picker-scroll">
236
+ {tables.map((table, objIdx) => (
237
+ <div key={`${table.id || table.source}:${objIdx}`} className={`dm-picker-item${selectedTable?.source === table.source ? " active" : ""}`}>
238
+ <button type="button" className="dm-picker-row" onClick={() => {
239
+ onSelectSource(table.source);
240
+ setOpen(false);
241
+ }}>
242
+ <LucideIcon name={table.icon || OBJECT_TYPE_PRESETS[table.objectType]?.icon || "Database"} size={14} />
243
+ <span>{table.label}</span>
244
+ {isLockedObject(table) && <Lock size={12} className="dm-picker-lock" />}
245
+ </button>
246
+ </div>
313
247
  ))}
314
248
  </div>
315
- )}
316
- <div className="dm-picker-tabs">
317
- {[
318
- { id: "all", label: "All" },
319
- { id: "objects", label: "Objects" },
320
- { id: "views", label: "Views" },
321
- ].map((item) => (
322
- <button key={item.id} type="button" className={mode === item.id ? "active" : ""} onClick={() => setMode(item.id)}>
323
- {item.label}
324
- </button>
325
- ))}
326
249
  </div>
327
- {objects.length > 0 && (
328
- <div className="dm-picker-section">
329
- <p>Objects</p>
330
- <div className="dm-picker-scroll">
331
- {objects.map((table, objIdx) => (
332
- <div key={`${table.id || table.source}:${objIdx}`} className={`dm-picker-item${selectedTable?.source === table.source ? " active" : ""}`}>
333
- <button type="button" className="dm-picker-row" onClick={() => {
334
- onSelectSource(table.source);
335
- setOpen(false);
336
- }}>
337
- <LucideIcon name={table.icon || OBJECT_TYPE_PRESETS[table.objectType]?.icon || "Database"} size={14} />
338
- <span>{table.label}</span>
339
- {isLockedObject(table) && <Lock size={12} className="dm-picker-lock" />}
340
- </button>
341
- </div>
342
- ))}
343
- </div>
344
- </div>
345
- )}
346
- {selectedTable && (
347
- <div className="dm-picker-section">
348
- <p>Views</p>
349
- <button type="button" className={`dm-picker-row${!activeView ? " active" : ""}`} onClick={() => applyView(null)}>
350
- <List size={14} />
351
- <span>{selectedTable.label}</span>
352
- {isLockedObject(selectedTable) && <Lock size={12} className="dm-picker-lock" />}
353
- </button>
354
- <div className="dm-picker-scroll">
355
- {views.map((view) => (
356
- <div key={view.id} className={`dm-picker-item${activeView?.id === view.id ? " active" : ""}`}>
357
- <button type="button" className="dm-picker-row" onClick={() => applyView(view)}>
358
- <List size={14} />
359
- <span>{view.name}</span>
360
- </button>
361
- <div className="dm-picker-actions">
362
- <button
363
- type="button"
364
- className="dm-picker-icon-btn"
365
- aria-label="View actions"
366
- onClick={(event) => {
367
- event.stopPropagation();
368
- setViewMenuId((current) => current === view.id ? "" : view.id);
369
- }}
370
- >
371
- <MoreHorizontal size={12} style={{ transform: "rotate(90deg)" }} />
372
- </button>
373
- {viewMenuId === view.id && (
374
- <div className="dm-picker-menu">
375
- <button type="button" onClick={() => toggleViewFavorite(view.id)}>
376
- <Pin size={13} />
377
- {view.favorite ? "Unpin" : "Pin"}
378
- </button>
379
- <button type="button" onClick={() => renameView(view)}>
380
- <Type size={13} />
381
- Rename
382
- </button>
383
- {!view.locked && (
384
- <button type="button" className="danger" onClick={() => deleteView(view.id)}>
385
- <X size={13} />
386
- Delete
387
- </button>
388
- )}
389
- </div>
390
- )}
391
- </div>
392
- </div>
393
- ))}
394
- </div>
395
- <div className="dm-picker-create">
396
- <input
397
- value={newViewName}
398
- placeholder="New view name"
399
- onChange={(event) => setNewViewName(event.target.value)}
400
- onKeyDown={(event) => {
401
- if (event.key === "Enter") createView();
402
- }}
403
- />
404
- <button type="button" className="dm-btn-outline" disabled={saving || !newViewName.trim()} onClick={createView}>
405
- <Plus size={13} />Add view
406
- </button>
407
- </div>
408
- </div>
409
- )}
410
250
  </div>
411
251
  )}
412
252
  </div>
@@ -1403,7 +1243,7 @@ function DataModelTableSurface({ table, tables, workspaceConfig, saving, onSave,
1403
1243
  setFilterDraft({ fieldId: table.columns[0] || "", operator: "eq", value: "" });
1404
1244
  }, [table.id, table.columns]);
1405
1245
 
1406
- const settings = table.fieldSettings || { hidden: [], order: table.columns, sort: [], filter: null, views: [], activeViewId: "" };
1246
+ const settings = table.fieldSettings || { hidden: [], order: table.columns, sort: [], filter: null };
1407
1247
  const orderedColumns = useMemo(() => mergeColumnOrder(settings.order, table.columns), [settings.order, table.columns]);
1408
1248
  const visibleColumns = useMemo(() => orderedColumns.filter((column) => !settings.hidden.includes(column)), [orderedColumns, settings.hidden]);
1409
1249
  const rowEntries = useMemo(() => {
@@ -1420,10 +1260,6 @@ function DataModelTableSurface({ table, tables, workspaceConfig, saving, onSave,
1420
1260
  return 0;
1421
1261
  });
1422
1262
  }, [table.rows, settings]);
1423
- const activeView = useMemo(
1424
- () => (settings.views || []).find((view) => view.id === settings.activeViewId) || null,
1425
- [settings.views, settings.activeViewId]
1426
- );
1427
1263
  const selectedRowCount = selectedRows.size;
1428
1264
  const pageCount = Math.max(1, Math.ceil(rowEntries.length / pageSize));
1429
1265
  const safePageIndex = Math.min(pageIndex, pageCount - 1);
@@ -1529,27 +1365,7 @@ function DataModelTableSurface({ table, tables, workspaceConfig, saving, onSave,
1529
1365
  hidden: [],
1530
1366
  order: table.columns,
1531
1367
  sort: [],
1532
- filter: null,
1533
- activeViewId: ""
1534
- }));
1535
- }
1536
-
1537
- function saveCurrentAsNewView() {
1538
- const name = window.prompt("View name");
1539
- if (!name?.trim()) return;
1540
- const viewId = `view_${Date.now().toString(36)}`;
1541
- updateSettings((current) => ({
1542
- ...current,
1543
- activeViewId: viewId,
1544
- views: [...(current.views || []), { id: viewId, name: name.trim(), favorite: false, locked: false, ...snapshotTableViewState(current) }]
1545
- }));
1546
- }
1547
-
1548
- function updateCurrentView() {
1549
- if (!activeView) return;
1550
- updateSettings((current) => ({
1551
- ...current,
1552
- views: (current.views || []).map((view) => view.id === activeView.id ? { ...view, ...snapshotTableViewState(current) } : view)
1368
+ filter: null
1553
1369
  }));
1554
1370
  }
1555
1371
 
@@ -1670,15 +1486,6 @@ function DataModelTableSurface({ table, tables, workspaceConfig, saving, onSave,
1670
1486
  </div>
1671
1487
  )}
1672
1488
  </span>
1673
- {activeView ? (
1674
- <button type="button" className="dm-btn-ghost" onClick={updateCurrentView}>
1675
- Update view
1676
- </button>
1677
- ) : (
1678
- <button type="button" className="dm-btn-ghost" onClick={saveCurrentAsNewView}>
1679
- Save as new view
1680
- </button>
1681
- )}
1682
1489
  {table.rows.length > 0 && (
1683
1490
  <button type="button" className="dm-btn-ghost" onClick={() => {
1684
1491
  const blob = new Blob([exportTableAsCsv(table)], { type: "text/csv" });
@@ -2280,6 +2087,20 @@ export default function DataModelShell() {
2280
2087
  if (!selectedSource && tables[0]) setSelectedSource(tables[0].source);
2281
2088
  }, [selectedSource, tables]);
2282
2089
 
2090
+ useEffect(() => {
2091
+ const objectParam = searchParams?.get("object");
2092
+ if (!objectParam || !tables.length) return;
2093
+ const target = tables.find((table) => (
2094
+ table.objectId === objectParam
2095
+ || table.id === objectParam
2096
+ || table.source === objectParam
2097
+ || table.label === objectParam
2098
+ ));
2099
+ if (target && target.source !== selectedSource) {
2100
+ setSelectedSource(target.source);
2101
+ }
2102
+ }, [searchParams, selectedSource, tables]);
2103
+
2283
2104
  // Flush any accumulated patch keys to the server. Called by the debounce
2284
2105
  // timer and on visibilitychange/beforeunload so no local edit is lost.
2285
2106
  const flushPendingPatch = useCallback(async () => {
@@ -2439,7 +2260,7 @@ export default function DataModelShell() {
2439
2260
  },
2440
2261
  {
2441
2262
  id: "helper.repair", group: "Ask helper", label: "Ask helper — repair workspace",
2442
- run: () => openHelperWith("repair", "Inspect this workspace for missing references, broken bindings, or incomplete views. Propose the smallest fix for each issue.")
2263
+ run: () => openHelperWith("repair", "Inspect this workspace for missing references, broken bindings, or incomplete object configuration. Propose the smallest fix for each issue.")
2443
2264
  },
2444
2265
  {
2445
2266
  id: "helper.explain", group: "Ask helper", label: "Ask helper — explain this workspace",
@@ -10,15 +10,19 @@ import {
10
10
  Code2,
11
11
  Database,
12
12
  FileText,
13
+ Folder,
14
+ FolderOpen,
13
15
  Globe,
14
16
  Hash,
15
17
  Layers,
18
+ LayoutDashboard,
16
19
  Link2,
17
20
  List,
18
21
  Mail,
19
22
  Plus,
20
23
  ShoppingCart,
21
24
  Star,
25
+ Table,
22
26
  Tag,
23
27
  Terminal,
24
28
  ToggleLeft,
@@ -30,14 +34,16 @@ import { OBJECT_TYPE_PRESETS } from "@/lib/workspace-data-model";
30
34
 
31
35
  const LUCIDE_MAP = {
32
36
  Activity, BarChart2, Box, Building2, Calendar, CheckSquare, Code2,
33
- Database, FileText, Globe, Hash, Layers, Link2, List, Mail, Plus,
34
- ShoppingCart, Star, Tag, Terminal, ToggleLeft, Type, Users, Zap,
37
+ Database, FileText, Folder, FolderOpen, Globe, Hash, Layers,
38
+ LayoutDashboard, Link2, List, Mail, Plus, ShoppingCart, Star, Table,
39
+ Tag, Terminal, ToggleLeft, Type, Users, Zap,
35
40
  };
36
41
 
37
42
  const ICON_PICKER_SET = [
38
43
  "Database", "Globe", "Code2", "Users", "CheckSquare", "Building2",
39
44
  "Tag", "Star", "Zap", "FileText", "Mail", "BarChart2",
40
45
  "Layers", "Box", "Activity", "ShoppingCart", "Terminal",
46
+ "Folder", "FolderOpen", "LayoutDashboard", "Table", "List",
41
47
  ];
42
48
 
43
49
  const OBJECT_TYPE_BADGE = {