@growthub/cli 0.9.4 → 0.9.6

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.
@@ -6,6 +6,14 @@ import { describePaymentAdapter } from "@/lib/adapters/payments";
6
6
  import { describePersistenceAdapter } from "@/lib/adapters/persistence";
7
7
  import { groupIntegrationsByLane } from "@/lib/domain/integrations";
8
8
  import { buildPortalWorkspace, portalCapabilities } from "@/lib/domain/portal";
9
+ import {
10
+ describePersistenceMode,
11
+ readWorkspaceConfig,
12
+ writeWorkspaceConfig
13
+ } from "@/lib/workspace-config";
14
+
15
+ const ALLOWED_PATCH_FIELDS = new Set(["dashboards", "widgetTypes", "canvas"]);
16
+
9
17
  async function GET() {
10
18
  const integrations = await listAgencyPortalIntegrations();
11
19
  const config = readAdapterConfig();
@@ -18,14 +26,66 @@ async function GET() {
18
26
  const settings = {
19
27
  integrations: groupIntegrationsByLane(integrations)
20
28
  };
29
+ const workspaceConfig = await readWorkspaceConfig();
30
+ const persistence = describePersistenceMode();
21
31
  return NextResponse.json({
22
32
  config,
23
33
  adapters,
24
34
  capabilities: portalCapabilities,
25
35
  settings,
26
- workspace: buildPortalWorkspace({ config, adapters, integrations: settings.integrations })
36
+ workspace: buildPortalWorkspace({ config, adapters, integrations: settings.integrations }),
37
+ workspaceConfig,
38
+ workspaceConfigPersistence: persistence
27
39
  });
28
40
  }
29
- export {
30
- GET
31
- };
41
+
42
+ async function PATCH(request) {
43
+ let patch;
44
+ try {
45
+ patch = await request.json();
46
+ } catch {
47
+ return NextResponse.json({ error: "invalid json body" }, { status: 400 });
48
+ }
49
+ if (!patch || typeof patch !== "object" || Array.isArray(patch)) {
50
+ return NextResponse.json({ error: "patch must be a plain object" }, { status: 400 });
51
+ }
52
+ const unknown = Object.keys(patch).filter((key) => !ALLOWED_PATCH_FIELDS.has(key));
53
+ if (unknown.length) {
54
+ return NextResponse.json(
55
+ { error: "patch contains unknown fields", details: unknown, allowed: Array.from(ALLOWED_PATCH_FIELDS) },
56
+ { status: 400 }
57
+ );
58
+ }
59
+ const sanitized = {};
60
+ for (const key of ALLOWED_PATCH_FIELDS) {
61
+ if (Object.prototype.hasOwnProperty.call(patch, key)) {
62
+ sanitized[key] = patch[key];
63
+ }
64
+ }
65
+ try {
66
+ const next = await writeWorkspaceConfig(sanitized);
67
+ return NextResponse.json({ workspaceConfig: next });
68
+ } catch (error) {
69
+ if (error.code === "WORKSPACE_PERSISTENCE_READ_ONLY") {
70
+ return NextResponse.json(
71
+ {
72
+ error: "workspace config is read-only in this runtime",
73
+ reason: error.message,
74
+ adapter: error.adapter,
75
+ guidance:
76
+ "Edit growthub.config.json locally, or set WORKSPACE_CONFIG_ALLOW_FS_WRITE=true on a writable runtime."
77
+ },
78
+ { status: 409 }
79
+ );
80
+ }
81
+ if (error.code === "WORKSPACE_PERSISTENCE_PATH_REFUSED") {
82
+ return NextResponse.json({ error: error.message }, { status: 500 });
83
+ }
84
+ if (error.code === "INVALID_WORKSPACE_CONFIG") {
85
+ return NextResponse.json({ error: error.message, details: error.details }, { status: 400 });
86
+ }
87
+ return NextResponse.json({ error: error.message || "failed to write workspace config" }, { status: 500 });
88
+ }
89
+ }
90
+
91
+ export { GET, PATCH };
@@ -544,23 +544,46 @@ code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0
544
544
  background: #f3f3f3;
545
545
  color: #222;
546
546
  }
