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

yext / answers-search-ui / 15166287981

21 May 2025 03:28PM UTC coverage: 61.803% (-0.004%) from 61.807%
15166287981

push

github

web-flow
Merge master (v1.18.3) into develop (#1933)

* Remove run shell injection vulnerability (#1926)

Prevents attackers from injecting their own code into the github actions runner using variable interpolation to steal screts and code. We now use an intermediate environment variable to store input data.

* Remove run shell injection vulnerability (pt 2) (#1928)

Prevents attackers from injecting their own code into the github actions runner using variable interpolation to steal secrets and code. We now use an intermediate environment variable to store input data (pt 2).

* Deduplicate generative answer sources (#1930)

If an entity is present in search results more than once (e.g. returned by two different verticals) and is cited by the GDA, it appeared in the sources more than one. This change dedupes the search results while determining the sources to prevent this.

* Fix search result vertical list reversal before GDA call (#1932)

The list of search result verticals was reversed before it being sent to the backend for generative answers computation. This patch fixes this bug by changing a mutating array reversal to act on a copy of the list of search result verticals rather than the list itself.

---------

Co-authored-by: Kyle Gerner <49618240+k-gerner@users.noreply.github.com>
Co-authored-by: mkouzel-yext <mkouzel@yext.com>
Co-authored-by: anguyen-yext2 <143001514+anguyen-yext2@users.noreply.github.com>

2033 of 3437 branches covered (59.15%)

Branch coverage included in aggregate %.

1 of 1 new or added line in 1 file covered. (100.0%)

1 existing line in 1 file now uncovered.

3486 of 5493 relevant lines covered (63.46%)

26.67 hits per line

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

83.33
/src/ui/components/component.js
1
/** @module Component */
2

3
import cloneDeep from 'lodash.clonedeep';
4

5
import { Renderers } from '../rendering/const';
6

7
import DOM from '../dom/dom';
8
import State from './state';
9
import { AnalyticsReporter } from '../../core'; // eslint-disable-line no-unused-vars
10
import AnalyticsEvent from '../../core/analytics/analyticsevent';
11
import { AnswersComponentError } from '../../core/errors/errors';
12

13
/**
14
 * Component is an abstraction that encapsulates state, behavior,
15
 * and view for a particular chunk of functionality on the page.
16
 *
17
 * The API exposes event life cycle hooks for when things are rendered,
18
 * mounted, created, etc.
19
 */
20
export default class Component {
21
  constructor (config = {}, systemConfig = {}) {
×
22
    this.moduleId = null;
248✔
23

24
    /**
25
     * A unique id number for the component.
26
     * @type {number}
27
     */
28
    this.uniqueId = systemConfig.uniqueId;
248✔
29

30
    /**
31
     * Name of this component instance.
32
     * @type {String}
33
     */
34
    this.name = config.name || this.constructor.type;
248✔
35

36
    /**
37
     * Cache the options so that we can propogate properly to child components
38
     * @type {Object}
39
     */
40
    this._config = config;
248✔
41

42
    /**
43
     * An identifier used to classify the type of component.
44
     * The component manager uses this information in order to persist and organize components
45
     * @type {string|ComponentType}
46
     */
47
    this._type = this.constructor.name;
248✔
48

49
    /**
50
     * A local reference to the parent component, if exists
51
     * @type {Component}
52
     */
53
    this._parentContainer = config.parentContainer || null;
248✔
54

55
    /**
56
     * A container for all the child components
57
     * @type {Component[]}
58
     */
59
    this._children = [];
248✔
60

61
    /**
62
     * The state (data) of the component to be provided to the template for rendering
63
     * @type {object}
64
     */
65
    this._state = new State(config.state);
248✔
66

67
    /**
68
     * TODO(billy) This should be 'services'
69
     */
70
    this.core = systemConfig.core || null;
248!
71

72
    /**
73
     * A local reference to the component manager, which contains all of the component classes
74
     * eligible to be created
75
     * @type {ComponentManager}
76
     */
77
    this.componentManager = systemConfig.componentManager || null;
248✔
78

79
    /**
80
     * A local reference to the analytics reporter, used to report events for this component
81
     * @type {AnalyticsReporter}
82
     */
83
    this.analyticsReporter = systemConfig.analyticsReporter || null;
248✔
84

85
    /**
86
     * Options to include with all analytic events sent by this component
87
     * @type {object}
88
     * @private
89
     */
90
    this._analyticsOptions = config.analyticsOptions || {};
248✔
91

92
    /**
93
     * Allows the main thread to regain control while rendering child components
94
     * @type {boolean}
95
     */
96
    this._progressivelyRenderChildren = config.progressivelyRenderChildren;
248✔
97

98
    /**
99
     * A reference to the DOM node that the component will be appended to when mounted/rendered.
100
     * @type {HTMLElement}
101
     */
102
    if (this._parentContainer === null) {
248✔
103
      if (typeof config.container === 'string') {
193✔
104
        this._container = DOM.query(config.container) || null;
155!
105
        if (this._container === null) {
155!
106
          throw new Error('Cannot find container DOM node: ' + config.container);
×
107
        }
108
      }
109
    } else {
110
      this._container = DOM.query(this._parentContainer, config.container);
55✔
111

112
      // If we have a parent, and the container is missing from the DOM,
113
      // we construct the container and append it to the parent
114
      if (this._container === null) {
55✔
115
        this._container = DOM.createEl('div', {
1✔
116
          class: config.container.substring(1, config.container.length)
117
        });
118
        DOM.append(this._parentContainer, this._container);
1✔
119
      }
120
    }
121

122
    /**
123
     * A custom class to be applied to {this._container} node. Note that the class
124
     * 'yxt-Answers-component' will be included as well.
125
     * @type {string}
126
     */
127
    this._className = config.class || 'component';
248✔
128

129
    /**
130
     * A custom render function to be used instead of using the default renderer
131
     * @type {Renderer}
132
     */
133
    this._render = config.render || null;
248✔
134

135
    /**
136
     * A local reference to the default {Renderer} that will be used for rendering the template
137
     * @type {Renderer}
138
     */
139
    this._renderer = systemConfig.renderer || Renderers.Handlebars;
248✔
140

141
    /**
142
     * The template string to use for rendering the component
143
     * If this is left empty, we lookup the template the base templates using the templateName
144
     * @type {string}
145
     */
146
    this._template = config.template ? this._renderer.compile(config.template) : null;
248✔
147

148
    /**
149
     * The templateName to use for rendering the component.
150
     * This is only used if _template is empty.
151
     * @type {string}
152
     */
153
    this._templateName = config.templateName || this.constructor.defaultTemplateName(config);
248✔
154

155
    /**
156
     * An internal state indicating whether or not the component has been mounted to the DOM
157
     * @type {boolean}
158
     */
159
    this._isMounted = false;
248✔
160

161
    /**
162
     * A local reference to the callback, thats used to transform the internal data
163
     * models of the components, before it gets applied to the component state.
164
     * By default, no transformation happens.
165
     * @type {function}
166
     */
167
    this.transformData = config.transformData;
248✔
168

169
    /**
170
     * A local reference to the callback that will be invoked when a component is created.
171
     * @type {function}
172
     */
173
    this.onCreate = config.onCreateOverride || this.onCreate || function () {};
248!
174
    this.onCreate = this.onCreate.bind(this);
248✔
175

176
    /**
177
     * A local reference to the callback that will be invoked before a component is mounted.
178
     * @type {function}
179
     */
180
    this.beforeMount = config.beforeMountOverride || this.beforeMount || function () {};
248!
181
    this.beforeMount = this.beforeMount.bind(this);
248✔
182

183
    /**
184
     * A local reference to the callback that will be invoked when a component is mounted.
185
     * @type {function}
186
     */
187
    this.onMount = config.onMountOverride || this.onMount || function () {};
248!
188
    this.onMount = this.onMount.bind(this);
248✔
189

190
    /**
191
     * A local reference to the callback that will be invoked when a components state is updated.
192
     * @type {function}
193
     */
194
    this.onUpdate = config.onUpdateOverride || this.onUpdate || function () { };
248!
195
    this.onUpdate = this.onUpdate.bind(this);
248✔
196

197
    /**
198
     * A user provided onCreate callback
199
     * @type {function}
200
     */
201
    this.userOnCreate = config.onCreate || function () {};
248✔
202

203
    /**
204
     * A user provided onMount callback
205
     * @type {function}
206
     */
207
    this.userOnMount = config.onMount || function () {};
248✔
208

209
    /**
210
     * A user provided onUpdate callback
211
     * @type {function}
212
     */
213
    this.userOnUpdate = config.onUpdate || function () {};
248✔
214
  }
215

216
  /**
217
   * The template to render
218
   * @returns {string}
219
   * @override
220
   */
221
  static defaultTemplateName (config) {
222
    return 'default';
9✔
223
  }
224

225
  static get type () {
226
    return 'Component';
34✔
227
  }
228

229
  static areDuplicateNamesAllowed () {
230
    return false;
1✔
231
  }
232

233
  init (opts) {
234
    try {
242✔
235
      this.setState(opts.data || opts.state || {});
242✔
236
      this.onCreate();
242✔
237
      this.userOnCreate();
242✔
238
    } catch (e) {
239
      throw new AnswersComponentError(
×
240
        'Error initializing component',
241
        this.constructor.type,
242
        e);
243
    }
244

245
    this._state.on('update', () => {
242✔
246
      try {
124✔
247
        this.onUpdate();
124✔
248
        this.userOnUpdate();
124✔
249
        this.unMount();
124✔
250
        this.mount();
124✔
251
      } catch (e) {
252
        throw new AnswersComponentError(
×
253
          'Error updating component',
254
          this.constructor.type,
255
          e);
256
      }
257
    });
258

259
    DOM.addClass(this._container, this._className);
242✔
260
    DOM.addClass(this._container, 'yxt-Answers-component');
242✔
261
    return this;
242✔
262
  }
263

264
  /**
265
   * Adds a class to the container of the component.
266
   * @param {string} className A comma separated value of classes
267
   */
268
  addContainerClass (className) {
269
    DOM.addClass(this._container, className);
81✔
270
  }
271

272
  /**
273
   * Removes the specified classes from the container of the component
274
   * @param {string} className A comma separated value of classes
275
   */
276
  removeContainerClass (className) {
277
    DOM.removeClass(this._container, className);
243✔
278
  }
279

280
  setState (data) {
281
    const newState = Object.assign({}, { _config: this._config }, data);
366✔
282
    this._state.set(newState);
366✔
283
    return this;
366✔
284
  }
285

286
  getState (prop) {
287
    return this._state.get(prop);
63✔
288
  }
289

290
  hasState (prop) {
291
    return this._state.has(prop);
8✔
292
  }
293

294
  addChild (data, type, opts) {
295
    const childComponent = this.componentManager.create(
2✔
296
      type,
297
      Object.assign({
298
        name: data.name,
299
        parentContainer: this._container,
300
        data: data
301
      }, opts || {}, {
2!
302
        _parentOpts: this._config
303
      })
304
    );
305

306
    this._children.push(childComponent);
2✔
307
    return childComponent;
2✔
308
  }
309

310
  /**
311
   * Unmount and remove this component and its children from the list
312
   * of active components
313
   */
314
  remove () {
315
    this._children.forEach(c => c.remove());
10✔
316
    this.componentManager.remove(this);
10✔
317
  }
318

319
  /**
320
   * Set the render method to be used for rendering the component
321
   * @param {Function} render
322
   * @return {string}
323
   */
324
  setRender (render) {
325
    this._render = render;
×
326
    return this;
×
327
  }
328

329
  /**
330
   * Set the renderer for the component
331
   * @param {RendererType} renderer
332
   */
333
  setRenderer (renderer) {
334
    this._renderer = Renderers[renderer];
×
335
    return this;
×
336
  }
337

338
  /**
339
   * Sets the template for the component to use when rendering
340
   * @param {string} template
341
   */
342
  setTemplate (template) {
343
    this._template = this._renderer.compile(template);
5✔
344
  }
345

346
  unMount () {
347
    if (!this._container) {
124✔
348
      return this;
15✔
349
    }
350

351
    this._children.forEach(child => {
109✔
352
      child.unMount();
×
353
    });
354

355
    DOM.empty(this._container);
109✔
356
    this._children.forEach(c => c.remove());
109✔
357
    this._children = [];
109✔
358
    this.onUnMount();
109✔
359
  }
360

361
  mount (container) {
362
    if (container) {
292✔
363
      this._container = container;
110✔
364
    }
365

366
    if (!this._container) {
292✔
367
      return this;
16✔
368
    }
369

370
    if (this.beforeMount() === false) {
276✔
371
      return this;
5✔
372
    }
373

374
    DOM.append(this._container, this.render(this._state.asJSON()));
271✔
375

376
    // Process the DOM to determine if we should create
377
    // in-memory sub-components for rendering
378
    const domComponents = DOM.queryAll(this._container, '[data-component]:not([data-is-component-mounted])');
271✔
379
    let data;
380
    try {
271✔
381
      data = this.transformData
271!
382
        ? this.transformData(cloneDeep(this._state.get()))
383
        : this._state.get();
384
    } catch (e) {
385
      console.error(`The following problem occurred while transforming data for sub-components of ${this.name}: `, e);
×
386
    }
387
    domComponents.forEach(c => {
271✔
388
      try {
3✔
389
        this._createSubcomponent(c, data);
3✔
390
      } catch (e) {
391
        console.error('The following problem occurred while initializing sub-component: ', c, e);
×
392
      }
393
    });
394
    if (this._progressivelyRenderChildren) {
271!
395
      this._children.forEach(child => {
×
396
        setTimeout(() => {
×
397
          try {
×
398
            child.mount();
×
399
          } catch (e) {
400
            console.error('The following problem occurred while mounting sub-component: ', child, e);
×
401
          }
402
        });
403
      });
404
    } else {
405
      this._children.forEach(child => {
271✔
406
        try {
2✔
407
          child.mount();
2✔
408
        } catch (e) {
409
          console.error('The following problem occurred while mounting sub-component: ', child, e);
×
410
        }
411
      });
412
    }
413

414
    // Attach analytics hooks as necessary
415
    if (this.analyticsReporter) {
271!
416
      const domHooks = DOM.queryAll(this._container, '[data-eventtype]:not([data-is-analytics-attached])');
271✔
417
      domHooks.forEach(this._createAnalyticsHook.bind(this));
271✔
418
    }
419

420
    this._isMounted = true;
271✔
421
    this.onMount(this);
271✔
422
    this.userOnMount(this);
271✔
423

424
    return this;
271✔
425
  }
426

427
  /**
428
   * render the template using the {Renderer} with the current state and template of the component
429
   * @returns {string}
430
   */
431
  render (data = this._state.get()) {
2✔
432
    this.beforeRender();
273✔
433
    // Temporary fix for passing immutable data to transformData().
434
    data = this.transformData
273!
435
      ? this.transformData(cloneDeep(data))
436
      : data;
437

438
    let html = '';
273✔
439
    // Use either the custom render function or the internal renderer
440
    // dependant on the component configuration
441
    if (typeof this._render === 'function') {
273!
442
      html = this._render(data);
×
443
      if (typeof html !== 'string') {
×
444
        throw new Error('Render method must return HTML as type {string}');
×
445
      }
446
    } else {
447
      // Render the existing templates as a string
448
      html = this._renderer.render({
273✔
449
        template: this._template,
450
        templateName: this._templateName
451
      }, data);
452
    }
453

454
    this.afterRender();
273✔
455
    return html;
273✔
456
  }
457

458
  _createSubcomponent (domComponent, data) {
459
    domComponent.dataset.isComponentMounted = true;
3✔
460
    const dataset = domComponent.dataset;
3✔
461
    const type = dataset.component;
3✔
462
    const prop = dataset.prop;
3✔
463
    let opts = dataset.opts ? JSON.parse(dataset.opts) : {};
3✔
464

465
    const childData = data[prop] || {};
3✔
466

467
    opts = {
3✔
468
      ...opts,
469
      container: domComponent
470
    };
471

472
    // TODO(billy) Right now, if we provide an array as the data prop,
473
    // the behavior is to create many components for each item in the array.
474
    // THAT interface SHOULD change to use a different property that defines
475
    // whether to array data should be used for a single component or
476
    // to create many components for each item.
477
    // Overloading and having this side effect is unintuitive and WRONG
478
    if (!Array.isArray(childData)) {
3✔
479
      // Rendering a sub component should be within the context,
480
      // of the node that we processed it from
481
      this.addChild(childData, type, opts);
2✔
482
      return;
2✔
483
    }
484

485
    childData.slice().reverse().forEach(data => {
1✔
UNCOV
486
      this.addChild(data, type, opts);
×
487
    });
488
  }
489

490
  _createAnalyticsHook (domComponent) {
491
    domComponent.dataset.isAnalyticsAttached = true;
101✔
492
    const dataset = domComponent.dataset;
101✔
493
    const type = dataset.eventtype;
101✔
494
    const label = dataset.eventlabel;
101✔
495
    const middleclick = dataset.middleclick;
101✔
496
    const options = dataset.eventoptions ? JSON.parse(dataset.eventoptions) : {};
101✔
497

498
    DOM.on(domComponent, 'mousedown', e => {
101✔
499
      if (e.button === 0 || (middleclick && e.button === 1)) {
4!
500
        const event = new AnalyticsEvent(type, label);
4✔
501
        event.addOptions(this._analyticsOptions);
4✔
502
        event.addOptions(options);
4✔
503
        this.analyticsReporter.report(event);
4✔
504
      }
505
    });
506
  }
507

508
  /**
509
   * onCreate is triggered when the component is constructed
510
   * @param {function} the callback to invoke upon emit
511
   */
512
  onCreate (cb) {
513

514
  }
515

516
  /**
517
   * onUpdate is triggered when the state of the component changes
518
   * @param {function} the callback to invoke upon emit
519
   */
520
  onUpdate (cb) {
521

522
  }
523

524
  /**
525
   * beforeRender event is triggered before the component is rendered
526
   * @param {function} the callback to invoke upon emit
527
   */
528
  beforeRender (cb) {
529

530
  }
531

532
  /**
533
   * afterRender event is triggered after the component is rendered
534
   * @param {function} the callback to invoke upon emit
535
   */
536
  afterRender (cb) {
537

538
  }
539

540
  /**
541
   * onMount is triggered when the component is appended to the DOM
542
   * @param {function} the callback to invoke upon emit
543
   */
544
  onMount (cb) {
545

546
  }
547

548
  /**
549
   * onUnMount is triggered when the component is removed from the DOM
550
   * @param {function} the callback to invoke upon emit
551
   */
552
  onUnMount (cb) {
553

554
  }
555

556
  /**
557
   * beforeMount is triggered before the component is mounted to the DOM
558
   * @param {function} the callback to invoke upon emit
559
   */
560
  beforeMount (cb) {
561

562
  }
563

564
  /**
565
   * onDestroy is triggered when the component is destroyed
566
   * @param {function} the callback to invoke upon emit
567
   */
568
  onDestroy (cb) {
569

570
  }
571
}
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