@nyaruka/temba-components 0.124.2 → 0.125.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/.eslintrc.js +3 -1
- package/CHANGELOG.md +20 -0
- package/demo/chart/example.html +346 -26
- package/demo/data/flows/sample-flow.json +1072 -0
- package/demo/data/server/opened-tickets.json +15 -3
- package/demo/data/server/sample-flow.json +0 -0
- package/demo/flow/example.html +46 -0
- package/demo/index.html +155 -144
- package/demo/webchat/example.html +71 -0
- package/dist/temba-components.js +255 -245
- package/dist/temba-components.js.map +1 -1
- package/out-tsc/src/chart/TembaChart.js +395 -65
- package/out-tsc/src/chart/TembaChart.js.map +1 -1
- package/out-tsc/src/flow/EditorNode.js +2 -1
- package/out-tsc/src/flow/EditorNode.js.map +1 -1
- package/out-tsc/src/flow/config.js +70 -20
- package/out-tsc/src/flow/config.js.map +1 -1
- package/out-tsc/src/formfield/FormField.js +4 -1
- package/out-tsc/src/formfield/FormField.js.map +1 -1
- package/out-tsc/src/store/Store.js +1 -0
- package/out-tsc/src/store/Store.js.map +1 -1
- package/out-tsc/src/utils/index.js +40 -0
- package/out-tsc/src/utils/index.js.map +1 -1
- package/out-tsc/src/webchat/WebChat.js +2 -0
- package/out-tsc/src/webchat/WebChat.js.map +1 -1
- package/out-tsc/test/temba-chart.test.js +6 -18
- package/out-tsc/test/temba-chart.test.js.map +1 -1
- package/out-tsc/test/temba-formfield.test.js +94 -0
- package/out-tsc/test/temba-formfield.test.js.map +1 -0
- package/out-tsc/test/temba-integration-markdown.test.js +36 -0
- package/out-tsc/test/temba-integration-markdown.test.js.map +1 -0
- package/out-tsc/test/temba-select.test.js +14 -1
- package/out-tsc/test/temba-select.test.js.map +1 -1
- package/package.json +2 -1
- package/screenshots/truth/formfield/markdown-errors.png +0 -0
- package/screenshots/truth/formfield/no-errors.png +0 -0
- package/screenshots/truth/formfield/plain-text-errors.png +0 -0
- package/screenshots/truth/formfield/widget-only-markdown-errors.png +0 -0
- package/screenshots/truth/integration/checkbox-markdown-errors.png +0 -0
- package/src/chart/TembaChart.ts +418 -71
- package/src/flow/EditorNode.ts +2 -1
- package/src/flow/config.ts +71 -20
- package/src/formfield/FormField.ts +4 -1
- package/src/store/Store.ts +1 -0
- package/src/utils/index.ts +43 -0
- package/src/webchat/WebChat.ts +2 -0
- package/test/temba-chart.test.ts +7 -23
- package/test/temba-formfield.test.ts +121 -0
- package/test/temba-integration-markdown.test.ts +45 -0
- package/test/temba-select.test.ts +17 -0
- package/web-dev-server.config.mjs +18 -0
package/src/flow/config.ts
CHANGED
|
@@ -13,58 +13,89 @@ export interface UIConfig {
|
|
|
13
13
|
render?: (node: any, action: any) => TemplateResult;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
const COLORS = {
|
|
17
|
+
send: '#3498db',
|
|
18
|
+
update: '#01c1af',
|
|
19
|
+
broadcast: '#8e5ea7',
|
|
20
|
+
call: '#e68628',
|
|
21
|
+
create: '#df419f',
|
|
22
|
+
save: '#1a777c',
|
|
23
|
+
split: '#aaaaaa',
|
|
24
|
+
execute: '#666666',
|
|
25
|
+
wait: '#4d7dad',
|
|
26
|
+
add: '#309c42',
|
|
27
|
+
remove: '#e74c3c'
|
|
28
|
+
};
|
|
29
|
+
|
|
16
30
|
export const EDITOR_CONFIG: {
|
|
17
31
|
[key: string]: UIConfig;
|
|
18
32
|
} = {
|
|
19
33
|
add_input_labels: {
|
|
20
34
|
name: 'Add Labels',
|
|
21
|
-
color:
|
|
35
|
+
color: COLORS.update
|
|
22
36
|
},
|
|
23
37
|
add_contact_urn: {
|
|
24
38
|
name: 'Add Contact URN',
|
|
25
|
-
color:
|
|
39
|
+
color: COLORS.update
|
|
26
40
|
},
|
|
27
41
|
set_contact_field: {
|
|
28
42
|
name: 'Update Contact Field',
|
|
29
|
-
color:
|
|
43
|
+
color: COLORS.update
|
|
44
|
+
},
|
|
45
|
+
set_contact_channel: {
|
|
46
|
+
name: 'Update Contact Channel',
|
|
47
|
+
color: COLORS.update
|
|
48
|
+
},
|
|
49
|
+
set_contact_language: {
|
|
50
|
+
name: 'Update Contact Language',
|
|
51
|
+
color: COLORS.update
|
|
30
52
|
},
|
|
31
53
|
send_broadcast: {
|
|
32
54
|
name: 'Send Broadcast',
|
|
33
|
-
color:
|
|
55
|
+
color: COLORS.broadcast
|
|
34
56
|
},
|
|
35
57
|
set_run_result: {
|
|
36
58
|
name: 'Save Flow Result',
|
|
37
|
-
color:
|
|
59
|
+
color: COLORS.save,
|
|
38
60
|
render: renderSetRunResult
|
|
39
61
|
},
|
|
40
62
|
send_msg: {
|
|
41
63
|
name: 'Send Message',
|
|
42
|
-
color:
|
|
64
|
+
color: COLORS.send,
|
|
43
65
|
render: renderSendMsg
|
|
44
66
|
},
|
|
45
67
|
send_email: {
|
|
46
68
|
name: 'Send Email',
|
|
47
|
-
color:
|
|
69
|
+
color: COLORS.broadcast
|
|
70
|
+
},
|
|
71
|
+
start_session: {
|
|
72
|
+
name: 'Start Somebody Else',
|
|
73
|
+
color: COLORS.broadcast
|
|
74
|
+
},
|
|
75
|
+
open_ticket: {
|
|
76
|
+
name: 'Open Ticket',
|
|
77
|
+
color: COLORS.execute
|
|
48
78
|
},
|
|
49
|
-
start_session: { name: 'Start Somebody Else', color: '#df419f' },
|
|
50
79
|
call_webhook: {
|
|
51
80
|
name: 'Call Webhook',
|
|
52
|
-
color:
|
|
81
|
+
color: COLORS.call,
|
|
53
82
|
render: renderCallWebhook
|
|
54
83
|
},
|
|
84
|
+
enter_flow: {
|
|
85
|
+
name: 'Enter Subflow',
|
|
86
|
+
color: COLORS.execute
|
|
87
|
+
},
|
|
55
88
|
call_llm: {
|
|
56
89
|
name: 'Call AI',
|
|
57
|
-
color:
|
|
90
|
+
color: COLORS.call
|
|
58
91
|
},
|
|
59
92
|
transfer_airtime: {
|
|
60
93
|
name: 'Send Airtime',
|
|
61
|
-
color:
|
|
94
|
+
color: COLORS.call
|
|
62
95
|
},
|
|
63
|
-
wait_for_response: {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
name: 'Split by <Contact Field Name>',
|
|
67
|
-
color: '#aaaaaa'
|
|
96
|
+
wait_for_response: {
|
|
97
|
+
name: 'Wait for Response',
|
|
98
|
+
color: COLORS.wait
|
|
68
99
|
},
|
|
69
100
|
set_contact_name: {
|
|
70
101
|
name: 'Update Contact',
|
|
@@ -73,19 +104,39 @@ export const EDITOR_CONFIG: {
|
|
|
73
104
|
},
|
|
74
105
|
add_contact_groups: {
|
|
75
106
|
name: 'Add to Group',
|
|
76
|
-
color:
|
|
107
|
+
color: COLORS.add,
|
|
77
108
|
render: renderAddToGroups
|
|
78
109
|
},
|
|
79
110
|
remove_contact_groups: {
|
|
80
111
|
name: 'Remove from Group',
|
|
81
|
-
color:
|
|
112
|
+
color: COLORS.remove
|
|
82
113
|
},
|
|
83
114
|
request_optin: {
|
|
84
115
|
name: 'Request Opt-in',
|
|
85
|
-
color:
|
|
116
|
+
color: COLORS.send
|
|
86
117
|
},
|
|
87
118
|
split_by_run_result: {
|
|
88
119
|
name: 'Split by Flow Result',
|
|
89
|
-
color:
|
|
120
|
+
color: COLORS.split
|
|
121
|
+
},
|
|
122
|
+
split_by_expression: {
|
|
123
|
+
name: 'Split by Expression',
|
|
124
|
+
color: COLORS.split
|
|
125
|
+
},
|
|
126
|
+
split_by_contact_field: {
|
|
127
|
+
name: 'Split by <Contact Field Name>',
|
|
128
|
+
color: COLORS.split
|
|
129
|
+
},
|
|
130
|
+
split_by_groups: {
|
|
131
|
+
name: 'Split by Group',
|
|
132
|
+
color: COLORS.split
|
|
133
|
+
},
|
|
134
|
+
split_by_scheme: {
|
|
135
|
+
name: 'Split by URN Type',
|
|
136
|
+
color: COLORS.split
|
|
137
|
+
},
|
|
138
|
+
split_by_random: {
|
|
139
|
+
name: 'Split by Random',
|
|
140
|
+
color: COLORS.split
|
|
90
141
|
}
|
|
91
142
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { TemplateResult, html, css, LitElement } from 'lit';
|
|
2
2
|
import { property } from 'lit/decorators.js';
|
|
3
|
+
import { renderMarkdown } from '../markdown';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* A small wrapper to display labels and help text in a smartmin style.
|
|
@@ -94,7 +95,9 @@ export class FormField extends LitElement {
|
|
|
94
95
|
public render(): TemplateResult {
|
|
95
96
|
const errors = !this.hideErrors
|
|
96
97
|
? (this.errors || []).map((error: string) => {
|
|
97
|
-
return html`
|
|
98
|
+
return html`
|
|
99
|
+
<div class="alert-error">${renderMarkdown(error)}</div>
|
|
100
|
+
`;
|
|
98
101
|
})
|
|
99
102
|
: [];
|
|
100
103
|
|
package/src/store/Store.ts
CHANGED
|
@@ -561,6 +561,7 @@ export class Store extends RapidElement {
|
|
|
561
561
|
const orginalUser = last[parts[parts.length - 1]];
|
|
562
562
|
orginalUser.avatar = user.avatar;
|
|
563
563
|
orginalUser.name = getFullName(user);
|
|
564
|
+
orginalUser.uuid = user.uuid;
|
|
564
565
|
last[parts[parts.length - 1]].avatar = user.avatar;
|
|
565
566
|
}
|
|
566
567
|
});
|
package/src/utils/index.ts
CHANGED
|
@@ -44,14 +44,17 @@ export enum Color {
|
|
|
44
44
|
|
|
45
45
|
export const log = (message: string | object, styling = '', details = []) => {
|
|
46
46
|
if (styling === '') {
|
|
47
|
+
// eslint-disable-next-line no-console
|
|
47
48
|
console.log(message);
|
|
48
49
|
return;
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
if (typeof message === 'object') {
|
|
53
|
+
// eslint-disable-next-line no-console
|
|
52
54
|
console.log('%c' + JSON.stringify(message, null, 2), styling);
|
|
53
55
|
return;
|
|
54
56
|
}
|
|
57
|
+
// eslint-disable-next-line no-console
|
|
55
58
|
console.log('%c' + message, styling, ...details);
|
|
56
59
|
};
|
|
57
60
|
|
|
@@ -802,6 +805,46 @@ export const hslToHex = (h, s, l) => {
|
|
|
802
805
|
return `#${f(0)}${f(8)}${f(4)}`;
|
|
803
806
|
};
|
|
804
807
|
|
|
808
|
+
export const darkenColor = (color: string, factor: number): string => {
|
|
809
|
+
// If rgba or rgb
|
|
810
|
+
const rgbaMatch = color.match(
|
|
811
|
+
/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([0-9.]+))?\)/
|
|
812
|
+
);
|
|
813
|
+
if (rgbaMatch) {
|
|
814
|
+
// eslint-disable-next-line prefer-const
|
|
815
|
+
let [r, g, b, a] = rgbaMatch
|
|
816
|
+
.slice(1)
|
|
817
|
+
.map((v, i) => (i < 3 ? parseInt(v) : parseFloat(v)));
|
|
818
|
+
r = Math.max(0, Math.floor(r * (1 - factor)));
|
|
819
|
+
g = Math.max(0, Math.floor(g * (1 - factor)));
|
|
820
|
+
b = Math.max(0, Math.floor(b * (1 - factor)));
|
|
821
|
+
if (rgbaMatch[4] !== undefined) {
|
|
822
|
+
return `rgba(${r},${g},${b},${a})`;
|
|
823
|
+
}
|
|
824
|
+
return `rgb(${r},${g},${b})`;
|
|
825
|
+
}
|
|
826
|
+
// If hex
|
|
827
|
+
if (color.startsWith('#')) {
|
|
828
|
+
let hex = color.replace('#', '');
|
|
829
|
+
if (hex.length === 3) {
|
|
830
|
+
hex = hex
|
|
831
|
+
.split('')
|
|
832
|
+
.map((c) => c + c)
|
|
833
|
+
.join('');
|
|
834
|
+
}
|
|
835
|
+
const num = parseInt(hex, 16);
|
|
836
|
+
let r = (num >> 16) & 255;
|
|
837
|
+
let g = (num >> 8) & 255;
|
|
838
|
+
let b = num & 255;
|
|
839
|
+
r = Math.max(0, Math.floor(r * (1 - factor)));
|
|
840
|
+
g = Math.max(0, Math.floor(g * (1 - factor)));
|
|
841
|
+
b = Math.max(0, Math.floor(b * (1 - factor)));
|
|
842
|
+
return `rgb(${r},${g},${b})`;
|
|
843
|
+
}
|
|
844
|
+
// fallback
|
|
845
|
+
return color;
|
|
846
|
+
};
|
|
847
|
+
|
|
805
848
|
export const renderAvatar = (input: {
|
|
806
849
|
name?: string;
|
|
807
850
|
user?: User;
|
package/src/webchat/WebChat.ts
CHANGED
|
@@ -412,6 +412,7 @@ export class WebChat extends LitElement {
|
|
|
412
412
|
private sendSockMessage(
|
|
413
413
|
cmd: GetHistoryCmd | StartChatCmd | SendMsgCmd | Ack
|
|
414
414
|
) {
|
|
415
|
+
// eslint-disable-next-line no-console
|
|
415
416
|
console.log('out', cmd);
|
|
416
417
|
this.sock.send(JSON.stringify(cmd));
|
|
417
418
|
}
|
|
@@ -447,6 +448,7 @@ export class WebChat extends LitElement {
|
|
|
447
448
|
this.sock.onmessage = function (event: MessageEvent) {
|
|
448
449
|
webChat.status = ChatStatus.CONNECTED;
|
|
449
450
|
const msg = JSON.parse(event.data) as SockMsg;
|
|
451
|
+
// eslint-disable-next-line no-console
|
|
450
452
|
console.log('in', msg);
|
|
451
453
|
if (msg.type === 'chat_started') {
|
|
452
454
|
const response = msg as StartChatResponse;
|
package/test/temba-chart.test.ts
CHANGED
|
@@ -61,11 +61,11 @@ describe('temba-chart', () => {
|
|
|
61
61
|
const chart: TembaChart = await getChart();
|
|
62
62
|
|
|
63
63
|
// Test that formatDuration property exists and defaults to false
|
|
64
|
-
expect(chart.
|
|
64
|
+
expect(chart.yType).to.equal('count');
|
|
65
65
|
|
|
66
66
|
// Test that we can set formatDuration to true
|
|
67
|
-
chart.
|
|
68
|
-
expect(chart.
|
|
67
|
+
chart.yType = 'duration';
|
|
68
|
+
expect(chart.yType).to.equal('duration');
|
|
69
69
|
});
|
|
70
70
|
|
|
71
71
|
it('formats duration values correctly', async () => {
|
|
@@ -83,7 +83,7 @@ describe('temba-chart', () => {
|
|
|
83
83
|
]
|
|
84
84
|
};
|
|
85
85
|
|
|
86
|
-
chart.
|
|
86
|
+
chart.yType = 'duration';
|
|
87
87
|
chart.data = durationData;
|
|
88
88
|
await chart.updateComplete;
|
|
89
89
|
|
|
@@ -91,7 +91,7 @@ describe('temba-chart', () => {
|
|
|
91
91
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
92
92
|
|
|
93
93
|
// Test that the chart was created and has the duration formatting enabled
|
|
94
|
-
expect(chart.
|
|
94
|
+
expect(chart.yType).to.equal('duration');
|
|
95
95
|
expect(chart.chart).to.exist;
|
|
96
96
|
|
|
97
97
|
// Test that the chart configuration includes the duration formatting
|
|
@@ -115,6 +115,7 @@ describe('temba-chart', () => {
|
|
|
115
115
|
dataset: { label: 'Process Time' },
|
|
116
116
|
parsed: { y: 68787 }
|
|
117
117
|
};
|
|
118
|
+
|
|
118
119
|
expect(tooltipCallback.call({}, mockContext)).to.equal(
|
|
119
120
|
'Process Time: 19h 6m'
|
|
120
121
|
);
|
|
@@ -122,7 +123,7 @@ describe('temba-chart', () => {
|
|
|
122
123
|
|
|
123
124
|
it('formats various duration edge cases correctly', async () => {
|
|
124
125
|
const chart: TembaChart = await getChart();
|
|
125
|
-
chart.
|
|
126
|
+
chart.yType = 'duration';
|
|
126
127
|
chart.data = sampleData;
|
|
127
128
|
await chart.updateComplete;
|
|
128
129
|
|
|
@@ -145,23 +146,6 @@ describe('temba-chart', () => {
|
|
|
145
146
|
expect(tickCallback.call({}, 604800, 9, [])).to.equal('7d'); // 1 week in seconds
|
|
146
147
|
expect(tickCallback.call({}, 1209600, 10, [])).to.equal('14d'); // 2 weeks in seconds
|
|
147
148
|
});
|
|
148
|
-
|
|
149
|
-
it('respects formatDuration property state', async () => {
|
|
150
|
-
const chart: TembaChart = await getChart();
|
|
151
|
-
|
|
152
|
-
// Test default state
|
|
153
|
-
expect(chart.formatDuration).to.equal(false);
|
|
154
|
-
|
|
155
|
-
chart.data = sampleData;
|
|
156
|
-
await chart.updateComplete;
|
|
157
|
-
|
|
158
|
-
// Test that formatDuration property can be toggled
|
|
159
|
-
chart.formatDuration = true;
|
|
160
|
-
expect(chart.formatDuration).to.equal(true);
|
|
161
|
-
|
|
162
|
-
chart.formatDuration = false;
|
|
163
|
-
expect(chart.formatDuration).to.equal(false);
|
|
164
|
-
});
|
|
165
149
|
});
|
|
166
150
|
|
|
167
151
|
describe('formatDurationFromSeconds', () => {
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { html, fixture, expect } from '@open-wc/testing';
|
|
2
|
+
import { FormField } from '../src/formfield/FormField';
|
|
3
|
+
import { assertScreenshot, getClip } from './utils.test';
|
|
4
|
+
|
|
5
|
+
describe('temba-field', () => {
|
|
6
|
+
it('renders field with plain text errors', async () => {
|
|
7
|
+
const formField: FormField = await fixture(html`
|
|
8
|
+
<temba-field
|
|
9
|
+
label="Test Field"
|
|
10
|
+
name="test"
|
|
11
|
+
.errors=${['This is a plain text error', 'Another error message']}
|
|
12
|
+
>
|
|
13
|
+
<input type="text" />
|
|
14
|
+
</temba-field>
|
|
15
|
+
`);
|
|
16
|
+
|
|
17
|
+
await formField.updateComplete;
|
|
18
|
+
|
|
19
|
+
// Check that errors are rendered
|
|
20
|
+
const errorElements = formField.shadowRoot.querySelectorAll('.alert-error');
|
|
21
|
+
expect(errorElements.length).to.equal(2);
|
|
22
|
+
expect(errorElements[0].textContent.trim()).to.equal(
|
|
23
|
+
'This is a plain text error'
|
|
24
|
+
);
|
|
25
|
+
expect(errorElements[1].textContent.trim()).to.equal(
|
|
26
|
+
'Another error message'
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
await assertScreenshot('formfield/plain-text-errors', getClip(formField));
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('renders field with markdown errors', async () => {
|
|
33
|
+
const formField: FormField = await fixture(html`
|
|
34
|
+
<temba-field
|
|
35
|
+
label="Test Field"
|
|
36
|
+
name="test"
|
|
37
|
+
.errors=${[
|
|
38
|
+
'This is **bold** text',
|
|
39
|
+
'This has a [link](https://example.com)',
|
|
40
|
+
'This is *italic* and **bold** with a [link](https://example.com)'
|
|
41
|
+
]}
|
|
42
|
+
>
|
|
43
|
+
<input type="text" />
|
|
44
|
+
</temba-field>
|
|
45
|
+
`);
|
|
46
|
+
|
|
47
|
+
await formField.updateComplete;
|
|
48
|
+
|
|
49
|
+
// Check that errors are rendered
|
|
50
|
+
const errorElements = formField.shadowRoot.querySelectorAll('.alert-error');
|
|
51
|
+
expect(errorElements.length).to.equal(3);
|
|
52
|
+
|
|
53
|
+
// First error should have bold text
|
|
54
|
+
const firstError = errorElements[0];
|
|
55
|
+
const boldElement = firstError.querySelector('strong');
|
|
56
|
+
expect(boldElement).to.not.be.null;
|
|
57
|
+
expect(boldElement.textContent).to.equal('bold');
|
|
58
|
+
|
|
59
|
+
// Second error should have a link
|
|
60
|
+
const secondError = errorElements[1];
|
|
61
|
+
const linkElement = secondError.querySelector('a');
|
|
62
|
+
expect(linkElement).to.not.be.null;
|
|
63
|
+
expect(linkElement.getAttribute('href')).to.equal('https://example.com');
|
|
64
|
+
expect(linkElement.textContent).to.equal('link');
|
|
65
|
+
|
|
66
|
+
// Third error should have both bold, italic, and link
|
|
67
|
+
const thirdError = errorElements[2];
|
|
68
|
+
const thirdBoldElement = thirdError.querySelector('strong');
|
|
69
|
+
const thirdItalicElement = thirdError.querySelector('em');
|
|
70
|
+
const thirdLinkElement = thirdError.querySelector('a');
|
|
71
|
+
expect(thirdBoldElement).to.not.be.null;
|
|
72
|
+
expect(thirdItalicElement).to.not.be.null;
|
|
73
|
+
expect(thirdLinkElement).to.not.be.null;
|
|
74
|
+
|
|
75
|
+
await assertScreenshot('formfield/markdown-errors', getClip(formField));
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('renders field without errors', async () => {
|
|
79
|
+
const formField: FormField = await fixture(html`
|
|
80
|
+
<temba-field label="Test Field" name="test">
|
|
81
|
+
<input type="text" />
|
|
82
|
+
</temba-field>
|
|
83
|
+
`);
|
|
84
|
+
|
|
85
|
+
await formField.updateComplete;
|
|
86
|
+
|
|
87
|
+
// Check that no errors are rendered
|
|
88
|
+
const errorElements = formField.shadowRoot.querySelectorAll('.alert-error');
|
|
89
|
+
expect(errorElements.length).to.equal(0);
|
|
90
|
+
|
|
91
|
+
await assertScreenshot('formfield/no-errors', getClip(formField));
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('renders in widget-only mode with errors', async () => {
|
|
95
|
+
const formField: FormField = await fixture(html`
|
|
96
|
+
<temba-field
|
|
97
|
+
widget_only
|
|
98
|
+
.errors=${['Widget only **error** with [link](https://example.com)']}
|
|
99
|
+
>
|
|
100
|
+
<input type="text" />
|
|
101
|
+
</temba-field>
|
|
102
|
+
`);
|
|
103
|
+
|
|
104
|
+
await formField.updateComplete;
|
|
105
|
+
|
|
106
|
+
// Check that error is rendered in widget-only mode
|
|
107
|
+
const errorElements = formField.shadowRoot.querySelectorAll('.alert-error');
|
|
108
|
+
expect(errorElements.length).to.equal(1);
|
|
109
|
+
|
|
110
|
+
const errorElement = errorElements[0];
|
|
111
|
+
const boldElement = errorElement.querySelector('strong');
|
|
112
|
+
const linkElement = errorElement.querySelector('a');
|
|
113
|
+
expect(boldElement).to.not.be.null;
|
|
114
|
+
expect(linkElement).to.not.be.null;
|
|
115
|
+
|
|
116
|
+
await assertScreenshot(
|
|
117
|
+
'formfield/widget-only-markdown-errors',
|
|
118
|
+
getClip(formField)
|
|
119
|
+
);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { html, fixture, expect } from '@open-wc/testing';
|
|
2
|
+
import { Checkbox } from '../src/checkbox/Checkbox';
|
|
3
|
+
import { assertScreenshot, getClip } from './utils.test';
|
|
4
|
+
|
|
5
|
+
describe('FormElement markdown integration', () => {
|
|
6
|
+
it('renders checkbox with markdown errors', async () => {
|
|
7
|
+
const checkbox: Checkbox = await fixture(html`
|
|
8
|
+
<temba-checkbox
|
|
9
|
+
label="Accept Terms"
|
|
10
|
+
.errors=${[
|
|
11
|
+
'Please read the **terms and conditions** at [this link](https://example.com)',
|
|
12
|
+
'This field *requires* acceptance'
|
|
13
|
+
]}
|
|
14
|
+
></temba-checkbox>
|
|
15
|
+
`);
|
|
16
|
+
|
|
17
|
+
await checkbox.updateComplete;
|
|
18
|
+
|
|
19
|
+
// Check that errors are rendered with markdown
|
|
20
|
+
const errorElements = checkbox.shadowRoot
|
|
21
|
+
.querySelectorAll('temba-field')[0]
|
|
22
|
+
.shadowRoot.querySelectorAll('.alert-error');
|
|
23
|
+
expect(errorElements.length).to.equal(2);
|
|
24
|
+
|
|
25
|
+
// First error should have bold text and link
|
|
26
|
+
const firstError = errorElements[0];
|
|
27
|
+
const boldElement = firstError.querySelector('strong');
|
|
28
|
+
const linkElement = firstError.querySelector('a');
|
|
29
|
+
expect(boldElement).to.not.be.null;
|
|
30
|
+
expect(boldElement.textContent).to.equal('terms and conditions');
|
|
31
|
+
expect(linkElement).to.not.be.null;
|
|
32
|
+
expect(linkElement.getAttribute('href')).to.equal('https://example.com');
|
|
33
|
+
|
|
34
|
+
// Second error should have italic text
|
|
35
|
+
const secondError = errorElements[1];
|
|
36
|
+
const italicElement = secondError.querySelector('em');
|
|
37
|
+
expect(italicElement).to.not.be.null;
|
|
38
|
+
expect(italicElement.textContent).to.equal('requires');
|
|
39
|
+
|
|
40
|
+
await assertScreenshot(
|
|
41
|
+
'integration/checkbox-markdown-errors',
|
|
42
|
+
getClip(checkbox)
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -5,6 +5,7 @@ import { Options } from '../src/options/Options';
|
|
|
5
5
|
import { Select, SelectOption } from '../src/select/Select';
|
|
6
6
|
import {
|
|
7
7
|
assertScreenshot,
|
|
8
|
+
delay,
|
|
8
9
|
getClip,
|
|
9
10
|
getOptions,
|
|
10
11
|
loadStore,
|
|
@@ -660,6 +661,22 @@ describe('temba-select', () => {
|
|
|
660
661
|
|
|
661
662
|
await openSelect(clock, select);
|
|
662
663
|
|
|
664
|
+
// Wait for pagination to complete - keep checking until fetching is false
|
|
665
|
+
// and we have the expected number of results (15 = 3 pages * 5 items)
|
|
666
|
+
let attempts = 0;
|
|
667
|
+
const maxAttempts = 10;
|
|
668
|
+
while (select.fetching || select.visibleOptions.length < 15) {
|
|
669
|
+
if (attempts >= maxAttempts) {
|
|
670
|
+
throw new Error(
|
|
671
|
+
`Pagination did not complete after ${maxAttempts} attempts. fetching: ${select.fetching}, visibleOptions: ${select.visibleOptions.length}`
|
|
672
|
+
);
|
|
673
|
+
}
|
|
674
|
+
await select.updateComplete;
|
|
675
|
+
clock.runAll();
|
|
676
|
+
attempts++;
|
|
677
|
+
await delay(100);
|
|
678
|
+
}
|
|
679
|
+
|
|
663
680
|
// should have all three pages visible right away
|
|
664
681
|
assert.equal(select.visibleOptions.length, 15);
|
|
665
682
|
});
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import replace from '@rollup/plugin-replace';
|
|
2
2
|
import { fromRollup } from '@web/dev-server-rollup';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
3
5
|
|
|
4
6
|
const replacePlugin = fromRollup(replace);
|
|
5
7
|
|
|
@@ -11,4 +13,20 @@ export default {
|
|
|
11
13
|
'process.env.NODE_ENV': JSON.stringify('development'),
|
|
12
14
|
}),
|
|
13
15
|
],
|
|
16
|
+
|
|
17
|
+
middlewares: [
|
|
18
|
+
async (ctx, next) => {
|
|
19
|
+
if (ctx.path.startsWith('/flow/revisions/')) {
|
|
20
|
+
const parts = ctx.path.split('/');
|
|
21
|
+
const uuid = parts[3];
|
|
22
|
+
ctx.set('Content-Type', 'application/json');
|
|
23
|
+
ctx.body = fs.readFileSync(
|
|
24
|
+
path.resolve(`./demo/data/flows/${uuid}.json`),
|
|
25
|
+
'utf-8',
|
|
26
|
+
);
|
|
27
|
+
} else {
|
|
28
|
+
await next();
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
],
|
|
14
32
|
};
|