@miso.ai/client-sdk-ui 1.11.0-beta.1 → 1.11.0-beta.11

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.
Files changed (118) hide show
  1. package/dist/cjs/actor/data.js +6 -7
  2. package/dist/cjs/actor/feedback.js +6 -39
  3. package/dist/cjs/actor/fields.js +14 -6
  4. package/dist/cjs/actor/hub.js +1 -2
  5. package/dist/cjs/actor/index.js +0 -7
  6. package/dist/cjs/actor/interactions.js +4 -15
  7. package/dist/cjs/actor/sessions.js +2 -33
  8. package/dist/cjs/actor/tracker.js +25 -26
  9. package/dist/cjs/actor/utils.js +0 -49
  10. package/dist/cjs/actor/view.js +40 -40
  11. package/dist/cjs/actor/views.js +32 -8
  12. package/dist/cjs/asset/svgs.js +18 -8
  13. package/dist/cjs/constants.js +18 -19
  14. package/dist/cjs/defaults/hybrid-search/answer-box.js +19 -3
  15. package/dist/cjs/defaults/hybrid-search/phrases.js +5 -5
  16. package/dist/cjs/defaults/hybrid-search/templates.js +37 -9
  17. package/dist/cjs/element/role/index.js +12 -5
  18. package/dist/cjs/element/role/{miso-hits.js → miso-more.js} +4 -4
  19. package/dist/cjs/element/role/miso-total.js +20 -0
  20. package/dist/cjs/layout/affiliation/affiliation.js +0 -3
  21. package/dist/cjs/layout/banner/banner.js +0 -4
  22. package/dist/cjs/layout/button/more-button.js +73 -0
  23. package/dist/cjs/layout/container/container.js +0 -3
  24. package/dist/cjs/layout/error/error.js +0 -4
  25. package/dist/cjs/layout/index.js +7 -0
  26. package/dist/cjs/layout/input/facets.js +5 -5
  27. package/dist/cjs/layout/input/option-list.js +0 -4
  28. package/dist/cjs/layout/input/search-box.js +35 -17
  29. package/dist/cjs/layout/list/cards.js +0 -3
  30. package/dist/cjs/layout/list/carousel.js +0 -3
  31. package/dist/cjs/layout/list/collection.js +22 -7
  32. package/dist/cjs/layout/list/list.js +0 -3
  33. package/dist/cjs/layout/radio/feedback.js +0 -3
  34. package/dist/cjs/layout/radio/radio.js +0 -4
  35. package/dist/cjs/layout/raf.js +4 -2
  36. package/dist/cjs/layout/text/text.js +0 -4
  37. package/dist/cjs/layout/text/typewriter/index.js +0 -3
  38. package/dist/cjs/layout/triggerable.js +79 -0
  39. package/dist/cjs/util/states.js +0 -15
  40. package/dist/cjs/version.js +1 -1
  41. package/dist/cjs/workflow/answer-based.js +109 -111
  42. package/dist/cjs/workflow/ask.js +35 -27
  43. package/dist/cjs/workflow/asks.js +1 -1
  44. package/dist/cjs/workflow/autocompletable.js +33 -0
  45. package/dist/cjs/workflow/autocomplete.js +122 -0
  46. package/dist/cjs/workflow/base.js +169 -102
  47. package/dist/cjs/workflow/explore.js +26 -19
  48. package/dist/cjs/workflow/hybrid-search-answer.js +99 -0
  49. package/dist/cjs/workflow/hybrid-search-results.js +66 -0
  50. package/dist/cjs/workflow/hybrid-search-views.js +122 -0
  51. package/dist/cjs/workflow/hybrid-search.js +98 -103
  52. package/dist/cjs/workflow/options.js +32 -17
  53. package/dist/cjs/workflow/processors.js +191 -17
  54. package/dist/cjs/workflow/recommendation.js +36 -49
  55. package/dist/cjs/workflow/search-based.js +228 -0
  56. package/dist/cjs/workflow/search.js +21 -68
  57. package/package.json +2 -2
  58. package/src/actor/data.js +2 -4
  59. package/src/actor/feedback.js +6 -23
  60. package/src/actor/fields.js +14 -6
  61. package/src/actor/hub.js +1 -2
  62. package/src/actor/index.js +0 -1
  63. package/src/actor/interactions.js +4 -10
  64. package/src/actor/sessions.js +3 -30
  65. package/src/actor/tracker.js +25 -19
  66. package/src/actor/utils.js +0 -47
  67. package/src/actor/view.js +27 -15
  68. package/src/actor/views.js +36 -8
  69. package/src/asset/svgs.js +19 -7
  70. package/src/constants.js +25 -16
  71. package/src/defaults/hybrid-search/answer-box.js +17 -5
  72. package/src/defaults/hybrid-search/phrases.js +4 -4
  73. package/src/defaults/hybrid-search/templates.js +25 -9
  74. package/src/element/role/index.js +2 -1
  75. package/src/element/role/{miso-hits.js → miso-more.js} +3 -3
  76. package/src/element/role/miso-total.js +16 -0
  77. package/src/layout/affiliation/affiliation.js +1 -5
  78. package/src/layout/banner/banner.js +0 -5
  79. package/src/layout/button/more-button.js +69 -0
  80. package/src/layout/container/container.js +1 -5
  81. package/src/layout/error/error.js +0 -5
  82. package/src/layout/index.js +2 -1
  83. package/src/layout/input/facets.js +5 -6
  84. package/src/layout/input/option-list.js +0 -5
  85. package/src/layout/input/search-box.js +29 -19
  86. package/src/layout/list/cards.js +0 -4
  87. package/src/layout/list/carousel.js +0 -4
  88. package/src/layout/list/collection.js +21 -9
  89. package/src/layout/list/list.js +0 -4
  90. package/src/layout/radio/feedback.js +0 -4
  91. package/src/layout/radio/radio.js +0 -5
  92. package/src/layout/raf.js +2 -2
  93. package/src/layout/text/text.js +0 -5
  94. package/src/layout/text/typewriter/index.js +1 -5
  95. package/src/layout/trackable.js +1 -1
  96. package/src/layout/triggerable.js +76 -0
  97. package/src/util/states.js +0 -14
  98. package/src/version.js +1 -1
  99. package/src/workflow/answer-based.js +101 -77
  100. package/src/workflow/ask.js +33 -25
  101. package/src/workflow/asks.js +1 -1
  102. package/src/workflow/autocompletable.js +29 -0
  103. package/src/workflow/autocomplete.js +113 -0
  104. package/src/workflow/base.js +147 -93
  105. package/src/workflow/explore.js +23 -8
  106. package/src/workflow/hybrid-search-answer.js +92 -0
  107. package/src/workflow/hybrid-search-results.js +62 -0
  108. package/src/workflow/hybrid-search-views.js +124 -0
  109. package/src/workflow/hybrid-search.js +102 -73
  110. package/src/workflow/options.js +26 -11
  111. package/src/workflow/processors.js +169 -14
  112. package/src/workflow/recommendation.js +32 -33
  113. package/src/workflow/search-based.js +222 -0
  114. package/src/workflow/search.js +21 -65
  115. package/dist/cjs/actor/autocomplete.js +0 -160
  116. package/dist/cjs/workflow/utils.js +0 -42
  117. package/src/actor/autocomplete.js +0 -133
  118. package/src/workflow/utils.js +0 -29
