@superleapai/flow-ui 2.5.12 → 2.5.14

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.
@@ -113,6 +113,24 @@
113
113
  return global.Icon;
114
114
  }
115
115
 
116
+ /**
117
+ * Get display name for the object from ObjectType: display_singular_name, display_plural_name, or slug.
118
+ * @param {{ display_singular_name?: string, display_plural_name?: string, slug?: string } | null} objectSchema - optional ObjectType
119
+ * @param {string} slug - objectSlug fallback
120
+ * @param {'singular'|'plural'} kind - which name to return
121
+ * @returns {string}
122
+ */
123
+ function getObjectDisplayName(objectSchema, slug, kind) {
124
+ var s = objectSchema;
125
+ var str = function (v) {
126
+ return typeof v === "string" && v ? v : "";
127
+ };
128
+ if (kind === "singular") {
129
+ return str(s && s.display_singular_name) || str(s && s.slug) || slug || "";
130
+ }
131
+ return str(s && s.display_plural_name) || str(s && s.display_singular_name) || str(s && s.slug) || slug || "";
132
+ }
133
+
116
134
  /**
117
135
  * Get iconStr and color for the object: from objectSchema.properties.icon_data first, else OBJECT_SLUG_TO_ICON, else IconDatabase fallback.
118
136
  * @param {string} slug - objectSlug
@@ -155,16 +173,21 @@
155
173
  * @param {number} [config.initialLimit] - Initial fetch limit (default 50)
156
174
  * @param {Array<string>} [config.displayFields] - Fields to display as secondary info (e.g. ["email", "phone"])
157
175
  * @param {Object} [config.initialFilter] - Optional filter object to merge with search (e.g. { field: "status", operator: "exact", value: "active" } or { and: [...] })
158
- * @param {Object} [config.objectSchema] - Optional object type/schema; properties.icon_data { icon?, color? } used for static icon (not used for user; user shows Vivid Avatar)
176
+ * @param {Object} [config.objectSchema] - Optional ObjectType; display_plural_name/display_singular_name/slug used for placeholder/search/summary label; properties.icon_data { icon?, color? } for static icon (not used for user; user shows Vivid Avatar)
159
177
  * @returns {HTMLElement} Record multiselect container element
160
178
  */
161
179
  function createRecordMultiSelect(config) {
162
180
  var fieldId = config.fieldId;
163
181
  var objectSlug = config.objectSlug;
164
182
  var objectSchema = config.objectSchema || null;
165
- var placeholder = config.placeholder || "Select records";
166
- var searchPlaceholder = config.searchPlaceholder || "Search " + (objectSlug || "") + "...";
167
- var summaryLabel = config.label || "selected";
183
+ var displayPlural = getObjectDisplayName(objectSchema, objectSlug, "plural");
184
+ var placeholder =
185
+ config.placeholder ||
186
+ (displayPlural ? "Select " + displayPlural : "Select records");
187
+ var searchPlaceholder =
188
+ config.searchPlaceholder ||
189
+ "Search " + (displayPlural || objectSlug || "") + "...";
190
+ var summaryLabel = config.label || (displayPlural || "selected");
168
191
  var onValuesChange = config.onValuesChange;
169
192
  var variant = config.variant || "default";
170
193
  var size = config.size || "default";
@@ -320,7 +343,7 @@
320
343
  },
321
344
  });
322
345
  var inputEl = searchInputWrapper.getInput();
323
- if (inputEl) inputEl.setAttribute("aria-label", "Search records");
346
+ if (inputEl) inputEl.setAttribute("aria-label", "Search " + (displayPlural || "records"));
324
347
  searchInputEl = inputEl;
325
348
  searchWrap.appendChild(searchInputWrapper);
326
349
  } else {
@@ -337,7 +360,7 @@
337
360
  searchInput.className =
338
361
  "w-full bg-transparent text-reg-13 text-typography-primary-text placeholder:text-typography-quaternary-text focus:outline-none border-none";
339
362
  searchInput.placeholder = searchPlaceholder;
340
- searchInput.setAttribute("aria-label", "Search records");
363
+ searchInput.setAttribute("aria-label", "Search " + (displayPlural || "records"));
341
364
  searchInputEl = searchInput;
342
365
  fallbackWrapper.appendChild(searchInput);
343
366
  searchWrap.appendChild(fallbackWrapper);
@@ -121,6 +121,24 @@
121
121
  return global.Icon;
122
122
  }
123
123
 
