• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

coveo / search-ui-extensions / 4

pending completion
4

push

jenkins

Michael Grondines
the s3 folder is wrong

392 of 424 branches covered (92.45%)

Branch coverage included in aggregate %.

1173 of 1205 relevant lines covered (97.34%)

33.25 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.74
/src/components/UserActions/UserActions.ts
1
import {
12✔
2
    Component,
3
    IComponentBindings,
4
    Initialization,
5
    ComponentOptions,
6
    QueryEvents,
7
    l,
8
    get,
9
    ResultListEvents,
10
    IDisplayedNewResultEventArgs,
11
    ResultList,
12
} from 'coveo-search-ui';
13
import { ResponsiveUserActions } from './ResponsiveUserActions';
12✔
14
import { arrowDown } from '../../utils/icons';
12✔
15
import { ClickedDocumentList } from './ClickedDocumentList';
12✔
16
import { QueryList } from './QueryList';
12✔
17
import { UserActivity } from './UserActivity';
12✔
18
import { UserProfileModel } from '../../Index';
12✔
19
import './Strings';
12✔
20
import { ViewedByCustomer } from '../ViewedByCustomer/ViewedByCustomer';
12✔
21
import { UserActionEvents } from './Events';
12✔
22

23
enum ResultLayoutType {
12✔
24
    LIST = 'list',
12✔
25
    TABLE = 'table',
12✔
26
    CARD = 'card',
12✔
27
}
28

29
/**
30
 * Initialization options of the **UserActions** class.
31
 */
32
export interface IUserActionsOptions {
33
    /**
34
     * Identifier of the user from which Clicked Documents are shown.
35
     *
36
     * **Require**
37
     */
38
    userId: string;
39

40
    /**
41
     * Label of the button used to open the user actions.
42
     *
43
     * Default: `User Actions`
44
     */
45
    buttonLabel: string;
46

47
    /**
48
     * Label of the summary section.
49
     *
50
     * Default: `Session Summary`
51
     */
52
    summaryLabel: string;
53

54
    /**
55
     * Label of the activity section.
56
     *
57
     * Default: `User Activity Timeline`
58
     */
59
    activityLabel: string;
60

61
    /**
62
     * Whether or not to add the ViewedByCustomer component
63
     *
64
     * Default: `True`
65
     */
66
    viewedByCustomer: Boolean;
67
    /**
68
     * Whether or not the UserAction component should be displayed
69
     * Can be used to use ViewedByCustomer alone.
70
     *
71
     * Default: `False`
72
     */
73
    hidden: Boolean;
74
    /**
75
     * Whether or not the UserAction component should use the CoveoSearchUI ResponsiveManager
76
     * Inoperant if `hidden` is true.
77
     *
78
     * Default: `True`
79
     */
80
    useResponsiveManager: Boolean;
81
}
82

83
/**
84
 * Display a panel that contains a summary of a user session and that also contains detailed information about user actions.
85
 */
