@nyaruka/temba-components 0.160.0 → 0.161.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": "@nyaruka/temba-components",
3
- "version": "0.160.0",
3
+ "version": "0.161.0",
4
4
  "description": "Web components to support rapidpro and related projects",
5
5
  "author": "Nyaruka <code@nyaruka.coim>",
6
6
  "main": "dist/index.js",
@@ -200,7 +200,10 @@ export const split_by_webhook: NodeConfig = {
200
200
  callWebhookAction?.headers ||
201
201
  getDefaultHeadersRecord(callWebhookAction?.method || 'GET'),
202
202
  body: callWebhookAction?.body || '',
203
- result_name: node.router?.result_name || ''
203
+ // Legacy webhooks store result_name on the action; modern ones store it
204
+ // on the router. Prefer the action so legacy nodes round-trip correctly.
205
+ result_name:
206
+ callWebhookAction?.result_name || node.router?.result_name || ''
204
207
  };
205
208
  },
206
209
  fromFormData: (formData: FormData, originalNode: Node): Node => {
@@ -213,9 +216,17 @@ export const split_by_webhook: NodeConfig = {
213
216
  // Find existing call_webhook action to preserve its UUID
214
217
  const existingCallWebhookAction = originalNode.actions?.find(
215
218
  (action) => action.type === 'call_webhook'
216
- );
219
+ ) as CallWebhook | undefined;
217
220
  const callWebhookUuid = existingCallWebhookAction?.uuid || generateUUID();
218
221
 
222
+ // Legacy webhooks keep the result_name on the action rather than the
223
+ // router. Detect them by the presence of result_name on the existing
224
+ // action so they continue to round-trip in their original format instead
225
+ // of being silently migrated (which would change how results are
226
+ // referenced downstream of the flow).
227
+ const isLegacyResultName = !!existingCallWebhookAction?.result_name;
228
+ const trimmedResultName = formData.result_name?.trim() || '';
229
+
219
230
  // Create call_webhook action
220
231
  const callWebhookAction: CallWebhook = {
221
232
  type: 'call_webhook',
@@ -226,6 +237,11 @@ export const split_by_webhook: NodeConfig = {
226
237
  body: formData.body || ''
227
238
  };
228
239
 
240
+ // Preserve the legacy placement: result_name stays on the action.
241
+ if (isLegacyResultName && trimmedResultName !== '') {
242
+ callWebhookAction.result_name = trimmedResultName;
243
+ }
244
+
229
245
  // Create categories and exits for Success and Failure
230
246
  const existingCategories = originalNode.router?.categories || [];
231
247
  const existingExits = originalNode.exits || [];
@@ -247,9 +263,11 @@ export const split_by_webhook: NodeConfig = {
247
263
  ...router
248
264
  };
249
265
 
250
- // Only set result_name if provided
251
- if (formData.result_name && formData.result_name.trim() !== '') {
252
- finalRouter.result_name = formData.result_name.trim();
266
+ // Modern webhooks store result_name on the router. For legacy webhooks the
267
+ // result_name lives on the action (set above), so we leave it off the
268
+ // router to avoid storing it in both places.
269
+ if (!isLegacyResultName && trimmedResultName !== '') {
270
+ finalRouter.result_name = trimmedResultName;
253
271
  }
254
272
 
255
273
  // Return the complete node
@@ -1,7 +1,7 @@
1
1
  import { css, html, TemplateResult } from 'lit';
2
2
  import { ContentList, ContentListColumn } from './ContentList';
3
3
  import { Icon } from '../Icons';
4
- import { Flow, ObjectReference } from '../interfaces';
4
+ import { CustomEventType, Flow, ObjectReference } from '../interfaces';
5
5
 
6
6
  /**
7
7
  * Flow CRUDL list — drop-in replacement for the rapidpro
@@ -89,27 +89,28 @@ export class FlowList extends ContentList<Flow> {
89
89
  key: 'name',
90
90
  label: 'Name',
91
91
  sortable: true,
92
- width: '280px',
92
+ minWidth: '160px',
93
+ maxWidth: '280px',
93
94
  pinned: true
94
95
  },
95
96
  {
96
97
  key: 'runs',
97
98
  label: 'Runs',
98
99
  sortable: true,
99
- width: '90px',
100
+ minWidth: '64px',
100
101
  align: 'right'
101
102
  },
102
103
  {
103
104
  key: 'ongoing',
104
105
  label: 'Ongoing',
105
106
  sortable: true,
106
- width: '90px',
107
+ minWidth: '64px',
107
108
  align: 'right'
108
109
  },
109
110
  {
110
111
  key: 'completion',
111
112
  label: 'Completion',
112
- width: '130px',
113
+ minWidth: '110px',
113
114
  align: 'right'
114
115
  },
115
116
  { key: 'activity', label: 'Activity', width: '120px', align: 'right' }
@@ -146,6 +147,25 @@ export class FlowList extends ContentList<Flow> {
146
147
  return item?.uuid ? `/flow/editor/${item.uuid}/` : null;
147
148
  }
148
149
 
150
+ /** Clicking a label chip opens that label's filtered flow list rather
151
+ * than falling through to the row click (which opens the flow editor). */
152
+ private handleLabelClick(label: ObjectReference, event: MouseEvent): void {
153
+ // Stop the click from bubbling to the row's navigation handler.
154
+ event.stopPropagation();
155
+ if (!label?.uuid) return;
156
+ const href = `/flow/filter/${label.uuid}/`;
157
+ // Guard the JSON-driven href against open-redirect, same as the
158
+ // row-click path in ContentList.handleRowClick.
159
+ if (!this.isSafeHref(href)) return;
160
+ // Meta/ctrl-click opens a new tab, matching ordinary links and the
161
+ // row-click behavior.
162
+ if (event.metaKey || event.ctrlKey) {
163
+ window.open(href, '_blank');
164
+ return;
165
+ }
166
+ this.fireCustomEvent(CustomEventType.Redirected, { url: href });
167
+ }
168
+
149
169
  protected renderCell(
150
170
  item: Flow,
151
171
  column: ContentListColumn
@@ -166,7 +186,11 @@ export class FlowList extends ContentList<Flow> {
166
186
  ? html`<span class="flow-labels">
167
187
  ${labels.map(
168
188
  (l: ObjectReference) =>
169
- html`<temba-label type="label" icon=${Icon.label}
189
+ html`<temba-label
190
+ type="label"
191
+ icon=${Icon.label}
192
+ clickable
193
+ @click=${(e: MouseEvent) => this.handleLabelClick(l, e)}
170
194
  >${l.name}</temba-label
171
195
  >`
172
196
  )}
package/src/utils.ts CHANGED
@@ -1291,9 +1291,19 @@ export const generateDefaultCategoryName = (
1291
1291
 
1292
1292
  // Handle different operator types
1293
1293
  switch (operator) {
1294
- // Word/phrase operators - capitalize first letter of value
1294
+ // Word-list operators - the argument is a list of words, so only the
1295
+ // first word seeds the category name (e.g. "red maroon fire" -> "Red").
1296
+ // Trailing punctuation is stripped so comma-separated lists like
1297
+ // "red, blue, green" yield "Red" rather than "Red,".
1295
1298
  case 'has_any_word':
1296
- case 'has_all_words':
1299
+ case 'has_all_words': {
1300
+ const firstWord = cleanValue1
1301
+ .split(/\s+/)[0]
1302
+ .replace(/[^\p{L}\p{N}]+$/u, '');
1303
+ return firstWord ? capitalize(firstWord) : '';
1304
+ }
1305
+
1306
+ // Phrase operators - the argument is a single phrase, capitalize as-is
1297
1307
  case 'has_phrase':
1298
1308
  case 'has_only_phrase':
1299
1309
  case 'has_beginning':