@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@growth-operations/ui-shared",
3
- "version": "1.5.9",
3
+ "version": "1.6.0",
4
4
  "description": "Shared HubSpot UI-extension modules (Home, Billing) for Growth Operations apps.",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -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,