547
+ .workspace-toolbar-actions select,
548
+ .workspace-widget-settings select {
549
+ min-height: 32px;
550
+ border: 1px solid #dcdcdc;
551
+ border-radius: 6px;
552
+ background: #ffffff;
553
+ color: #444;
554
+ font: inherit;
555
+ padding: 0 8px;
556
+ }
557
+ .workspace-hidden-input {
558
+ display: none;
559
+ }
560
+ .workspace-config-message {
561
+ margin: -8px 0 10px;
562
+ color: #777;
563
+ font-size: 12px;
564
+ }
547
565
  .workspace-grid {
548
566
  position: relative;
549
567
  display: grid;
550
568
  grid-template-columns: repeat(var(--workspace-columns), minmax(48px, 1fr));
569
+ grid-template-rows: repeat(var(--workspace-rows), 52px);
551
570
  grid-auto-rows: 52px;
552
571
  gap: 8px;
553
- min-height: 620px;
554
572
  padding: 8px;
555
573
  }
556
574
  .workspace-grid-cell {
575
+ appearance: none;
557
576
  border: 1px solid #ededed;
558
577
  border-radius: 7px;
559
578
  background: #fdfdfd;
579
+ cursor: crosshair;
580
+ padding: 0;
581
+ }
582
+ .workspace-grid-cell:disabled {
583
+ pointer-events: none;
584
+ opacity: 1;
560
585
  }
561
586
  .workspace-add-widget {
562
- grid-column: 1 / span 4;
563
- grid-row: 1 / span 4;
564
587
  z-index: 1;
565
588
  display: grid;
566
589
  place-items: center;
@@ -572,6 +595,11 @@ code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0
572
595
  color: #3d3d3d;
573
596
  cursor: pointer;
574
597
  }
598
+ .workspace-add-widget.selecting {
599
+ border-color: #7aa2ff;
600
+ background: rgba(122, 162, 255, 0.16);
601
+ box-shadow: inset 0 0 0 1px rgba(84, 125, 255, 0.35);
602
+ }
575
603
  .workspace-add-widget small {
576
604
  color: #979797;
577
605
  }
@@ -593,16 +621,138 @@ code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0
593
621
  background: #b9ecff;
594
622
  }
595
623
  .workspace-widget-preview {
624
+ position: relative;
596
625
  z-index: 2;
597
626
  border: 1px solid #dedede;
598
627
  border-radius: 7px;
599
628
  background: #ffffff;
600
- padding: 10px;
629
+ padding: 12px;
630
+ cursor: pointer;
631
+ overflow: hidden;
601
632
  }