124
+ /**
125
+ * Get display name for the object from ObjectType: display_singular_name, display_plural_name, or slug.
126
+ * @param {{ display_singular_name?: string, display_plural_name?: string, slug?: string } | null} objectSchema - optional ObjectType
127
+ * @param {string} slug - objectSlug fallback
128
+ * @param {'singular'|'plural'} kind - which name to return
129
+ * @returns {string}
130
+ */
131
+ function getObjectDisplayName(objectSchema, slug, kind) {
132
+ var s = objectSchema;
133
+ var str = function (v) {
134
+ return typeof v === "string" && v ? v : "";
135
+ };
136
+ if (kind === "singular") {
137
+ return str(s && s.display_singular_name) || str(s && s.slug) || slug || "";
138
+ }
139
+ return str(s && s.display_plural_name) || str(s && s.display_singular_name) || str(s && s.slug) || slug || "";
140
+ }
141
+
124
142
  /**
125
143
  * Get iconStr and color for the object: from objectSchema.properties.icon_data first, else OBJECT_SLUG_TO_ICON, else IconDatabase fallback.
126
144
  * @param {string} slug - objectSlug
@@ -174,16 +192,21 @@
174
192
  * @param {boolean} [config.canClear] - Show clear button when value is set
175
193
  * @param {number} [config.initialLimit] - Initial fetch limit (default 50)
176
194
  * @param {Object} [config.initialFilter] - Optional filter object to merge with search (e.g. { field: "status", operator: "exact", value: "active" } or { and: [...] })
177
- * @param {Object} [config.objectSchema] - Optional object type/schema; properties.icon_data { svg?, color? } used for static icon (not used for user; user shows Vivid Avatar)
195
+ * @param {Object} [config.objectSchema] - Optional ObjectType; display_singular_name/display_plural_name/slug used for placeholder/search text; properties.icon_data { icon?, color? } for static icon (not used for user; user shows Vivid Avatar)
178
196
  * @returns {HTMLElement} Record select container element
179
197
  */
180
198
  function createRecordSelect(config) {
181
199
  var fieldId = config.fieldId;
182
200
  var objectSlug = config.objectSlug;
183
201
  var objectSchema = config.objectSchema || null;
184
- var placeholder = config.placeholder || "Select a record";
202
+ var displaySingular = getObjectDisplayName(objectSchema, objectSlug, "singular");
203
+ var displayPlural = getObjectDisplayName(objectSchema, objectSlug, "plural");
204
+ var placeholder =
205
+ config.placeholder ||
206
+ (displaySingular ? "Select a " + displaySingular : "Select a record");
185
207
  var searchPlaceholder =
186
- config.searchPlaceholder || "Search " + (objectSlug || "") + "...";
208
+ config.searchPlaceholder ||
209
+ "Search " + (displayPlural || objectSlug || "") + "...";
187
210
  var onChange = config.onChange;
188
211
  var variant = config.variant || "default";
189
212
  var size = config.size || "default";
@@ -317,7 +340,7 @@
317
340
  },
318
341
  });
319
342
  var inputEl = searchInputWrapper.getInput();
320
- if (inputEl) inputEl.setAttribute("aria-label", "Search records");
343
+ if (inputEl) inputEl.setAttribute("aria-label", "Search " + (displayPlural || "records"));
321
344
  searchInputEl = inputEl;
322
345
  searchWrap.appendChild(searchInputWrapper);
