@coveo/quantic 3.37.9 → 3.38.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/force-app/main/default/lwc/quanticFeedback/__tests__/quanticFeedback.test.js +11 -0
- package/force-app/main/default/lwc/quanticFeedback/quanticFeedback.html +6 -4
- package/force-app/main/default/lwc/quanticFeedback/quanticFeedback.js +8 -0
- package/force-app/main/default/lwc/quanticGeneratedAnswer/__tests__/quanticGeneratedAnswer.test.js +32 -3
- package/force-app/main/default/lwc/quanticGeneratedAnswer/quanticGeneratedAnswer.css +1 -8
- package/force-app/main/default/lwc/quanticGeneratedAnswer/quanticGeneratedAnswer.js +6 -28
- package/force-app/main/default/lwc/quanticGeneratedAnswer/templates/generatedAnswer.css +0 -13
- package/force-app/main/default/lwc/quanticGeneratedAnswer/templates/generatedAnswer.html +120 -93
- package/force-app/main/default/lwc/quanticGeneratedAnswer/templates/retryPrompt.html +24 -15
- package/force-app/main/default/lwc/quanticGeneratedAnswerCopyToClipboard/__tests__/quanticGeneratedAnswerCopyToClipboard.test.js +125 -0
- package/force-app/main/default/lwc/quanticGeneratedAnswerCopyToClipboard/quanticGeneratedAnswerCopyToClipboard.html +1 -1
- package/force-app/main/default/lwc/quanticGeneratedAnswerCopyToClipboard/quanticGeneratedAnswerCopyToClipboard.js +20 -1
- package/force-app/main/default/lwc/quanticResultHighlightedTextField/quanticResultHighlightedTextField.js +10 -1
- package/force-app/main/default/lwc/quanticResultLink/quanticResultLink.js +11 -4
- package/force-app/main/default/lwc/quanticResultQuickview/quanticResultQuickview.js +14 -2
- package/force-app/main/default/lwc/quanticThreadItem/__tests__/quanticThreadItem.test.js +176 -0
- package/force-app/main/default/lwc/quanticThreadItem/quanticThreadItem.css +64 -0
- package/force-app/main/default/lwc/quanticThreadItem/quanticThreadItem.html +48 -0
- package/force-app/main/default/lwc/quanticThreadItem/quanticThreadItem.js +80 -0
- package/force-app/main/default/lwc/quanticThreadItem/quanticThreadItem.js-meta.xml +5 -0
- package/force-app/main/default/lwc/quanticUtils/__tests__/quanticUtils.test.js +30 -0
- package/force-app/main/default/lwc/quanticUtils/quanticUtils.js +24 -0
- package/force-app/main/default/staticresources/coveoheadless/case-assist/headless.js +15 -15
- package/force-app/main/default/staticresources/coveoheadless/definitions/api/knowledge/stream-answer-api.d.ts +1 -1
- package/force-app/main/default/staticresources/coveoheadless/definitions/api/platform-client.d.ts +5 -0
- package/force-app/main/default/staticresources/coveoheadless/definitions/app/case-assist-engine/case-assist-engine-configuration.d.ts +1 -1
- package/force-app/main/default/staticresources/coveoheadless/definitions/app/insight-engine/insight-engine-configuration.d.ts +1 -1
- package/force-app/main/default/staticresources/coveoheadless/definitions/app/recommendation-engine/recommendation-engine-configuration.d.ts +1 -1
- package/force-app/main/default/staticresources/coveoheadless/definitions/app/search-engine/search-engine-configuration.d.ts +1 -1
- package/force-app/main/default/staticresources/coveoheadless/definitions/features/case-assist-configuration/case-assist-configuration-actions.d.ts +1 -1
- package/force-app/main/default/staticresources/coveoheadless/definitions/features/case-assist-configuration/case-assist-configuration-state.d.ts +1 -1
- package/force-app/main/default/staticresources/coveoheadless/definitions/features/configuration/configuration-actions.d.ts +1 -1
- package/force-app/main/default/staticresources/coveoheadless/definitions/features/configuration/configuration-state.d.ts +1 -1
- package/force-app/main/default/staticresources/coveoheadless/definitions/features/generated-answer/generated-answer-request.d.ts +2 -2
- package/force-app/main/default/staticresources/coveoheadless/headless.js +17 -17
- package/force-app/main/default/staticresources/coveoheadless/insight/headless.js +16 -16
- package/force-app/main/default/staticresources/coveoheadless/recommendation/headless.js +14 -14
- package/force-app/main/default/staticresources/dompurify/purify.min.js +2 -2
- package/package.json +6 -6
|
@@ -12,8 +12,27 @@ import {LightningElement, api} from 'lwc';
|
|
|
12
12
|
export default class QuanticGeneratedAnswerCopyToClipboard extends LightningElement {
|
|
13
13
|
/**
|
|
14
14
|
* The answer to copy
|
|
15
|
+
* @type {string}
|
|
15
16
|
*/
|
|
16
|
-
@api answer;
|
|
17
|
+
@api answer = '';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The size of the copy icon.
|
|
21
|
+
* @api
|
|
22
|
+
* @type {'xx-small' | 'x-small' | 'small' | 'medium' | 'large'}
|
|
23
|
+
*/
|
|
24
|
+
@api
|
|
25
|
+
get size() {
|
|
26
|
+
return this._size;
|
|
27
|
+
}
|
|
28
|
+
set size(value) {
|
|
29
|
+
if (['xx-small', 'x-small', 'small', 'medium', 'large'].includes(value)) {
|
|
30
|
+
this._size = value;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** @type {'xx-small' | 'x-small' | 'small' | 'medium' | 'large'} */
|
|
35
|
+
_size = 'xx-small';
|
|
17
36
|
|
|
18
37
|
labels = {
|
|
19
38
|
copy,
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
initializeWithHeadless,
|
|
5
5
|
getHeadlessBundle,
|
|
6
6
|
} from 'c/quanticHeadlessLoader';
|
|
7
|
+
import {unwrapLockerProxiedObject} from 'c/quanticUtils';
|
|
7
8
|
import {LightningElement, api} from 'lwc';
|
|
8
9
|
|
|
9
10
|
/** @typedef {import("coveo").Result} Result */
|
|
@@ -28,7 +29,13 @@ export default class QuanticResultHighlightedTextField extends LightningElement
|
|
|
28
29
|
* @api
|
|
29
30
|
* @type {Result}
|
|
30
31
|
*/
|
|
31
|
-
@api
|
|
32
|
+
@api
|
|
33
|
+
get result() {
|
|
34
|
+
return this._result;
|
|
35
|
+
}
|
|
36
|
+
set result(result) {
|
|
37
|
+
this._result = unwrapLockerProxiedObject(result);
|
|
38
|
+
}
|
|
32
39
|
/**
|
|
33
40
|
* (Optional) The label to display.
|
|
34
41
|
* @api
|
|
@@ -49,6 +56,8 @@ export default class QuanticResultHighlightedTextField extends LightningElement
|
|
|
49
56
|
isInitialized = false;
|
|
50
57
|
/** @type {boolean} */
|
|
51
58
|
validated = false;
|
|
59
|
+
/** @type {Result} */
|
|
60
|
+
_result;
|
|
52
61
|
|
|
53
62
|
connectedCallback() {
|
|
54
63
|
this.validateProps();
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
getHeadlessBundle,
|
|
5
5
|
getHeadlessEnginePromise,
|
|
6
6
|
} from 'c/quanticHeadlessLoader';
|
|
7
|
-
import {ResultUtils} from 'c/quanticUtils';
|
|
7
|
+
import {ResultUtils, unwrapLockerProxiedObject} from 'c/quanticUtils';
|
|
8
8
|
import {NavigationMixin} from 'lightning/navigation';
|
|
9
9
|
import {LightningElement, api} from 'lwc';
|
|
10
10
|
|
|
@@ -38,7 +38,13 @@ export default class QuanticResultLink extends NavigationMixin(
|
|
|
38
38
|
* @api
|
|
39
39
|
* @type {Result}
|
|
40
40
|
*/
|
|
41
|
-
@api
|
|
41
|
+
@api
|
|
42
|
+
get result() {
|
|
43
|
+
return this._result;
|
|
44
|
+
}
|
|
45
|
+
set result(result) {
|
|
46
|
+
this._result = unwrapLockerProxiedObject(result);
|
|
47
|
+
}
|
|
42
48
|
/**
|
|
43
49
|
* Where to display the linked URL, as the name for a browsing context (a tab, window, or <iframe>).
|
|
44
50
|
* The following keywords have special meanings for where to load the URL:
|
|
@@ -75,6 +81,8 @@ export default class QuanticResultLink extends NavigationMixin(
|
|
|
75
81
|
engine;
|
|
76
82
|
/** @type {AnyHeadless} */
|
|
77
83
|
headless;
|
|
84
|
+
/** @type {Result} */
|
|
85
|
+
_result;
|
|
78
86
|
/** @type {string} */
|
|
79
87
|
salesforceRecordUrl;
|
|
80
88
|
|
|
@@ -113,8 +121,7 @@ export default class QuanticResultLink extends NavigationMixin(
|
|
|
113
121
|
this.engine = engine;
|
|
114
122
|
ResultUtils.bindClickEventsOnResult(
|
|
115
123
|
this.engine,
|
|
116
|
-
|
|
117
|
-
{...this.result, raw: {...this.result.raw}},
|
|
124
|
+
this.result,
|
|
118
125
|
this.template,
|
|
119
126
|
this.headless.buildInteractiveResult
|
|
120
127
|
);
|
|
@@ -8,7 +8,11 @@ import {
|
|
|
8
8
|
HeadlessBundleNames,
|
|
9
9
|
isHeadlessBundle,
|
|
10
10
|
} from 'c/quanticHeadlessLoader';
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
I18nUtils,
|
|
13
|
+
getLastFocusableElement,
|
|
14
|
+
unwrapLockerProxiedObject,
|
|
15
|
+
} from 'c/quanticUtils';
|
|
12
16
|
import {LightningElement, api, track} from 'lwc';
|
|
13
17
|
|
|
14
18
|
/** @typedef {import("coveo").Result} Result */
|
|
@@ -42,7 +46,13 @@ export default class QuanticResultQuickview extends LightningElement {
|
|
|
42
46
|
* @api
|
|
43
47
|
* @type {ResultWithFolding}
|
|
44
48
|
*/
|
|
45
|
-
@api
|
|
49
|
+
@api
|
|
50
|
+
get result() {
|
|
51
|
+
return this._result;
|
|
52
|
+
}
|
|
53
|
+
set result(result) {
|
|
54
|
+
this._result = unwrapLockerProxiedObject(result);
|
|
55
|
+
}
|
|
46
56
|
/**
|
|
47
57
|
* The maximum preview size to retrieve, in bytes. By default, the full preview is retrieved.
|
|
48
58
|
* @api
|
|
@@ -84,6 +94,8 @@ export default class QuanticResultQuickview extends LightningElement {
|
|
|
84
94
|
|
|
85
95
|
/** @type {Quickview} */
|
|
86
96
|
quickview;
|
|
97
|
+
/** @type {ResultWithFolding} */
|
|
98
|
+
_result;
|
|
87
99
|
/** @type {boolean} */
|
|
88
100
|
isQuickviewOpen = false;
|
|
89
101
|
/** @type {Function} */
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// @ts-ignore
|
|
2
|
+
import QuanticThreadItem from '../quanticThreadItem';
|
|
3
|
+
import {buildCreateTestComponent, cleanup, flushPromises} from 'c/testUtils';
|
|
4
|
+
|
|
5
|
+
const selectors = {
|
|
6
|
+
titleButton: '[data-testid="thread-item-title-button"]',
|
|
7
|
+
titleSpan: '[data-testid="thread-item-title-static"]',
|
|
8
|
+
boldTitle: '.slds-text-title_bold',
|
|
9
|
+
contentWrapper: '[data-testid="thread-item-content"]',
|
|
10
|
+
visibleContent: '[data-testid="thread-item-content"] > div:not([hidden])',
|
|
11
|
+
line: '[data-testid="thread-item-line"]',
|
|
12
|
+
dot: '[data-testid="thread-item-dot"]',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const createTestComponent = buildCreateTestComponent(
|
|
16
|
+
QuanticThreadItem,
|
|
17
|
+
'c-quantic-thread-item',
|
|
18
|
+
{
|
|
19
|
+
title: 'Test title',
|
|
20
|
+
}
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
describe('c-quantic-thread-item', () => {
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
cleanup();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe('initial rendering', () => {
|
|
29
|
+
it('renders a button when collapse is enabled', async () => {
|
|
30
|
+
const element = createTestComponent();
|
|
31
|
+
await flushPromises();
|
|
32
|
+
|
|
33
|
+
const button = element.shadowRoot.querySelector(selectors.titleButton);
|
|
34
|
+
expect(button).not.toBeNull();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('renders a span instead of a button when disableCollapse is true', async () => {
|
|
38
|
+
const element = createTestComponent({disableCollapse: true});
|
|
39
|
+
await flushPromises();
|
|
40
|
+
|
|
41
|
+
const button = element.shadowRoot.querySelector(selectors.titleButton);
|
|
42
|
+
const span = element.shadowRoot.querySelector(selectors.titleSpan);
|
|
43
|
+
expect(button).toBeNull();
|
|
44
|
+
expect(span).not.toBeNull();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('renders the timeline line by default', async () => {
|
|
48
|
+
const element = createTestComponent();
|
|
49
|
+
await flushPromises();
|
|
50
|
+
|
|
51
|
+
const line = element.shadowRoot.querySelector(selectors.line);
|
|
52
|
+
expect(line).not.toBeNull();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('hides the timeline line when hideLine is true', async () => {
|
|
56
|
+
const element = createTestComponent({hideLine: true});
|
|
57
|
+
await flushPromises();
|
|
58
|
+
|
|
59
|
+
const line = element.shadowRoot.querySelector(selectors.line);
|
|
60
|
+
expect(line).toBeNull();
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe('collapsed state', () => {
|
|
65
|
+
it('does not render the content by default', async () => {
|
|
66
|
+
const element = createTestComponent();
|
|
67
|
+
await flushPromises();
|
|
68
|
+
|
|
69
|
+
const content = element.shadowRoot.querySelector(
|
|
70
|
+
selectors.visibleContent
|
|
71
|
+
);
|
|
72
|
+
expect(content).toBeNull();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('button has aria-expanded set to false when collapsed', async () => {
|
|
76
|
+
const element = createTestComponent();
|
|
77
|
+
await flushPromises();
|
|
78
|
+
|
|
79
|
+
const button = element.shadowRoot.querySelector(selectors.titleButton);
|
|
80
|
+
expect(button.getAttribute('aria-expanded')).toBe('false');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('dot does not have expanded class when collapsed', async () => {
|
|
84
|
+
const element = createTestComponent();
|
|
85
|
+
await flushPromises();
|
|
86
|
+
|
|
87
|
+
const dot = element.shadowRoot.querySelector(selectors.dot);
|
|
88
|
+
expect(dot.className).not.toContain('thread-item__dot--expanded');
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe('expanded state', () => {
|
|
93
|
+
it('renders the content when isExpanded is true', async () => {
|
|
94
|
+
const element = createTestComponent({isExpanded: true});
|
|
95
|
+
await flushPromises();
|
|
96
|
+
|
|
97
|
+
const content = element.shadowRoot.querySelector(
|
|
98
|
+
selectors.visibleContent
|
|
99
|
+
);
|
|
100
|
+
expect(content).not.toBeNull();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('button has aria-expanded set to true when expanded', async () => {
|
|
104
|
+
const element = createTestComponent({isExpanded: true});
|
|
105
|
+
await flushPromises();
|
|
106
|
+
|
|
107
|
+
const button = element.shadowRoot.querySelector(selectors.titleButton);
|
|
108
|
+
expect(button.getAttribute('aria-expanded')).toBe('true');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('dot has expanded class when expanded', async () => {
|
|
112
|
+
const element = createTestComponent({isExpanded: true});
|
|
113
|
+
await flushPromises();
|
|
114
|
+
|
|
115
|
+
const dot = element.shadowRoot.querySelector(selectors.dot);
|
|
116
|
+
expect(dot.className).toContain('thread-item__dot--expanded');
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe('toggle interaction', () => {
|
|
121
|
+
it('renders the content when the button is clicked while collapsed', async () => {
|
|
122
|
+
const element = createTestComponent();
|
|
123
|
+
await flushPromises();
|
|
124
|
+
|
|
125
|
+
element.shadowRoot.querySelector(selectors.titleButton).click();
|
|
126
|
+
await flushPromises();
|
|
127
|
+
|
|
128
|
+
const content = element.shadowRoot.querySelector(
|
|
129
|
+
selectors.visibleContent
|
|
130
|
+
);
|
|
131
|
+
expect(content).not.toBeNull();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('does not render the content when the button is clicked while expanded', async () => {
|
|
135
|
+
const element = createTestComponent({isExpanded: true});
|
|
136
|
+
await flushPromises();
|
|
137
|
+
|
|
138
|
+
element.shadowRoot.querySelector(selectors.titleButton).click();
|
|
139
|
+
await flushPromises();
|
|
140
|
+
|
|
141
|
+
const content = element.shadowRoot.querySelector(
|
|
142
|
+
selectors.visibleContent
|
|
143
|
+
);
|
|
144
|
+
expect(content).toBeNull();
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe('disableCollapse', () => {
|
|
149
|
+
it('forces item to be expanded regardless of isExpanded prop', async () => {
|
|
150
|
+
const element = createTestComponent({
|
|
151
|
+
disableCollapse: true,
|
|
152
|
+
isExpanded: false,
|
|
153
|
+
});
|
|
154
|
+
await flushPromises();
|
|
155
|
+
|
|
156
|
+
const content = element.shadowRoot.querySelector(
|
|
157
|
+
selectors.visibleContent
|
|
158
|
+
);
|
|
159
|
+
expect(content).not.toBeNull();
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('renders a bold title when collapse is disabled', async () => {
|
|
163
|
+
const element = createTestComponent({
|
|
164
|
+
disableCollapse: true,
|
|
165
|
+
isExpanded: false,
|
|
166
|
+
});
|
|
167
|
+
await flushPromises();
|
|
168
|
+
|
|
169
|
+
const title = element.shadowRoot.querySelector(selectors.titleSpan);
|
|
170
|
+
expect(title.className).toContain('slds-text-title_bold');
|
|
171
|
+
expect(
|
|
172
|
+
element.shadowRoot.querySelector(selectors.boldTitle)
|
|
173
|
+
).not.toBeNull();
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
.thread-item {
|
|
2
|
+
list-style: none;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
.thread-item__dot-column,
|
|
6
|
+
.thread-item__line-column {
|
|
7
|
+
width: 10px;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.thread-item__line-column {
|
|
11
|
+
align-items: stretch;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.thread-item__dot {
|
|
15
|
+
height: 8px;
|
|
16
|
+
width: 8px;
|
|
17
|
+
border-radius: 50%;
|
|
18
|
+
background-color: var(--lwc-colorBorder, #dddbda);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.thread-item__dot--expanded {
|
|
22
|
+
background-color: var(--lwc-colorTextDefault, #080707);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.thread-item__line {
|
|
26
|
+
position: relative;
|
|
27
|
+
width: 1px;
|
|
28
|
+
height: 100%;
|
|
29
|
+
background-color: var(--lwc-colorBorder, #dddbda);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.thread-item__line::before,
|
|
33
|
+
.thread-item__line::after {
|
|
34
|
+
content: '';
|
|
35
|
+
position: absolute;
|
|
36
|
+
left: 0;
|
|
37
|
+
width: 1px;
|
|
38
|
+
height: 8px;
|
|
39
|
+
background-color: var(--lwc-colorBorder, #dddbda);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.thread-item__line::before {
|
|
43
|
+
top: -8px;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.thread-item__line::after {
|
|
47
|
+
bottom: -8px;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.thread-item__clamped-text {
|
|
51
|
+
display: -webkit-box;
|
|
52
|
+
-webkit-line-clamp: 3;
|
|
53
|
+
-webkit-box-orient: vertical;
|
|
54
|
+
overflow: hidden;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.thread-item__title-button {
|
|
58
|
+
width: fit-content;
|
|
59
|
+
border-radius: var(--slds-g-radius-border-2, 4px);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.thread-item__title-button:hover {
|
|
63
|
+
background-color: var(--lwc-colorBackgroundRowHover, #f3f2f2);
|
|
64
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<li class="thread-item">
|
|
3
|
+
<div class="slds-grid slds-grid_vertical-align-center">
|
|
4
|
+
<div
|
|
5
|
+
class="slds-grid slds-grid_align-center slds-grid_vertical-align-center slds-shrink-none slds-m-right_x-small thread-item__dot-column"
|
|
6
|
+
>
|
|
7
|
+
<span class={dotClass} data-testid="thread-item-dot"></span>
|
|
8
|
+
</div>
|
|
9
|
+
<div class="slds-col slds-has-flexi-truncate">
|
|
10
|
+
<template lwc:if={disableCollapse}>
|
|
11
|
+
<span class={titleClass} data-testid="thread-item-title-static"
|
|
12
|
+
>{title}</span
|
|
13
|
+
>
|
|
14
|
+
</template>
|
|
15
|
+
<template lwc:else>
|
|
16
|
+
<button
|
|
17
|
+
type="button"
|
|
18
|
+
class={titleButtonClass}
|
|
19
|
+
aria-expanded={isExpanded}
|
|
20
|
+
data-testid="thread-item-title-button"
|
|
21
|
+
onclick={handleTitleClick}
|
|
22
|
+
aria-controls="thread-item-content"
|
|
23
|
+
>
|
|
24
|
+
{title}
|
|
25
|
+
</button>
|
|
26
|
+
</template>
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
<div class="slds-grid">
|
|
30
|
+
<div
|
|
31
|
+
class="slds-grid slds-grid_align-center slds-shrink-none thread-item__line-column slds-m-right_x-small"
|
|
32
|
+
>
|
|
33
|
+
<template lwc:if={shouldDisplayLine}>
|
|
34
|
+
<span class="thread-item__line" data-testid="thread-item-line"></span>
|
|
35
|
+
</template>
|
|
36
|
+
</div>
|
|
37
|
+
<div
|
|
38
|
+
id="thread-item-content"
|
|
39
|
+
class="slds-col slds-p-left_x-small slds-p-vertical_xx-small"
|
|
40
|
+
data-testid="thread-item-content"
|
|
41
|
+
>
|
|
42
|
+
<div hidden={contentHidden}>
|
|
43
|
+
<slot></slot>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
</li>
|
|
48
|
+
</template>
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import {LightningElement, api} from 'lwc';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The `QuanticThreadItem` component renders a thread item with timeline visuals and collapsible content.
|
|
5
|
+
* @category Internal
|
|
6
|
+
* @example
|
|
7
|
+
* <c-quantic-thread-item title="Step title" hide-line is-expanded></c-quantic-thread-item>
|
|
8
|
+
*/
|
|
9
|
+
export default class QuanticThreadItem extends LightningElement {
|
|
10
|
+
/**
|
|
11
|
+
* The title displayed for the thread item.
|
|
12
|
+
* @api
|
|
13
|
+
* @type {string}
|
|
14
|
+
*/
|
|
15
|
+
@api title = '';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Whether the thread item can be expanded or collapsed.
|
|
19
|
+
* @api
|
|
20
|
+
* @type {boolean}
|
|
21
|
+
* @defaultValue `false`
|
|
22
|
+
*/
|
|
23
|
+
@api disableCollapse = false;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Whether the timeline line should be hidden.
|
|
27
|
+
* @api
|
|
28
|
+
* @type {boolean}
|
|
29
|
+
* @defaultValue `false`
|
|
30
|
+
*/
|
|
31
|
+
@api hideLine = false;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Whether the thread item is expanded.
|
|
35
|
+
* @api
|
|
36
|
+
* @type {boolean}
|
|
37
|
+
* @defaultValue `false`
|
|
38
|
+
*/
|
|
39
|
+
@api
|
|
40
|
+
get isExpanded() {
|
|
41
|
+
return this._isExpanded;
|
|
42
|
+
}
|
|
43
|
+
set isExpanded(value) {
|
|
44
|
+
this._isExpanded = value;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** @type {boolean} */
|
|
48
|
+
_isExpanded = false;
|
|
49
|
+
|
|
50
|
+
connectedCallback() {
|
|
51
|
+
this._isExpanded = this.disableCollapse ? true : this.isExpanded;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
handleTitleClick() {
|
|
55
|
+
if (this.disableCollapse) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
this._isExpanded = !this._isExpanded;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
get shouldDisplayLine() {
|
|
62
|
+
return !this.hideLine;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
get contentHidden() {
|
|
66
|
+
return !this._isExpanded;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
get titleClass() {
|
|
70
|
+
return `thread-item__clamped-text slds-text-body_regular slds-p-horizontal_x-small slds-p-vertical_xx-small${this._isExpanded ? ' slds-text-title_bold' : ''}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
get titleButtonClass() {
|
|
74
|
+
return `slds-button_reset slds-p-horizontal_x-small slds-p-vertical_xx-small thread-item__title-button thread-item__clamped-text${this._isExpanded ? ' slds-text-title_bold' : ''}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
get dotClass() {
|
|
78
|
+
return `thread-item__dot${this._isExpanded ? ' thread-item__dot--expanded' : ''}`;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
I18nUtils,
|
|
3
3
|
buildTemplateTextFromResult,
|
|
4
4
|
copyToClipboard,
|
|
5
|
+
unwrapLockerProxiedObject,
|
|
5
6
|
} from 'c/quanticUtils';
|
|
6
7
|
|
|
7
8
|
describe('c/quanticUtils', () => {
|
|
@@ -200,4 +201,33 @@ describe('c/quanticUtils', () => {
|
|
|
200
201
|
);
|
|
201
202
|
});
|
|
202
203
|
});
|
|
204
|
+
|
|
205
|
+
describe('unwrapLockerObject', () => {
|
|
206
|
+
it('should deeply clone complex objects while preserving primitive values', () => {
|
|
207
|
+
const original = {
|
|
208
|
+
title: 'Example',
|
|
209
|
+
raw: {
|
|
210
|
+
foo: 'bar',
|
|
211
|
+
tags: ['a', {value: 'b'}],
|
|
212
|
+
},
|
|
213
|
+
score: 42,
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const unwrapped = unwrapLockerProxiedObject(original);
|
|
217
|
+
|
|
218
|
+
expect(unwrapped).toEqual(original);
|
|
219
|
+
expect(unwrapped).not.toBe(original);
|
|
220
|
+
expect(unwrapped.raw).not.toBe(original.raw);
|
|
221
|
+
expect(unwrapped.raw.tags).not.toBe(original.raw.tags);
|
|
222
|
+
expect(unwrapped.raw.tags[1]).not.toBe(original.raw.tags[1]);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('should return primitive values as-is', () => {
|
|
226
|
+
expect(unwrapLockerProxiedObject(undefined)).toBeUndefined();
|
|
227
|
+
expect(unwrapLockerProxiedObject(null)).toBeNull();
|
|
228
|
+
expect(unwrapLockerProxiedObject('value')).toBe('value');
|
|
229
|
+
expect(unwrapLockerProxiedObject(0)).toBe(0);
|
|
230
|
+
expect(unwrapLockerProxiedObject(false)).toBe(false);
|
|
231
|
+
});
|
|
232
|
+
});
|
|
203
233
|
});
|
|
@@ -259,6 +259,30 @@ export function parseXML(string) {
|
|
|
259
259
|
return new window.DOMParser().parseFromString(string, 'text/xml');
|
|
260
260
|
}
|
|
261
261
|
|
|
262
|
+
/**
|
|
263
|
+
* Recursively clones objects to break Locker proxy chains.
|
|
264
|
+
* @param {any} value
|
|
265
|
+
* @returns {any}
|
|
266
|
+
*/
|
|
267
|
+
export function unwrapLockerProxiedObject(value) {
|
|
268
|
+
if (value === null || typeof value !== 'object') {
|
|
269
|
+
return value;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (Array.isArray(value)) {
|
|
273
|
+
return value.map((item) => unwrapLockerProxiedObject(item));
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const unwrappedValue = {};
|
|
277
|
+
for (const key in value) {
|
|
278
|
+
if (Object.prototype.hasOwnProperty.call(value, key)) {
|
|
279
|
+
unwrappedValue[key] = unwrapLockerProxiedObject(value[key]);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return unwrappedValue;
|
|
284
|
+
}
|
|
285
|
+
|
|
262
286
|
/**
|
|
263
287
|
* Utility class for managing a simple in-memory store.
|
|
264
288
|
* Supports registering and retrieving facet and sort option data.
|