602
- .workspace-widget-preview span {
633
+ .workspace-widget-preview.selected {
634
+ border-color: #3f68ff;
635
+ box-shadow: inset 0 0 0 1px rgba(63, 104, 255, 0.45);
636
+ }
637
+ .workspace-resize-handle {
638
+ position: absolute;
639
+ z-index: 3;
640
+ width: 13px;
641
+ height: 13px;
642
+ border: 1px solid #3f68ff;
643
+ border-radius: 4px;
644
+ background: #ffffff;
645
+ box-shadow: 0 1px 3px rgba(63, 104, 255, 0.22);
646
+ padding: 0;
647
+ }
648
+ .workspace-resize-handle.nw {
649
+ top: -6px;
650
+ left: -6px;
651
+ cursor: nwse-resize;
652
+ }
653
+ .workspace-resize-handle.ne {
654
+ top: -6px;
655
+ right: -6px;
656
+ cursor: nesw-resize;
657
+ }
658
+ .workspace-resize-handle.sw {
659
+ bottom: -6px;
660
+ left: -6px;
661
+ cursor: nesw-resize;
662
+ }
663
+ .workspace-resize-handle.se {
664
+ right: -6px;
665
+ bottom: -6px;
666
+ cursor: nwse-resize;
667
+ }
668
+ .workspace-widget-preview-title {
669
+ display: flex;
670
+ align-items: center;
671
+ gap: 8px;
672
+ min-height: 22px;
673
+ }
674
+ .workspace-widget-preview-title span,
675
+ .workspace-widget-preview-title button {
603
676
  color: #888;
604
677
  font-size: 11px;
605
678
  }
679
+ .workspace-widget-preview-title strong {
680
+ min-width: 0;
681
+ flex: 1;
682
+ overflow: hidden;
683
+ color: #333;
684
+ font-size: 13px;
685
+ text-overflow: ellipsis;
686
+ white-space: nowrap;
687
+ }
688
+ .workspace-widget-preview-title button {
689
+ width: 22px;
690
+ height: 22px;
691
+ border: 0;
692
+ border-radius: 5px;
693
+ background: transparent;
694
+ }
695
+ .workspace-widget-preview:not(.selected) .workspace-widget-preview-title button {
696
+ visibility: hidden;
697
+ }
698
+ .workspace-view-table {
699
+ margin-top: 10px;
700
+ border: 1px solid #eeeeee;
701
+ border-radius: 4px;
702
+ overflow: hidden;
703
+ color: #555;
704
+ font-size: 12px;
705
+ }
706
+ .workspace-view-table div {
707
+ display: grid;
708
+ grid-template-columns: repeat(var(--workspace-view-columns, 2), minmax(0, 1fr));
709
+ min-height: 25px;
710
+ border-bottom: 1px solid #eeeeee;
711
+ }
712
+ .workspace-view-table span {
713
+ min-width: 0;
714
+ overflow: hidden;
715
+ padding: 5px 8px;
716
+ text-overflow: ellipsis;
717
+ white-space: nowrap;
718
+ }
719
+ .workspace-view-table span + span {
720
+ border-left: 1px solid #eeeeee;
721
+ }
722
+ .workspace-view-table footer {
723
+ padding: 7px 8px;
724
+ color: #999;
725
+ }
726
+ .workspace-iframe-preview {
727
+ height: calc(100% - 34px);
728
+ min-height: 90px;
729
+ display: grid;
730
+ place-items: center;
731
+ margin-top: 10px;
732
+ border-radius: 7px;
733
+ background: #fdfdfd;
734
+ color: #d54040;
735
+ font-size: 12px;
736
+ }
737
+ .workspace-rich-text-preview {
738
+ margin: 18px 10px 0;
739
+ color: #444;
740
+ font-size: 13px;
741
+ line-height: 1.5;
742
+ }
743
+ .workspace-chart-preview {
744
+ height: calc(100% - 42px);
745
+ min-height: 96px;
746
+ display: flex;
747
+ align-items: end;
748
+ gap: 8px;
749
+ padding: 16px 10px 6px;
750
+ }
751
+ .workspace-chart-preview span {
752
+ flex: 1;
753
+ border-radius: 5px 5px 0 0;
754
+ background: #d9e4ff;
755
+ }
606
756
  .workspace-widget-panel {
607
757
  border-left: 1px solid #e8e8e8;
608
758
  padding: 0 12px 16px;
@@ -614,16 +764,28 @@ code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0
614
764
  gap: 10px;
615
765
  border-bottom: 1px solid #ececec;
616
766
  }
617
- .workspace-panel-title button {
767
+ .workspace-panel-title button,
768
+ .workspace-panel-title > span {
618
769
  width: 24px;
619
770
  height: 24px;
771
+ display: grid;
772
+ place-items: center;
620
773
  border: 0;
621
774
  border-radius: 5px;
622
775
  background: #f1f1f1;
623
776
  color: #888;
624
777
  }
625
778
  .workspace-panel-title strong {
779
+ min-width: 0;
626
780
  font-size: 14px;
781
+ overflow: hidden;
782
+ text-overflow: ellipsis;
783
+ white-space: nowrap;
784
+ }
785
+ .workspace-panel-title em {
786
+ color: #999;
787
+ font-size: 12px;
788
+ font-style: normal;
627
789
  }
628
790
  .workspace-panel-label {
629
791
  margin: 14px 0 7px;
@@ -647,7 +809,6 @@ code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0
647
809
  padding: 0 8px;
648
810
  text-align: left;
649
811
  }
650
- .workspace-widget-types button:first-child,
651
812
  .workspace-widget-types button:hover {
652
813
  background: #f2f2f2;
653
814
  }
@@ -678,6 +839,61 @@ code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0
678
839
  color: #777;
679
840
  background: #f4f4f4;
680
841
  }
842
+ .workspace-widget-settings {
843
+ display: grid;
844
+ gap: 14px;
845
+ padding-top: 14px;
846
+ }
847
+ .workspace-field-stack {
848
+ display: grid;
849
+ gap: 12px;
850
+ }
851
+ .workspace-widget-settings label {
852
+ display: grid;
853
+ gap: 6px;
854
+ }
855
+ .workspace-widget-settings label span {
856
+ color: #999;
857
+ font-size: 11px;
858
+ font-weight: 700;
859
+ }
860
+ .workspace-widget-settings input,
861
+ .workspace-widget-settings textarea {
862
+ width: 100%;
863
+ box-sizing: border-box;
864
+ border: 1px solid #e5e5e5;
865
+ border-radius: 6px;
866
+ background: #fafafa;
867
+ color: #444;
868
+ font: inherit;
869
+ padding: 8px 9px;
870
+ }
871
+ .workspace-widget-settings textarea {
872
+ min-height: 120px;
873
+ resize: vertical;
874
+ }
875
+ .workspace-settings-list {
876
+ display: grid;
877
+ gap: 2px;
878
+ }
879
+ .workspace-settings-list div {
880
+ display: flex;
881
+ align-items: center;
882
+ justify-content: space-between;
883
+ min-height: 32px;
884
+ border-radius: 5px;
885
+ color: #666;
886
+ font-size: 13px;
887
+ padding: 0 8px;
888
+ }
889
+ .workspace-settings-list div:nth-child(3),
890
+ .workspace-settings-list div:hover {
891
+ background: #f3f3f3;
892
+ }
893
+ .workspace-settings-list code {
894
+ color: #aaa;
895
+ background: transparent;
896
+ }
681
897
 
682
898
  @media (max-width: 1080px) {
683
899
  .workspace-builder {
@@ -1,115 +1,21 @@
1
1
  import workspaceConfig from "../growthub.config.json";
2
2
  import { readAdapterConfig } from "@/lib/adapters/env";
3
3
  import { describeIntegrationAdapter } from "@/lib/adapters/integrations";
4
- import Link from "next/link";
4
+ import { describePersistenceMode } from "@/lib/workspace-config";
5
+ import WorkspaceBuilder from "./workspace-builder.jsx";
5
6
 
6
7
  function Home() {
7
8
  const adapterConfig = readAdapterConfig();
8
9
  const integrationAdapter = describeIntegrationAdapter();
9
- const canvas = workspaceConfig.canvas;
10
- const dashboards = workspaceConfig.dashboards;
11
- const widgetTypes = workspaceConfig.widgetTypes;
12
-
13
- return <main className="workspace-builder">
14
- <aside className="workspace-rail" aria-label="Workspace navigation">
15
- <div className="workspace-brand">
16
- <span className="workspace-mark">G</span>
17
- <span>Growthub Workspace</span>
18
- </div>
19
- <nav className="workspace-nav">
20
- <a className="active" href="#dashboards">Dashboards</a>
21
- <a href="#canvas">Canvas</a>
22
- <a href="#widgets">Widgets</a>
23
- <a href="#bindings">Bindings</a>
24
- <Link href="/settings/integrations">Integrations</Link>
25
- </nav>
26
- <div className="workspace-rail-status">
27
- <span className="status-dot" />
28
- {integrationAdapter.authority}
29
- </div>
30
- </aside>
31
-
32
- <section className="workspace-surface">
33
- <header className="workspace-toolbar">
34
- <div>
35
- <p>Official starter</p>
36
- <h1>{workspaceConfig.name}</h1>
37
- </div>
38
- <div className="workspace-toolbar-actions">
39
- <button type="button">New Dashboard</button>
40
- <button type="button">Save</button>
41
- </div>
42
- </header>
43
-
44
- <section className="workspace-table" id="dashboards" aria-label="Dashboards">
45
- <div className="workspace-table-heading">
46
- <strong>Dashboards</strong>
47
- <span>{dashboards.length} template</span>
48
- </div>
49
- <div className="workspace-table-row workspace-table-head">
50
- <span>Title</span>
51
- <span>Created by</span>
52
- <span>Last update</span>
53
- <span>Status</span>
54
- </div>
55
- {dashboards.map((dashboard) => <div className="workspace-table-row" key={dashboard.id}>
56
- <span>{dashboard.name}</span>
57
- <span>{dashboard.createdBy}</span>
58
- <span>{dashboard.updatedAt}</span>
59
- <code>{dashboard.status}</code>
60
- </div>)}
61
- </section>
62
-
63
- <section className="workspace-canvas" id="canvas" aria-label="Composable dashboard canvas">
64
- <div className="workspace-tabs">
65
- <button className="active" type="button">{canvas.name}</button>
66
- <button type="button">New Tab</button>
67
- </div>
68
- <div className="workspace-grid" style={{ "--workspace-columns": canvas.layout.columns }}>
69
- <button className="workspace-add-widget" type="button">
70
- <span className="workspace-widget-icon" aria-hidden="true"><span /></span>
71
- <strong>Add widget</strong>
72
- <small>Click to add your first widget</small>
73
- </button>
74
- {Array.from({ length: 96 }).map((_, index) => <span aria-hidden="true" className="workspace-grid-cell" key={index} />)}
75
- {canvas.widgets.map((widget) => <article className="workspace-widget-preview" key={widget.id} style={{
76
- gridColumn: `${widget.position.x + 1} / span ${widget.position.w}`,
77
- gridRow: `${widget.position.y + 1} / span ${widget.position.h}`
78
- }}>
79
- <span>{widget.kind}</span>
80
- <strong>{widget.title}</strong>
81
- </article>)}
82
- </div>
83
- </section>
84
- </section>
85
-
86
- <aside className="workspace-widget-panel" id="widgets" aria-label="Widget configuration">
87
- <div className="workspace-panel-title">
88
- <button type="button" aria-label="Close widget panel">x</button>
89
- <strong>New widget</strong>
90
- </div>
91
- <section>
92
- <p className="workspace-panel-label">Widget type</p>
93
- <div className="workspace-widget-types">
94
- {widgetTypes.map((widget) => <button type="button" key={widget.kind}>
95
- <span>{widget.icon}</span>
96
- {widget.label}
97
- </button>)}
98
- </div>
99
- </section>
100
- <section className="workspace-bindings" id="bindings">
101
- <p className="workspace-panel-label">Config bindings</p>
102
- {Object.entries(canvas.bindings).map(([key, value]) => <div key={key}>
103
- <span>{key}</span>
104
- <code>{String(value)}</code>
105
- </div>)}
106
- <div>
107
- <span>integrationAdapter</span>
108
- <code>{adapterConfig.integrationAdapter}</code>
109
- </div>
110
- </section>
111
- </aside>
112
- </main>;
10
+ const persistence = describePersistenceMode();
11
+ return (
12
+ <WorkspaceBuilder
13
+ initialConfig={workspaceConfig}
14
+ adapterConfig={adapterConfig}
15
+ integrationAdapter={integrationAdapter}
16
+ persistence={persistence}
17
+ />
18
+ );
113
19
  }
114
20
 
115
21
  export {