86
export class UserActions extends Component {
12✔
87
    /**
88
     * Identifier of the Search-UI component.
89
     */
90
    static readonly ID = 'UserActions';
12✔
91
    static readonly Events = {
12✔
92
        Hide: 'userActionsPanelHide',
93
        Show: 'userActionsPanelShow',
94
    };
95

96
    /**
97
     * Default initialization options of the **UserActions** class.
98
     */
99
    static readonly options: IUserActionsOptions = {
12✔
100
        userId: ComponentOptions.buildStringOption({ required: true }),
101
        buttonLabel: ComponentOptions.buildStringOption({
102
            defaultValue: 'User Actions',
103
        }),
104
        summaryLabel: ComponentOptions.buildStringOption({
105
            defaultValue: 'Session Summary',
106
        }),
107
        activityLabel: ComponentOptions.buildStringOption({
108
            defaultValue: 'User Activity Timeline',
109
        }),
110
        viewedByCustomer: ComponentOptions.buildBooleanOption({
111
            defaultValue: true,
112
        }),
113
        hidden: ComponentOptions.buildBooleanOption({
114
            defaultValue: false,
115
        }),
116
        useResponsiveManager: ComponentOptions.buildBooleanOption({
117
            defaultValue: true,
118
        }),
119
    };
120

121
    private static readonly USER_ACTION_OPENED = 'coveo-user-actions-opened';
12✔
122
    private isOpened: boolean;
123

124
    /**
125
     * Create an instance of the **UserActions** class. Initialize is needed the **UserProfileModel** and fetch user actions related to the **UserId**.
126
     *
127
     * @param element Element on which to bind the component.
128
     * @param options Initialization options of the component.
129
     * @param bindings Bindings of the Search-UI environment.
130
     */
131
    constructor(public element: HTMLElement, public options: IUserActionsOptions, public bindings: IComponentBindings) {
43✔
132
        super(element, UserActions.ID, bindings);
43✔
133

134
        this.options = ComponentOptions.initComponentOptions(element, UserActions, options);
43✔
135

136
        if (!this.options.userId) {
43✔
137
            this.disable();
1✔
138
            return;
1✔
139
        }
140

141
        if (this.options.viewedByCustomer) {
42✔
142
            this.showViewedByCustomer();
40✔
143
        }
144

145
        this.tagViewsOfUser();
42✔
146

147
        if (!options.hidden) {
42✔
148
            if (options.useResponsiveManager) {
41✔
149
                ResponsiveUserActions.init(this.root, this);
40✔
150
            }
151
            this.bind.onRootElement(QueryEvents.newQuery, () => this.hide());
41✔
152
            this.hide();
41✔
153
        }
154
    }
155

156
    /**
157
     * Collapse the panel.
158
     */
159
    public hide() {
160
        if (this.isOpened) {
54✔
161
            this.isOpened = false;
6✔
162
            (get(this.root, UserProfileModel) as UserProfileModel).deleteActions(this.options.userId);
6✔
163
            this.root.classList.remove(UserActions.USER_ACTION_OPENED);
6✔
164
            this.element.dispatchEvent(new CustomEvent(UserActions.Events.Hide));
6✔
165
        }
166
    }
167

168
    /**
169
     * Open the panel.
170
     */
171
    public async show() {
172
        if (!this.isOpened) {
26✔
173
            this.isOpened = true;
24✔
174
            this.renderLoading();
24✔
175
            this.element.dispatchEvent(new CustomEvent(UserActions.Events.Show));
24✔
176
            this.bindings.usageAnalytics.logCustomEvent(UserActionEvents.open, {}, this.element);
24✔
177
            this.root.classList.add(UserActions.USER_ACTION_OPENED);
24✔
178
            try {
24✔
179
                const userActions = await (get(this.root, UserProfileModel) as UserProfileModel).getActions(this.options.userId);
24✔
180
                if (userActions.length > 0) {
21✔
181
                    this.render();
8✔
182
                } else {
183
                    this.renderNoActions();
13✔
184
                }
185
            } catch (e) {
186
                if (e?.statusCode === 404) {
3✔
187
                    this.renderEnablePrompt();
1✔
188
                } else {
189
                    this.renderNoActions();
2✔
190
                }
191
            }
192
        }
193
    }
194

195
    /**
196
     * Toggle the visibility of the panel.
197
     */
198
    public async toggle() {
199
        if (this.isOpened) {
3✔
200
            this.hide();
1✔
201
        } else {
202
            await this.show();
2✔
203
        }
204
    }
205

206
    private buildAccordionHeader(title: string) {
207
        const div = document.createElement('div');
16✔
208
        div.classList.add('coveo-accordion-header');
16✔
209

210
        const headerTitle = document.createElement('div');
16✔
211
        headerTitle.classList.add('coveo-accordion-header-title');
16✔
212
        headerTitle.innerText = title;
16✔
213

214
        const arrow = document.createElement('div');
16✔
215
        arrow.classList.add('coveo-arrow-down');
16✔
216
        arrow.innerHTML = arrowDown;
16✔
217

218
        div.appendChild(headerTitle);
16✔
219
        div.appendChild(arrow);
16✔
220

221
        return div;
16✔
222
    }
223

224
    private buildAccordion(title: string, elements: HTMLElement[]) {
225
        const div = document.createElement('div');
16✔
226
        div.classList.add('coveo-accordion');
16✔
227

228
        const header = this.buildAccordionHeader(title);
16✔
229

230
        const foldable = document.createElement('div');
16✔
231
        foldable.classList.add('coveo-accordion-foldable');
16✔
232

233
        elements.forEach((el) => foldable.appendChild(el));
24✔
234

235
        div.appendChild(header);
16✔
236
        div.appendChild(foldable);
16✔
237

238
        header.addEventListener('click', () => {
16✔
239
            if (div.classList.contains('coveo-folded')) {
4✔
240
                div.classList.remove('coveo-folded');
2✔
241
            } else {
242
                div.classList.add('coveo-folded');
2✔
243
            }
244
        });
245

246
        return div;
16✔
247
    }
248

249
    private buildCoveoElement(klass: any) {
250
        const el = document.createElement('div');
24✔
251
        el.classList.add(`Coveo${klass.ID}`);
24✔
252
        return el;
24✔
253
    }
254

255
    /**
256
     * Initialize child Search-UI component and pass down critical options.
257
     *
258
     * @param element Parent element of each child that would be initialize.
259
     */
260
    private initializeSearchUIComponents(element: HTMLElement) {
261
        const originalOptions = this.searchInterface.options.originalOptionsObject;
8✔
262

263
        Initialization.automaticallyCreateComponentsInside(element, {
8✔
264
            options: {
265
                ...originalOptions,
266
                QueryList: {
267
                    ...originalOptions.QueryList,
268
                    userId: this.options.userId,
269
                },
270
                ClickedDocumentList: {
271
                    ...originalOptions.ClickedDocumentList,
272
                    userId: this.options.userId,
273
                },
274
                UserActivity: {
275
                    ...originalOptions.UserActivity,
276
                    userId: this.options.userId,
277
                },
278
            },
279
            bindings: this.bindings,
280
        });
281
    }
282

283
    private renderLoading() {
284
        this.element.innerHTML = '';
24✔
285
        const loadingElement = document.createElement('div');
24✔
286
        loadingElement.classList.add('coveo-loading-container');
24✔
287
        loadingElement.innerHTML = `
24✔
288
        <div role="status" class="slds-spinner slds-spinner--medium">
289
            <span class="slds-assistive-text">Loading</span>
290
            <div class="slds-spinner__dot-a"></div>
291
            <div class="slds-spinner__dot-b"></div>
292
        </div>`;
293
        this.element.appendChild(loadingElement);
24✔
294
    }
295

296
    private render() {
297
        const element = document.createElement('div');
8✔
298

299
        const summarySection = this.buildAccordion(this.options.summaryLabel, [
8✔
300
            this.buildCoveoElement(ClickedDocumentList),
301
            this.buildCoveoElement(QueryList),
302
        ]);
303
        summarySection.classList.add(`coveo-summary`);
8✔
304

305
        const detailsSection = this.buildAccordion(this.options.activityLabel, [this.buildCoveoElement(UserActivity)]);
8✔
306
        detailsSection.classList.add('coveo-details');
8✔
307

308
        element.appendChild(summarySection);
8✔
309
        element.appendChild(detailsSection);
8✔
310

311
        this.initializeSearchUIComponents(element);
8✔
312

313
        this.element.innerHTML = '';
8✔
314
        this.element.appendChild(element);
8✔
315
    }
316

317
    private renderNoActions() {
318
        const messageContainer = document.createElement('div');
15✔
319
        messageContainer.classList.add('coveo-no-actions');
15✔
320
        messageContainer.innerHTML = `
15✔
321
        <div class="coveo-user-actions-title">${l(UserActions.ID)}</div>
322
        <p>${l(UserActions.ID + '_no_actions_title')}.</p>
323
        <div>
324
            <span>${l(UserActions.ID + '_no_actions_causes_title')}</span>
325
            <ul class="coveo-no-actions-causes">
326
                <li>${l(UserActions.ID + '_no_actions_cause_not_associated')}.</li>
327
                <li>${l(UserActions.ID + '_no_actions_cause_case_too_old')}.</li>
328
            </ul>
329
        </div>
330
        <p>${l(UserActions.ID + '_no_actions_contact_admin')}.</p>
331
        `;
332

333
        this.element.innerHTML = '';
15✔
334
        this.element.appendChild(messageContainer);
15✔
335
    }
336

337
    private renderEnablePrompt() {
338
        const messageContainer = document.createElement('div');
1✔
339
        messageContainer.classList.add('coveo-no-actions');
1✔
340
        messageContainer.innerHTML = `
1✔
341
        <div class="coveo-user-actions-title">${l(UserActions.ID)}</div>
342
        <p>${l(UserActions.ID + '_no_actions_cause_not_enabled')}.</p>
343
        <p>${l(UserActions.ID + '_no_actions_contact_admin')}.</p>
344
        `;
345

346
        this.element.innerHTML = '';
1✔
347
        this.element.appendChild(messageContainer);
1✔
348
    }
349

350
    private showViewedByCustomer() {
351
        this.bind.onRootElement(ResultListEvents.newResultDisplayed, (args: IDisplayedNewResultEventArgs) => {
40✔
352
            if (Boolean(args.item.getElementsByClassName('CoveoViewedByCustomer').length)) {
4✔
353
                return;
1✔
354
            }
355
            if (this.inferResultListLayout() !== ResultLayoutType.TABLE) {
3✔
356
                const resultLastRow = '.coveo-result-row:last-child';
2✔
357
                args.item
2✔
358
                    .querySelector(resultLastRow)
359
                    .parentNode.appendChild(ViewedByCustomer.getViewedByCustomerResultRowDom(this.bindings, args.result));
360
            }
361
        });
362
    }
363

364
    private tagViewsOfUser() {
365
        Coveo.$$(this.root).on('buildingQuery', (e, args) => {
42✔
366
            try {
4✔
367
                args.queryBuilder.userActions = {
4✔
368
                    tagViewsOfUser: this.options.userId,
369
                };
370
            } catch (e) {
371
                this.logger.warn("CreatedBy Email wasn't found", e);
2✔
372
            }
373
        });
374
    }
375

376
    private inferResultListLayout(): ResultLayoutType {
377
        const resultLists = this.root.querySelectorAll<HTMLElement>(`${Component.computeSelectorForType(ResultList.ID)}:not(.coveo-hidden)`);
3✔
378
        const resultListLayoutTypes = [ResultLayoutType.CARD, ResultLayoutType.TABLE, ResultLayoutType.LIST] as string[];
3✔
379

380
        if (resultLists.length > 0 && resultListLayoutTypes.indexOf(resultLists[0].dataset.layout) !== -1) {
3!
381
            return resultLists[0].dataset.layout as ResultLayoutType;
3✔
382
        }
383
        return ResultLayoutType.LIST;
×
384
    }
385
}
386

387
Initialization.registerAutoCreateComponent(UserActions);
12✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc