@growth-operations/ui-shared 1.5.9 → 1.6.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/package.json +1 -1
- package/src/forms/OptionSelect.jsx +115 -0
- package/src/index.js +3 -0
package/package.json
CHANGED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import { Select, LoadingSpinner, Alert, Flex, Text } from "@hubspot/ui-extensions";
|
|
3
|
+
import { useToken } from "../sdk/index";
|
|
4
|
+
import { getForms } from "../sdk/hubspot/forms";
|
|
5
|
+
import { getLists } from "../sdk/hubspot/lists";
|
|
6
|
+
import { getPipelines, getPipelineStages } from "../sdk/hubspot/pipeline";
|
|
7
|
+
import { useStrictModeEffect } from "../lib/useStrictModeEffect";
|
|
8
|
+
|
|
9
|
+
// OptionSelect — one shared HubSpot-options dropdown, replacing the per-app
|
|
10
|
+
// FormSelector / SegmentSelector / PipelineStageSelector that every app
|
|
11
|
+
// re-implemented (fetch options → map to {label,value} → <Select> → save on
|
|
12
|
+
// change). The data-fetch half already lives in the SDK (getForms / getLists /
|
|
13
|
+
// getPipelineStages); this owns the loading/error/Select chrome around it.
|
|
14
|
+
//
|
|
15
|
+
// Pick options ONE of two ways:
|
|
16
|
+
// - source="forms" | "lists" | "pipelines" | "pipelineStages" — built-in SDK
|
|
17
|
+
// fetchers. "pipelines"/"pipelineStages" take opts.objectType; the latter
|
|
18
|
+
// also needs opts.pipelineId.
|
|
19
|
+
// - fetchOptions={async (context, token) => [{label, value}, ...]} — any
|
|
20
|
+
// custom source. Takes precedence over `source`.
|
|
21
|
+
//
|
|
22
|
+
// onChange(value) fires with the selected value; the caller owns persistence
|
|
23
|
+
// (e.g. ui-shared updateSettings). `value` is the controlled selection.
|
|
24
|
+
const SOURCES = {
|
|
25
|
+
forms: async (context, token) => {
|
|
26
|
+
const { results = [] } = await getForms(context, token);
|
|
27
|
+
return results.map((f) => ({ label: f.name, value: f.id }));
|
|
28
|
+
},
|
|
29
|
+
lists: async (context, token, opts) => {
|
|
30
|
+
const { lists = [] } = await getLists(context, token, opts?.query ?? "");
|
|
31
|
+
return lists.map((l) => ({ label: l.name, value: String(l.listId) }));
|
|
32
|
+
},
|
|
33
|
+
pipelines: async (context, token, opts) => {
|
|
34
|
+
const { results = [] } = await getPipelines(context, token, opts?.objectType);
|
|
35
|
+
return results.map((p) => ({ label: p.label, value: p.id }));
|
|
36
|
+
},
|
|
37
|
+
pipelineStages: async (context, token, opts) => {
|
|
38
|
+
if (!opts?.pipelineId) return [];
|
|
39
|
+
const { results = [] } = await getPipelineStages(
|
|
40
|
+
context,
|
|
41
|
+
token,
|
|
42
|
+
opts.pipelineId,
|
|
43
|
+
opts.objectType
|
|
44
|
+
);
|
|
45
|
+
return results.map((s) => ({ label: s.label, value: s.id }));
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export function OptionSelect({
|
|
50
|
+
context,
|
|
51
|
+
value,
|
|
52
|
+
onChange,
|
|
53
|
+
source,
|
|
54
|
+
fetchOptions,
|
|
55
|
+
label = "Select",
|
|
56
|
+
description,
|
|
57
|
+
placeholder = "Choose…",
|
|
58
|
+
// Extra source args (pipelineId/objectType for pipelineStages, query for lists)
|
|
59
|
+
...opts
|
|
60
|
+
}) {
|
|
61
|
+
const [options, setOptions] = useState([]);
|
|
62
|
+
const [loading, setLoading] = useState(true);
|
|
63
|
+
const [error, setError] = useState(null);
|
|
64
|
+
const { ensureValidToken } = useToken();
|
|
65
|
+
|
|
66
|
+
// Re-fetch when the source or its key args change (e.g. pipelineId).
|
|
67
|
+
const depKey = JSON.stringify({
|
|
68
|
+
source,
|
|
69
|
+
pipelineId: opts.pipelineId,
|
|
70
|
+
objectType: opts.objectType,
|
|
71
|
+
query: opts.query,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
useStrictModeEffect(
|
|
75
|
+
async ({ mounted }) => {
|
|
76
|
+
try {
|
|
77
|
+
const token = await ensureValidToken();
|
|
78
|
+
if (!token || !mounted.current) return;
|
|
79
|
+
const loader =
|
|
80
|
+
fetchOptions ?? SOURCES[source] ?? (async () => []);
|
|
81
|
+
const opted = await loader(context, token, opts);
|
|
82
|
+
if (mounted.current) setOptions(opted);
|
|
83
|
+
} catch (e) {
|
|
84
|
+
if (mounted.current) setError(String(e?.message ?? e));
|
|
85
|
+
} finally {
|
|
86
|
+
if (mounted.current) setLoading(false);
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
[context, depKey]
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
if (loading) {
|
|
93
|
+
return <LoadingSpinner size="small" label={`Loading ${label.toLowerCase()}…`} showLabel />;
|
|
94
|
+
}
|
|
95
|
+
if (error) {
|
|
96
|
+
return (
|
|
97
|
+
<Alert title={`Couldn't load ${label.toLowerCase()}`} variant="danger">
|
|
98
|
+
<Text>{error}</Text>
|
|
99
|
+
</Alert>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<Flex direction="column" gap="extra-small">
|
|
105
|
+
<Select
|
|
106
|
+
label={label}
|
|
107
|
+
description={description}
|
|
108
|
+
placeholder={placeholder}
|
|
109
|
+
options={options}
|
|
110
|
+
value={value ?? ""}
|
|
111
|
+
onChange={(v) => onChange?.(v)}
|
|
112
|
+
/>
|
|
113
|
+
</Flex>
|
|
114
|
+
);
|
|
115
|
+
}
|
package/src/index.js
CHANGED
|
@@ -27,6 +27,9 @@ export { UsageTab } from "./usage/UsageTab";
|
|
|
27
27
|
// Sandbox — the shared "Install on a sandbox" action (mints a signed link).
|
|
28
28
|
export { SandboxInstall } from "./sandbox/SandboxInstall";
|
|
29
29
|
|
|
30
|
+
// Forms — shared HubSpot-options dropdown (forms/lists/pipeline-stages picker).
|
|
31
|
+
export { OptionSelect } from "./forms/OptionSelect";
|
|
32
|
+
|
|
30
33
|
// Lib helpers.
|
|
31
34
|
export {
|
|
32
35
|
fmtDate,
|