@growthub/cli 0.9.6 → 0.9.8
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/globals.css +261 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-builder.jsx +583 -96
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-schema.js +310 -1
- package/package.json +1 -1
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import Link from "next/link";
|
|
4
|
-
import { useCallback, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
5
5
|
import {
|
|
6
6
|
DASHBOARD_TEMPLATES,
|
|
7
7
|
SAMPLE_DATA_BINDINGS,
|
|
8
8
|
SAMPLE_VIEW_ROWS,
|
|
9
|
+
cloneTemplateToDashboard,
|
|
10
|
+
cloneTemplateToTab,
|
|
9
11
|
defaultConfigFor,
|
|
10
|
-
|
|
12
|
+
normalizeWorkspaceTemplate,
|
|
13
|
+
unwrapWorkspaceTemplateImport,
|
|
14
|
+
validateWorkspaceConfig,
|
|
15
|
+
wrapWorkspaceTemplateExport
|
|
11
16
|
} from "@/lib/workspace-schema";
|
|
12
17
|
|
|
13
18
|
const DEFAULT_POSITION = { x: 4, y: 0, w: 4, h: 5 };
|
|
@@ -70,6 +75,120 @@ function commitTabs(canvas, tabs, activeTabId) {
|
|
|
70
75
|
return next;
|
|
71
76
|
}
|
|
72
77
|
|
|
78
|
+
function createDashboardRecord(name = "Untitled") {
|
|
79
|
+
const tab = createEmptyTab(name);
|
|
80
|
+
return {
|
|
81
|
+
id: generateId("dashboard"),
|
|
82
|
+
name,
|
|
83
|
+
createdBy: "Workspace owner",
|
|
84
|
+
updatedAt: "new",
|
|
85
|
+
status: "draft",
|
|
86
|
+
tabs: [tab],
|
|
87
|
+
activeTabId: tab.id
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function createEmptyTab(name = "Untitled") {
|
|
92
|
+
return {
|
|
93
|
+
id: generateId("tab"),
|
|
94
|
+
name,
|
|
95
|
+
widgets: []
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function cloneTabForDashboard(tab, name) {
|
|
100
|
+
return {
|
|
101
|
+
id: generateId("tab"),
|
|
102
|
+
name,
|
|
103
|
+
widgets: (tab?.widgets || []).map((widget) => ({
|
|
104
|
+
...cloneConfig(widget),
|
|
105
|
+
id: generateId("widget")
|
|
106
|
+
}))
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function normalizeDashboard(dashboard, fallbackCanvas) {
|
|
111
|
+
const tabs = Array.isArray(dashboard?.tabs) && dashboard.tabs.length
|
|
112
|
+
? dashboard.tabs
|
|
113
|
+
: getTabs(fallbackCanvas).map((tab) => ({
|
|
114
|
+
...tab,
|
|
115
|
+
id: tab.id === DEFAULT_TAB_ID ? generateId("tab") : tab.id
|
|
116
|
+
}));
|
|
117
|
+
const activeTabId = dashboard?.activeTabId && tabs.some((tab) => tab.id === dashboard.activeTabId)
|
|
118
|
+
? dashboard.activeTabId
|
|
119
|
+
: tabs[0].id;
|
|
120
|
+
return {
|
|
121
|
+
...dashboard,
|
|
122
|
+
tabs,
|
|
123
|
+
activeTabId
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function dashboardCanvasFrom(dashboard, baseCanvas) {
|
|
128
|
+
const normalized = normalizeDashboard(dashboard, baseCanvas);
|
|
129
|
+
return commitTabs(baseCanvas, normalized.tabs, normalized.activeTabId);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function updateDashboardCanvas(dashboard, canvas) {
|
|
133
|
+
const tabs = getTabs(canvas);
|
|
134
|
+
const activeTabId = getActiveTabId(canvas);
|
|
135
|
+
return {
|
|
136
|
+
...dashboard,
|
|
137
|
+
tabs,
|
|
138
|
+
activeTabId
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function createDashboardFromTab(name, tab, source = {}) {
|
|
143
|
+
const clonedTab = cloneTabForDashboard(tab, name);
|
|
144
|
+
return {
|
|
145
|
+
...source,
|
|
146
|
+
id: generateId("dashboard"),
|
|
147
|
+
name,
|
|
148
|
+
createdBy: source.createdBy || "Workspace owner",
|
|
149
|
+
updatedAt: "new",
|
|
150
|
+
status: "draft",
|
|
151
|
+
tabs: [clonedTab],
|
|
152
|
+
activeTabId: clonedTab.id
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function getActiveDashboardId(dashboards, fallback) {
|
|
157
|
+
if (fallback && dashboards.some((dashboard) => dashboard.id === fallback)) {
|
|
158
|
+
return fallback;
|
|
159
|
+
}
|
|
160
|
+
return dashboards[0]?.id || null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function activeDashboardIndex(dashboards, activeDashboardId) {
|
|
164
|
+
const index = dashboards.findIndex((dashboard) => dashboard.id === activeDashboardId);
|
|
165
|
+
return index >= 0 ? index : 0;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function syncActiveDashboard(config, activeDashboardId) {
|
|
169
|
+
const dashboards = config.dashboards || [];
|
|
170
|
+
const resolvedId = getActiveDashboardId(dashboards, activeDashboardId);
|
|
171
|
+
if (!resolvedId) return config;
|
|
172
|
+
return {
|
|
173
|
+
...config,
|
|
174
|
+
dashboards: dashboards.map((dashboard) =>
|
|
175
|
+
dashboard.id === resolvedId ? updateDashboardCanvas(dashboard, config.canvas) : dashboard
|
|
176
|
+
)
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function commitDashboardCanvas(config, activeDashboardId, nextCanvas) {
|
|
181
|
+
const dashboards = config.dashboards || [];
|
|
182
|
+
const resolvedId = getActiveDashboardId(dashboards, activeDashboardId);
|
|
183
|
+
return {
|
|
184
|
+
...config,
|
|
185
|
+
dashboards: dashboards.map((dashboard) =>
|
|
186
|
+
dashboard.id === resolvedId ? updateDashboardCanvas(dashboard, nextCanvas) : dashboard
|
|
187
|
+
),
|
|
188
|
+
canvas: nextCanvas
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
73
192
|
function findFreePosition(widgets) {
|
|
74
193
|
const occupied = new Set();
|
|
75
194
|
for (const widget of widgets) {
|
|
@@ -242,15 +361,98 @@ function serializeManualRows(rows, columns) {
|
|
|
242
361
|
.join("\n");
|
|
243
362
|
}
|
|
244
363
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
364
|
+
const NORMALIZED_TEMPLATES = DASHBOARD_TEMPLATES.map((template) => ({
|
|
365
|
+
...normalizeWorkspaceTemplate(template),
|
|
366
|
+
widgets: template.widgets
|
|
367
|
+
}));
|
|
368
|
+
|
|
369
|
+
function widgetKindFill(kind) {
|
|
370
|
+
switch (kind) {
|
|
371
|
+
case "chart": return "#dbeafe";
|
|
372
|
+
case "view": return "#fef3c7";
|
|
373
|
+
case "iframe": return "#ede9fe";
|
|
374
|
+
case "rich-text": return "#dcfce7";
|
|
375
|
+
default: return "#e5e7eb";
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function TemplateMiniGrid({ template }) {
|
|
380
|
+
const widgets = Array.isArray(template?.widgets) ? template.widgets : [];
|
|
381
|
+
return <div
|
|
382
|
+
className="template-mini-grid"
|
|
383
|
+
aria-hidden="true"
|
|
384
|
+
style={{
|
|
385
|
+
"--template-mini-columns": GRID_COLUMNS,
|
|
386
|
+
"--template-mini-rows": GRID_ROWS
|
|
387
|
+
}}
|
|
388
|
+
>
|
|
389
|
+
{widgets.map((widget, index) => <span
|
|
390
|
+
className={`template-mini-widget kind-${widget.kind}`}
|
|
391
|
+
key={`${widget.kind}-${index}`}
|
|
392
|
+
style={{
|
|
393
|
+
gridColumn: `${widget.position.x + 1} / span ${widget.position.w}`,
|
|
394
|
+
gridRow: `${widget.position.y + 1} / span ${widget.position.h}`,
|
|
395
|
+
background: widgetKindFill(widget.kind)
|
|
396
|
+
}}
|
|
397
|
+
/>)}
|
|
398
|
+
</div>;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function TemplateGallery({
|
|
402
|
+
templates,
|
|
403
|
+
previewTemplateId,
|
|
404
|
+
onPreview,
|
|
405
|
+
onClose,
|
|
406
|
+
onApplyToCurrentTab,
|
|
407
|
+
onCloneAsDashboard
|
|
408
|
+
}) {
|
|
409
|
+
const previewTemplate = templates.find((template) => template.id === previewTemplateId) || null;
|
|
410
|
+
return <div className="template-gallery" role="dialog" aria-modal="true" aria-label="Workspace templates">
|
|
411
|
+
<div className="template-gallery-backdrop" onClick={onClose} aria-hidden="true" />
|
|
412
|
+
<section className="template-gallery-panel">
|
|
413
|
+
<header className="template-gallery-header">
|
|
414
|
+
<div>
|
|
415
|
+
<p>Workspace templates</p>
|
|
416
|
+
<h2>Pick a starting layout</h2>
|
|
417
|
+
</div>
|
|
418
|
+
<button type="button" aria-label="Close template gallery" onClick={onClose}>x</button>
|
|
419
|
+
</header>
|
|
420
|
+
<div className="template-gallery-grid">
|
|
421
|
+
{templates.map((template) => {
|
|
422
|
+
const isPreviewing = previewTemplate?.id === template.id;
|
|
423
|
+
return <article
|
|
424
|
+
className={`template-card${isPreviewing ? " previewing" : ""}`}
|
|
425
|
+
key={template.id}
|
|
426
|
+
>
|
|
427
|
+
<div className="template-card-header">
|
|
428
|
+
<strong>{template.name}</strong>
|
|
429
|
+
<span className="template-card-category">{template.category}</span>
|
|
430
|
+
</div>
|
|
431
|
+
<p className="template-card-description">{template.description}</p>
|
|
432
|
+
<div className="template-card-preview">
|
|
433
|
+
<TemplateMiniGrid template={template} />
|
|
434
|
+
</div>
|
|
435
|
+
<div className="template-card-meta">
|
|
436
|
+
<span>{template.widgetCount} widget{template.widgetCount === 1 ? "" : "s"}</span>
|
|
437
|
+
{template.bestFor.length ? <span>· Best for: {template.bestFor.join(", ")}</span> : null}
|
|
438
|
+
</div>
|
|
439
|
+
{template.tags.length ? <div className="template-card-tags">
|
|
440
|
+
{template.tags.map((tag) => <span key={tag}>#{tag}</span>)}
|
|
441
|
+
</div> : null}
|
|
442
|
+
<div className="template-card-actions">
|
|
443
|
+
<button type="button" onClick={() => onPreview(template.id)}>{isPreviewing ? "Previewing" : "Preview"}</button>
|
|
444
|
+
<button type="button" onClick={() => onApplyToCurrentTab(template.id)}>Apply to Current Tab</button>
|
|
445
|
+
<button type="button" onClick={() => onCloneAsDashboard(template.id)}>Clone as New Dashboard</button>
|
|
446
|
+
</div>
|
|
447
|
+
</article>;
|
|
448
|
+
})}
|
|
449
|
+
</div>
|
|
450
|
+
{previewTemplate ? <footer className="template-gallery-footer" aria-live="polite">
|
|
451
|
+
<strong>{previewTemplate.name}</strong>
|
|
452
|
+
<span>{previewTemplate.preview?.summary || previewTemplate.description}</span>
|
|
453
|
+
</footer> : null}
|
|
454
|
+
</section>
|
|
455
|
+
</div>;
|
|
254
456
|
}
|
|
255
457
|
|
|
256
458
|
function WidgetPreview({ widget, selected, onSelect, onRemove, onResizeStart }) {
|
|
@@ -306,12 +508,34 @@ function WidgetPreview({ widget, selected, onSelect, onRemove, onResizeStart })
|
|
|
306
508
|
}
|
|
307
509
|
|
|
308
510
|
function WorkspaceBuilder({ initialConfig, adapterConfig, integrationAdapter }) {
|
|
309
|
-
const [config, setConfig] = useState(
|
|
511
|
+
const [config, setConfig] = useState(() => {
|
|
512
|
+
const dashboards = Array.isArray(initialConfig.dashboards) && initialConfig.dashboards.length
|
|
513
|
+
? initialConfig.dashboards.map((dashboard, index) =>
|
|
514
|
+
normalizeDashboard(dashboard, index === 0 ? initialConfig.canvas : undefined)
|
|
515
|
+
)
|
|
516
|
+
: [createDashboardRecord("Untitled")];
|
|
517
|
+
return {
|
|
518
|
+
...initialConfig,
|
|
519
|
+
dashboards,
|
|
520
|
+
canvas: dashboardCanvasFrom(dashboards[0], initialConfig.canvas)
|
|
521
|
+
};
|
|
522
|
+
});
|
|
310
523
|
const [saving, setSaving] = useState(false);
|
|
311
524
|
const [panelOpen, setPanelOpen] = useState(true);
|
|
525
|
+
const [templateGalleryOpen, setTemplateGalleryOpen] = useState(false);
|
|
526
|
+
const [previewTemplateId, setPreviewTemplateId] = useState(null);
|
|
527
|
+
const [editingDashboardId, setEditingDashboardId] = useState(null);
|
|
528
|
+
const [activeDashboardId, setActiveDashboardId] = useState(() =>
|
|
529
|
+
getActiveDashboardId(
|
|
530
|
+
Array.isArray(initialConfig.dashboards) && initialConfig.dashboards.length ? initialConfig.dashboards : [],
|
|
531
|
+
null
|
|
532
|
+
)
|
|
533
|
+
);
|
|
312
534
|
const gridRef = useRef(null);
|
|
313
535
|
const canvas = config.canvas;
|
|
314
|
-
const dashboards = config.dashboards;
|
|
536
|
+
const dashboards = config.dashboards || [];
|
|
537
|
+
const resolvedActiveDashboardId = getActiveDashboardId(dashboards, activeDashboardId);
|
|
538
|
+
const resolvedActiveDashboardIndex = activeDashboardIndex(dashboards, resolvedActiveDashboardId);
|
|
315
539
|
const widgetTypes = config.widgetTypes;
|
|
316
540
|
const tabs = getTabs(canvas);
|
|
317
541
|
const activeTabId = getActiveTabId(canvas);
|
|
@@ -361,9 +585,9 @@ function WorkspaceBuilder({ initialConfig, adapterConfig, integrationAdapter })
|
|
|
361
585
|
setSelectedWidgetId(widget.id);
|
|
362
586
|
setSelectedPosition(findFreePosition([...existingWidgets, widget]));
|
|
363
587
|
setDragPreview(null);
|
|
364
|
-
return
|
|
588
|
+
return commitDashboardCanvas(prev, activeDashboardId, commitTabs(prev.canvas, nextTabs, prevActiveId));
|
|
365
589
|
});
|
|
366
|
-
}, [addSlot]);
|
|
590
|
+
}, [activeDashboardId, addSlot]);
|
|
367
591
|
|
|
368
592
|
const switchTab = useCallback((tabId) => {
|
|
369
593
|
setConfig((prev) => {
|
|
@@ -374,9 +598,9 @@ function WorkspaceBuilder({ initialConfig, adapterConfig, integrationAdapter })
|
|
|
374
598
|
setSelectedWidgetId(null);
|
|
375
599
|
setSelectedPosition(findFreePosition(nextTab?.widgets || []));
|
|
376
600
|
setDragPreview(null);
|
|
377
|
-
return
|
|
601
|
+
return commitDashboardCanvas(prev, activeDashboardId, commitTabs(prev.canvas, prevTabs, tabId));
|
|
378
602
|
});
|
|
379
|
-
}, []);
|
|
603
|
+
}, [activeDashboardId]);
|
|
380
604
|
|
|
381
605
|
const addTab = useCallback(() => {
|
|
382
606
|
setConfig((prev) => {
|
|
@@ -395,49 +619,160 @@ function WorkspaceBuilder({ initialConfig, adapterConfig, integrationAdapter })
|
|
|
395
619
|
setSelectedWidgetId(null);
|
|
396
620
|
setSelectedPosition({ ...DEFAULT_POSITION });
|
|
397
621
|
setDragPreview(null);
|
|
398
|
-
return
|
|
622
|
+
return commitDashboardCanvas(prev, activeDashboardId, commitTabs(prev.canvas, nextTabs, newTab.id));
|
|
399
623
|
});
|
|
400
|
-
}, []);
|
|
624
|
+
}, [activeDashboardId]);
|
|
401
625
|
|
|
402
626
|
const addDashboard = useCallback(() => {
|
|
627
|
+
setConfig((prev) => {
|
|
628
|
+
const synced = syncActiveDashboard(prev, activeDashboardId);
|
|
629
|
+
const prevDashboards = synced.dashboards || [];
|
|
630
|
+
const name = `Dashboard ${prevDashboards.length + 1}`;
|
|
631
|
+
const dashboard = createDashboardRecord(name);
|
|
632
|
+
setSelectedWidgetId(null);
|
|
633
|
+
setSelectedPosition({ ...DEFAULT_POSITION });
|
|
634
|
+
setDragPreview(null);
|
|
635
|
+
setEditingDashboardId(dashboard.id);
|
|
636
|
+
setActiveDashboardId(dashboard.id);
|
|
637
|
+
setConfigMessage(`Created ${name}`);
|
|
638
|
+
return {
|
|
639
|
+
...synced,
|
|
640
|
+
dashboards: [...prevDashboards, dashboard],
|
|
641
|
+
canvas: dashboardCanvasFrom(dashboard, synced.canvas)
|
|
642
|
+
};
|
|
643
|
+
});
|
|
644
|
+
}, [activeDashboardId]);
|
|
645
|
+
|
|
646
|
+
const selectDashboard = useCallback((index) => {
|
|
647
|
+
setConfig((prev) => {
|
|
648
|
+
const synced = syncActiveDashboard(prev, activeDashboardId);
|
|
649
|
+
const prevDashboards = synced.dashboards || [];
|
|
650
|
+
const dashboard = prevDashboards[index];
|
|
651
|
+
if (!dashboard) return prev;
|
|
652
|
+
const normalized = normalizeDashboard(dashboard, index === 0 ? synced.canvas : undefined);
|
|
653
|
+
setSelectedWidgetId(null);
|
|
654
|
+
setSelectedPosition(findFreePosition(getTabs(dashboardCanvasFrom(normalized, prev.canvas))[0]?.widgets || []));
|
|
655
|
+
setDragPreview(null);
|
|
656
|
+
setEditingDashboardId(dashboard.id);
|
|
657
|
+
setActiveDashboardId(dashboard.id);
|
|
658
|
+
setConfigMessage(`Editing ${dashboard.name}`);
|
|
659
|
+
return {
|
|
660
|
+
...synced,
|
|
661
|
+
dashboards: prevDashboards.map((item) => item.id === dashboard.id ? normalized : item),
|
|
662
|
+
canvas: dashboardCanvasFrom(normalized, synced.canvas)
|
|
663
|
+
};
|
|
664
|
+
});
|
|
665
|
+
}, [activeDashboardId]);
|
|
666
|
+
|
|
667
|
+
const renameDashboard = useCallback((dashboardId, name) => {
|
|
668
|
+
const nextName = name.trimStart();
|
|
669
|
+
setConfig((prev) => {
|
|
670
|
+
const prevDashboards = prev.dashboards || [];
|
|
671
|
+
const index = prevDashboards.findIndex((dashboard) => dashboard.id === dashboardId);
|
|
672
|
+
if (index < 0) return prev;
|
|
673
|
+
const displayName = nextName || "Untitled";
|
|
674
|
+
const nextDashboards = prevDashboards.map((dashboard) =>
|
|
675
|
+
dashboard.id === dashboardId ? { ...dashboard, name: displayName, updatedAt: "new" } : dashboard
|
|
676
|
+
);
|
|
677
|
+
const nextDashboardsWithTabs = nextDashboards.map((dashboard, dashboardIndex) => {
|
|
678
|
+
if (dashboardIndex !== index) return dashboard;
|
|
679
|
+
const normalized = normalizeDashboard(dashboard, index === 0 ? prev.canvas : undefined);
|
|
680
|
+
const renamedTabs = normalized.tabs.map((tab, tabIndex) =>
|
|
681
|
+
tabIndex === 0 ? { ...tab, name: displayName } : tab
|
|
682
|
+
);
|
|
683
|
+
return { ...normalized, tabs: renamedTabs };
|
|
684
|
+
});
|
|
685
|
+
const activeDashboard = nextDashboardsWithTabs.find((dashboard) => dashboard.id === getActiveDashboardId(nextDashboardsWithTabs, activeDashboardId));
|
|
686
|
+
return {
|
|
687
|
+
...prev,
|
|
688
|
+
dashboards: nextDashboardsWithTabs,
|
|
689
|
+
canvas: dashboardCanvasFrom(activeDashboard || nextDashboardsWithTabs[0], prev.canvas)
|
|
690
|
+
};
|
|
691
|
+
});
|
|
692
|
+
}, [activeDashboardId]);
|
|
693
|
+
|
|
694
|
+
const updateDashboardStatus = useCallback((dashboardId, status) => {
|
|
403
695
|
setConfig((prev) => ({
|
|
404
696
|
...prev,
|
|
405
|
-
dashboards: [
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
id: generateId("dashboard"),
|
|
409
|
-
name: "Untitled",
|
|
410
|
-
createdBy: "Workspace owner",
|
|
411
|
-
updatedAt: "new",
|
|
412
|
-
status: "draft"
|
|
413
|
-
}
|
|
414
|
-
]
|
|
697
|
+
dashboards: (prev.dashboards || []).map((dashboard) =>
|
|
698
|
+
dashboard.id === dashboardId ? { ...dashboard, status, updatedAt: "new" } : dashboard
|
|
699
|
+
)
|
|
415
700
|
}));
|
|
416
701
|
}, []);
|
|
417
702
|
|
|
418
|
-
const
|
|
703
|
+
const cloneDashboard = useCallback((index) => {
|
|
419
704
|
setConfig((prev) => {
|
|
420
|
-
const
|
|
421
|
-
|
|
422
|
-
|
|
705
|
+
const synced = syncActiveDashboard(prev, activeDashboardId);
|
|
706
|
+
const prevDashboards = synced.dashboards || [];
|
|
707
|
+
const sourceDashboard = prevDashboards[index];
|
|
708
|
+
if (!sourceDashboard) return prev;
|
|
709
|
+
const normalizedSource = normalizeDashboard(sourceDashboard, index === resolvedActiveDashboardIndex ? synced.canvas : undefined);
|
|
710
|
+
const name = `${sourceDashboard.name} Copy`;
|
|
711
|
+
const dashboard = {
|
|
712
|
+
...normalizedSource,
|
|
713
|
+
id: generateId("dashboard"),
|
|
714
|
+
name,
|
|
423
715
|
updatedAt: "new",
|
|
424
|
-
status: "draft"
|
|
716
|
+
status: "draft",
|
|
717
|
+
tabs: normalizedSource.tabs.map((tab, tabIndex) =>
|
|
718
|
+
cloneTabForDashboard(tab, tabIndex === 0 ? name : tab.name)
|
|
719
|
+
)
|
|
425
720
|
};
|
|
721
|
+
dashboard.activeTabId = dashboard.tabs[0].id;
|
|
722
|
+
setSelectedWidgetId(null);
|
|
723
|
+
setSelectedPosition(findFreePosition(dashboard.tabs[0].widgets));
|
|
724
|
+
setDragPreview(null);
|
|
725
|
+
setEditingDashboardId(dashboard.id);
|
|
726
|
+
setActiveDashboardId(dashboard.id);
|
|
727
|
+
setConfigMessage(`Cloned ${sourceDashboard.name}`);
|
|
426
728
|
return {
|
|
427
|
-
...
|
|
428
|
-
dashboards: [
|
|
429
|
-
|
|
430
|
-
{
|
|
431
|
-
...source,
|
|
432
|
-
id: generateId("dashboard"),
|
|
433
|
-
name: `${source.name} Copy`,
|
|
434
|
-
updatedAt: "new",
|
|
435
|
-
status: "draft"
|
|
436
|
-
}
|
|
437
|
-
]
|
|
729
|
+
...synced,
|
|
730
|
+
dashboards: [...prevDashboards, dashboard],
|
|
731
|
+
canvas: dashboardCanvasFrom(dashboard, synced.canvas)
|
|
438
732
|
};
|
|
439
733
|
});
|
|
440
|
-
}, []);
|
|
734
|
+
}, [activeDashboardId, resolvedActiveDashboardIndex]);
|
|
735
|
+
|
|
736
|
+
const deleteDashboard = useCallback((index) => {
|
|
737
|
+
setConfig((prev) => {
|
|
738
|
+
const synced = syncActiveDashboard(prev, activeDashboardId);
|
|
739
|
+
const prevDashboards = synced.dashboards || [];
|
|
740
|
+
if (!prevDashboards[index]) return prev;
|
|
741
|
+
if (prevDashboards.length <= 1) {
|
|
742
|
+
const dashboard = createDashboardRecord("Untitled");
|
|
743
|
+
setSelectedWidgetId(null);
|
|
744
|
+
setSelectedPosition({ ...DEFAULT_POSITION });
|
|
745
|
+
setDragPreview(null);
|
|
746
|
+
setEditingDashboardId(dashboard.id);
|
|
747
|
+
setActiveDashboardId(dashboard.id);
|
|
748
|
+
setConfigMessage("Reset dashboard");
|
|
749
|
+
return {
|
|
750
|
+
...synced,
|
|
751
|
+
dashboards: [dashboard],
|
|
752
|
+
canvas: dashboardCanvasFrom(dashboard, synced.canvas)
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
const removed = prevDashboards[index];
|
|
756
|
+
const nextDashboards = prevDashboards.filter((_, dashboardIndex) => dashboardIndex !== index);
|
|
757
|
+
const nextActiveIndex = Math.min(index, nextDashboards.length - 1);
|
|
758
|
+
const nextActiveDashboard = normalizeDashboard(nextDashboards[nextActiveIndex], synced.canvas);
|
|
759
|
+
setSelectedWidgetId(null);
|
|
760
|
+
setSelectedPosition(findFreePosition(nextActiveDashboard.tabs[0]?.widgets || []));
|
|
761
|
+
setDragPreview(null);
|
|
762
|
+
setEditingDashboardId(nextActiveDashboard.id);
|
|
763
|
+
setActiveDashboardId(nextActiveDashboard.id);
|
|
764
|
+
setConfigMessage(`Deleted ${removed.name}`);
|
|
765
|
+
return {
|
|
766
|
+
...synced,
|
|
767
|
+
dashboards: nextDashboards.map((dashboard) => dashboard.id === nextActiveDashboard.id ? nextActiveDashboard : dashboard),
|
|
768
|
+
canvas: dashboardCanvasFrom(nextActiveDashboard, synced.canvas)
|
|
769
|
+
};
|
|
770
|
+
});
|
|
771
|
+
}, [activeDashboardId]);
|
|
772
|
+
|
|
773
|
+
const duplicateDashboard = useCallback(() => {
|
|
774
|
+
cloneDashboard(resolvedActiveDashboardIndex);
|
|
775
|
+
}, [cloneDashboard, resolvedActiveDashboardIndex]);
|
|
441
776
|
|
|
442
777
|
const duplicateTab = useCallback(() => {
|
|
443
778
|
setConfig((prev) => {
|
|
@@ -460,65 +795,137 @@ function WorkspaceBuilder({ initialConfig, adapterConfig, integrationAdapter })
|
|
|
460
795
|
setSelectedWidgetId(null);
|
|
461
796
|
setSelectedPosition(findFreePosition(cloned.widgets));
|
|
462
797
|
setDragPreview(null);
|
|
463
|
-
return
|
|
798
|
+
return commitDashboardCanvas(prev, activeDashboardId, commitTabs(prev.canvas, nextTabs, cloned.id));
|
|
464
799
|
});
|
|
465
|
-
}, []);
|
|
800
|
+
}, [activeDashboardId]);
|
|
801
|
+
|
|
802
|
+
const deleteTab = useCallback((tabId) => {
|
|
803
|
+
setConfig((prev) => {
|
|
804
|
+
const prevTabs = getTabs(prev.canvas);
|
|
805
|
+
const tab = prevTabs.find((item) => item.id === tabId);
|
|
806
|
+
if (!tab) return prev;
|
|
807
|
+
if (prevTabs.length <= 1) {
|
|
808
|
+
const fallback = createEmptyTab(tab.name || "Tab 1");
|
|
809
|
+
setSelectedWidgetId(null);
|
|
810
|
+
setSelectedPosition({ ...DEFAULT_POSITION });
|
|
811
|
+
setDragPreview(null);
|
|
812
|
+
setConfigMessage(`Cleared ${tab.name}`);
|
|
813
|
+
return commitDashboardCanvas(prev, activeDashboardId, commitTabs(prev.canvas, [fallback], fallback.id));
|
|
814
|
+
}
|
|
815
|
+
const nextTabs = prevTabs.filter((item) => item.id !== tabId);
|
|
816
|
+
const activeIndex = prevTabs.findIndex((item) => item.id === tabId);
|
|
817
|
+
const nextActiveTab = nextTabs[Math.min(activeIndex, nextTabs.length - 1)] || nextTabs[0];
|
|
818
|
+
setSelectedWidgetId(null);
|
|
819
|
+
setSelectedPosition(findFreePosition(nextActiveTab.widgets || []));
|
|
820
|
+
setDragPreview(null);
|
|
821
|
+
setConfigMessage(`Deleted ${tab.name}`);
|
|
822
|
+
return commitDashboardCanvas(prev, activeDashboardId, commitTabs(prev.canvas, nextTabs, nextActiveTab.id));
|
|
823
|
+
});
|
|
824
|
+
}, [activeDashboardId]);
|
|
466
825
|
|
|
467
|
-
const
|
|
826
|
+
const applyTemplateToCurrentTab = useCallback((templateId) => {
|
|
468
827
|
const template = DASHBOARD_TEMPLATES.find((item) => item.id === templateId);
|
|
469
828
|
if (!template) return;
|
|
829
|
+
const clonedTab = cloneTemplateToTab(template, { tabName: template.name, idFactory: generateId });
|
|
470
830
|
setConfig((prev) => {
|
|
471
|
-
const hydrated = hydrateTemplate(template);
|
|
472
831
|
const prevTabs = getTabs(prev.canvas);
|
|
473
832
|
const prevActiveId = getActiveTabId(prev.canvas);
|
|
833
|
+
const dashboardIndex = activeDashboardIndex(prev.dashboards || [], activeDashboardId);
|
|
474
834
|
const stableTabs = prevTabs.length === 1 && prevTabs[0].id === DEFAULT_TAB_ID
|
|
475
835
|
? [{ ...prevTabs[0], id: DEFAULT_TAB_ID }]
|
|
476
836
|
: prevTabs;
|
|
477
837
|
const nextTabs = stableTabs.map((tab) =>
|
|
478
|
-
tab.id === prevActiveId ? { ...tab, name:
|
|
838
|
+
tab.id === prevActiveId ? { ...tab, name: clonedTab.name, widgets: clonedTab.widgets } : tab
|
|
839
|
+
);
|
|
840
|
+
const nextCanvas = commitTabs(prev.canvas, nextTabs, prevActiveId);
|
|
841
|
+
const nextDashboards = (prev.dashboards || []).map((dashboard, index) =>
|
|
842
|
+
index === dashboardIndex
|
|
843
|
+
? updateDashboardCanvas({ ...dashboard, name: template.name, updatedAt: "new", status: "draft" }, nextCanvas)
|
|
844
|
+
: dashboard
|
|
479
845
|
);
|
|
480
|
-
setSelectedWidgetId(null);
|
|
481
|
-
setSelectedPosition(findFreePosition(hydrated.widgets));
|
|
482
|
-
setDragPreview(null);
|
|
483
|
-
setConfigMessage(`Applied ${template.name}`);
|
|
484
846
|
return {
|
|
485
847
|
...prev,
|
|
486
|
-
dashboards:
|
|
487
|
-
|
|
488
|
-
),
|
|
489
|
-
canvas: commitTabs(prev.canvas, nextTabs, prevActiveId)
|
|
848
|
+
dashboards: nextDashboards,
|
|
849
|
+
canvas: nextCanvas
|
|
490
850
|
};
|
|
491
851
|
});
|
|
492
|
-
|
|
852
|
+
setSelectedWidgetId(null);
|
|
853
|
+
setSelectedPosition(findFreePosition(clonedTab.widgets));
|
|
854
|
+
setDragPreview(null);
|
|
855
|
+
setConfigMessage(`Applied ${template.name} to current tab`);
|
|
856
|
+
setTemplateGalleryOpen(false);
|
|
857
|
+
setPreviewTemplateId(null);
|
|
858
|
+
}, [activeDashboardId]);
|
|
859
|
+
|
|
860
|
+
const cloneTemplateAsDashboard = useCallback((templateId) => {
|
|
861
|
+
const template = DASHBOARD_TEMPLATES.find((item) => item.id === templateId);
|
|
862
|
+
if (!template) return;
|
|
863
|
+
const cloned = cloneTemplateToDashboard(template, { idFactory: generateId });
|
|
864
|
+
setConfig((prev) => {
|
|
865
|
+
const synced = syncActiveDashboard(prev, activeDashboardId);
|
|
866
|
+
const dashboard = {
|
|
867
|
+
...cloned.dashboard,
|
|
868
|
+
tabs: [cloned.tab],
|
|
869
|
+
activeTabId: cloned.tab.id
|
|
870
|
+
};
|
|
871
|
+
setActiveDashboardId(dashboard.id);
|
|
872
|
+
return {
|
|
873
|
+
...synced,
|
|
874
|
+
dashboards: [...(synced.dashboards || []), dashboard],
|
|
875
|
+
canvas: dashboardCanvasFrom(dashboard, synced.canvas)
|
|
876
|
+
};
|
|
877
|
+
});
|
|
878
|
+
setSelectedWidgetId(null);
|
|
879
|
+
setSelectedPosition(findFreePosition(cloned.tab.widgets));
|
|
880
|
+
setDragPreview(null);
|
|
881
|
+
setConfigMessage(`Cloned ${template.name} as dashboard`);
|
|
882
|
+
setTemplateGalleryOpen(false);
|
|
883
|
+
setPreviewTemplateId(null);
|
|
884
|
+
}, [activeDashboardId]);
|
|
493
885
|
|
|
494
886
|
const exportConfig = useCallback(() => {
|
|
495
|
-
const
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
887
|
+
const syncedConfig = syncActiveDashboard(config, activeDashboardId);
|
|
888
|
+
const primaryDashboard = syncedConfig.dashboards?.[0] || {};
|
|
889
|
+
const wrapped = wrapWorkspaceTemplateExport(
|
|
890
|
+
{
|
|
891
|
+
dashboards: syncedConfig.dashboards,
|
|
892
|
+
widgetTypes: syncedConfig.widgetTypes,
|
|
893
|
+
canvas: syncedConfig.canvas
|
|
894
|
+
},
|
|
895
|
+
{
|
|
896
|
+
name: primaryDashboard.name || syncedConfig.name || "Workspace template",
|
|
897
|
+
description: syncedConfig.description || ""
|
|
898
|
+
}
|
|
899
|
+
);
|
|
900
|
+
const blob = new Blob([`${JSON.stringify(wrapped, null, 2)}\n`], { type: "application/json" });
|
|
500
901
|
const url = URL.createObjectURL(blob);
|
|
501
902
|
const anchor = document.createElement("a");
|
|
502
903
|
anchor.href = url;
|
|
503
|
-
anchor.download = "growthub-dashboard.
|
|
904
|
+
anchor.download = "growthub-dashboard.template.json";
|
|
504
905
|
anchor.click();
|
|
505
906
|
URL.revokeObjectURL(url);
|
|
506
|
-
setConfigMessage("Exported
|
|
507
|
-
}, [config]);
|
|
907
|
+
setConfigMessage("Exported workspace template");
|
|
908
|
+
}, [activeDashboardId, config]);
|
|
508
909
|
|
|
509
910
|
const importConfig = useCallback(async (event) => {
|
|
510
911
|
const file = event.target.files?.[0];
|
|
511
912
|
if (!file) return;
|
|
512
913
|
try {
|
|
513
|
-
const
|
|
514
|
-
|
|
914
|
+
const parsed = JSON.parse(await file.text());
|
|
915
|
+
const payload = unwrapWorkspaceTemplateImport(parsed);
|
|
916
|
+
validateWorkspaceConfig(payload);
|
|
917
|
+
const importedDashboards = (payload.dashboards || []).map((dashboard, index) =>
|
|
918
|
+
normalizeDashboard(dashboard, index === 0 ? payload.canvas : undefined)
|
|
919
|
+
);
|
|
920
|
+
const importedActiveDashboard = importedDashboards[0];
|
|
515
921
|
setConfig((prev) => ({
|
|
516
922
|
...prev,
|
|
517
|
-
dashboards:
|
|
518
|
-
widgetTypes:
|
|
519
|
-
canvas:
|
|
923
|
+
dashboards: importedDashboards,
|
|
924
|
+
widgetTypes: payload.widgetTypes,
|
|
925
|
+
canvas: importedActiveDashboard ? dashboardCanvasFrom(importedActiveDashboard, payload.canvas) : payload.canvas
|
|
520
926
|
}));
|
|
521
|
-
|
|
927
|
+
setActiveDashboardId(importedActiveDashboard?.id || null);
|
|
928
|
+
const importedTabs = getTabs(importedActiveDashboard ? dashboardCanvasFrom(importedActiveDashboard, payload.canvas) : payload.canvas);
|
|
522
929
|
setSelectedWidgetId(null);
|
|
523
930
|
setSelectedPosition(findFreePosition(importedTabs[0]?.widgets || []));
|
|
524
931
|
setDragPreview(null);
|
|
@@ -535,21 +942,32 @@ function WorkspaceBuilder({ initialConfig, adapterConfig, integrationAdapter })
|
|
|
535
942
|
setSaving(true);
|
|
536
943
|
try {
|
|
537
944
|
const stamp = todayIsoDate();
|
|
538
|
-
const
|
|
539
|
-
|
|
945
|
+
const syncedConfig = syncActiveDashboard(config, activeDashboardId);
|
|
946
|
+
const updatedDashboards = (syncedConfig.dashboards || []).map((dashboard) =>
|
|
947
|
+
dashboard.id === getActiveDashboardId(syncedConfig.dashboards || [], activeDashboardId)
|
|
948
|
+
? { ...dashboard, updatedAt: stamp }
|
|
949
|
+
: dashboard
|
|
540
950
|
);
|
|
541
951
|
const response = await fetch("/api/workspace", {
|
|
542
952
|
method: "PATCH",
|
|
543
953
|
headers: { "content-type": "application/json" },
|
|
544
954
|
body: JSON.stringify({
|
|
545
955
|
dashboards: updatedDashboards,
|
|
546
|
-
widgetTypes:
|
|
547
|
-
canvas:
|
|
956
|
+
widgetTypes: syncedConfig.widgetTypes,
|
|
957
|
+
canvas: syncedConfig.canvas
|
|
548
958
|
})
|
|
549
959
|
});
|
|
550
960
|
const payload = await response.json();
|
|
551
961
|
if (response.ok && payload.workspaceConfig) {
|
|
552
|
-
|
|
962
|
+
const savedDashboards = (payload.workspaceConfig.dashboards || []).map((dashboard, index) =>
|
|
963
|
+
normalizeDashboard(dashboard, index === 0 ? payload.workspaceConfig.canvas : undefined)
|
|
964
|
+
);
|
|
965
|
+
const savedActiveDashboard = savedDashboards.find((dashboard) => dashboard.id === activeDashboardId) || savedDashboards[0];
|
|
966
|
+
setConfig({
|
|
967
|
+
...payload.workspaceConfig,
|
|
968
|
+
dashboards: savedDashboards,
|
|
969
|
+
canvas: savedActiveDashboard ? dashboardCanvasFrom(savedActiveDashboard, payload.workspaceConfig.canvas) : payload.workspaceConfig.canvas
|
|
970
|
+
});
|
|
553
971
|
setConfigMessage("Saved dashboard config");
|
|
554
972
|
} else {
|
|
555
973
|
setConfigMessage(payload.error || "Save failed");
|
|
@@ -559,7 +977,7 @@ function WorkspaceBuilder({ initialConfig, adapterConfig, integrationAdapter })
|
|
|
559
977
|
} finally {
|
|
560
978
|
setSaving(false);
|
|
561
979
|
}
|
|
562
|
-
}, [saving, config]);
|
|
980
|
+
}, [activeDashboardId, saving, config]);
|
|
563
981
|
|
|
564
982
|
const reopenPanel = useCallback(() => setPanelOpen(true), []);
|
|
565
983
|
const closePanel = useCallback(() => setPanelOpen(false), []);
|
|
@@ -626,9 +1044,9 @@ function WorkspaceBuilder({ initialConfig, adapterConfig, integrationAdapter })
|
|
|
626
1044
|
)
|
|
627
1045
|
};
|
|
628
1046
|
});
|
|
629
|
-
return
|
|
1047
|
+
return commitDashboardCanvas(prev, activeDashboardId, commitTabs(prev.canvas, nextTabs, prevActiveId));
|
|
630
1048
|
});
|
|
631
|
-
}, [activeWidgets]);
|
|
1049
|
+
}, [activeDashboardId, activeWidgets]);
|
|
632
1050
|
const finishResizeDrag = useCallback(() => {
|
|
633
1051
|
if (!resizeDragRef.current) return;
|
|
634
1052
|
resizeDragRef.current = null;
|
|
@@ -652,9 +1070,9 @@ function WorkspaceBuilder({ initialConfig, adapterConfig, integrationAdapter })
|
|
|
652
1070
|
)
|
|
653
1071
|
};
|
|
654
1072
|
});
|
|
655
|
-
return
|
|
1073
|
+
return commitDashboardCanvas(prev, activeDashboardId, commitTabs(prev.canvas, nextTabs, prevActiveId));
|
|
656
1074
|
});
|
|
657
|
-
}, [selectedWidgetId]);
|
|
1075
|
+
}, [activeDashboardId, selectedWidgetId]);
|
|
658
1076
|
const updateSelectedWidgetConfig = useCallback((updates) => {
|
|
659
1077
|
if (!selectedWidget) return;
|
|
660
1078
|
updateSelectedWidget({ config: { ...(selectedWidget.config || {}), ...updates } });
|
|
@@ -670,10 +1088,24 @@ function WorkspaceBuilder({ initialConfig, adapterConfig, integrationAdapter })
|
|
|
670
1088
|
const nextActiveWidgets = nextTabs.find((tab) => tab.id === prevActiveId)?.widgets || [];
|
|
671
1089
|
setSelectedWidgetId(null);
|
|
672
1090
|
setSelectedPosition(findFreePosition(nextActiveWidgets));
|
|
673
|
-
return
|
|
1091
|
+
return commitDashboardCanvas(prev, activeDashboardId, commitTabs(prev.canvas, nextTabs, prevActiveId));
|
|
674
1092
|
});
|
|
1093
|
+
}, [activeDashboardId]);
|
|
1094
|
+
|
|
1095
|
+
const closeTemplateGallery = useCallback(() => {
|
|
1096
|
+
setTemplateGalleryOpen(false);
|
|
1097
|
+
setPreviewTemplateId(null);
|
|
675
1098
|
}, []);
|
|
676
1099
|
|
|
1100
|
+
useEffect(() => {
|
|
1101
|
+
if (!templateGalleryOpen) return undefined;
|
|
1102
|
+
const handler = (event) => {
|
|
1103
|
+
if (event.key === "Escape") closeTemplateGallery();
|
|
1104
|
+
};
|
|
1105
|
+
window.addEventListener("keydown", handler);
|
|
1106
|
+
return () => window.removeEventListener("keydown", handler);
|
|
1107
|
+
}, [templateGalleryOpen, closeTemplateGallery]);
|
|
1108
|
+
|
|
677
1109
|
const builderStyle = panelOpen ? undefined : { gridTemplateColumns: COLLAPSED_GRID_COLUMNS };
|
|
678
1110
|
|
|
679
1111
|
return <main className="workspace-builder" style={builderStyle}>
|
|
@@ -702,13 +1134,7 @@ function WorkspaceBuilder({ initialConfig, adapterConfig, integrationAdapter })
|
|
|
702
1134
|
<h1>{config.name}</h1>
|
|
703
1135
|
</div>
|
|
704
1136
|
<div className="workspace-toolbar-actions">
|
|
705
|
-
<
|
|
706
|
-
if (event.target.value) applyTemplate(event.target.value);
|
|
707
|
-
event.target.value = "";
|
|
708
|
-
}}>
|
|
709
|
-
<option value="">Templates</option>
|
|
710
|
-
{DASHBOARD_TEMPLATES.map((template) => <option key={template.id} value={template.id}>{template.name}</option>)}
|
|
711
|
-
</select>
|
|
1137
|
+
<button type="button" onClick={() => setTemplateGalleryOpen(true)}>Templates</button>
|
|
712
1138
|
<button type="button" onClick={addDashboard}>New Dashboard</button>
|
|
713
1139
|
<button type="button" onClick={duplicateDashboard}>Duplicate Dashboard</button>
|
|
714
1140
|
<button type="button" onClick={() => importInputRef.current?.click()}>Import</button>
|
|
@@ -735,12 +1161,45 @@ function WorkspaceBuilder({ initialConfig, adapterConfig, integrationAdapter })
|
|
|
735
1161
|
<span>Created by</span>
|
|
736
1162
|
<span>Last update</span>
|
|
737
1163
|
<span>Status</span>
|
|
1164
|
+
<span>Actions</span>
|
|
738
1165
|
</div>
|
|
739
|
-
{dashboards.map((dashboard) => <div className="workspace-table-row" key={dashboard.id}>
|
|
740
|
-
<span
|
|
1166
|
+
{dashboards.map((dashboard, index) => <div className="workspace-table-row" key={dashboard.id}>
|
|
1167
|
+
<span className="workspace-dashboard-title">
|
|
1168
|
+
{editingDashboardId === dashboard.id ? <input
|
|
1169
|
+
aria-label={`Rename ${dashboard.name}`}
|
|
1170
|
+
onBlur={() => setEditingDashboardId(null)}
|
|
1171
|
+
onChange={(event) => renameDashboard(dashboard.id, event.target.value)}
|
|
1172
|
+
onKeyDown={(event) => {
|
|
1173
|
+
if (event.key === "Enter" || event.key === "Escape") {
|
|
1174
|
+
event.currentTarget.blur();
|
|
1175
|
+
}
|
|
1176
|
+
}}
|
|
1177
|
+
value={dashboard.name}
|
|
1178
|
+
/> : <button
|
|
1179
|
+
className={index === resolvedActiveDashboardIndex ? "active" : ""}
|
|
1180
|
+
onClick={() => selectDashboard(index)}
|
|
1181
|
+
type="button"
|
|
1182
|
+
>{dashboard.name}</button>}
|
|
1183
|
+
</span>
|
|
741
1184
|
<span>{dashboard.createdBy}</span>
|
|
742
1185
|
<span>{dashboard.updatedAt}</span>
|
|
743
|
-
<
|
|
1186
|
+
<span>
|
|
1187
|
+
<select
|
|
1188
|
+
aria-label={`Status for ${dashboard.name}`}
|
|
1189
|
+
onChange={(event) => updateDashboardStatus(dashboard.id, event.target.value)}
|
|
1190
|
+
value={dashboard.status}
|
|
1191
|
+
>
|
|
1192
|
+
<option value="draft">draft</option>
|
|
1193
|
+
<option value="active">active</option>
|
|
1194
|
+
<option value="archived">archived</option>
|
|
1195
|
+
</select>
|
|
1196
|
+
</span>
|
|
1197
|
+
<span className="workspace-dashboard-actions">
|
|
1198
|
+
<button type="button" onClick={() => selectDashboard(index)}>Edit</button>
|
|
1199
|
+
<button type="button" onClick={() => setEditingDashboardId(dashboard.id)}>Rename</button>
|
|
1200
|
+
<button type="button" onClick={() => cloneDashboard(index)}>Clone</button>
|
|
1201
|
+
<button type="button" onClick={() => deleteDashboard(index)}>Delete</button>
|
|
1202
|
+
</span>
|
|
744
1203
|
</div>)}
|
|
745
1204
|
</section>
|
|
746
1205
|
|
|
@@ -751,7 +1210,26 @@ function WorkspaceBuilder({ initialConfig, adapterConfig, integrationAdapter })
|
|
|
751
1210
|
className={tab.id === activeTabId ? "active" : ""}
|
|
752
1211
|
type="button"
|
|
753
1212
|
onClick={() => switchTab(tab.id)}
|
|
754
|
-
>
|
|
1213
|
+
>
|
|
1214
|
+
<span>{tab.name}</span>
|
|
1215
|
+
<span
|
|
1216
|
+
aria-label={`Delete tab ${tab.name}`}
|
|
1217
|
+
className="workspace-tab-delete"
|
|
1218
|
+
onClick={(event) => {
|
|
1219
|
+
event.stopPropagation();
|
|
1220
|
+
deleteTab(tab.id);
|
|
1221
|
+
}}
|
|
1222
|
+
onKeyDown={(event) => {
|
|
1223
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
1224
|
+
event.preventDefault();
|
|
1225
|
+
event.stopPropagation();
|
|
1226
|
+
deleteTab(tab.id);
|
|
1227
|
+
}
|
|
1228
|
+
}}
|
|
1229
|
+
role="button"
|
|
1230
|
+
tabIndex={0}
|
|
1231
|
+
>x</span>
|
|
1232
|
+
</button>)}
|
|
755
1233
|
<button type="button" onClick={addTab}>New Tab</button>
|
|
756
1234
|
<button type="button" onClick={duplicateTab}>Duplicate Tab</button>
|
|
757
1235
|
</div>
|
|
@@ -806,6 +1284,15 @@ function WorkspaceBuilder({ initialConfig, adapterConfig, integrationAdapter })
|
|
|
806
1284
|
</section>
|
|
807
1285
|
</section>
|
|
808
1286
|
|
|
1287
|
+
{templateGalleryOpen ? <TemplateGallery
|
|
1288
|
+
templates={NORMALIZED_TEMPLATES}
|
|
1289
|
+
previewTemplateId={previewTemplateId}
|
|
1290
|
+
onPreview={setPreviewTemplateId}
|
|
1291
|
+
onClose={closeTemplateGallery}
|
|
1292
|
+
onApplyToCurrentTab={applyTemplateToCurrentTab}
|
|
1293
|
+
onCloneAsDashboard={cloneTemplateAsDashboard}
|
|
1294
|
+
/> : null}
|
|
1295
|
+
|
|
809
1296
|
{panelOpen ? <aside className="workspace-widget-panel" id="widgets" aria-label="Widget configuration">
|
|
810
1297
|
<div className="workspace-panel-title">
|
|
811
1298
|
<button type="button" aria-label="Close widget panel" onClick={closePanel}>x</button>
|