@growthub/cli 0.9.17 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/reference-options/route.js +62 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/refresh-sources/route.js +13 -2
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/resolver-templates/route.js +23 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-run/route.js +35 -5
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/test-source/route.js +15 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/DataModelShell.jsx +2048 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/DataTable.jsx +1 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/FieldEditor.jsx +1 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/FieldManager.jsx +9 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/ObjectSidebar.jsx +41 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/RecordDrawer.jsx +1 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/ReferencePicker.jsx +244 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SandboxRunPanel.jsx +21 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SourceTestPanel.jsx +15 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/StatusPill.jsx +13 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/ToggleField.jsx +41 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/dm-shared.jsx +99 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/page.jsx +2 -1528
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +66 -5
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/docs/connector-template-authoring.md +8 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/docs/data-model-reference-fields.md +15 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/docs/mcp-chrome-tool-connectors.md +12 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/docs/resolver-template-library.md +17 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/source-resolver-registry.js +13 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/templates/README.md +12 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/templates/chrome-bridge.js +22 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/templates/custom-http.js +23 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/templates/generic-commerce.js +22 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/templates/generic-crm.js +23 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/templates/generic-project-management.js +22 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/templates/generic-spreadsheet.js +22 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/templates/mcp-tool.js +22 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/templates/template-registry.js +50 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/templates/webhook.js +22 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/references/collect-reference-options.js +133 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/references/reference-resolver-registry.js +17 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/references/resolver-loader.js +6 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/references/resolvers/README.md +8 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/references/resolvers/local-data-model.js +11 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/references/resolvers/source-records.js +34 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/adapters/README.md +5 -3
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/default-local-intelligence.js +203 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/index.js +1 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/data-model/field-contracts.js +81 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/data-model/reference-option-schema.js +59 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/data-model/reference-options.js +29 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-data-model.js +527 -23
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-schema.js +131 -1
- package/dist/index.js +3043 -1340
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { DataModelTableSurface as DataTable } from "./DataModelShell.jsx";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { RecordFieldEditor as FieldEditor } from "./DataModelShell.jsx";
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Plus } from "lucide-react";
|
|
4
|
+
import { LucideIcon, OBJECT_TYPE_PRESETS, objectTypeBadge, pluralize } from "./dm-shared.jsx";
|
|
5
|
+
|
|
6
|
+
function ObjectRow({ table, selected, onSelect }) {
|
|
7
|
+
const badge = objectTypeBadge(table.objectType);
|
|
8
|
+
const iconName = table.icon || OBJECT_TYPE_PRESETS[table.objectType]?.icon || "Database";
|
|
9
|
+
return (
|
|
10
|
+
<button type="button" className={`dm-obj-row${selected ? " active" : ""}`} onClick={onSelect}>
|
|
11
|
+
<LucideIcon name={iconName} size={13} className="dm-obj-icon" />
|
|
12
|
+
<span className="dm-obj-name">{table.label}</span>
|
|
13
|
+
<span className={`dm-badge ${badge.cls}`}>{badge.label}</span>
|
|
14
|
+
</button>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function ObjectSidebar({ tables, selectedTable, onSelectSource, onAddObject }) {
|
|
19
|
+
return (
|
|
20
|
+
<aside className="dm-obj-col">
|
|
21
|
+
<div className="dm-obj-col-head">
|
|
22
|
+
<span>{pluralize(tables.length, "object")}</span>
|
|
23
|
+
</div>
|
|
24
|
+
<div className="dm-obj-col-body">
|
|
25
|
+
{tables.map((table) => (
|
|
26
|
+
<ObjectRow
|
|
27
|
+
key={`${table.source}-${table.id}`}
|
|
28
|
+
table={table}
|
|
29
|
+
selected={selectedTable?.id === table.id}
|
|
30
|
+
onSelect={() => onSelectSource(table.source)}
|
|
31
|
+
/>
|
|
32
|
+
))}
|
|
33
|
+
</div>
|
|
34
|
+
<div className="dm-obj-col-foot">
|
|
35
|
+
<button type="button" className="dm-obj-add-btn" onClick={onAddObject}>
|
|
36
|
+
<Plus size={13} />New object
|
|
37
|
+
</button>
|
|
38
|
+
</div>
|
|
39
|
+
</aside>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { DataModelRecordDrawer as RecordDrawer } from "./DataModelShell.jsx";
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { ChevronDown, Search, AlertTriangle } from "lucide-react";
|
|
4
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
5
|
+
import { fetchReferenceOptions } from "@/lib/data-model/reference-options";
|
|
6
|
+
|
|
7
|
+
const EMPTY_CONTEXT = Object.freeze({});
|
|
8
|
+
|
|
9
|
+
function SearchableSelect({
|
|
10
|
+
value,
|
|
11
|
+
options,
|
|
12
|
+
disabled,
|
|
13
|
+
placeholder = "Select...",
|
|
14
|
+
onChange,
|
|
15
|
+
pageSize = 8,
|
|
16
|
+
footer,
|
|
17
|
+
loading,
|
|
18
|
+
emptyHint,
|
|
19
|
+
serverDriven,
|
|
20
|
+
onSearchChange
|
|
21
|
+
}) {
|
|
22
|
+
const [open, setOpen] = useState(false);
|
|
23
|
+
const [query, setQuery] = useState("");
|
|
24
|
+
const [page, setPage] = useState(0);
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
if (!serverDriven || !onSearchChange) return undefined;
|
|
28
|
+
const handle = setTimeout(() => onSearchChange(query), 220);
|
|
29
|
+
return () => clearTimeout(handle);
|
|
30
|
+
}, [query, serverDriven, onSearchChange]);
|
|
31
|
+
|
|
32
|
+
const selected = options.find((option) => option.value === String(value || ""));
|
|
33
|
+
const filtered = useMemo(() => {
|
|
34
|
+
if (serverDriven) return options;
|
|
35
|
+
const needle = query.trim().toLowerCase();
|
|
36
|
+
if (!needle) return options;
|
|
37
|
+
return options.filter((option) =>
|
|
38
|
+
`${option.label} ${option.value} ${option.secondaryLabel || ""}`.toLowerCase().includes(needle)
|
|
39
|
+
);
|
|
40
|
+
}, [options, query, serverDriven]);
|
|
41
|
+
const pageCount = Math.max(1, Math.ceil(filtered.length / pageSize));
|
|
42
|
+
const currentPage = Math.min(page, pageCount - 1);
|
|
43
|
+
const visibleOptions = filtered.slice(currentPage * pageSize, currentPage * pageSize + pageSize);
|
|
44
|
+
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
setPage(0);
|
|
47
|
+
}, [query, options.length]);
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<div
|
|
51
|
+
className={`dm-select${open ? " open" : ""}${disabled ? " disabled" : ""}`}
|
|
52
|
+
onClick={(event) => event.stopPropagation()}
|
|
53
|
+
onBlur={(event) => {
|
|
54
|
+
if (!event.currentTarget.contains(event.relatedTarget)) setOpen(false);
|
|
55
|
+
}}
|
|
56
|
+
>
|
|
57
|
+
<button
|
|
58
|
+
type="button"
|
|
59
|
+
className="dm-select-trigger"
|
|
60
|
+
disabled={disabled}
|
|
61
|
+
aria-haspopup="listbox"
|
|
62
|
+
aria-expanded={open}
|
|
63
|
+
onClick={() => setOpen((current) => !current)}
|
|
64
|
+
>
|
|
65
|
+
<span className={selected ? "" : "empty"}>{selected?.label || placeholder}</span>
|
|
66
|
+
<ChevronDown size={15} aria-hidden="true" />
|
|
67
|
+
</button>
|
|
68
|
+
{open && (
|
|
69
|
+
<div className="dm-select-popover">
|
|
70
|
+
<label className="dm-select-search">
|
|
71
|
+
<Search size={14} aria-hidden="true" />
|
|
72
|
+
<input
|
|
73
|
+
autoFocus
|
|
74
|
+
value={query}
|
|
75
|
+
placeholder="Search..."
|
|
76
|
+
onChange={(event) => {
|
|
77
|
+
const next = event.target.value;
|
|
78
|
+
setQuery(next);
|
|
79
|
+
}}
|
|
80
|
+
/>
|
|
81
|
+
</label>
|
|
82
|
+
{loading && <p className="dm-select-empty" style={{ padding: 8 }}>Loading…</p>}
|
|
83
|
+
{!loading && (
|
|
84
|
+
<div className="dm-select-list" role="listbox">
|
|
85
|
+
<button
|
|
86
|
+
type="button"
|
|
87
|
+
className={`dm-select-option${!value ? " selected" : ""}`}
|
|
88
|
+
role="option"
|
|
89
|
+
aria-selected={!value}
|
|
90
|
+
onClick={() => {
|
|
91
|
+
onChange("");
|
|
92
|
+
setOpen(false);
|
|
93
|
+
}}
|
|
94
|
+
>
|
|
95
|
+
<span>{placeholder}</span>
|
|
96
|
+
</button>
|
|
97
|
+
{visibleOptions.map((option) => (
|
|
98
|
+
<button
|
|
99
|
+
type="button"
|
|
100
|
+
key={`${option.value}:${option.label}`}
|
|
101
|
+
className={`dm-select-option${option.value === String(value || "") ? " selected" : ""}`}
|
|
102
|
+
role="option"
|
|
103
|
+
aria-selected={option.value === String(value || "")}
|
|
104
|
+
onClick={() => {
|
|
105
|
+
onChange(option.value);
|
|
106
|
+
setOpen(false);
|
|
107
|
+
setQuery("");
|
|
108
|
+
if (serverDriven && onSearchChange) onSearchChange("");
|
|
109
|
+
}}
|
|
110
|
+
>
|
|
111
|
+
<span>{option.label}</span>
|
|
112
|
+
{option.secondaryLabel && <em>{option.secondaryLabel}</em>}
|
|
113
|
+
</button>
|
|
114
|
+
))}
|
|
115
|
+
{visibleOptions.length === 0 && (
|
|
116
|
+
<p className="dm-select-empty">{emptyHint || "No matches"}</p>
|
|
117
|
+
)}
|
|
118
|
+
</div>
|
|
119
|
+
)}
|
|
120
|
+
{filtered.length > pageSize && (
|
|
121
|
+
<div className="dm-select-pager">
|
|
122
|
+
<button type="button" disabled={currentPage === 0} onClick={() => setPage((next) => Math.max(0, next - 1))}>Prev</button>
|
|
123
|
+
<span>{currentPage + 1} / {pageCount}</span>
|
|
124
|
+
<button type="button" disabled={currentPage >= pageCount - 1} onClick={() => setPage((next) => Math.min(pageCount - 1, next + 1))}>Next</button>
|
|
125
|
+
</div>
|
|
126
|
+
)}
|
|
127
|
+
{footer}
|
|
128
|
+
</div>
|
|
129
|
+
)}
|
|
130
|
+
</div>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function ReferencePicker({
|
|
135
|
+
objectId,
|
|
136
|
+
field,
|
|
137
|
+
value,
|
|
138
|
+
onChange,
|
|
139
|
+
disabled,
|
|
140
|
+
placeholder = "Select reference…",
|
|
141
|
+
context = EMPTY_CONTEXT
|
|
142
|
+
}) {
|
|
143
|
+
const [options, setOptions] = useState([]);
|
|
144
|
+
const [nextCursor, setNextCursor] = useState(null);
|
|
145
|
+
const [loading, setLoading] = useState(false);
|
|
146
|
+
const [error, setError] = useState("");
|
|
147
|
+
const [liveQuery, setLiveQuery] = useState("");
|
|
148
|
+
const mountedRef = useRef(false);
|
|
149
|
+
const requestIdRef = useRef(0);
|
|
150
|
+
|
|
151
|
+
useEffect(() => {
|
|
152
|
+
mountedRef.current = true;
|
|
153
|
+
return () => {
|
|
154
|
+
mountedRef.current = false;
|
|
155
|
+
};
|
|
156
|
+
}, []);
|
|
157
|
+
|
|
158
|
+
const loadPage = useCallback(async ({ query, cursor, append }) => {
|
|
159
|
+
if (!objectId || !field) return;
|
|
160
|
+
const requestId = requestIdRef.current + 1;
|
|
161
|
+
requestIdRef.current = requestId;
|
|
162
|
+
setLoading(true);
|
|
163
|
+
setError("");
|
|
164
|
+
try {
|
|
165
|
+
const payload = await fetchReferenceOptions({
|
|
166
|
+
objectId,
|
|
167
|
+
field,
|
|
168
|
+
query: query || "",
|
|
169
|
+
cursor,
|
|
170
|
+
limit: 25,
|
|
171
|
+
context
|
|
172
|
+
});
|
|
173
|
+
if (!mountedRef.current || requestId !== requestIdRef.current) return;
|
|
174
|
+
const next = Array.isArray(payload.options) ? payload.options : [];
|
|
175
|
+
setOptions((prev) => (append ? [...prev, ...next] : next));
|
|
176
|
+
setNextCursor(payload.nextCursor || null);
|
|
177
|
+
} catch (err) {
|
|
178
|
+
if (!mountedRef.current || requestId !== requestIdRef.current) return;
|
|
179
|
+
setError(err?.message || "Failed to load options");
|
|
180
|
+
if (!append) setOptions([]);
|
|
181
|
+
} finally {
|
|
182
|
+
if (!mountedRef.current || requestId !== requestIdRef.current) return;
|
|
183
|
+
setLoading(false);
|
|
184
|
+
}
|
|
185
|
+
}, [objectId, field, context]);
|
|
186
|
+
|
|
187
|
+
useEffect(() => {
|
|
188
|
+
loadPage({ query: liveQuery, cursor: null, append: false });
|
|
189
|
+
}, [liveQuery, objectId, field, loadPage]);
|
|
190
|
+
|
|
191
|
+
useEffect(() => {
|
|
192
|
+
setLiveQuery("");
|
|
193
|
+
}, [objectId, field]);
|
|
194
|
+
|
|
195
|
+
const valueInOptions = useMemo(
|
|
196
|
+
() => options.some((o) => String(o.value) === String(value || "")),
|
|
197
|
+
[options, value]
|
|
198
|
+
);
|
|
199
|
+
const showRepair = Boolean(value) && !valueInOptions && !loading;
|
|
200
|
+
|
|
201
|
+
return (
|
|
202
|
+
<div className="dm-reference-picker">
|
|
203
|
+
{error && <p className="dm-field-error" style={{ fontSize: 11 }}>{error}</p>}
|
|
204
|
+
{showRepair && (
|
|
205
|
+
<p className="dm-validation-banner" style={{ fontSize: 11, marginBottom: 6 }}>
|
|
206
|
+
<AlertTriangle size={12} aria-hidden />
|
|
207
|
+
<span>Selected reference is missing or filtered out. Pick a new row or adjust API Registry status.</span>
|
|
208
|
+
</p>
|
|
209
|
+
)}
|
|
210
|
+
<SearchableSelect
|
|
211
|
+
value={value || ""}
|
|
212
|
+
options={options.map((o) => ({
|
|
213
|
+
value: String(o.value),
|
|
214
|
+
label: String(o.label || o.value),
|
|
215
|
+
secondaryLabel: o.secondaryLabel
|
|
216
|
+
}))}
|
|
217
|
+
disabled={disabled}
|
|
218
|
+
placeholder={placeholder}
|
|
219
|
+
pageSize={10}
|
|
220
|
+
loading={loading}
|
|
221
|
+
emptyHint="No matches — try another search"
|
|
222
|
+
onChange={onChange}
|
|
223
|
+
serverDriven
|
|
224
|
+
onSearchChange={setLiveQuery}
|
|
225
|
+
footer={
|
|
226
|
+
nextCursor ? (
|
|
227
|
+
<div style={{ padding: 8, borderTop: "1px solid rgba(255,255,255,0.06)" }}>
|
|
228
|
+
<button
|
|
229
|
+
type="button"
|
|
230
|
+
className="dm-btn-ghost"
|
|
231
|
+
disabled={loading || disabled}
|
|
232
|
+
onClick={() => loadPage({ query: liveQuery, cursor: nextCursor, append: true })}
|
|
233
|
+
>
|
|
234
|
+
Load more
|
|
235
|
+
</button>
|
|
236
|
+
</div>
|
|
237
|
+
) : null
|
|
238
|
+
}
|
|
239
|
+
/>
|
|
240
|
+
</div>
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export { SearchableSelect };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Play } from "lucide-react";
|
|
4
|
+
import { StatusPill } from "./StatusPill.jsx";
|
|
5
|
+
|
|
6
|
+
export function SandboxRunPanel({ status, sandboxRunning, sandboxMessage, onRun, disabled, canRun }) {
|
|
7
|
+
return (
|
|
8
|
+
<div className="dm-record-testbar">
|
|
9
|
+
<StatusPill value={status} />
|
|
10
|
+
<button
|
|
11
|
+
type="button"
|
|
12
|
+
className="dm-btn-primary-sm"
|
|
13
|
+
disabled={sandboxRunning || disabled || !canRun}
|
|
14
|
+
onClick={onRun}
|
|
15
|
+
>
|
|
16
|
+
{sandboxRunning ? "Running…" : (<><Play size={13} aria-hidden /> Run sandbox</>)}
|
|
17
|
+
</button>
|
|
18
|
+
{sandboxMessage && <span>{sandboxMessage}</span>}
|
|
19
|
+
</div>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { StatusPill } from "./StatusPill.jsx";
|
|
4
|
+
|
|
5
|
+
export function SourceTestPanel({ status, testing, testMessage, onTest, disabled }) {
|
|
6
|
+
return (
|
|
7
|
+
<div className="dm-record-testbar">
|
|
8
|
+
<StatusPill value={status} />
|
|
9
|
+
<button type="button" className="dm-btn-primary-sm" disabled={testing || disabled} onClick={onTest}>
|
|
10
|
+
{testing ? "Testing…" : "Test connection"}
|
|
11
|
+
</button>
|
|
12
|
+
{testMessage && <span>{testMessage}</span>}
|
|
13
|
+
</div>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
export function StatusPill({ value }) {
|
|
4
|
+
const status = String(value || "untested").toLowerCase();
|
|
5
|
+
const ok = ["connected", "approved", "ok", "success"].includes(status);
|
|
6
|
+
const bad = ["failed", "error", "disconnected"].includes(status);
|
|
7
|
+
return (
|
|
8
|
+
<span className={`dm-db-status ${ok ? "ok" : bad ? "bad" : ""}`}>
|
|
9
|
+
<span />
|
|
10
|
+
{value || "untested"}
|
|
11
|
+
</span>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
export function ToggleField({ checked, disabled, label, onChange, description }) {
|
|
4
|
+
return (
|
|
5
|
+
<label className="dm-check-row">
|
|
6
|
+
<input
|
|
7
|
+
type="checkbox"
|
|
8
|
+
checked={Boolean(checked)}
|
|
9
|
+
disabled={disabled}
|
|
10
|
+
onChange={(event) => onChange(event.target.checked)}
|
|
11
|
+
/>
|
|
12
|
+
<span>
|
|
13
|
+
{label}
|
|
14
|
+
{description && <span className="dm-cell-empty" style={{ display: "block", marginTop: 4 }}>{description}</span>}
|
|
15
|
+
</span>
|
|
16
|
+
</label>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function SegmentedToggle({ value, options, disabled, onChange, label, name = "segmented" }) {
|
|
21
|
+
const group = String(name || "segmented");
|
|
22
|
+
return (
|
|
23
|
+
<div className="dm-record-field">
|
|
24
|
+
{label && <span>{label}</span>}
|
|
25
|
+
<div className="dm-radio-row">
|
|
26
|
+
{options.map((opt) => (
|
|
27
|
+
<label key={opt}>
|
|
28
|
+
<input
|
|
29
|
+
type="radio"
|
|
30
|
+
name={group}
|
|
31
|
+
checked={value === opt}
|
|
32
|
+
disabled={disabled}
|
|
33
|
+
onChange={() => onChange(opt)}
|
|
34
|
+
/>
|
|
35
|
+
<span>{opt}</span>
|
|
36
|
+
</label>
|
|
37
|
+
))}
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Activity,
|
|
5
|
+
BarChart2,
|
|
6
|
+
Box,
|
|
7
|
+
Building2,
|
|
8
|
+
Calendar,
|
|
9
|
+
CheckSquare,
|
|
10
|
+
Code2,
|
|
11
|
+
Database,
|
|
12
|
+
FileText,
|
|
13
|
+
Globe,
|
|
14
|
+
Hash,
|
|
15
|
+
Layers,
|
|
16
|
+
Link2,
|
|
17
|
+
List,
|
|
18
|
+
Mail,
|
|
19
|
+
Plus,
|
|
20
|
+
ShoppingCart,
|
|
21
|
+
Star,
|
|
22
|
+
Tag,
|
|
23
|
+
Terminal,
|
|
24
|
+
ToggleLeft,
|
|
25
|
+
Type,
|
|
26
|
+
Users,
|
|
27
|
+
Zap,
|
|
28
|
+
} from "lucide-react";
|
|
29
|
+
import { OBJECT_TYPE_PRESETS } from "@/lib/workspace-data-model";
|
|
30
|
+
|
|
31
|
+
const LUCIDE_MAP = {
|
|
32
|
+
Activity, BarChart2, Box, Building2, Calendar, CheckSquare, Code2,
|
|
33
|
+
Database, FileText, Globe, Hash, Layers, Link2, List, Mail, Plus,
|
|
34
|
+
ShoppingCart, Star, Tag, Terminal, ToggleLeft, Type, Users, Zap,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const ICON_PICKER_SET = [
|
|
38
|
+
"Database", "Globe", "Code2", "Users", "CheckSquare", "Building2",
|
|
39
|
+
"Tag", "Star", "Zap", "FileText", "Mail", "BarChart2",
|
|
40
|
+
"Layers", "Box", "Activity", "ShoppingCart", "Terminal",
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
const OBJECT_TYPE_BADGE = {
|
|
44
|
+
"data-source": { label: "Data Source", cls: "dm-badge-datasource" },
|
|
45
|
+
"api-registry": { label: "API Registry", cls: "dm-badge-registry" },
|
|
46
|
+
"sandbox-environment": { label: "Sandbox Environment", cls: "dm-badge-sandbox" },
|
|
47
|
+
people: { label: "People", cls: "dm-badge-people" },
|
|
48
|
+
tasks: { label: "Tasks", cls: "dm-badge-tasks" },
|
|
49
|
+
custom: { label: "Custom", cls: "dm-badge-manual" },
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const FIELD_TYPE_ICON_NAMES = {
|
|
53
|
+
text: "Type", number: "Hash", date: "Calendar", url: "Link2", select: "List", boolean: "ToggleLeft",
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
function LucideIcon({ name, size = 14, className, style }) {
|
|
57
|
+
const Icon = LUCIDE_MAP[name] || Database;
|
|
58
|
+
return <Icon size={size} className={className} style={style} aria-hidden="true" />;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function inferFieldType(name) {
|
|
62
|
+
const n = name.toLowerCase();
|
|
63
|
+
if (n.includes("date") || n.includes("_at") || n.includes("created") || n.includes("updated")) return "date";
|
|
64
|
+
if (n.includes("url") || n.includes("link") || n.includes("website") || n === "endpoint" || n === "baseurl") return "url";
|
|
65
|
+
if (n.includes("count") || n.includes("num") || n.includes("amount") || n.includes("arr") || n.includes("price")) return "number";
|
|
66
|
+
if (n === "status" || n === "stage" || n === "type" || n === "icp" || n === "priority" || n === "authtype" || n === "method") return "select";
|
|
67
|
+
if (n.startsWith("is_") || n.includes("active") || n.includes("enabled")) return "boolean";
|
|
68
|
+
return "text";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function pluralize(count, word) {
|
|
72
|
+
return `${count} ${count === 1 ? word : `${word}s`}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function objectTypeBadge(objectType) {
|
|
76
|
+
return OBJECT_TYPE_BADGE[objectType] || OBJECT_TYPE_BADGE.custom;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function textColorForAccent(accent) {
|
|
80
|
+
const hex = String(accent || "").replace("#", "");
|
|
81
|
+
if (!/^[0-9a-f]{6}$/i.test(hex)) return "#ffffff";
|
|
82
|
+
const r = parseInt(hex.slice(0, 2), 16);
|
|
83
|
+
const g = parseInt(hex.slice(2, 4), 16);
|
|
84
|
+
const b = parseInt(hex.slice(4, 6), 16);
|
|
85
|
+
return (0.299 * r + 0.587 * g + 0.114 * b) / 255 > 0.62 ? "#252525" : "#ffffff";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export {
|
|
89
|
+
FIELD_TYPE_ICON_NAMES,
|
|
90
|
+
ICON_PICKER_SET,
|
|
91
|
+
LUCIDE_MAP,
|
|
92
|
+
LucideIcon,
|
|
93
|
+
OBJECT_TYPE_BADGE,
|
|
94
|
+
OBJECT_TYPE_PRESETS,
|
|
95
|
+
inferFieldType,
|
|
96
|
+
objectTypeBadge,
|
|
97
|
+
pluralize,
|
|
98
|
+
textColorForAccent
|
|
99
|
+
};
|