@@ -0,0 +1,124 @@
1
+ import { ROLE } from '../constants.js';
2
+
3
+ function getSubworkflowByRole(role) {
4
+ // TODO: ROLE.ERROR
5
+ switch (role) {
6
+ case ROLE.PRODUCTS:
7
+ case ROLE.TOTAL:
8
+ case ROLE.FACETS:
9
+ case ROLE.KEYWORDS:
10
+ case ROLE.MORE:
11
+ return 'results';
12
+ default:
13
+ return 'answer';
14
+ }
15
+ }
16
+
17
+ export default class HybridSearchViewsActor {
18
+
19
+ constructor(workflow) {
20
+ this._workflow = workflow;
21
+ this._containers = new Set();
22
+ }
23
+
24
+ get(role) {
25
+ return this._getSubviews(role).get(role);
26
+ }
27
+
28
+ addContainer(element) {
29
+ if (this._containers.has(element)) {
30
+ return;
31
+ }
32
+ this._containers.add(element);
33
+
34
+ const { components } = element;
35
+ for (const component of components) {
36
+ this.addComponent(component);
37
+ }
38
+ }
39
+
40
+ removeContainer(element) {
41
+ for (const subworkflow of this._workflow._subworkflows) {
42
+ subworkflow._views.removeContainer(element);
43
+ }
44
+ this._containers.delete(element);
45
+ }
46
+
47
+ addComponent(element) {
48
+ this._getViewByElement(element).element = element;
49
+ this._addContainerToSubviews(element._container, element);
50
+ }
51
+
52
+ removeComponent(element) {
53
+ this._getViewByElement(element).element = undefined;
54
+ this._removeContainerFromSubviewsIfNecessary(element._container);
55
+ }
56
+
57
+ updateComponentRole(element, oldRole, newRole) {
58
+ // TODO
59
+ }
60
+
61
+ refreshElement(element) {
62
+ const view = element.isContainer ? this._containers.get(element) : this._getViewByElement(element);
63
+ view && view.refresh({ force: true });
64
+ }
65
+
66
+ // helpers //
67
+ _getSubworkflow(role) {
68
+ return this._workflow._getSubworkflow(getSubworkflowByRole(role));
69
+ }
70
+
71
+ _getAllSubviews() {
72
+ return this._workflow._subworkflows.flatMap(subworkflow => subworkflow._views);
73
+ }
74
+
75
+ _getSubviews(role) {
76
+ return this._getSubworkflow(role)._views;
77
+ }
78
+
79
+ _getViewByElement(element) {
80
+ let { role } = element;
81
+ if (!role) {
82
+ throw new Error('Component must have a role');
83
+ }
84
+ return this.get(role);
85
+ }
86
+
87
+ _addContainerToSubviews(container, child) {
88
+ if (!container) {
89
+ return;
90
+ }
91
+ const { role } = child;
92
+ const subviews = this._getSubviews(role);
93
+ if (subviews._containers.has(container)) {
94
+ return;
95
+ }
96
+ // check if the container is in other subviews
97
+ for (const otherSubviews of this._getAllSubviews()) {
98
+ if (otherSubviews === subviews) {
99
+ continue;
100
+ }
101
+ if (otherSubviews._containers.has(container)) {
102
+ // find the conflicting element
103
+ for (const component of container.components) {
104
+ if (otherSubviews.containsElement(component)) {
105
+ throw new Error(`<${container.tagName}> cannot contain both <${component.tagName}> and <${child.tagName}>, for they don't share the same data lifecycle.`);
106
+ }
107
+ }
108
+ throw new Error(`Failed to add container <${container.tagName}> with child component <${child.tagName}>.`);
109
+ }
110
+ }
111
+ // add the container to the subviews
112
+ subviews.addContainer(container);
113
+ }
114
+
115
+ _removeContainerFromSubviewsIfNecessary(container) {
116
+ if (!container || container.components.length > 0) {
117
+ return;
118
+ }
119
+ for (const subviews of this._getAllSubviews()) {
120
+ subviews.removeContainer(container);
121
+ }
122
+ }
123
+
124
+ }
@@ -1,125 +1,154 @@
1
- import { trimObj, API } from '@miso.ai/commons';
1
+ import { API } from '@miso.ai/commons';
2
+ import { InteractionsActor } from '../actor/index.js';
3
+ import Workflow from './base.js';
4
+ import SearchBasedWorkflow from './search-based.js';
2
5
  import AnswerBasedWorkflow from './answer-based.js';
3
- import { mergeApiOptions } from './options.js';
4
- import { fields } from '../actor/index.js';
5
- import { ROLE, ORGANIC_QUESTION_SOURCE } from '../constants.js';
6
- import { ListLayout, TextLayout, FacetsLayout } from '../layout/index.js';
7
- import { writeKeywordsToData, addDataInstructions } from './processors.js';
6
+ import { ROLE } from '../constants.js';
7
+ import { SearchBoxLayout, TextLayout } from '../layout/index.js';
8
+ import HybridSearchAnswer from './hybrid-search-answer.js';
9
+ import HybridSearchResults from './hybrid-search-results.js';
10
+ import HybridSearchViewsActor from './hybrid-search-views.js';
8
11
 
9
12
  const DEFAULT_API_OPTIONS = Object.freeze({
10
- ...AnswerBasedWorkflow.DEFAULT_API_OPTIONS,
13
+ group: API.GROUP.ASK,
11
14
  name: API.NAME.SEARCH,
12
15
  payload: {
13
16
  ...AnswerBasedWorkflow.DEFAULT_API_OPTIONS.payload,
17
+ ...SearchBasedWorkflow.DEFAULT_API_OPTIONS.payload,
18
+ source_fl: ['cover_image', 'url', 'created_at', 'updated_at', 'published_at', 'title'],
14
19
  },
15
20
  });
16
21
 
17
22
  const DEFAULT_LAYOUTS = Object.freeze({
18
23
  ...AnswerBasedWorkflow.DEFAULT_LAYOUTS,
19
- [ROLE.PRODUCTS]: [ListLayout.type, { itemType: 'article' }],
20
- [ROLE.KEYWORDS]: [TextLayout.type, { raw: true }],
21
- [ROLE.HITS]: [TextLayout.type, { raw: true, format: 'number' }],
22
- [ROLE.FACETS]: [FacetsLayout.type],
24
+ ...SearchBasedWorkflow.DEFAULT_LAYOUTS,
25
+ [ROLE.QUERY]: [SearchBoxLayout.type, { placeholder: '' }],
26
+ [ROLE.QUESTION]: [TextLayout.type, { raw: true }],
23
27
  });
24
28
 
25
29
  const DEFAULT_TRACKERS = Object.freeze({
26
30
  ...AnswerBasedWorkflow.DEFAULT_TRACKERS,
31
+ ...SearchBasedWorkflow.DEFAULT_TRACKERS,
32
+ });
33
+
34
+ const DEFAULT_PAGINATION = Object.freeze({
35
+ ...SearchBasedWorkflow.DEFAULT_PAGINATION,
36
+ active: true,
37
+ });
38
+
39
+ const DEFAULT_AUTOCOMPLETE_OPTIONS = Object.freeze({
40
+ api: {
41
+ group: API.GROUP.ASK,
42
+ name: API.NAME.SEARCH_AUTOCOMPLETE,
43
+ payload: {
44
+ completion_fields: ['suggested_queries'],
45
+ },
46
+ },
27
47
  });
28
48
 
29
49
  const DEFAULT_OPTIONS = Object.freeze({
30
50
  ...AnswerBasedWorkflow.DEFAULT_OPTIONS,
51
+ ...SearchBasedWorkflow.DEFAULT_OPTIONS,
31
52
  api: DEFAULT_API_OPTIONS,
32
53
  layouts: DEFAULT_LAYOUTS,
33
54
  trackers: DEFAULT_TRACKERS,
55
+ autocomplete: DEFAULT_AUTOCOMPLETE_OPTIONS,
56
+ pagination: DEFAULT_PAGINATION,
34
57
  });
35
58
 
36
- const ROLES_CONFIG = Object.freeze({
37
- [ROLE.QUESTION]: {
38
- mapping: ROLE.KEYWORDS,
39
- },
40
- [ROLE.FACETS]: {
41
- mapping: 'facet_counts',
42
- },
59
+ const ROLES_OPTIONS = Object.freeze({
60
+ main: ROLE.PRODUCTS,
61
+ members: Object.keys(DEFAULT_LAYOUTS),
62
+ mappings: Object.freeze({
63
+ ...AnswerBasedWorkflow.ROLES_OPTIONS.mappings,
64
+ ...SearchBasedWorkflow.ROLES_OPTIONS.mappings,
65
+ }),
43
66
  });
44
67
 
45
- export default class HybridSearch extends AnswerBasedWorkflow {
68
+ export default class HybridSearch extends Workflow {
46
69
 
47
70
  constructor(plugin, client) {
48
71
  super({
49
72
  name: 'hybrid-search',
50
73
  plugin,
51
74
  client,
52
- roles: Object.keys(DEFAULT_LAYOUTS),
53
- rolesConfig: ROLES_CONFIG,
75
+ roles: ROLES_OPTIONS,
54
76
  defaults: DEFAULT_OPTIONS,
55
77
  });
56
78
  }
57
79
 
58
- _initSubscriptions(args) {
59
- super._initSubscriptions(args);
60
- this._unsubscribes = [
61
- ...this._unsubscribes,
62
- this._hub.on(fields.filters(), filters => this._refine(filters)),
80
+ _initProperties(args) {
81
+ super._initProperties(args);
82
+ this._subworkflows = [
83
+ this._answer = new HybridSearchAnswer(this),
84
+ this._results = new HybridSearchResults(this),
63
85
  ];
64
86
  }
65
87
 
66
- // query //
67
- _buildPayload({ q, qs, filters, ...payload } = {}) {
68
- const fq = this._buildFq(filters); // TODO: combine with fq in payload?
69
- return trimObj({
70
- ...payload,
71
- q, // q, not question
72
- fq,
73
- _meta: {
74
- ...payload._meta,
75
- question_source: qs || ORGANIC_QUESTION_SOURCE, // might be null, not undefined
76
- },
77
- });
88
+ _getSubworkflow(name) {
89
+ switch (name) {
90
+ case 'answer':
91
+ return this._answer;
92
+ case 'results':
93
+ return this._results;
94
+ default:
95
+ throw new Error(`Invalid subworkflow: ${name}`);
96
+ }
78
97
  }
79
98
 
80
- _buildFq(filters) {
81
- if (!filters) {
82
- return undefined;
83
- }
84
- const clauses = [];
85
- const { facets } = filters;
86
- for (const field in facets) {
87
- const values = facets[field];
88
- if (!values || !values.length) {
89
- continue; // just in case
90
- }
91
- clauses.push(`${field}:(${values.map(v => `"${v}"`).join(' OR ')})`);
92
- }
93
- return clauses.map(c => `(${c})`).join(' AND ');
99
+ _initActors() {
100
+ const hub = this._hub;
101
+ const client = this._client;
102
+ const options = this._options;
103
+
104
+ this._views = new HybridSearchViewsActor(this);
105
+ this._interactions = new InteractionsActor(hub, { client, options });
94
106
  }
95
107
 
96
- _refine(filters) {
97
- const query = this._hub.states[fields.query()];
98
- const payload = this._buildPayload({ ...query, filters, answer: false });
99
- const { session } = this;
108
+ _initSession() {
109
+ this._results._sessions.restart();
110
+ this._answer._sessions.restart();
111
+ }
100
112
 
101
- const event = mergeApiOptions(this._options.resolved.api, { payload, session, _skipLoading: true });
102
- this._request(event);
113
+ // configuration //
114
+ useLink(fn) {
115
+ this._answer.useLink(fn);
116
+ return this;
117
+ }
103
118
 
104
- // emulate loading status exclusively for products
105
- // TODO
119
+ // properties //
120
+ get questionId() {
121
+ return this._answer.questionId;
106
122
  }
107
123
 
108
- // data //
109
- _defaultProcessData(data) {
110
- data = super._defaultProcessData(data);
111
- data = writeKeywordsToData(data);
112
- data = instructPartialUpdates(data);
113
- return data;
124
+ get filters() {
125
+ return this._results._views.filters;
114
126
  }
115
127
 
116
- }
128
+ get autocomplete() {
129
+ return this._answer.autocomplete;
130
+ }
131
+
132
+ // query //
133
+ autoQuery(options) {
134
+ this._answer.autoQuery(options);
135
+ }
117
136
 
118
- function instructPartialUpdates(data) {
119
- if (!data.value) {
120
- return data;
137
+ query(args) {
138
+ this._answer.query(args);
121
139
  }
122
- return addDataInstructions(data, data.value.products ?
123
- { includes: [ ROLE.PRODUCTS, ROLE.HITS, ROLE.FACETS, ROLE.KEYWORDS, ROLE.QUESTION ], merge: true } :
124
- { excludes: [ ROLE.PRODUCTS, ROLE.HITS, ROLE.FACETS ], merge: true });
140
+
141
+ // interactions //
142
+ _defaultProcessInteraction(payload, args) {
143
+ return this._answer._defaultProcessInteraction(payload, args);
144
+ }
145
+
146
+ // destroy //
147
+ _destroy() {
148
+ for (const subworkflow of this._subworkflows) {
149
+ subworkflow.destroy();
150
+ }
151
+ super._destroy();
152
+ }
153
+
125
154
  }
@@ -110,15 +110,14 @@ export function normalizeInteractionsOptions(options) {
110
110
  return { ...options, preprocess };
111
111
  }
112
112
 
113
- export function normalizeAutocompleteOptions(options) {
113
+ export function normalizePaginationOptions(options) {
114
114
  if (options === undefined) {
115
- throw new Error(`Expect autocomplete options to be an object or a boolean value: ${options}`);
115
+ throw new Error(`Expect pagination options to be an object or a boolean value: ${options}`);
116
116
  }
117
117
  if (typeof options === 'boolean') {
118
- options = { actor: options };
118
+ options = { active: options };
119
119
  }
120
- const processData = normalizeDataProcessorOptions(options.processData);
121
- return { ...options, processData };
120
+ return options;
122
121
  }
123
122
 
124
123
  // merge //
@@ -206,17 +205,33 @@ export function mergeInteractionsOptions(...optionsList) {
206
205
  }));
207
206
  }
208
207
 
209
- export function mergeAutocompleteOptions(...optionsList) {
208
+ export function mergePaginationOptions(...optionsList) {
210
209
  if (optionsList[optionsList.length - 1] === false) {
211
210
  return false;
212
211
  }
212
+ return mergeOptions(optionsList, (merged, options) => Object.assign(merged, options));
213
+ }
214
+
215
+ export function mergeAutocompleteOptions(...optionsList) {
213
216
  return mergeOptions(optionsList, (merged, options) => Object.assign(merged, {
214
217
  ...options,
215
218
  api: mergeApiOptions(merged.api, options.api),
216
- processData: mergeDataProcessorOptions(merged.processData, options.processData),
217
219
  }));
218
220
  }
219
221
 
222
+ export function mergeRolesOptions(base, overrides) {
223
+ return Object.freeze({
224
+ ...base,
225
+ ...overrides,
226
+ members: mergeRolesMembers(base.members, overrides.members),
227
+ mappings: Object.freeze({ ...base.mappings, ...overrides.mappings }),
228
+ });
229
+ }
230
+
231
+ function mergeRolesMembers(base = [], overrides = []) {
232
+ return [...new Set([...base, ...overrides])];
233
+ }
234
+
220
235
 
221
236
 
222
237
  // manifest //
@@ -247,9 +262,9 @@ const FEATURES = [
247
262
  merge: mergeInteractionsOptions,
248
263
  },
249
264
  {
250
- key: 'autocomplete',
251
- normalize: normalizeAutocompleteOptions,
252
- merge: mergeAutocompleteOptions,
265
+ key: 'pagination',
266
+ normalize: normalizePaginationOptions,
267
+ merge: mergePaginationOptions,
253
268
  },
254
269
  ];
255
270
 
@@ -272,7 +287,7 @@ export class WorkflowOptions {
272
287
 
273
288
  constructor(context = {}, defaults = {}) {
274
289
  this._events = new EventEmitter({ target: this });
275
- this._defaults = defaults;
290
+ this._defaults = defaults; // TODO: we should call normalize() on defaults
276
291
  this._context = context || {};
277
292
  this._locals = {};
278
293
 
@@ -1,10 +1,15 @@
1
1
  import { trimObj } from '@miso.ai/commons';
2
- import { STATUS } from '../constants.js';
2
+ import { STATUS, ROLE, EVENT_TYPE, isPerformanceEventType, isProductRole } from '../constants.js';
3
+
4
+ // data //
5
+ export function getRevision(data) {
6
+ return data && data.value && data.value.revision;
7
+ }
3
8
 
4
9
  export function writeDataStatus(data) {
5
10
  const status = getStatus(data);
6
- const ongoing = status === STATUS.READY ? !!data.ongoing : undefined;
7
- return trimObj({ ...data, status, ongoing });
11
+ const ongoing = status === STATUS.READY ? data.ongoing : undefined;
12
+ return trimObj({ status, ongoing, ...data }); // let orinigal data override status and ongoing
8
13
  }
9
14
 
10
15
  function getStatus(data) {
@@ -35,7 +40,8 @@ export function writeMisoIdToMeta(data) {
35
40
 
36
41
  export function writeKeywordsToData(data) {
37
42
  const { request, value } = data;
38
- if (!request || !value) {
43
+ // we still want the value in loading status
44
+ if (!request) {
39
45
  return data;
40
46
  }
41
47
  const { payload } = request || {};
@@ -73,23 +79,172 @@ export function writeAnswerStageToMeta(data) {
73
79
  };
74
80
  }
75
81
 
76
- export function addDataInstructions(data, instructions) {
77
- if (!instructions) {
82
+ export function writeFiltersToPayload(payload = {}, filters) {
83
+ return trimObj({
84
+ ...payload,
85
+ ...composeFiltersPayload(filters),
86
+ });
87
+ }
88
+
89
+ function composeFiltersPayload(filters) {
90
+ if (!filters) {
91
+ return undefined;
92
+ }
93
+ const { facets } = filters;
94
+ return trimObj({
95
+ facet_filters: composeFacetFilters(facets),
96
+ });
97
+ }
98
+
99
+ function composeFacetFilters(facets) {
100
+ if (!facets) {
101
+ return undefined;
102
+ }
103
+ const filters = {};
104
+ for (const field in facets) {
105
+ const values = facets[field];
106
+ if (!values || !values.length) {
107
+ continue; // just in case
108
+ }
109
+ filters[field] = {
110
+ terms: values,
111
+ };
112
+ }
113
+ return Object.keys(filters).length ? filters : undefined;
114
+ }
115
+
116
+ export function retainFacetCountsInData(data, currentFacetCounts) {
117
+ if (!currentFacetCounts) {
118
+ return data;
119
+ }
120
+ const { status, value } = data;
121
+ switch (status) {
122
+ case STATUS.INITIAL:
123
+ case STATUS.LOADING:
124
+ break;
125
+ default:
126
+ return data;
127
+ }
128
+ if (value && value.facet_counts) {
78
129
  return data;
79
130
  }
80
131
  return {
81
- _inst: {
82
- ...data._inst,
83
- ...instructions,
84
- },
85
132
  ...data,
133
+ value: {
134
+ ...value,
135
+ facet_counts: currentFacetCounts,
136
+ },
86
137
  };
87
138
  }
88
139
 
89
- export function removeDataInstructions(data) {
90
- if (typeof data !== 'object') {
140
+ export function concatItemsFromMoreResponse(oldData, newData, { role = ROLE.PRODUCTS } = {}) {
141
+ if (!newData.value) {
142
+ return oldData;
143
+ }
144
+ const { [role]: oldProducts = [], facet_counts, total } = oldData.value;
145
+ const { [role]: newProducts = [] } = newData.value;
146
+ // keep old exhaustion flag, just in case
147
+ // TODO
148
+
149
+ // use old values of facet_counts and total, for they are affected by product_id exclusion
150
+ return {
151
+ ...newData,
152
+ facet_counts,
153
+ total,
154
+ value: {
155
+ ...newData.value,
156
+ [role]: [...oldProducts, ...newProducts],
157
+ },
158
+ }
159
+ }
160
+
161
+ export function writeExhaustionToData(data, { role = ROLE.PRODUCTS } = {}) {
162
+ const { status, request, value } = data;
163
+ // keep track of the exhaustion state
164
+ if (status !== STATUS.READY || !request || !value) {
91
165
  return data;
92
166
  }
93
- const { _inst, ...rest } = data;
94
- return rest;
167
+ const { [role]: items } = value;
168
+ if (!items) {
169
+ return data;
170
+ }
171
+ const { rows } = request.payload;
172
+ const { length } = items;
173
+ const exhausted = length < rows;
174
+ if (!exhausted) {
175
+ return data;
176
+ }
177
+ return {
178
+ ...data,
179
+ meta: {
180
+ ...data.meta,
181
+ exhausted: true,
182
+ },
183
+ };
184
+ }
185
+
186
+ // interactions //
187
+ export function buildBaseInteraction(args) {
188
+ const { role, type, items } = args;
189
+ const payload = {
190
+ type: type === 'viewable' ? 'viewable_impression' : type,
191
+ context: {
192
+ custom_context: trimObj({
193
+ property: role,
194
+ }),
195
+ },
196
+ };
197
+
198
+ if (isPerformanceEventType(type)) {
199
+ if (isProductRole(role)) {
200
+ payload.product_ids = (items || []).map(v => typeof v === 'string' ? v : v.product_id).filter(v => v);
201
+ } else {
202
+ payload.product_ids = [];
203
+ payload.context.custom_context.items = (items || []).map(v => v.text || v.id || v);
204
+ }
205
+ }
206
+
207
+ switch (type) {
208
+ case EVENT_TYPE.SUBMIT:
209
+ payload.context.custom_context.value = args.value;
210
+ break;
211
+ case EVENT_TYPE.FEEDBACK:
212
+ payload.context.custom_context.value = payload.value = args.value;
213
+ payload.context.custom_context.result_type = payload.result_type = args.result_type;
214
+ break;
215
+ }
216
+
217
+ return trimObj(payload);
218
+ }
219
+
220
+ export function writeAffiliationInfoToInteraction(payload, args) {
221
+ if (args.role !== ROLE.AFFILIATION) {
222
+ return payload;
223
+ }
224
+ const { items } = args;
225
+ const channel = (items[0] && items[0].channel) || undefined;
226
+ const positions = items.map(v => v.position);
227
+ return mergeInteraction(payload, {
228
+ context: {
229
+ custom_context: {
230
+ channel,
231
+ positions,
232
+ },
233
+ },
234
+ });
235
+ }
236
+
237
+ export function mergeInteraction(base = {}, patch = {}) {
238
+ return trimObj({
239
+ ...base,
240
+ ...patch,
241
+ context:trimObj({
242
+ ...base.context,
243
+ ...patch.context,
244
+ custom_context: trimObj({
245
+ ...(base.context && base.context.custom_context),
246
+ ...(patch.context && patch.context.custom_context),
247
+ }),
248
+ }),
249
+ });
95
250
  }