323
346
  } else {
@@ -334,7 +357,7 @@
334
357
  searchInput.className =
335
358
  "w-full bg-transparent text-reg-13 text-typography-primary-text placeholder:text-typography-quaternary-text focus:outline-none border-none";
336
359
  searchInput.placeholder = searchPlaceholder;
337
- searchInput.setAttribute("aria-label", "Search records");
360
+ searchInput.setAttribute("aria-label", "Search " + (displayPlural || "records"));
338
361
  searchInputEl = searchInput;
339
362
  fallbackWrapper.appendChild(searchInput);
340
363
  searchWrap.appendChild(fallbackWrapper);
package/core/bridge.js CHANGED
@@ -27,7 +27,7 @@
27
27
  // Closure state (reset on each connect / disconnect cycle)
28
28
  // ---------------------------------------------------------------------------
29
29
 
30
- var _instanceId = null;
30
+ var _bridgeId = null;
31
31
  var _nonce = null;
32
32
  var _port = null; // MessagePort (iframe transport, post-handshake)
33
33
  var _connected = false;
@@ -44,47 +44,6 @@
44
44
  // Helpers – crypto
45
45
  // ---------------------------------------------------------------------------
46
46
 
47
- function generateUUID() {
48
- // crypto.randomUUID where available (modern browsers)
49
- if (
50
- typeof crypto !== "undefined" &&
51
- typeof crypto.randomUUID === "function"
52
- ) {
53
- return crypto.randomUUID();
54
- }
55
- // Fallback: manual v4 UUID via getRandomValues or Math.random
56
- var bytes;
57
- if (
58
- typeof crypto !== "undefined" &&
59
- typeof crypto.getRandomValues === "function"
60
- ) {
61
- bytes = new Uint8Array(16);
62
- crypto.getRandomValues(bytes);
63
- } else {
64
- bytes = new Uint8Array(16);
65
- for (var i = 0; i < 16; i++) {
66
- bytes[i] = (Math.random() * 256) | 0;
67
- }
68
- }
69
- bytes[6] = (bytes[6] & 0x0f) | 0x40; // version 4
70
- bytes[8] = (bytes[8] & 0x3f) | 0x80; // variant 1
71
- var hex = [];
72
- for (var j = 0; j < 16; j++) {
73
- hex.push(("0" + bytes[j].toString(16)).slice(-2));
74
- }
75
- return (
76
- hex.slice(0, 4).join("") +
77
- "-" +
78
- hex.slice(4, 6).join("") +
79
- "-" +
80
- hex.slice(6, 8).join("") +
81
- "-" +
82
- hex.slice(8, 10).join("") +
83
- "-" +
84
- hex.slice(10).join("")
85
- );
86
- }
87
-
88
47
  function generateNonce() {
89
48
  var bytes;
90
49
  if (
@@ -135,7 +94,7 @@
135
94
  function createEnvelope(action, payload) {
136
95
  return {
137
96
  type: MESSAGE_TYPE,
138
- instanceId: _instanceId,
97
+ bridgeId: _bridgeId,
139
98
  action: action,
140
99
  payload: payload || {},
141
100
  };
@@ -185,8 +144,8 @@
185
144
  function handleIncomingMessage(data, origin) {
186
145
  // Ignore messages that aren't ours
187
146
  if (!data || data.type !== MESSAGE_TYPE) return;
188
- // Ignore messages for a different instance
189
- if (data.instanceId && data.instanceId !== _instanceId) return;
147
+ // Ignore messages for a different bridge
148
+ if (data.bridgeId && data.bridgeId !== _bridgeId) return;
190
149
 
191
150
  var action = data.action;
192
151
 
@@ -319,7 +278,7 @@
319
278
  if (_transport === "react-native" && global.__superleapBridge) {
320
279
  delete global.__superleapBridge;
321
280
  }
322
- _instanceId = null;
281
+ _bridgeId = null;
323
282
  _nonce = null;
324
283
  _connected = false;
325
284
  _transport = null;
@@ -337,10 +296,10 @@
337
296
  * Initiate a handshake with the host CRM.
338
297
  *
339
298
  * @param {Object} options
340
- * @param {string} options.flowId identifies this flow to the CRM
341
- * @param {string} [options.version] – library version (informational)
299
+ * @param {string} [options.bridgeId] explicit bridgeId (auto-read from URL if omitted)
300
+ * @param {string} [options.version] – library version (informational)
342
301
  * @param {string} [options.crmOrigin] – expected CRM origin for validation
343
- * @param {number} [options.timeout] – handshake timeout in ms (default 5000)
302
+ * @param {number} [options.timeout] – handshake timeout in ms (default 5000)
344
303
  * @returns {Promise<Object>} resolves with the CRM's handshake-ack payload
345
304
  */
346
305
  function connect(options) {
@@ -362,7 +321,18 @@
362
321
  return;
363
322
  }
364
323
 
365
- _instanceId = generateUUID();
324
+ // Read bridgeId from URL query param (set by CRM) or explicit option
325
+ var urlParams = new URLSearchParams(global.location.search);
326
+ _bridgeId = opts.bridgeId || urlParams.get("_bridgeId");
327
+ if (!_bridgeId) {
328
+ reject(
329
+ new Error(
330
+ "SuperleapBridge: No _bridgeId found in URL. Is this page embedded in the SuperLeap CRM?",
331
+ ),
332
+ );
333
+ return;
334
+ }
335
+
366
336
  _nonce = generateNonce();
367
337
  _crmOrigin = opts.crmOrigin || null;
368
338
  _pendingResolve = resolve;
@@ -409,9 +379,8 @@
409
379
 
410
380
  // --- Send handshake-request ---
411
381
  var envelope = createEnvelope("handshake-request", {
412
- flowId: opts.flowId || "",
413
- version: opts.version || "",
414
382
  nonce: _nonce,
383
+ version: opts.version || "",
415
384
  });
416
385
  sendRaw(envelope);
417
386
 
@@ -500,8 +469,8 @@
500
469
  /**
501
470
  * @returns {string|null}
502
471
  */
503
- function getInstanceId() {
504
- return _instanceId;
472
+ function getBridgeId() {
473
+ return _bridgeId;
505
474
  }
506
475
 
507
476
  // ---------------------------------------------------------------------------
@@ -515,7 +484,7 @@
515
484
  onMessage: onMessage,
516
485
  offMessage: offMessage,
517
486
  isConnected: isConnected,
518
- getInstanceId: getInstanceId,
487
+ getBridgeId: getBridgeId,
519
488
  };
520
489
 
521
490
  if (global) {
package/core/crm.js CHANGED
@@ -10,18 +10,9 @@
10
10
  * - Context management (orgId, userId, etc.)
11
11
  * - Custom event send / listen with namespacing
12
12
  *
13
- * Usage (inside an iframe that imports superleap-flow):
14
- *
15
- * const { context, config } = await SuperLeap.connect({
16
- * flowId: 'my-onboarding-form',
17
- * });
18
- * // SDK is now initialized, context has orgId/userId/etc.
19
- *
20
- * SuperLeap.toast('Record saved!', 'success');
21
- * SuperLeap.closeForm();
22
- *
23
- * SuperLeap.on('prefill', (data) => { ... });
24
- * SuperLeap.send('form-submitted', { values });
13
+ * The library auto-connects when loaded inside a CRM iframe (bridgeId is
14
+ * read from the URL). User code should listen for the 'superleap-flow:ready'
15
+ * event — by the time it fires, the SDK is initialized and context is available.
25
16
  *
26
17
  * This extends the existing SuperLeap API from superleapClient.js which
27
18
  * already has: init(), getSdk(), isAvailable(), getDefaultConfig().
@@ -38,6 +29,7 @@
38
29
  var _config = null; // full config from CRM
39
30
  var _bridge = null; // resolved reference to SuperleapBridge
40
31
  var _connected = false;
32
+ var _connectPromise = null; // in-flight connect promise (deduplication)
41
33
 
42
34
  // Library version — sent during handshake so the CRM knows what it's talking to
43
35
  var LIB_VERSION = "2.1.0";
@@ -100,14 +92,27 @@
100
92
  * credentials and context, and (by default) auto-initializes the
101
93
  * SuperLeap SDK.
102
94
  *
103
- * @param {Object} options
104
- * @param {string} options.flowId – identifies this flow to the CRM
105
- * @param {string} [options.crmOrigin] – expected CRM origin for validation
106
- * @param {boolean} [options.autoInit] – auto-call SuperLeap.init() (default true)
107
- * @param {number} [options.timeout] – handshake timeout in ms (default 5000)
95
+ * If already connected, silently resolves with the existing context/config.
96
+ * If a connection is in flight (e.g. auto-connect), returns the same promise.
97
+ *
98
+ * @param {Object} [options]
99
+ * @param {string} [options.bridgeId] – explicit bridgeId override (auto-read from URL)
100
+ * @param {string} [options.crmOrigin] – expected CRM origin for validation
101
+ * @param {boolean} [options.autoInit] – auto-call SuperLeap.init() (default true)
102
+ * @param {number} [options.timeout] – handshake timeout in ms (default 5000)
108
103
  * @returns {Promise<{ context: Object, config: Object }>}
109
104
  */
110
105
  function connect(options) {
106
+ // Already connected — silently resolve
107
+ if (_connected) {
108
+ return Promise.resolve({ context: _context, config: _config });
109
+ }
110
+
111
+ // Connection in flight (e.g. auto-connect started) — return same promise
112
+ if (_connectPromise) {
113
+ return _connectPromise;
114
+ }
115
+
111
116
  var opts = options || {};
112
117
  var bridge = getBridge();
113
118
 
@@ -117,17 +122,9 @@
117
122
  );
118
123
  }
119
124
 
120
- if (_connected) {
121
- return Promise.reject(
122
- new Error(
123
- "SuperleapCRM: Already connected. Call disconnect() first.",
124
- ),
125
- );
126
- }
127
-
128
- return bridge
125
+ _connectPromise = bridge
129
126
  .connect({
130
- flowId: opts.flowId || "",
127
+ bridgeId: opts.bridgeId,
131
128
  version: LIB_VERSION,
132
129
  crmOrigin: opts.crmOrigin,
133
130
  timeout: opts.timeout,
@@ -146,6 +143,7 @@
146
143
  }
147
144
 
148
145
  _connected = true;
146
+ _connectPromise = null;
149
147
 
150
148
  // Listen for context pushes from the CRM
151
149
  bridge.onMessage("data:context", function (newCtx) {
@@ -169,7 +167,13 @@
169
167
  }
170
168
 
171
169
  return { context: _context, config: _config };
170
+ })
171
+ .catch(function (err) {
172
+ _connectPromise = null;
173
+ throw err;
172
174
  });
175
+
176
+ return _connectPromise;
173
177
  }
174
178
 
175
179
  /**
@@ -190,6 +194,7 @@
190
194
  _context = null;
191
195
  _config = null;
192
196
  _connected = false;
197
+ _connectPromise = null;
193
198
  _bridge = null;
194
199
  }
195
200