@nyaruka/temba-components 0.156.15 → 0.156.17
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/CHANGELOG.md +15 -0
- package/dist/temba-components.js +270 -259
- package/dist/temba-components.js.map +1 -1
- package/package.json +1 -1
- package/src/display/Options.ts +8 -0
- package/src/excellent/caret-utils.ts +179 -55
- package/src/flow/Editor.ts +11 -17
- package/src/flow/RevisionsWindow.ts +142 -36
- package/src/flow/revision-summary.ts +62 -0
- package/src/flow/types.ts +1 -2
- package/src/form/RichEditor.ts +21 -0
- package/web-test-runner.config.mjs +7 -7
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export interface RevisionChanges {
|
|
2
|
+
tags: string[];
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
const TAG_LABELS: Record<string, { label: string; order: number }> = {
|
|
6
|
+
metadata: { label: 'metadata', order: 0 },
|
|
7
|
+
nodes: { label: 'nodes', order: 1 },
|
|
8
|
+
routing: { label: 'routing', order: 2 },
|
|
9
|
+
actions: { label: 'actions', order: 3 },
|
|
10
|
+
stickies: { label: 'stickies', order: 5 },
|
|
11
|
+
layout: { label: 'layout', order: 6 }
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
function tagToLabel(tag: string): { label: string; order: number } | null {
|
|
15
|
+
if (Object.prototype.hasOwnProperty.call(TAG_LABELS, tag)) {
|
|
16
|
+
return TAG_LABELS[tag];
|
|
17
|
+
}
|
|
18
|
+
if (tag.startsWith('localization:')) {
|
|
19
|
+
return { label: 'translations', order: 4 };
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function labelsFor(
|
|
25
|
+
changes: RevisionChanges | null | undefined
|
|
26
|
+
): Set<string> {
|
|
27
|
+
const result = new Set<string>();
|
|
28
|
+
for (const tag of changes?.tags || []) {
|
|
29
|
+
const entry = tagToLabel(tag);
|
|
30
|
+
if (entry) result.add(entry.label);
|
|
31
|
+
}
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function summarizeChanges(
|
|
36
|
+
changes: RevisionChanges | null | undefined
|
|
37
|
+
): string {
|
|
38
|
+
if (!changes) return '';
|
|
39
|
+
const tags = changes.tags || [];
|
|
40
|
+
if (tags.length === 0) return '';
|
|
41
|
+
|
|
42
|
+
const orders = new Map<string, number>();
|
|
43
|
+
for (const tag of tags) {
|
|
44
|
+
const entry = tagToLabel(tag);
|
|
45
|
+
if (entry && !orders.has(entry.label)) {
|
|
46
|
+
orders.set(entry.label, entry.order);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (orders.size === 0) return '';
|
|
50
|
+
|
|
51
|
+
const labels = Array.from(orders.entries())
|
|
52
|
+
.sort((a, b) => a[1] - b[1])
|
|
53
|
+
.map(([label]) => label);
|
|
54
|
+
|
|
55
|
+
return `Changed ${joinNaturally(labels)}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function joinNaturally(parts: string[]): string {
|
|
59
|
+
if (parts.length <= 1) return parts.join('');
|
|
60
|
+
if (parts.length === 2) return `${parts[0]} and ${parts[1]}`;
|
|
61
|
+
return `${parts.slice(0, -1).join(', ')}, and ${parts[parts.length - 1]}`;
|
|
62
|
+
}
|
package/src/flow/types.ts
CHANGED
|
@@ -50,8 +50,7 @@ export const CONTEXT_MENU_SHORTCUTS: Record<FlowType, ContextMenuShortcut[]> = {
|
|
|
50
50
|
export const Features = {
|
|
51
51
|
AI: 'ai',
|
|
52
52
|
AIRTIME: 'airtime',
|
|
53
|
-
LOCATIONS: 'locations'
|
|
54
|
-
AUTO_TRANSLATE: 'auto_translate'
|
|
53
|
+
LOCATIONS: 'locations'
|
|
55
54
|
} as const;
|
|
56
55
|
|
|
57
56
|
export type Feature = (typeof Features)[keyof typeof Features];
|
package/src/form/RichEditor.ts
CHANGED
|
@@ -97,6 +97,7 @@ export class RichEditor extends FieldElement {
|
|
|
97
97
|
overflow-y: auto;
|
|
98
98
|
min-height: var(--textarea-min-height, 100px);
|
|
99
99
|
resize: none;
|
|
100
|
+
position: relative;
|
|
100
101
|
}
|
|
101
102
|
|
|
102
103
|
:host(:not([textarea])) {
|
|
@@ -119,6 +120,12 @@ export class RichEditor extends FieldElement {
|
|
|
119
120
|
content: attr(data-placeholder);
|
|
120
121
|
color: var(--color-placeholder, #999);
|
|
121
122
|
pointer-events: none;
|
|
123
|
+
/* Take the placeholder out of flow so the caret sits at offset 0
|
|
124
|
+
visually, not after the placeholder text (Firefox honors flow
|
|
125
|
+
strictly here while Chrome happens to overlay the caret). */
|
|
126
|
+
position: absolute;
|
|
127
|
+
inset: 0;
|
|
128
|
+
padding: inherit;
|
|
122
129
|
}
|
|
123
130
|
|
|
124
131
|
/* Token styles (shared) */
|
|
@@ -302,6 +309,11 @@ export class RichEditor extends FieldElement {
|
|
|
302
309
|
|
|
303
310
|
if (this.disableCompletion) {
|
|
304
311
|
div.textContent = text || '';
|
|
312
|
+
if (text && text.endsWith('\n')) {
|
|
313
|
+
const br = document.createElement('br');
|
|
314
|
+
br.setAttribute('data-sentinel', '');
|
|
315
|
+
div.appendChild(br);
|
|
316
|
+
}
|
|
305
317
|
return;
|
|
306
318
|
}
|
|
307
319
|
|
|
@@ -359,6 +371,15 @@ export class RichEditor extends FieldElement {
|
|
|
359
371
|
// Ensure there's at least an empty text node for cursor placement
|
|
360
372
|
if (!text || text === '') {
|
|
361
373
|
div.appendChild(document.createTextNode(''));
|
|
374
|
+
} else if (text.endsWith('\n')) {
|
|
375
|
+
// A trailing "\n" text node is collapsed by Firefox in contenteditable,
|
|
376
|
+
// making the empty final line invisible. A sentinel <br> forces the
|
|
377
|
+
// line break to render. The data-sentinel attribute distinguishes it
|
|
378
|
+
// from browser-inserted <br>s (which represent real newlines and must
|
|
379
|
+
// be preserved by the caret/text utilities).
|
|
380
|
+
const br = document.createElement('br');
|
|
381
|
+
br.setAttribute('data-sentinel', '');
|
|
382
|
+
div.appendChild(br);
|
|
362
383
|
}
|
|
363
384
|
}
|
|
364
385
|
|
|
@@ -140,13 +140,13 @@ const checkScreenshot = async (filename, excluded, threshold) => {
|
|
|
140
140
|
});
|
|
141
141
|
};
|
|
142
142
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
143
|
+
// clear out any past tests once per process — clearing per-page would race
|
|
144
|
+
// with other concurrent pages that have already written test screenshots and
|
|
145
|
+
// are about to read them back, causing intermittent ENOENT failures.
|
|
146
|
+
rimraf.sync(path.resolve(SCREENSHOTS, DIFF));
|
|
147
|
+
rimraf.sync(path.resolve(SCREENSHOTS, TEST));
|
|
147
148
|
|
|
148
|
-
|
|
149
|
-
rimraf.sync(tests);
|
|
149
|
+
const wireScreenshots = async (page, context, wait, replaceScreenshots) => {
|
|
150
150
|
|
|
151
151
|
await page.exposeFunction(
|
|
152
152
|
'matchPageSnapshot',
|
|
@@ -335,7 +335,7 @@ const wireScreenshots = async (page, context, wait, replaceScreenshots) => {
|
|
|
335
335
|
|
|
336
336
|
export default {
|
|
337
337
|
rootDir: './',
|
|
338
|
-
files: '**/test/**/*.test.ts',
|
|
338
|
+
files: ['**/test/**/*.test.ts', '!**/test/utils.test.ts'],
|
|
339
339
|
nodeResolve: true,
|
|
340
340
|
concurrency: 4,
|
|
341
341
|
filterBrowserLogs(log) {
|