@growthub/cli 0.9.10 → 0.9.12
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/README.md +1 -1
- package/assets/worker-kits/creative-strategist-v1/kit.json +5 -2
- package/assets/worker-kits/growthub-agency-portal-starter-v1/kit.json +4 -1
- package/assets/worker-kits/growthub-ai-website-cloner-v1/kit.json +6 -3
- package/assets/worker-kits/growthub-creative-video-pipeline-v1/kit.json +4 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/README.md +4 -4
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/integration-entities/route.js +50 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/route.js +1 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/page.jsx +389 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +362 -15
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/page.jsx +5 -2
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/integrations/page.jsx +5 -5
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-builder.jsx +625 -56
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/growthub-connection-normalizer.js +12 -16
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/index.js +61 -11
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/domain/integrations.js +31 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-config.js +3 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-data-model.js +433 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-schema.js +112 -14
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/kit.json +5 -2
- package/assets/worker-kits/growthub-email-marketing-v1/kit.json +5 -2
- package/assets/worker-kits/growthub-geo-seo-v1/kit.json +5 -2
- package/assets/worker-kits/growthub-hyperframes-studio-v1/kit.json +5 -2
- package/assets/worker-kits/growthub-marketing-skills-v1/kit.json +6 -3
- package/assets/worker-kits/growthub-open-higgsfield-studio-v1/kit.json +5 -2
- package/assets/worker-kits/growthub-open-montage-studio-v1/kit.json +6 -3
- package/assets/worker-kits/growthub-postiz-social-v1/kit.json +5 -2
- package/assets/worker-kits/growthub-twenty-crm-v1/kit.json +6 -3
- package/assets/worker-kits/growthub-video-use-studio-v1/kit.json +5 -2
- package/assets/worker-kits/growthub-zernio-social-v1/kit.json +5 -2
- package/dist/index.js +1750 -433
- package/package.json +1 -1
|
@@ -5,6 +5,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
|
5
5
|
import {
|
|
6
6
|
BarChart3,
|
|
7
7
|
Bolt,
|
|
8
|
+
Check,
|
|
8
9
|
ChevronDown,
|
|
9
10
|
Code2,
|
|
10
11
|
Columns3,
|
|
@@ -33,7 +34,6 @@ import {
|
|
|
33
34
|
Settings,
|
|
34
35
|
Sigma,
|
|
35
36
|
SlidersHorizontal,
|
|
36
|
-
Sparkles,
|
|
37
37
|
Table2,
|
|
38
38
|
Trash2,
|
|
39
39
|
Type,
|
|
@@ -57,12 +57,33 @@ import {
|
|
|
57
57
|
wrapWorkspaceTemplateExport
|
|
58
58
|
} from "@/lib/workspace-schema";
|
|
59
59
|
import { governedWorkspaceIntegrationCatalog } from "@/lib/domain/integrations";
|
|
60
|
+
import { listWorkspaceDataModelTables } from "@/lib/workspace-data-model";
|
|
60
61
|
|
|
61
62
|
const DEFAULT_CHART_TYPE = "bar-vertical";
|
|
62
63
|
const DEFAULT_FILTER_OP = "and";
|
|
63
64
|
const DEFAULT_FILTER_OPERATOR = "contains";
|
|
64
65
|
const DEFAULT_SORT_DIRECTION = "asc";
|
|
65
66
|
const SUB_PANEL_ROOT = "root";
|
|
67
|
+
const MANAGED_INTEGRATION_SOURCE_TYPE = "managed-integrations";
|
|
68
|
+
const CUSTOM_API_SOURCE_TYPE = "custom-api-webhooks";
|
|
69
|
+
const DATA_MODEL_SOURCE_TYPE = "workspace-data-model";
|
|
70
|
+
|
|
71
|
+
const SOURCE_TYPE_OBJECTS = [
|
|
72
|
+
{
|
|
73
|
+
id: MANAGED_INTEGRATION_SOURCE_TYPE,
|
|
74
|
+
label: "Managed Integrations",
|
|
75
|
+
authority: "Growthub Bridge",
|
|
76
|
+
description: "Bridge or BYO adapters resolve metadata server-side."
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
id: CUSTOM_API_SOURCE_TYPE,
|
|
80
|
+
label: "Custom APIs/Webhooks",
|
|
81
|
+
authority: "Custom endpoint",
|
|
82
|
+
description: "Reference a governed endpoint object without storing credentials in widget config."
|
|
83
|
+
}
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
const ENTITY_REFERENCE_FIELD_IDS = ["id", "entityId"];
|
|
66
87
|
|
|
67
88
|
const CHART_TYPE_LABELS = {
|
|
68
89
|
"bar-vertical": "Vertical Bar",
|
|
@@ -128,7 +149,7 @@ function generateId(prefix) {
|
|
|
128
149
|
function defaultTitleFor(kind) {
|
|
129
150
|
switch (kind) {
|
|
130
151
|
case "chart": return "Untitled chart";
|
|
131
|
-
case "view": return "
|
|
152
|
+
case "view": return "Untitled view";
|
|
132
153
|
case "iframe": return "Untitled iFrame";
|
|
133
154
|
case "rich-text": return "Untitled Rich Text";
|
|
134
155
|
default: return "Untitled widget";
|
|
@@ -622,11 +643,51 @@ function getChartStyle(widget) {
|
|
|
622
643
|
|
|
623
644
|
function summarizeSource(widget) {
|
|
624
645
|
const binding = widget?.config?.binding;
|
|
625
|
-
if (binding?.
|
|
646
|
+
if (binding?.sourceType === DATA_MODEL_SOURCE_TYPE) return binding.source || widget?.config?.source || "Data Model object";
|
|
647
|
+
if (binding?.mode === "integration") {
|
|
648
|
+
const source = binding.source || "Integration";
|
|
649
|
+
if (binding.entityLabel) return `${source} · ${binding.entityLabel}`;
|
|
650
|
+
if (binding.entityId) return `${source} · ${binding.entityId}`;
|
|
651
|
+
return source;
|
|
652
|
+
}
|
|
626
653
|
if (widget?.config?.source) return widget.config.source;
|
|
627
654
|
return "Static";
|
|
628
655
|
}
|
|
629
656
|
|
|
657
|
+
function summarizeSourceType(binding) {
|
|
658
|
+
if (binding?.sourceType === DATA_MODEL_SOURCE_TYPE) return "Data Model";
|
|
659
|
+
if (binding?.sourceType === CUSTOM_API_SOURCE_TYPE) return "Custom APIs/Webhooks";
|
|
660
|
+
if (binding?.mode === "integration" || binding?.sourceType === MANAGED_INTEGRATION_SOURCE_TYPE) return "Managed Integrations";
|
|
661
|
+
return "Static data";
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function resolveBindingSourceType(binding) {
|
|
665
|
+
if (binding?.sourceType) return binding.sourceType;
|
|
666
|
+
if (binding?.mode === "integration") return MANAGED_INTEGRATION_SOURCE_TYPE;
|
|
667
|
+
return "static";
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
function resolveDataModelTable(dataModelTables, binding) {
|
|
671
|
+
if (binding?.sourceType !== DATA_MODEL_SOURCE_TYPE) return null;
|
|
672
|
+
const tables = Array.isArray(dataModelTables) ? dataModelTables : [];
|
|
673
|
+
return tables.find((table) => table.objectId === binding.objectId || table.id === binding.objectId || table.source === binding.source) || null;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function resolveViewWidget(widget, dataModelTables) {
|
|
677
|
+
if (widget?.kind !== "view") return widget;
|
|
678
|
+
const table = resolveDataModelTable(dataModelTables, widget.config?.binding);
|
|
679
|
+
if (!table) return widget;
|
|
680
|
+
return {
|
|
681
|
+
...widget,
|
|
682
|
+
config: {
|
|
683
|
+
...(widget.config || {}),
|
|
684
|
+
source: table.source,
|
|
685
|
+
columns: table.columns,
|
|
686
|
+
rows: table.rows
|
|
687
|
+
}
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
|
|
630
691
|
function summarizeFields(widget) {
|
|
631
692
|
const total = getColumnList(widget).length;
|
|
632
693
|
const hidden = getHiddenColumnSet(widget).size;
|
|
@@ -655,6 +716,142 @@ function describeIntegrationLane(integration) {
|
|
|
655
716
|
return integration?.lane === "data-source" ? "Data Sources" : "Workspace Tools";
|
|
656
717
|
}
|
|
657
718
|
|
|
719
|
+
function flattenIntegrationSettings(integrationSettings) {
|
|
720
|
+
const grouped = integrationSettings?.integrations || integrationSettings || {};
|
|
721
|
+
const runtime = [
|
|
722
|
+
...(Array.isArray(grouped.dataSources) ? grouped.dataSources : []),
|
|
723
|
+
...(Array.isArray(grouped.workspaceIntegrations) ? grouped.workspaceIntegrations : [])
|
|
724
|
+
];
|
|
725
|
+
const byId = new Map();
|
|
726
|
+
for (const item of [...governedWorkspaceIntegrationCatalog, ...runtime]) {
|
|
727
|
+
if (item?.id) byId.set(item.id, { ...byId.get(item.id), ...item });
|
|
728
|
+
}
|
|
729
|
+
return Array.from(byId.values());
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
function getFilterFieldOptions(widget, entities = []) {
|
|
733
|
+
const fields = new Set(getColumnList(widget));
|
|
734
|
+
const binding = widget?.config?.binding || {};
|
|
735
|
+
if (binding.mode === "integration") {
|
|
736
|
+
["id", "label", "secondaryLabel", "entityType", "provider", "lane", "status"].forEach((field) => fields.add(field));
|
|
737
|
+
fields.add("provider");
|
|
738
|
+
fields.add("lane");
|
|
739
|
+
for (const entity of entities) {
|
|
740
|
+
if (entity?.metadata && typeof entity.metadata === "object") {
|
|
741
|
+
Object.keys(entity.metadata).forEach((field) => fields.add(field));
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
if (binding.sourceType === CUSTOM_API_SOURCE_TYPE) {
|
|
746
|
+
const customFields = Array.isArray(binding.fields) ? binding.fields : ["entityId", "status", "createdAt"];
|
|
747
|
+
customFields.forEach((field) => fields.add(field));
|
|
748
|
+
}
|
|
749
|
+
return Array.from(fields).filter(Boolean);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
function getEntityFieldValue(entity, fieldId) {
|
|
753
|
+
if (!entity || !fieldId) return "";
|
|
754
|
+
if (fieldId === "id" || fieldId === "entityId") return entity.id || "";
|
|
755
|
+
if (fieldId === "label" || fieldId === "name") return entity.label || "";
|
|
756
|
+
if (fieldId === "secondaryLabel") return entity.secondaryLabel || "";
|
|
757
|
+
if (fieldId === "entityType") return entity.entityType || "";
|
|
758
|
+
if (fieldId === "provider") return entity.provider || "";
|
|
759
|
+
if (fieldId === "lane") return entity.lane || "";
|
|
760
|
+
if (fieldId === "status") return entity.status || "";
|
|
761
|
+
if (entity.metadata && typeof entity.metadata === "object" && entity.metadata[fieldId] !== undefined) {
|
|
762
|
+
return String(entity.metadata[fieldId]);
|
|
763
|
+
}
|
|
764
|
+
return "";
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
function getEntityFieldChoices(entities) {
|
|
768
|
+
const fields = new Map();
|
|
769
|
+
const add = (id, label) => {
|
|
770
|
+
if (id && !fields.has(id)) fields.set(id, { id, label });
|
|
771
|
+
};
|
|
772
|
+
add("id", "Stable ID");
|
|
773
|
+
add("label", "Primary label");
|
|
774
|
+
add("secondaryLabel", "Secondary label");
|
|
775
|
+
add("entityType", "Entity type");
|
|
776
|
+
add("provider", "Provider");
|
|
777
|
+
add("lane", "Lane");
|
|
778
|
+
add("status", "Status");
|
|
779
|
+
for (const entity of entities) {
|
|
780
|
+
if (entity?.metadata && typeof entity.metadata === "object") {
|
|
781
|
+
Object.keys(entity.metadata).forEach((field) => add(field, field));
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
return Array.from(fields.values());
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
function getFilterFieldChoices(widget, entities = []) {
|
|
788
|
+
const binding = widget?.config?.binding || {};
|
|
789
|
+
if (binding.mode === "integration" && entities.length) return getEntityFieldChoices(entities);
|
|
790
|
+
return getFilterFieldOptions(widget, entities).map((id) => ({ id, label: id }));
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
function getEntityValueChoices(entities, fieldId) {
|
|
794
|
+
const seen = new Map();
|
|
795
|
+
for (const entity of entities) {
|
|
796
|
+
const value = getEntityFieldValue(entity, fieldId);
|
|
797
|
+
if (!value || seen.has(value)) continue;
|
|
798
|
+
const label = fieldId === "id" || fieldId === "entityId"
|
|
799
|
+
? `${entity.label || value} · ${value}`
|
|
800
|
+
: value;
|
|
801
|
+
seen.set(value, { value, label, entity });
|
|
802
|
+
}
|
|
803
|
+
return Array.from(seen.values());
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
function findEntityByFieldValue(entities, fieldId, value) {
|
|
807
|
+
if (!value) return null;
|
|
808
|
+
return entities.find((entity) => getEntityFieldValue(entity, fieldId) === value) || null;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
function updateWidgetEntityBinding(widget, entity) {
|
|
812
|
+
const binding = widget.config?.binding || {};
|
|
813
|
+
const existingFilter = widget.config?.filter;
|
|
814
|
+
const existingClauses = Array.isArray(existingFilter?.clauses) ? existingFilter.clauses : [];
|
|
815
|
+
|
|
816
|
+
if (!entity) {
|
|
817
|
+
const { entityId, entityType, entityLabel, ...restBinding } = binding;
|
|
818
|
+
const cleanedClauses = existingClauses.filter(
|
|
819
|
+
(clause) => !(ENTITY_REFERENCE_FIELD_IDS.includes(clause.fieldId) && clause.operator === "eq")
|
|
820
|
+
);
|
|
821
|
+
return {
|
|
822
|
+
...widget.config,
|
|
823
|
+
binding: restBinding,
|
|
824
|
+
filter: { op: existingFilter?.op || DEFAULT_FILTER_OP, clauses: cleanedClauses }
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
const entityClause = { fieldId: "id", operator: "eq", value: entity.id };
|
|
829
|
+
const otherClauses = existingClauses.filter(
|
|
830
|
+
(clause) => !(ENTITY_REFERENCE_FIELD_IDS.includes(clause.fieldId) && clause.operator === "eq")
|
|
831
|
+
);
|
|
832
|
+
const nextBinding = {
|
|
833
|
+
...binding,
|
|
834
|
+
entityId: entity.id,
|
|
835
|
+
entityLabel: entity.label
|
|
836
|
+
};
|
|
837
|
+
if (entity.entityType) nextBinding.entityType = entity.entityType;
|
|
838
|
+
else delete nextBinding.entityType;
|
|
839
|
+
return {
|
|
840
|
+
...widget.config,
|
|
841
|
+
source: binding.source || entity.label,
|
|
842
|
+
binding: nextBinding,
|
|
843
|
+
filter: { op: existingFilter?.op || DEFAULT_FILTER_OP, clauses: [entityClause, ...otherClauses] }
|
|
844
|
+
};
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
function resolveChartColor(style, branding) {
|
|
848
|
+
if (style?.colors === "manual" && style.manualColor) return style.manualColor;
|
|
849
|
+
if (style?.colors === "brand-local") return branding?.accent || "#3f68ff";
|
|
850
|
+
if (style?.colors === "brand-bridge") return branding?.bridgeAccent || branding?.accent || "#3f68ff";
|
|
851
|
+
if (style?.colors === "accent") return "#38bdf8";
|
|
852
|
+
return null;
|
|
853
|
+
}
|
|
854
|
+
|
|
658
855
|
const NORMALIZED_TEMPLATES = DASHBOARD_TEMPLATES.map((template) => ({
|
|
659
856
|
...normalizeWorkspaceTemplate(template),
|
|
660
857
|
widgets: template.widgets
|
|
@@ -809,11 +1006,111 @@ function SubPanelHeader({ title, breadcrumb, onBack }) {
|
|
|
809
1006
|
</div>;
|
|
810
1007
|
}
|
|
811
1008
|
|
|
812
|
-
|
|
1009
|
+
/**
|
|
1010
|
+
* EntityBadge — chip showing the selected entity on the source panel and root inspector.
|
|
1011
|
+
* Displays primary label + muted secondary label (stable ID). onClear is optional.
|
|
1012
|
+
*/
|
|
1013
|
+
function EntityBadge({ entity, onClear }) {
|
|
1014
|
+
const initials = entity.entityType
|
|
1015
|
+
? entity.entityType[0].toUpperCase()
|
|
1016
|
+
: (entity.label?.[0] || "•").toUpperCase();
|
|
1017
|
+
return <div className="workspace-entity-badge">
|
|
1018
|
+
<span className="workspace-entity-badge-icon" aria-hidden="true">{initials}</span>
|
|
1019
|
+
<span className="workspace-entity-badge-meta">
|
|
1020
|
+
<strong title={entity.label}>{entity.label}</strong>
|
|
1021
|
+
{entity.secondaryLabel ? <em title={entity.secondaryLabel}>{entity.secondaryLabel}</em> : null}
|
|
1022
|
+
</span>
|
|
1023
|
+
{onClear ? <button
|
|
1024
|
+
type="button"
|
|
1025
|
+
className="workspace-entity-badge-clear"
|
|
1026
|
+
aria-label={`Clear selected entity ${entity.label}`}
|
|
1027
|
+
onClick={onClear}
|
|
1028
|
+
>
|
|
1029
|
+
<X size={11} />
|
|
1030
|
+
</button> : null}
|
|
1031
|
+
</div>;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
function UniversalSourceInfoCard() {
|
|
1035
|
+
return <p className="workspace-source-info-card">
|
|
1036
|
+
Universal source objects support managed integrations and custom APIs/webhooks through normalized metadata and stable saved references.
|
|
1037
|
+
</p>;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
/**
|
|
1041
|
+
* EntitySelector — compact dropdown for picking a normalized source object after
|
|
1042
|
+
* an integration is selected from the SourceSubPanel.
|
|
1043
|
+
*
|
|
1044
|
+
* Governed invariant: only the object `id` is persisted. The `label` is
|
|
1045
|
+
* display-only and may be refreshed from adapter metadata at any time.
|
|
1046
|
+
* The browser never holds source credentials or executes source queries.
|
|
1047
|
+
*/
|
|
1048
|
+
function EntitySelector({ integration, entities, selectedEntityId, selectedEntityLabel, selectedEntityType, onSelect, loading }) {
|
|
1049
|
+
const selected = entities.find((e) => e.id === selectedEntityId)
|
|
1050
|
+
|| (selectedEntityId ? {
|
|
1051
|
+
id: selectedEntityId,
|
|
1052
|
+
label: selectedEntityLabel || selectedEntityId,
|
|
1053
|
+
secondaryLabel: selectedEntityId,
|
|
1054
|
+
entityType: selectedEntityType
|
|
1055
|
+
} : null);
|
|
1056
|
+
|
|
1057
|
+
const clearSelected = () => {
|
|
1058
|
+
if (!selectedEntityId || window.confirm("Remove the selected source object from this widget?")) {
|
|
1059
|
+
onSelect(null);
|
|
1060
|
+
}
|
|
1061
|
+
};
|
|
1062
|
+
|
|
1063
|
+
return <div className="workspace-entity-selector">
|
|
1064
|
+
<p className="workspace-panel-label">Source object</p>
|
|
1065
|
+
{selected ? <EntityBadge entity={selected} onClear={clearSelected} /> : null}
|
|
1066
|
+
{loading ? <p className="workspace-entity-empty">Loading source objects…</p> : null}
|
|
1067
|
+
{!loading && !entities.length ? <p className="workspace-entity-empty">
|
|
1068
|
+
No source objects returned. Configure a server-side API/webhook object resolver for this integration.
|
|
1069
|
+
</p> : null}
|
|
1070
|
+
{!loading && entities.length ? <label className="workspace-entity-dropdown">
|
|
1071
|
+
<span>Select source object</span>
|
|
1072
|
+
<select
|
|
1073
|
+
aria-label="Select source object"
|
|
1074
|
+
value={selectedEntityId || ""}
|
|
1075
|
+
onChange={(event) => {
|
|
1076
|
+
const entity = entities.find((item) => item.id === event.target.value);
|
|
1077
|
+
onSelect(entity || null);
|
|
1078
|
+
}}
|
|
1079
|
+
>
|
|
1080
|
+
<option value="">Choose an object</option>
|
|
1081
|
+
{entities.map((entity) => <option key={entity.id} value={entity.id}>
|
|
1082
|
+
{entity.label}{entity.secondaryLabel ? ` · ${entity.secondaryLabel}` : ""}
|
|
1083
|
+
</option>)}
|
|
1084
|
+
</select>
|
|
1085
|
+
</label> : null}
|
|
1086
|
+
</div>;
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
function SourceSubPanel({ widget, integrations, dataModelTables, onChange, onBack }) {
|
|
813
1090
|
const binding = widget.config?.binding || {};
|
|
814
1091
|
const currentMode = binding.mode || (widget.kind === "view" ? "manual" : "json");
|
|
815
|
-
|
|
816
|
-
|
|
1092
|
+
const activeSourceType = resolveBindingSourceType(binding);
|
|
1093
|
+
const [query, setQuery] = useState("");
|
|
1094
|
+
const [laneFilter, setLaneFilter] = useState("all");
|
|
1095
|
+
const hasConnectedSource = Boolean(
|
|
1096
|
+
binding.integrationId ||
|
|
1097
|
+
binding.endpointRef ||
|
|
1098
|
+
binding.sourceType === DATA_MODEL_SOURCE_TYPE ||
|
|
1099
|
+
binding.sourceType === MANAGED_INTEGRATION_SOURCE_TYPE ||
|
|
1100
|
+
binding.sourceType === CUSTOM_API_SOURCE_TYPE
|
|
1101
|
+
);
|
|
1102
|
+
const confirmSourceChange = useCallback((nextLabel) => {
|
|
1103
|
+
if (!hasConnectedSource) return true;
|
|
1104
|
+
const currentLabel = summarizeSource(widget);
|
|
1105
|
+
return window.confirm(`Change source from ${currentLabel} to ${nextLabel}? This updates the widget binding and can clear source-object filters.`);
|
|
1106
|
+
}, [hasConnectedSource, widget]);
|
|
1107
|
+
|
|
1108
|
+
const activeIntegration = useMemo(() => {
|
|
1109
|
+
if (currentMode !== "integration" || !binding.integrationId) return null;
|
|
1110
|
+
const list = Array.isArray(integrations) ? integrations : [];
|
|
1111
|
+
return list.find((item) => item.id === binding.integrationId) || null;
|
|
1112
|
+
}, [currentMode, binding.integrationId, integrations]);
|
|
1113
|
+
|
|
817
1114
|
const groups = useMemo(() => {
|
|
818
1115
|
const list = Array.isArray(integrations) ? integrations : [];
|
|
819
1116
|
const filtered = list.filter((item) => {
|
|
@@ -826,37 +1123,146 @@ function SourceSubPanel({ widget, integrations, onChange, onBack }) {
|
|
|
826
1123
|
"workspace-integration": filtered.filter((item) => item.lane === "workspace-integration")
|
|
827
1124
|
};
|
|
828
1125
|
}, [integrations, laneFilter, query]);
|
|
829
|
-
|
|
830
|
-
|
|
1126
|
+
|
|
1127
|
+
const availableDataObjects = useMemo(() => {
|
|
1128
|
+
const list = Array.isArray(dataModelTables) ? dataModelTables : [];
|
|
1129
|
+
const trimmed = query.trim().toLowerCase();
|
|
1130
|
+
return list.filter((table) => {
|
|
1131
|
+
if (table.storage !== "manual-object") return false;
|
|
1132
|
+
if (!trimmed) return true;
|
|
1133
|
+
return `${table.label} ${table.source}`.toLowerCase().includes(trimmed);
|
|
1134
|
+
});
|
|
1135
|
+
}, [dataModelTables, query]);
|
|
1136
|
+
|
|
1137
|
+
const selectStatic = useCallback(() => {
|
|
1138
|
+
if (!confirmSourceChange("Static rows")) return;
|
|
1139
|
+
if (widget.kind === "chart") {
|
|
831
1140
|
onChange({ ...widget.config, binding: SAMPLE_DATA_BINDINGS.reportingJson });
|
|
832
1141
|
} else {
|
|
833
1142
|
onChange({
|
|
834
1143
|
...widget.config,
|
|
835
|
-
source: widget.config?.source || "
|
|
836
|
-
binding:
|
|
1144
|
+
source: widget.config?.source || "Static rows",
|
|
1145
|
+
binding: { mode: "manual", source: "Static rows", rows: Array.isArray(widget.config?.rows) ? widget.config.rows : [] }
|
|
837
1146
|
});
|
|
838
1147
|
}
|
|
839
|
-
|
|
840
|
-
|
|
1148
|
+
}, [confirmSourceChange, onChange, widget.config, widget.kind]);
|
|
1149
|
+
|
|
1150
|
+
const selectDataModelObject = useCallback((table) => {
|
|
1151
|
+
if (!table || !confirmSourceChange(table.label)) return;
|
|
841
1152
|
onChange({
|
|
842
1153
|
...widget.config,
|
|
1154
|
+
source: table.source,
|
|
1155
|
+
columns: table.columns,
|
|
1156
|
+
rows: [],
|
|
1157
|
+
binding: {
|
|
1158
|
+
mode: "manual",
|
|
1159
|
+
source: table.source,
|
|
1160
|
+
sourceType: DATA_MODEL_SOURCE_TYPE,
|
|
1161
|
+
sourceAuthority: "workspace-config",
|
|
1162
|
+
objectId: table.objectId,
|
|
1163
|
+
rows: []
|
|
1164
|
+
},
|
|
1165
|
+
fieldSettings: {
|
|
1166
|
+
hidden: [],
|
|
1167
|
+
order: table.columns
|
|
1168
|
+
}
|
|
1169
|
+
});
|
|
1170
|
+
}, [confirmSourceChange, onChange, widget.config]);
|
|
1171
|
+
|
|
1172
|
+
const selectCustomApi = useCallback(() => {
|
|
1173
|
+
if (!confirmSourceChange("Custom APIs/Webhooks")) return;
|
|
1174
|
+
onChange({
|
|
1175
|
+
...widget.config,
|
|
1176
|
+
source: "Custom APIs/Webhooks",
|
|
1177
|
+
binding: {
|
|
1178
|
+
...binding,
|
|
1179
|
+
mode: "json",
|
|
1180
|
+
source: "Custom APIs/Webhooks",
|
|
1181
|
+
sourceType: CUSTOM_API_SOURCE_TYPE,
|
|
1182
|
+
sourceAuthority: "custom-api",
|
|
1183
|
+
endpointRef: binding.endpointRef || "",
|
|
1184
|
+
fields: Array.isArray(binding.fields) ? binding.fields : ["entityId", "status", "createdAt"]
|
|
1185
|
+
}
|
|
1186
|
+
});
|
|
1187
|
+
}, [binding, confirmSourceChange, onChange, widget.config]);
|
|
1188
|
+
|
|
1189
|
+
const updateCustomFields = useCallback((value) => {
|
|
1190
|
+
const fields = value.split(",").map((item) => item.trim()).filter(Boolean);
|
|
1191
|
+
onChange({
|
|
1192
|
+
...widget.config,
|
|
1193
|
+
binding: {
|
|
1194
|
+
...binding,
|
|
1195
|
+
mode: "json",
|
|
1196
|
+
source: "Custom APIs/Webhooks",
|
|
1197
|
+
sourceType: CUSTOM_API_SOURCE_TYPE,
|
|
1198
|
+
sourceAuthority: "custom-api",
|
|
1199
|
+
fields
|
|
1200
|
+
}
|
|
1201
|
+
});
|
|
1202
|
+
}, [binding, onChange, widget.config]);
|
|
1203
|
+
|
|
1204
|
+
const updateEndpointRef = useCallback((value) => {
|
|
1205
|
+
onChange({
|
|
1206
|
+
...widget.config,
|
|
1207
|
+
binding: {
|
|
1208
|
+
...binding,
|
|
1209
|
+
mode: "json",
|
|
1210
|
+
source: "Custom APIs/Webhooks",
|
|
1211
|
+
sourceType: CUSTOM_API_SOURCE_TYPE,
|
|
1212
|
+
sourceAuthority: "custom-api",
|
|
1213
|
+
endpointRef: value
|
|
1214
|
+
}
|
|
1215
|
+
});
|
|
1216
|
+
}, [binding, onChange, widget.config]);
|
|
1217
|
+
|
|
1218
|
+
const selectIntegration = useCallback((integration) => {
|
|
1219
|
+
if (binding.integrationId && binding.integrationId !== integration.id && !confirmSourceChange(integration.label)) return;
|
|
1220
|
+
onChange({
|
|
1221
|
+
...widget.config,
|
|
843
1222
|
source: integration.label,
|
|
844
1223
|
binding: {
|
|
845
1224
|
mode: "integration",
|
|
846
1225
|
source: integration.label,
|
|
1226
|
+
sourceType: MANAGED_INTEGRATION_SOURCE_TYPE,
|
|
1227
|
+
sourceAuthority: "growthub-bridge",
|
|
847
1228
|
integrationId: integration.id,
|
|
848
|
-
lane: integration.lane
|
|
1229
|
+
lane: integration.lane,
|
|
1230
|
+
provider: integration.provider
|
|
849
1231
|
}
|
|
850
1232
|
});
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
1233
|
+
}, [binding.integrationId, confirmSourceChange, onChange, widget.config]);
|
|
1234
|
+
|
|
1235
|
+
return <section className="workspace-widget-subpanel">
|
|
1236
|
+
<SubPanelHeader title="Source" breadcrumb={widget.title} onBack={onBack} />
|
|
1237
|
+
<UniversalSourceInfoCard />
|
|
1238
|
+
<p className="workspace-panel-label">Source type</p>
|
|
1239
|
+
<div className="workspace-source-object-list">
|
|
1240
|
+
{SOURCE_TYPE_OBJECTS.map((sourceType) => {
|
|
1241
|
+
const isActive = activeSourceType === sourceType.id;
|
|
1242
|
+
return <button
|
|
1243
|
+
key={sourceType.id}
|
|
1244
|
+
type="button"
|
|
1245
|
+
className={`workspace-source-object-row${isActive ? " active" : ""}`}
|
|
1246
|
+
onClick={sourceType.id === CUSTOM_API_SOURCE_TYPE ? selectCustomApi : undefined}
|
|
1247
|
+
disabled={sourceType.id === MANAGED_INTEGRATION_SOURCE_TYPE}
|
|
1248
|
+
>
|
|
1249
|
+
<span className="workspace-source-object-icon" aria-hidden="true">
|
|
1250
|
+
{sourceType.id === MANAGED_INTEGRATION_SOURCE_TYPE ? <Database size={15} /> : <LinkIcon size={15} />}
|
|
1251
|
+
</span>
|
|
1252
|
+
<span className="workspace-source-meta">
|
|
1253
|
+
<strong>{sourceType.label}</strong>
|
|
1254
|
+
<em>{sourceType.authority} · {sourceType.description}</em>
|
|
1255
|
+
</span>
|
|
1256
|
+
{isActive ? <span className="workspace-source-tick" aria-hidden="true"><Check size={16} strokeWidth={2.4} /></span> : null}
|
|
1257
|
+
</button>;
|
|
1258
|
+
})}
|
|
1259
|
+
</div>
|
|
854
1260
|
<div className="workspace-source-controls">
|
|
855
1261
|
<label>
|
|
856
1262
|
<Search size={14} aria-hidden="true" />
|
|
857
1263
|
<input
|
|
858
1264
|
aria-label="Search sources"
|
|
859
|
-
placeholder="Search
|
|
1265
|
+
placeholder="Search connectors"
|
|
860
1266
|
value={query}
|
|
861
1267
|
onChange={(event) => setQuery(event.target.value)}
|
|
862
1268
|
/>
|
|
@@ -868,7 +1274,7 @@ function SourceSubPanel({ widget, integrations, onChange, onBack }) {
|
|
|
868
1274
|
value={laneFilter}
|
|
869
1275
|
onChange={(event) => setLaneFilter(event.target.value)}
|
|
870
1276
|
>
|
|
871
|
-
<option value="all">All
|
|
1277
|
+
<option value="all">All connector lanes</option>
|
|
872
1278
|
<option value="data-source">Data sources</option>
|
|
873
1279
|
<option value="workspace-integration">Workspace tools</option>
|
|
874
1280
|
</select>
|
|
@@ -879,17 +1285,38 @@ function SourceSubPanel({ widget, integrations, onChange, onBack }) {
|
|
|
879
1285
|
<div className="workspace-source-list">
|
|
880
1286
|
<button
|
|
881
1287
|
type="button"
|
|
882
|
-
className={`workspace-source-row${
|
|
1288
|
+
className={`workspace-source-row${activeSourceType === "static" ? " active" : ""}`}
|
|
883
1289
|
onClick={selectStatic}
|
|
884
1290
|
>
|
|
885
1291
|
<span className="workspace-source-icon" aria-hidden="true"><Grid2X2 size={15} /></span>
|
|
886
1292
|
<span className="workspace-source-meta">
|
|
887
1293
|
<strong>Static rows</strong>
|
|
888
|
-
<em>Inline
|
|
1294
|
+
<em>Inline JSON, CSV, or manual rows remain supported.</em>
|
|
889
1295
|
</span>
|
|
890
|
-
{
|
|
1296
|
+
{activeSourceType === "static" ? <span className="workspace-source-tick" aria-hidden="true"><Check size={16} strokeWidth={2.4} /></span> : null}
|
|
891
1297
|
</button>
|
|
892
1298
|
</div>
|
|
1299
|
+
{widget.kind === "view" ? <>
|
|
1300
|
+
<p className="workspace-panel-label">Data Model objects</p>
|
|
1301
|
+
<div className="workspace-source-list">
|
|
1302
|
+
{availableDataObjects.length ? availableDataObjects.map((table) => {
|
|
1303
|
+
const isActive = binding.sourceType === DATA_MODEL_SOURCE_TYPE && binding.objectId === table.objectId;
|
|
1304
|
+
return <button
|
|
1305
|
+
key={table.id}
|
|
1306
|
+
type="button"
|
|
1307
|
+
className={`workspace-source-row${isActive ? " active" : ""}`}
|
|
1308
|
+
onClick={() => selectDataModelObject(table)}
|
|
1309
|
+
>
|
|
1310
|
+
<span className="workspace-source-icon" aria-hidden="true"><Database size={15} /></span>
|
|
1311
|
+
<span className="workspace-source-meta">
|
|
1312
|
+
<strong>{table.label}</strong>
|
|
1313
|
+
<em>{table.columns.length} fields · {table.rows.length} records · workspace config</em>
|
|
1314
|
+
</span>
|
|
1315
|
+
{isActive ? <span className="workspace-source-tick" aria-hidden="true"><Check size={16} strokeWidth={2.4} /></span> : null}
|
|
1316
|
+
</button>;
|
|
1317
|
+
}) : <p className="workspace-entity-empty">No manual Data Model objects yet.</p>}
|
|
1318
|
+
</div>
|
|
1319
|
+
</> : null}
|
|
893
1320
|
{Object.entries(groups).map(([lane, items]) => items.length ? <div key={lane}>
|
|
894
1321
|
<p className="workspace-panel-label">{lane === "data-source" ? "Data Sources" : "Workspace Tools"}</p>
|
|
895
1322
|
<div className="workspace-source-list">
|
|
@@ -907,33 +1334,58 @@ function SourceSubPanel({ widget, integrations, onChange, onBack }) {
|
|
|
907
1334
|
<strong>{integration.label}</strong>
|
|
908
1335
|
<em>{describeIntegrationLane(integration)} · {connected ? "connected" : "needs connection"}</em>
|
|
909
1336
|
</span>
|
|
910
|
-
{isActive ? <span className="workspace-source-tick" aria-hidden="true"><
|
|
1337
|
+
{isActive ? <span className="workspace-source-tick" aria-hidden="true"><Check size={16} strokeWidth={2.4} /></span> : null}
|
|
911
1338
|
</button>;
|
|
912
1339
|
})}
|
|
913
1340
|
</div>
|
|
914
1341
|
</div> : null)}
|
|
1342
|
+
{activeSourceType === CUSTOM_API_SOURCE_TYPE ? <div className="workspace-custom-source-config">
|
|
1343
|
+
<label>
|
|
1344
|
+
<span>Endpoint reference</span>
|
|
1345
|
+
<input
|
|
1346
|
+
value={binding.endpointRef || ""}
|
|
1347
|
+
placeholder="api.clients.primary"
|
|
1348
|
+
onChange={(event) => updateEndpointRef(event.target.value)}
|
|
1349
|
+
/>
|
|
1350
|
+
</label>
|
|
1351
|
+
<label>
|
|
1352
|
+
<span>Available fields</span>
|
|
1353
|
+
<input
|
|
1354
|
+
value={(Array.isArray(binding.fields) ? binding.fields : []).join(", ")}
|
|
1355
|
+
placeholder="entityId, status, createdAt"
|
|
1356
|
+
onChange={(event) => updateCustomFields(event.target.value)}
|
|
1357
|
+
/>
|
|
1358
|
+
</label>
|
|
1359
|
+
</div> : null}
|
|
1360
|
+
{currentMode === "integration" && binding.integrationId ? <div className="workspace-active-source-state">
|
|
1361
|
+
<span>Active source</span>
|
|
1362
|
+
<strong>{activeIntegration?.label || binding.source || binding.integrationId}</strong>
|
|
1363
|
+
<code>{binding.integrationId}</code>
|
|
1364
|
+
</div> : null}
|
|
915
1365
|
<p className="workspace-panel-hint">
|
|
916
|
-
Selecting a source writes a binding reference only. The browser
|
|
1366
|
+
Selecting a source writes a binding reference only. The browser only calls local workspace routes and never stores source credentials.
|
|
917
1367
|
</p>
|
|
918
1368
|
</section>;
|
|
919
1369
|
}
|
|
920
1370
|
|
|
921
|
-
function FieldsSubPanel({ widget, onChange, onBack }) {
|
|
922
|
-
const
|
|
923
|
-
const
|
|
1371
|
+
function FieldsSubPanel({ widget, dataModelTable, onChange, onBack }) {
|
|
1372
|
+
const viewWidget = dataModelTable ? resolveViewWidget(widget, [dataModelTable]) : widget;
|
|
1373
|
+
const ordered = getOrderedColumns(viewWidget);
|
|
1374
|
+
const hidden = getHiddenColumnSet(viewWidget);
|
|
924
1375
|
const visible = ordered.filter((name) => !hidden.has(name));
|
|
925
1376
|
const hiddenList = ordered.filter((name) => hidden.has(name));
|
|
926
1377
|
const [hiddenOpen, setHiddenOpen] = useState(true);
|
|
927
1378
|
const [draftField, setDraftField] = useState("");
|
|
928
1379
|
const move = (fieldId, direction) => {
|
|
929
|
-
const next = reorderColumn(
|
|
1380
|
+
const next = reorderColumn(viewWidget, fieldId, direction);
|
|
930
1381
|
onChange({ ...widget.config, fieldSettings: next });
|
|
931
1382
|
};
|
|
932
1383
|
const toggle = (fieldId) => {
|
|
933
|
-
const next = toggleColumnHidden(
|
|
1384
|
+
const next = toggleColumnHidden(viewWidget, fieldId);
|
|
934
1385
|
onChange({ ...widget.config, fieldSettings: next });
|
|
935
1386
|
};
|
|
936
1387
|
const removeColumn = (fieldId) => {
|
|
1388
|
+
if (dataModelTable) return;
|
|
937
1389
|
const nextColumns = ordered.filter((name) => name !== fieldId);
|
|
938
1390
|
const fs = widget.config?.fieldSettings || {};
|
|
939
1391
|
onChange({
|
|
@@ -946,6 +1398,7 @@ function FieldsSubPanel({ widget, onChange, onBack }) {
|
|
|
946
1398
|
});
|
|
947
1399
|
};
|
|
948
1400
|
const addColumn = () => {
|
|
1401
|
+
if (dataModelTable) return;
|
|
949
1402
|
const trimmed = draftField.trim();
|
|
950
1403
|
if (!trimmed || ordered.includes(trimmed)) return;
|
|
951
1404
|
onChange({ ...widget.config, columns: [...ordered, trimmed] });
|
|
@@ -953,6 +1406,7 @@ function FieldsSubPanel({ widget, onChange, onBack }) {
|
|
|
953
1406
|
};
|
|
954
1407
|
return <section className="workspace-widget-subpanel">
|
|
955
1408
|
<SubPanelHeader title="Fields" breadcrumb={widget.title} onBack={onBack} />
|
|
1409
|
+
{dataModelTable ? <p className="workspace-panel-hint">This View is bound to a Data Model object. Field order and visibility are widget-local; add or remove object fields on the Data Model page.</p> : null}
|
|
956
1410
|
<p className="workspace-panel-label">Visible fields</p>
|
|
957
1411
|
<div className="workspace-field-rows">
|
|
958
1412
|
{visible.length === 0 ? <p className="workspace-panel-hint">No visible fields. Add one below or unhide an existing field.</p> : null}
|
|
@@ -964,7 +1418,7 @@ function FieldsSubPanel({ widget, onChange, onBack }) {
|
|
|
964
1418
|
<button type="button" aria-label={`Move ${name} up`} disabled={index === 0} onClick={() => move(name, "up")}>↑</button>
|
|
965
1419
|
<button type="button" aria-label={`Move ${name} down`} disabled={index === visible.length - 1} onClick={() => move(name, "down")}>↓</button>
|
|
966
1420
|
<button type="button" aria-label={`Hide ${name}`} onClick={() => toggle(name)}>👁</button>
|
|
967
|
-
<button type="button" aria-label={`Remove ${name}`} onClick={() => removeColumn(name)}>✕</button>
|
|
1421
|
+
<button type="button" aria-label={`Remove ${name}`} disabled={Boolean(dataModelTable)} onClick={() => removeColumn(name)}>✕</button>
|
|
968
1422
|
</span>
|
|
969
1423
|
</div>)}
|
|
970
1424
|
</div>
|
|
@@ -984,7 +1438,7 @@ function FieldsSubPanel({ widget, onChange, onBack }) {
|
|
|
984
1438
|
<span className="workspace-field-row-name">{name}</span>
|
|
985
1439
|
<span className="workspace-field-row-actions">
|
|
986
1440
|
<button type="button" aria-label={`Show ${name}`} onClick={() => toggle(name)}>👁</button>
|
|
987
|
-
<button type="button" aria-label={`Remove ${name}`} onClick={() => removeColumn(name)}>✕</button>
|
|
1441
|
+
<button type="button" aria-label={`Remove ${name}`} disabled={Boolean(dataModelTable)} onClick={() => removeColumn(name)}>✕</button>
|
|
988
1442
|
</span>
|
|
989
1443
|
</div>)}
|
|
990
1444
|
</div> : null}
|
|
@@ -1001,14 +1455,15 @@ function FieldsSubPanel({ widget, onChange, onBack }) {
|
|
|
1001
1455
|
}
|
|
1002
1456
|
}}
|
|
1003
1457
|
/>
|
|
1004
|
-
<button type="button" onClick={addColumn} disabled={!draftField.trim()}>Add</button>
|
|
1458
|
+
<button type="button" onClick={addColumn} disabled={Boolean(dataModelTable) || !draftField.trim()}>Add</button>
|
|
1005
1459
|
</div>
|
|
1006
1460
|
</section>;
|
|
1007
1461
|
}
|
|
1008
1462
|
|
|
1009
|
-
function SortSubPanel({ widget, onChange, onBack }) {
|
|
1463
|
+
function SortSubPanel({ widget, dataModelTable, onChange, onBack }) {
|
|
1464
|
+
const viewWidget = dataModelTable ? resolveViewWidget(widget, [dataModelTable]) : widget;
|
|
1010
1465
|
const sort = getSortClauses(widget);
|
|
1011
|
-
const columns = getColumnList(
|
|
1466
|
+
const columns = getColumnList(viewWidget);
|
|
1012
1467
|
const updateSort = (next) => onChange({ ...widget.config, sort: next });
|
|
1013
1468
|
const addClause = () => {
|
|
1014
1469
|
const fieldId = columns[0] || "";
|
|
@@ -1052,24 +1507,86 @@ function SortSubPanel({ widget, onChange, onBack }) {
|
|
|
1052
1507
|
</section>;
|
|
1053
1508
|
}
|
|
1054
1509
|
|
|
1055
|
-
function FilterSubPanel({ widget, onChange, onBack }) {
|
|
1510
|
+
function FilterSubPanel({ widget, integrations, dataModelTable, onChange, onBack }) {
|
|
1511
|
+
const viewWidget = dataModelTable ? resolveViewWidget(widget, [dataModelTable]) : widget;
|
|
1512
|
+
const binding = widget.config?.binding || {};
|
|
1056
1513
|
const filter = getFilterConfig(widget);
|
|
1057
|
-
const
|
|
1514
|
+
const [entities, setEntities] = useState([]);
|
|
1515
|
+
const [entitiesLoading, setEntitiesLoading] = useState(false);
|
|
1516
|
+
const fieldChoices = getFilterFieldChoices(viewWidget, entities);
|
|
1517
|
+
const columns = fieldChoices.map((field) => field.id);
|
|
1058
1518
|
const setFilter = (next) => onChange({ ...widget.config, filter: next });
|
|
1059
1519
|
const setOp = (op) => setFilter({ ...filter, op });
|
|
1520
|
+
const activeIntegration = useMemo(() => {
|
|
1521
|
+
if (binding.mode !== "integration" || !binding.integrationId) return null;
|
|
1522
|
+
const list = Array.isArray(integrations) ? integrations : [];
|
|
1523
|
+
return list.find((item) => item.id === binding.integrationId) || null;
|
|
1524
|
+
}, [binding.integrationId, binding.mode, integrations]);
|
|
1525
|
+
|
|
1526
|
+
useEffect(() => {
|
|
1527
|
+
if (!binding.integrationId || binding.mode !== "integration") {
|
|
1528
|
+
setEntities([]);
|
|
1529
|
+
return;
|
|
1530
|
+
}
|
|
1531
|
+
let cancelled = false;
|
|
1532
|
+
setEntitiesLoading(true);
|
|
1533
|
+
fetch(`/api/workspace/integration-entities?integrationId=${encodeURIComponent(binding.integrationId)}`, { cache: "no-store" })
|
|
1534
|
+
.then((res) => res.ok ? res.json() : { entities: [] })
|
|
1535
|
+
.then((data) => {
|
|
1536
|
+
if (!cancelled) {
|
|
1537
|
+
setEntities(Array.isArray(data.entities) ? data.entities : []);
|
|
1538
|
+
setEntitiesLoading(false);
|
|
1539
|
+
}
|
|
1540
|
+
})
|
|
1541
|
+
.catch(() => {
|
|
1542
|
+
if (!cancelled) {
|
|
1543
|
+
setEntities([]);
|
|
1544
|
+
setEntitiesLoading(false);
|
|
1545
|
+
}
|
|
1546
|
+
});
|
|
1547
|
+
return () => { cancelled = true; };
|
|
1548
|
+
}, [binding.integrationId, binding.mode]);
|
|
1549
|
+
|
|
1060
1550
|
const addClause = () => {
|
|
1061
|
-
const fieldId = columns[0] || "";
|
|
1551
|
+
const fieldId = binding.mode === "integration" && entities.length ? "id" : (columns[0] || "");
|
|
1062
1552
|
if (!fieldId) return;
|
|
1063
|
-
setFilter({ ...filter, clauses: [...filter.clauses, { fieldId, operator:
|
|
1553
|
+
setFilter({ ...filter, clauses: [...filter.clauses, { fieldId, operator: "eq", value: "" }] });
|
|
1554
|
+
};
|
|
1555
|
+
const updateField = (index, fieldId) => {
|
|
1556
|
+
setFilter({
|
|
1557
|
+
...filter,
|
|
1558
|
+
clauses: filter.clauses.map((clause, idx) => idx === index ? { ...clause, fieldId, value: "" } : clause)
|
|
1559
|
+
});
|
|
1064
1560
|
};
|
|
1561
|
+
const selectEntity = useCallback((entity) => {
|
|
1562
|
+
onChange(updateWidgetEntityBinding(widget, entity));
|
|
1563
|
+
}, [onChange, widget]);
|
|
1065
1564
|
const updateClause = (index, patch) => {
|
|
1066
1565
|
setFilter({ ...filter, clauses: filter.clauses.map((clause, idx) => idx === index ? { ...clause, ...patch } : clause) });
|
|
1067
1566
|
};
|
|
1068
1567
|
const removeClause = (index) => {
|
|
1069
1568
|
setFilter({ ...filter, clauses: filter.clauses.filter((_, idx) => idx !== index) });
|
|
1070
1569
|
};
|
|
1071
|
-
|
|
1072
|
-
|
|
1570
|
+
return <section className="workspace-widget-subpanel">
|
|
1571
|
+
<SubPanelHeader title="Filter" breadcrumb={widget.title} onBack={onBack} />
|
|
1572
|
+
<UniversalSourceInfoCard />
|
|
1573
|
+
<div className="workspace-filter-source-state">
|
|
1574
|
+
<span>{summarizeSourceType(binding)}</span>
|
|
1575
|
+
<strong>{summarizeSource(widget)}</strong>
|
|
1576
|
+
</div>
|
|
1577
|
+
{binding.mode === "integration" && binding.integrationId ? <EntitySelector
|
|
1578
|
+
integration={activeIntegration}
|
|
1579
|
+
entities={entities}
|
|
1580
|
+
selectedEntityId={binding.entityId || null}
|
|
1581
|
+
selectedEntityLabel={binding.entityLabel || null}
|
|
1582
|
+
selectedEntityType={binding.entityType || null}
|
|
1583
|
+
onSelect={selectEntity}
|
|
1584
|
+
loading={entitiesLoading}
|
|
1585
|
+
/> : null}
|
|
1586
|
+
{binding.sourceType === CUSTOM_API_SOURCE_TYPE ? <div className="workspace-filter-source-state">
|
|
1587
|
+
<span>Custom endpoint</span>
|
|
1588
|
+
<code>{binding.endpointRef || "No endpoint reference set"}</code>
|
|
1589
|
+
</div> : null}
|
|
1073
1590
|
<div className="workspace-filter-op-toggle" role="radiogroup" aria-label="Filter conjunction">
|
|
1074
1591
|
{KNOWN_FILTER_CONJUNCTIONS.map((op) => <button
|
|
1075
1592
|
key={op}
|
|
@@ -1084,14 +1601,15 @@ function FilterSubPanel({ widget, onChange, onBack }) {
|
|
|
1084
1601
|
{filter.clauses.length === 0 ? <p className="workspace-panel-hint">No filter clauses.</p> : null}
|
|
1085
1602
|
{filter.clauses.map((clause, index) => {
|
|
1086
1603
|
const valueless = clause.operator === "isEmpty" || clause.operator === "isNotEmpty";
|
|
1604
|
+
const valueChoices = binding.mode === "integration" ? getEntityValueChoices(entities, clause.fieldId) : [];
|
|
1087
1605
|
return <div key={index} className="workspace-filter-clause">
|
|
1088
1606
|
<select
|
|
1089
1607
|
aria-label={`Filter ${index + 1} field`}
|
|
1090
1608
|
value={clause.fieldId}
|
|
1091
|
-
onChange={(event) =>
|
|
1609
|
+
onChange={(event) => updateField(index, event.target.value)}
|
|
1092
1610
|
>
|
|
1093
1611
|
{!columns.includes(clause.fieldId) && clause.fieldId ? <option value={clause.fieldId}>{clause.fieldId}</option> : null}
|
|
1094
|
-
{
|
|
1612
|
+
{fieldChoices.map((field) => <option key={field.id} value={field.id}>{field.label}</option>)}
|
|
1095
1613
|
</select>
|
|
1096
1614
|
<select
|
|
1097
1615
|
aria-label={`Filter ${index + 1} operator`}
|
|
@@ -1100,7 +1618,20 @@ function FilterSubPanel({ widget, onChange, onBack }) {
|
|
|
1100
1618
|
>
|
|
1101
1619
|
{KNOWN_FILTER_OPERATORS.map((op) => <option key={op} value={op}>{FILTER_OPERATOR_LABELS[op] || op}</option>)}
|
|
1102
1620
|
</select>
|
|
1103
|
-
{!valueless ? <
|
|
1621
|
+
{!valueless && binding.mode === "integration" && valueChoices.length ? <select
|
|
1622
|
+
aria-label={`Filter ${index + 1} value`}
|
|
1623
|
+
value={clause.value ?? ""}
|
|
1624
|
+
onChange={(event) => {
|
|
1625
|
+
const entity = findEntityByFieldValue(entities, clause.fieldId, event.target.value);
|
|
1626
|
+
updateClause(index, { value: event.target.value });
|
|
1627
|
+
if (entity && (clause.fieldId === "id" || clause.fieldId === "entityId")) {
|
|
1628
|
+
onChange(updateWidgetEntityBinding(widget, entity));
|
|
1629
|
+
}
|
|
1630
|
+
}}
|
|
1631
|
+
>
|
|
1632
|
+
<option value="">Select value</option>
|
|
1633
|
+
{valueChoices.map((choice) => <option key={choice.value} value={choice.value}>{choice.label}</option>)}
|
|
1634
|
+
</select> : !valueless ? <input
|
|
1104
1635
|
aria-label={`Filter ${index + 1} value`}
|
|
1105
1636
|
value={clause.value ?? ""}
|
|
1106
1637
|
placeholder="value"
|
|
@@ -1119,11 +1650,12 @@ function FilterSubPanel({ widget, onChange, onBack }) {
|
|
|
1119
1650
|
</section>;
|
|
1120
1651
|
}
|
|
1121
1652
|
|
|
1122
|
-
function ChartConfigPanel({ widget, onChange, onSubPage }) {
|
|
1653
|
+
function ChartConfigPanel({ widget, branding, onChange, onSubPage }) {
|
|
1123
1654
|
const chartType = getChartType(widget) === "line" ? DEFAULT_CHART_TYPE : getChartType(widget);
|
|
1124
1655
|
const xAxis = getChartAxis(widget, "xAxis");
|
|
1125
1656
|
const yAxis = getChartAxis(widget, "yAxis");
|
|
1126
1657
|
const style = getChartStyle(widget);
|
|
1658
|
+
const activeColor = resolveChartColor(style, branding) || "#d9e4ff";
|
|
1127
1659
|
const setChartType = (type) => onChange({ ...widget.config, chartType: type });
|
|
1128
1660
|
const setXAxis = (patch) => onChange({ ...widget.config, xAxis: { ...xAxis, ...patch } });
|
|
1129
1661
|
const setYAxis = (patch) => onChange({ ...widget.config, yAxis: { ...yAxis, ...patch } });
|
|
@@ -1148,11 +1680,17 @@ function ChartConfigPanel({ widget, onChange, onSubPage }) {
|
|
|
1148
1680
|
})}
|
|
1149
1681
|
</div>
|
|
1150
1682
|
<button type="button" className="workspace-settings-row" onClick={() => onSubPage("source")}>
|
|
1151
|
-
<span>Source</span><code>{summarizeSource(widget)}</code>
|
|
1683
|
+
<span>Source</span><code>{summarizeSourceType(widget.config?.binding)} · {summarizeSource(widget)}</code>
|
|
1152
1684
|
</button>
|
|
1153
1685
|
<button type="button" className="workspace-settings-row" onClick={() => onSubPage("filter")}>
|
|
1154
1686
|
<span>Filter</span><code>{summarizeFilter(widget)}</code>
|
|
1155
1687
|
</button>
|
|
1688
|
+
{widget.config?.binding?.entityId ? <EntityBadge entity={{
|
|
1689
|
+
id: widget.config.binding.entityId,
|
|
1690
|
+
label: widget.config.binding.entityLabel || widget.config.binding.entityId,
|
|
1691
|
+
secondaryLabel: widget.config.binding.entityId,
|
|
1692
|
+
entityType: widget.config.binding.entityType
|
|
1693
|
+
}} /> : null}
|
|
1156
1694
|
<p className="workspace-panel-label">X axis</p>
|
|
1157
1695
|
<label>
|
|
1158
1696
|
<span>Data on display</span>
|
|
@@ -1225,9 +1763,16 @@ function ChartConfigPanel({ widget, onChange, onSubPage }) {
|
|
|
1225
1763
|
<select value={style.colors || "auto"} onChange={(event) => setStyle({ colors: event.target.value })}>
|
|
1226
1764
|
<option value="auto">Auto</option>
|
|
1227
1765
|
<option value="accent">Accent</option>
|
|
1766
|
+
<option value="brand-local">Local brand kit</option>
|
|
1767
|
+
<option value="brand-bridge">Bridge brand kit</option>
|
|
1228
1768
|
<option value="manual">Manual</option>
|
|
1229
1769
|
</select>
|
|
1230
1770
|
</label>
|
|
1771
|
+
<div className="workspace-color-preview-row">
|
|
1772
|
+
<span>Active color</span>
|
|
1773
|
+
<em style={{ background: activeColor }} />
|
|
1774
|
+
<code>{activeColor}</code>
|
|
1775
|
+
</div>
|
|
1231
1776
|
{style.colors === "manual" ? <div className="workspace-color-picker-row">
|
|
1232
1777
|
<label>
|
|
1233
1778
|
<span>Manual color</span>
|
|
@@ -1414,7 +1959,7 @@ function IframePreviewModal({ widget, onClose }) {
|
|
|
1414
1959
|
</div>;
|
|
1415
1960
|
}
|
|
1416
1961
|
|
|
1417
|
-
function WidgetPreview({ widget, selected, onSelect, onMoveStart, onRemove, onResizeStart, onExpandIframe }) {
|
|
1962
|
+
function WidgetPreview({ widget, branding, selected, onSelect, onMoveStart, onRemove, onResizeStart, onExpandIframe }) {
|
|
1418
1963
|
const fallbackColumns = widget.config?.columns?.length ? widget.config.columns : ["Name", "Domain Name"];
|
|
1419
1964
|
const visibleColumns = widget.kind === "view" ? getVisibleColumns(widget) : fallbackColumns;
|
|
1420
1965
|
const viewColumns = visibleColumns.length ? visibleColumns : fallbackColumns;
|
|
@@ -1423,7 +1968,11 @@ function WidgetPreview({ widget, selected, onSelect, onMoveStart, onRemove, onRe
|
|
|
1423
1968
|
const chartType = widget.kind === "chart" ? (getChartType(widget) === "line" ? DEFAULT_CHART_TYPE : getChartType(widget)) : null;
|
|
1424
1969
|
const dataLabels = widget.kind === "chart" ? Boolean(widget.config?.style?.dataLabels) : false;
|
|
1425
1970
|
const chartStyle = widget.kind === "chart" ? getChartStyle(widget) : {};
|
|
1426
|
-
const chartColor = chartStyle
|
|
1971
|
+
const chartColor = resolveChartColor(chartStyle, branding);
|
|
1972
|
+
const selectedSourceObject = widget.config?.binding?.entityId ? {
|
|
1973
|
+
id: widget.config.binding.entityId,
|
|
1974
|
+
label: widget.config.binding.entityLabel || widget.config.binding.entityId
|
|
1975
|
+
} : null;
|
|
1427
1976
|
return <article
|
|
1428
1977
|
className={`workspace-widget-preview${selected ? " selected" : ""}`}
|
|
1429
1978
|
onClick={onSelect}
|
|
@@ -1439,6 +1988,10 @@ function WidgetPreview({ widget, selected, onSelect, onMoveStart, onRemove, onRe
|
|
|
1439
1988
|
onPointerDown={(event) => onMoveStart(event)}
|
|
1440
1989
|
>::</span>
|
|
1441
1990
|
<strong>{widget.title}</strong>
|
|
1991
|
+
{selectedSourceObject ? <span
|
|
1992
|
+
className="workspace-widget-source-chip"
|
|
1993
|
+
title={`${selectedSourceObject.label} · ${selectedSourceObject.id}`}
|
|
1994
|
+
>{selectedSourceObject.label}</span> : null}
|
|
1442
1995
|
<button
|
|
1443
1996
|
aria-label={`Remove ${widget.title}`}
|
|
1444
1997
|
onClick={(event) => {
|
|
@@ -1532,7 +2085,7 @@ function WorkspaceSettingsPanel({ config, persistence, adapterConfig, integratio
|
|
|
1532
2085
|
Inspect-only. Sourced from <code>growthub.config.json</code> + <code>GET /api/workspace</code>.
|
|
1533
2086
|
Edit branding by updating <code>growthub.config.json</code> inside your governed fork.
|
|
1534
2087
|
The builder itself never holds tokens, never executes hosted workflows, and never bypasses the PATCH allowlist
|
|
1535
|
-
(<code>dashboards</code>, <code>widgetTypes</code>, <code>canvas</code>).
|
|
2088
|
+
(<code>dashboards</code>, <code>widgetTypes</code>, <code>canvas</code>, <code>dataModel</code>).
|
|
1536
2089
|
</p>
|
|
1537
2090
|
<div className="workspace-readiness">
|
|
1538
2091
|
<article className="workspace-readiness-section">
|
|
@@ -1613,7 +2166,7 @@ function WorkspaceManagementPanel({ config, persistence, adapterConfig, onClose
|
|
|
1613
2166
|
</article>
|
|
1614
2167
|
<article className="workspace-readiness-section">
|
|
1615
2168
|
<h3>API</h3>
|
|
1616
|
-
<div className="workspace-readiness-row"><span>PATCH allowlist</span><code>dashboards | widgetTypes | canvas</code></div>
|
|
2169
|
+
<div className="workspace-readiness-row"><span>PATCH allowlist</span><code>dashboards | widgetTypes | canvas | dataModel</code></div>
|
|
1617
2170
|
<div className="workspace-readiness-row"><span>Unknown field</span><code>400</code></div>
|
|
1618
2171
|
<div className="workspace-readiness-row"><span>Read-only runtime</span><code>409 + guidance</code></div>
|
|
1619
2172
|
<div className="workspace-readiness-row"><span>Can save now</span>
|
|
@@ -1651,7 +2204,7 @@ function WorkspaceManagementPanel({ config, persistence, adapterConfig, onClose
|
|
|
1651
2204
|
</div>;
|
|
1652
2205
|
}
|
|
1653
2206
|
|
|
1654
|
-
function WorkspaceBuilder({ initialConfig, adapterConfig, integrationAdapter, persistence }) {
|
|
2207
|
+
function WorkspaceBuilder({ initialConfig, adapterConfig, integrationAdapter, integrationSettings, persistence }) {
|
|
1655
2208
|
const [config, setConfig] = useState(() => {
|
|
1656
2209
|
const dashboards = Array.isArray(initialConfig.dashboards) && initialConfig.dashboards.length
|
|
1657
2210
|
? initialConfig.dashboards.map((dashboard, index) =>
|
|
@@ -1706,6 +2259,10 @@ function WorkspaceBuilder({ initialConfig, adapterConfig, integrationAdapter, pe
|
|
|
1706
2259
|
const importInputRef = useRef(null);
|
|
1707
2260
|
const addSlot = dragPreview || selectedPosition;
|
|
1708
2261
|
const selectedWidget = activeWidgets.find((widget) => widget.id === selectedWidgetId) || null;
|
|
2262
|
+
const availableIntegrations = useMemo(() => flattenIntegrationSettings(integrationSettings), [integrationSettings]);
|
|
2263
|
+
const dataModelTables = useMemo(() => listWorkspaceDataModelTables(config), [config]);
|
|
2264
|
+
const selectedResolvedWidget = selectedWidget ? resolveViewWidget(selectedWidget, dataModelTables) : null;
|
|
2265
|
+
const branding = config.branding || {};
|
|
1709
2266
|
const occupiedCells = useMemo(() => {
|
|
1710
2267
|
const cells = new Set();
|
|
1711
2268
|
for (const widget of activeWidgets) {
|
|
@@ -2585,6 +3142,7 @@ function WorkspaceBuilder({ initialConfig, adapterConfig, integrationAdapter, pe
|
|
|
2585
3142
|
</div>
|
|
2586
3143
|
<nav className="workspace-nav">
|
|
2587
3144
|
<button type="button" className={workspaceView === "dashboards" ? "active workspace-nav-button" : "workspace-nav-button"} onClick={showDashboardHome}>Dashboards</button>
|
|
3145
|
+
<Link href="/data-model">Data Model</Link>
|
|
2588
3146
|
<Link href="/settings/integrations">Integrations</Link>
|
|
2589
3147
|
<button type="button" className="workspace-nav-button" onClick={() => setSettingsOpen(true)}>Workspace Settings</button>
|
|
2590
3148
|
<button type="button" className="workspace-nav-button" onClick={() => setManagementOpen(true)}>Management</button>
|
|
@@ -2769,13 +3327,14 @@ function WorkspaceBuilder({ initialConfig, adapterConfig, integrationAdapter, pe
|
|
|
2769
3327
|
</button>
|
|
2770
3328
|
{activeWidgets.map((widget) => <WidgetPreview
|
|
2771
3329
|
key={widget.id}
|
|
3330
|
+
branding={branding}
|
|
2772
3331
|
onMoveStart={(event) => beginMoveDrag(widget, event)}
|
|
2773
3332
|
onRemove={() => removeSelectedWidget(widget.id)}
|
|
2774
3333
|
onResizeStart={(corner, event) => beginResizeDrag(widget, corner, event)}
|
|
2775
3334
|
onSelect={() => selectWidget(widget.id)}
|
|
2776
3335
|
onExpandIframe={setExpandedIframeWidget}
|
|
2777
3336
|
selected={widget.id === selectedWidgetId}
|
|
2778
|
-
widget={widget}
|
|
3337
|
+
widget={resolveViewWidget(widget, dataModelTables)}
|
|
2779
3338
|
/>)}
|
|
2780
3339
|
</div>
|
|
2781
3340
|
</section> : null}
|
|
@@ -2820,22 +3379,27 @@ function WorkspaceBuilder({ initialConfig, adapterConfig, integrationAdapter, pe
|
|
|
2820
3379
|
</div> : null}
|
|
2821
3380
|
{selectedWidget && inspectorPath === "source" ? <SourceSubPanel
|
|
2822
3381
|
widget={selectedWidget}
|
|
2823
|
-
integrations={
|
|
3382
|
+
integrations={availableIntegrations}
|
|
3383
|
+
dataModelTables={dataModelTables}
|
|
2824
3384
|
onChange={replaceSelectedWidgetConfig}
|
|
2825
3385
|
onBack={() => setInspectorPath(SUB_PANEL_ROOT)}
|
|
2826
3386
|
/> : null}
|
|
2827
3387
|
{selectedWidget && inspectorPath === "fields" ? <FieldsSubPanel
|
|
2828
3388
|
widget={selectedWidget}
|
|
3389
|
+
dataModelTable={resolveDataModelTable(dataModelTables, selectedWidget.config?.binding)}
|
|
2829
3390
|
onChange={replaceSelectedWidgetConfig}
|
|
2830
3391
|
onBack={() => setInspectorPath(SUB_PANEL_ROOT)}
|
|
2831
3392
|
/> : null}
|
|
2832
3393
|
{selectedWidget && inspectorPath === "sort" ? <SortSubPanel
|
|
2833
3394
|
widget={selectedWidget}
|
|
3395
|
+
dataModelTable={resolveDataModelTable(dataModelTables, selectedWidget.config?.binding)}
|
|
2834
3396
|
onChange={replaceSelectedWidgetConfig}
|
|
2835
3397
|
onBack={() => setInspectorPath(SUB_PANEL_ROOT)}
|
|
2836
3398
|
/> : null}
|
|
2837
3399
|
{selectedWidget && inspectorPath === "filter" ? <FilterSubPanel
|
|
2838
3400
|
widget={selectedWidget}
|
|
3401
|
+
integrations={availableIntegrations}
|
|
3402
|
+
dataModelTable={resolveDataModelTable(dataModelTables, selectedWidget.config?.binding)}
|
|
2839
3403
|
onChange={replaceSelectedWidgetConfig}
|
|
2840
3404
|
onBack={() => setInspectorPath(SUB_PANEL_ROOT)}
|
|
2841
3405
|
/> : null}
|
|
@@ -2846,6 +3410,7 @@ function WorkspaceBuilder({ initialConfig, adapterConfig, integrationAdapter, pe
|
|
|
2846
3410
|
</label>
|
|
2847
3411
|
{selectedWidget.kind === "chart" ? <ChartConfigPanel
|
|
2848
3412
|
widget={selectedWidget}
|
|
3413
|
+
branding={branding}
|
|
2849
3414
|
onChange={replaceSelectedWidgetConfig}
|
|
2850
3415
|
onSubPage={(name) => setInspectorPath(name)}
|
|
2851
3416
|
/> : null}
|
|
@@ -2897,7 +3462,11 @@ function WorkspaceBuilder({ initialConfig, adapterConfig, integrationAdapter, pe
|
|
|
2897
3462
|
</small>
|
|
2898
3463
|
</label> : null}
|
|
2899
3464
|
{selectedWidget.kind === "view" ? <section className="workspace-field-stack">
|
|
2900
|
-
<
|
|
3465
|
+
{selectedWidget.config?.binding?.sourceType === DATA_MODEL_SOURCE_TYPE ? <div className="workspace-active-source-state">
|
|
3466
|
+
<span>Data Model object</span>
|
|
3467
|
+
<strong>{summarizeSource(selectedWidget)}</strong>
|
|
3468
|
+
<code>{selectedWidget.config?.binding?.objectId || "workspace-config"}</code>
|
|
3469
|
+
</div> : <label>
|
|
2901
3470
|
<span>Manual Rows</span>
|
|
2902
3471
|
<textarea
|
|
2903
3472
|
value={serializeManualRows(selectedWidget.config?.rows || [], selectedWidget.config?.columns || [])}
|
|
@@ -2909,7 +3478,7 @@ function WorkspaceBuilder({ initialConfig, adapterConfig, integrationAdapter, pe
|
|
|
2909
3478
|
});
|
|
2910
3479
|
}}
|
|
2911
3480
|
/>
|
|
2912
|
-
</label>
|
|
3481
|
+
</label>}
|
|
2913
3482
|
<div className="workspace-settings-list" role="group" aria-label="View widget settings">
|
|
2914
3483
|
<p className="workspace-panel-label">Settings</p>
|
|
2915
3484
|
<button type="button" className="workspace-settings-row" disabled>
|
|
@@ -2919,13 +3488,13 @@ function WorkspaceBuilder({ initialConfig, adapterConfig, integrationAdapter, pe
|
|
|
2919
3488
|
<span>Source</span><code>{summarizeSource(selectedWidget)}</code>
|
|
2920
3489
|
</button>
|
|
2921
3490
|
<button type="button" className="workspace-settings-row" onClick={() => setInspectorPath("fields")}>
|
|
2922
|
-
<span>Fields</span><code>{summarizeFields(selectedWidget)}</code>
|
|
3491
|
+
<span>Fields</span><code>{summarizeFields(selectedResolvedWidget || selectedWidget)}</code>
|
|
2923
3492
|
</button>
|
|
2924
3493
|
<button type="button" className="workspace-settings-row" onClick={() => setInspectorPath("filter")}>
|
|
2925
|
-
<span>Filter</span><code>{summarizeFilter(selectedWidget)}</code>
|
|
3494
|
+
<span>Filter</span><code>{summarizeFilter(selectedResolvedWidget || selectedWidget)}</code>
|
|
2926
3495
|
</button>
|
|
2927
3496
|
<button type="button" className="workspace-settings-row" onClick={() => setInspectorPath("sort")}>
|
|
2928
|
-
<span>Sort</span><code>{summarizeSort(selectedWidget)}</code>
|
|
3497
|
+
<span>Sort</span><code>{summarizeSort(selectedResolvedWidget || selectedWidget)}</code>
|
|
2929
3498
|
</button>
|
|
2930
3499
|
</div>
|
|
2931
3500
|
</section> : null}
|