@miso.ai/client-sdk-ui 1.11.0-beta.10 → 1.11.0-beta.12

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.
@@ -107,10 +107,7 @@ function completionList(layout, type, items) {
107
107
  className,
108
108
  templates
109
109
  } = layout;
110
- return `
111
- <ul class="${className}__${type}-list" data-role="${type}-list">
112
- ${items.map(item => templates.completionItem(layout, type, item)).join('')}
113
- </ul>`.trim();
110
+ return `<ul class="${className}__${type}-list" data-role="${type}-list">${items.map(item => templates.completionItem(layout, type, item)).join('')}</ul>`;
114
111
  }
115
112
  function completionItem(layout, type, item) {
116
113
  const {
@@ -334,7 +331,7 @@ class SearchBoxLayout extends _template.default {
334
331
  target,
335
332
  isComposing
336
333
  } = _ref8;
337
- if (!isComposing && key === 'Enter' && target.matches('input')) {
334
+ if (!isComposing && key === 'Enter' && target.matches('[data-role="input"]')) {
338
335
  this._submit(target.value);
339
336
  target.blur();
340
337
  }
@@ -22,7 +22,7 @@ function root(layout, state) {
22
22
  status
23
23
  } = state;
24
24
  const roleAttr = role ? `data-role="${role}"` : '';
25
- return `<div class="${className} ${status}" ${roleAttr}>${status === _constants.STATUS.READY ? templates[status](layout, state) : ''}${templates.trigger(layout, state)}${templates.loadingBar(layout, state)}</div>`;
25
+ return `<div class="${className} ${status}" ${roleAttr}>${status === _constants.STATUS.READY ? templates[status](layout, state) : ''}${templates.trigger(layout, state)}${templates.loading(layout, state)}</div>`;
26
26
  }
27
27
  function ready(layout, state) {
28
28
  const {
@@ -78,11 +78,8 @@ function trigger(layout, state) {
78
78
  } = layout;
79
79
  return `<div class="${className}__trigger" data-role="trigger"></div>`;
80
80
  }
81
- function loadingBar(layout, state) {
82
- const {
83
- className
84
- } = layout;
85
- return `<div class="${className}__loading-bar"></div>`;
81
+ function loading() {
82
+ return `<div class="miso-loading" aria-label="Loading" data-role="loading"></div>`;
86
83
  }
87
84
 
88
85
  // TODO: let templates.js control what to be included here
@@ -111,7 +108,7 @@ const DEFAULT_TEMPLATES = Object.freeze({
111
108
  imageBlock: _templates.imageBlock,
112
109
  indexBlock: _templates.indexBlock,
113
110
  trigger,
114
- loadingBar
111
+ loading
115
112
  });
116
113
  class CollectionLayout extends _template.default {
117
114
  static get defaultTemplates() {
@@ -4,4 +4,4 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.default = void 0;
7
- var _default = exports.default = '1.11.0-beta.10';
7
+ var _default = exports.default = '1.11.0-beta.12';
@@ -12,6 +12,7 @@ var _index2 = require("../layout/index.js");
12
12
  var _index3 = require("../affiliation/index.js");
13
13
  var _options = require("./options.js");
14
14
  var _processors = require("./processors.js");
15
+ var _useLink = require("./use-link.js");
15
16
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
16
17
  const DEFAULT_API_OPTIONS = Object.freeze(Object.assign({}, _base.default.DEFAULT_API_OPTIONS, {
17
18
  group: _commons.API.GROUP.ASK,
@@ -84,15 +85,6 @@ class AnswerBasedWorkflow extends _base.default {
84
85
  this._unsubscribes = [...this._unsubscribes, this._hub.on(_index.fields.query(), args => this._query(args)), this._views.get(_constants.ROLE.ANSWER).on('citation-click', event => this._onCitationClick(event))];
85
86
  }
86
87
 
87
- // configuration //
88
- useLink(fn) {
89
- if (typeof fn !== 'function' && fn !== false) {
90
- throw new Error('useLink(fn) expects fn to be a function or false');
91
- }
92
- this._linkFn = fn;
93
- return this;
94
- }
95
-
96
88
  // lifecycle //
97
89
  restart() {
98
90
  // clear question id from previous session
@@ -166,7 +158,7 @@ class AnswerBasedWorkflow extends _base.default {
166
158
  _query() {
167
159
  let args = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
168
160
  if (this._linkFn) {
169
- this._submit(args);
161
+ this._submitToPage(args);
170
162
  return;
171
163
  }
172
164
  // start a new session
@@ -181,14 +173,6 @@ class AnswerBasedWorkflow extends _base.default {
181
173
  payload
182
174
  });
183
175
  }
184
- _submit(args) {
185
- if (!this._linkFn) {
186
- return;
187
- }
188
- // TODO: we want a submit event, but only with linkFn
189
- const url = this._linkFn(args.q);
190
- window.open(url, '_blank');
191
- }
192
176
  _writeQuestionSourceToSession(args) {
193
177
  this.session.meta.question_source = args.qs || _constants.ORGANIC_QUESTION_SOURCE; // might be null, not undefined
194
178
  }
@@ -323,6 +307,7 @@ class AnswerBasedWorkflow extends _base.default {
323
307
  }
324
308
  }
325
309
  exports.default = AnswerBasedWorkflow;
310
+ (0, _useLink.enableUseLink)(AnswerBasedWorkflow.prototype);
326
311
  Object.assign(AnswerBasedWorkflow, {
327
312
  DEFAULT_API_OPTIONS,
328
313
  DEFAULT_AUTOCOMPLETE_OPTIONS,
@@ -10,6 +10,7 @@ var _index = require("../actor/index.js");
10
10
  var _constants = require("../constants.js");
11
11
  var _index2 = require("../layout/index.js");
12
12
  var _options = require("./options.js");
13
+ var _useLink = require("./use-link.js");
13
14
  const _excluded = ["value"];
14
15
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
15
16
  function _objectWithoutPropertiesLoose(r, e) { if (null == r) return {}; var t = {}; for (var n in r) if ({}.hasOwnProperty.call(r, n)) { if (e.includes(n)) continue; t[n] = r[n]; } return t; }
@@ -61,7 +62,7 @@ class Explore extends _base.default {
61
62
  }
62
63
  _initSubscriptions(args) {
63
64
  super._initSubscriptions(args);
64
- this._unsubscribes = [...this._unsubscribes, this._views.get(_constants.ROLE.RELATED_QUESTIONS).on('click', event => this._handleRelatedQuestionClick(event)), this._hub.on(_index.fields.query(), args => this._submit(args))];
65
+ this._unsubscribes = [...this._unsubscribes, this._views.get(_constants.ROLE.RELATED_QUESTIONS).on('click', event => this._handleRelatedQuestionClick(event)), this._hub.on(_index.fields.query(), args => this._query(args))];
65
66
  }
66
67
 
67
68
  // configuration //
@@ -80,13 +81,6 @@ class Explore extends _base.default {
80
81
  }
81
82
  return super.useApi(options);
82
83
  }
83
- useLink(fn) {
84
- if (typeof fn !== 'function' && fn !== false) {
85
- throw new Error('useLink(fn) expects fn to be a function or false');
86
- }
87
- this._linkFn = fn;
88
- return this;
89
- }
90
84
 
91
85
  // lifecycle //
92
86
  start() {
@@ -94,7 +88,7 @@ class Explore extends _base.default {
94
88
  relatedQuestions = true
95
89
  } = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
96
90
  if (this._linkFn === undefined) {
97
- throw new Error('Define link mapping function before calling start()');
91
+ throw new Error('Must define link mapping function with useLink(fn) before calling start()');
98
92
  }
99
93
  // in explore workflow, start() triggers query
100
94
  // TODO: we should still make the query lifecycle
@@ -125,16 +119,24 @@ class Explore extends _base.default {
125
119
  }
126
120
  _defaultProcessData(data) {
127
121
  data = super._defaultProcessData(data);
128
- let {
122
+ data = this._addUrlToRelatedQuestions(data);
123
+ return data;
124
+ }
125
+ _addUrlToRelatedQuestions(data) {
126
+ const {
129
127
  value
130
128
  } = data;
131
- if (!value) {
129
+ if (!value || !value.related_questions) {
132
130
  return data;
133
131
  }
134
- // patch value with links
135
- const related_questions = value.related_questions.map(this._linkFn ? text => ({
132
+ const linkFn = this._linkFn && this._linkFn[0];
133
+ const related_questions = value.related_questions.map(linkFn ? text => ({
136
134
  text,
137
- url: this._getAnswersUrl(text, true)
135
+ url: this._getSubmitUrl({
136
+ q: text
137
+ }, {
138
+ generated: true
139
+ })
138
140
  }) : text => ({
139
141
  text
140
142
  }));
@@ -159,26 +161,19 @@ class Explore extends _base.default {
159
161
  }
160
162
  this._hub.update(_index.fields.query(), args);
161
163
  }
162
- _submit() {
163
- let {
164
- q
165
- } = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
166
- if (!this._linkFn) {
167
- return;
168
- }
169
- const url = this._getAnswersUrl(q);
170
- window.open(url, '_blank');
164
+ _query(args) {
165
+ this._submitToPage(args);
171
166
  }
172
- _getAnswersUrl(text) {
173
- let generated = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
174
- if (!this._linkFn) {
175
- return;
176
- }
177
- const url = this._linkFn(text);
167
+ _getSubmitUrl(args) {
168
+ let {
169
+ generated = false
170
+ } = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
171
+ let url = _useLink.UseLinkMixin.prototype._getSubmitUrl.call(this, args);
178
172
  if (!generated || !this._productId) {
179
173
  return url;
180
174
  }
181
175
  return `${url}&qs=${encodeURIComponent(this._productId)}`;
182
176
  }
183
177
  }
184
- exports.default = Explore;
178
+ exports.default = Explore;
179
+ (0, _useLink.enableUseLink)(Explore.prototype);
@@ -9,7 +9,6 @@ var _searchBased = _interopRequireDefault(require("./search-based.js"));
9
9
  var _index = require("../actor/index.js");
10
10
  var _constants = require("../constants.js");
11
11
  var _options = require("./options.js");
12
- var _processors = require("./processors.js");
13
12
  const _excluded = ["filters"];
14
13
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
15
14
  function _objectWithoutPropertiesLoose(r, e) { if (null == r) return {}; var t = {}; for (var n in r) if ({}.hasOwnProperty.call(r, n)) { if (e.includes(n)) continue; t[n] = r[n]; } return t; }
@@ -47,13 +46,10 @@ class HybridSearchResults extends _searchBased.default {
47
46
  }
48
47
  _buildPayload() {
49
48
  let _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
50
- let {
51
- filters
52
- } = _ref,
53
- payload = _objectWithoutPropertiesLoose(_ref, _excluded);
49
+ let payload = _objectWithoutPropertiesLoose(_ref, _excluded);
54
50
  // borrow the work from the sibling
55
51
  payload = this._superworkflow._answer._buildPayload(payload);
56
- payload = (0, _processors.writeFiltersToPayload)(payload, filters);
52
+ payload = _searchBased.default.prototype._buildPayload.call(this, payload);
57
53
  payload = this._writeQuestionIdToPayload(payload);
58
54
  return Object.assign({}, payload, {
59
55
  answer: false
@@ -95,8 +95,8 @@ class HybridSearch extends _base.default {
95
95
  }
96
96
 
97
97
  // configuration //
98
- useLink(fn) {
99
- this._answer.useLink(fn);
98
+ useLink(fn, options) {
99
+ this._answer.useLink(fn, options);
100
100
  return this;
101
101
  }
102
102
 
@@ -4,14 +4,14 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.buildBaseInteraction = buildBaseInteraction;
7
- exports.concatResults = concatResults;
7
+ exports.concatItemsFromMoreResponse = concatItemsFromMoreResponse;
8
8
  exports.getRevision = getRevision;
9
- exports.markExhaustion = markExhaustion;
10
9
  exports.mergeInteraction = mergeInteraction;
11
- exports.retainFacetCounts = retainFacetCounts;
10
+ exports.retainFacetCountsInData = retainFacetCountsInData;
12
11
  exports.writeAffiliationInfoToInteraction = writeAffiliationInfoToInteraction;
13
12
  exports.writeAnswerStageToMeta = writeAnswerStageToMeta;
14
13
  exports.writeDataStatus = writeDataStatus;
14
+ exports.writeExhaustionToData = writeExhaustionToData;
15
15
  exports.writeFiltersToPayload = writeFiltersToPayload;
16
16
  exports.writeKeywordsToData = writeKeywordsToData;
17
17
  exports.writeMisoIdToMeta = writeMisoIdToMeta;
@@ -137,7 +137,7 @@ function composeFacetFilters(facets) {
137
137
  }
138
138
  return Object.keys(filters).length ? filters : undefined;
139
139
  }
140
- function retainFacetCounts(data, currentFacetCounts) {
140
+ function retainFacetCountsInData(data, currentFacetCounts) {
141
141
  if (!currentFacetCounts) {
142
142
  return data;
143
143
  }
@@ -161,7 +161,7 @@ function retainFacetCounts(data, currentFacetCounts) {
161
161
  })
162
162
  });
163
163
  }
164
- function concatResults(oldData, newData) {
164
+ function concatItemsFromMoreResponse(oldData, newData) {
165
165
  let {
166
166
  role = _constants.ROLE.PRODUCTS
167
167
  } = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
@@ -169,20 +169,26 @@ function concatResults(oldData, newData) {
169
169
  return oldData;
170
170
  }
171
171
  const {
172
- [role]: oldProducts = []
172
+ [role]: oldProducts = [],
173
+ facet_counts,
174
+ total
173
175
  } = oldData.value;
174
176
  const {
175
177
  [role]: newProducts = []
176
178
  } = newData.value;
177
- // keep exhaustion flag
179
+ // keep old exhaustion flag, just in case
178
180
  // TODO
181
+
182
+ // use old values of facet_counts and total, for they are affected by product_id exclusion
179
183
  return Object.assign({}, newData, {
184
+ facet_counts,
185
+ total,
180
186
  value: Object.assign({}, newData.value, {
181
187
  [role]: [...oldProducts, ...newProducts]
182
188
  })
183
189
  });
184
190
  }
185
- function markExhaustion(data) {
191
+ function writeExhaustionToData(data) {
186
192
  let {
187
193
  role = _constants.ROLE.PRODUCTS
188
194
  } = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
@@ -196,9 +202,9 @@ function markExhaustion(data) {
196
202
  return data;
197
203
  }
198
204
  const {
199
- [role]: products
205
+ [role]: items
200
206
  } = value;
201
- if (!products) {
207
+ if (!items) {
202
208
  return data;
203
209
  }
204
210
  const {
@@ -206,7 +212,7 @@ function markExhaustion(data) {
206
212
  } = request.payload;
207
213
  const {
208
214
  length
209
- } = products;
215
+ } = items;
210
216
  const exhausted = length < rows;
211
217
  if (!exhausted) {
212
218
  return data;
@@ -75,6 +75,9 @@ class SearchBasedWorkflow extends _base.default {
75
75
  const data = this._hub.states[_index.fields.data()];
76
76
  return !!(data && data.meta && data.meta.exhausted);
77
77
  }
78
+ get page() {
79
+ return this._page;
80
+ }
78
81
 
79
82
  // query //
80
83
  query(args) {
@@ -116,25 +119,31 @@ class SearchBasedWorkflow extends _base.default {
116
119
  });
117
120
  }
118
121
  _more() {
122
+ // if still loading, complain
123
+ if (this.status !== _constants.STATUS.READY) {
124
+ throw new Error(`Can not call more() while loading.`);
125
+ }
126
+ // no more pages, ignore
119
127
  if (this.exhausted || this._page >= DEFAULT_PAGE_LIMIT - 1) {
120
- return; // ignore
128
+ return;
121
129
  }
122
130
  // don't create a new session!
123
131
 
132
+ this._page++;
133
+
124
134
  // payload
125
135
  const query = this._getQuery();
126
136
  const filters = this._hub.states[_index.fields.filters()]; // get stored filters
127
- const start = (this._page + 1) * this._getPageSize();
128
- const payload = this._buildPayload(Object.assign({}, query, {
129
- filters,
130
- start
137
+ let payload = this._buildPayload(Object.assign({}, query, {
138
+ filters
131
139
  }));
140
+ payload = this._processPayloadForMoreRequest(payload);
132
141
  this._request({
133
142
  payload
134
143
  });
135
- this._page++;
136
144
  }
137
145
  _getQuery() {
146
+ // TODO: just get from the hub
138
147
  return this._queryArgs;
139
148
  }
140
149
  _getPageSize() {
@@ -147,15 +156,40 @@ class SearchBasedWorkflow extends _base.default {
147
156
  } = _ref,
148
157
  payload = _objectWithoutPropertiesLoose(_ref, _excluded);
149
158
  payload = (0, _processors.writeFiltersToPayload)(payload, filters);
159
+ payload = this._writePageInfoToPayload(payload);
150
160
  return payload;
151
161
  }
162
+ _writePageInfoToPayload(payload) {
163
+ return Object.assign({}, payload, {
164
+ _meta: Object.assign({}, payload._meta, {
165
+ page: this._page
166
+ })
167
+ });
168
+ }
169
+ _processPayloadForMoreRequest() {
170
+ let payload = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
171
+ const data = this._hub.states[_index.fields.data()];
172
+ const {
173
+ [this._roles.main]: items = []
174
+ } = data && data.value || {};
175
+ const productIds = items.map(item => item.product_id).filter(v => v);
176
+ const exclude = [...(payload.exclude || []), ...productIds];
177
+ if (exclude.length === 0) {
178
+ return payload;
179
+ }
180
+ return Object.assign({}, payload, {
181
+ exclude
182
+ });
183
+ }
152
184
 
153
185
  // data //
154
186
  _defaultProcessData(data) {
155
187
  data = super._defaultProcessData(data);
156
188
  data = (0, _processors.writeKeywordsToData)(data);
157
- data = (0, _processors.retainFacetCounts)(data, this._currentFacetCounts);
158
- data = (0, _processors.markExhaustion)(data);
189
+ data = (0, _processors.retainFacetCountsInData)(data, this._currentFacetCounts);
190
+ data = (0, _processors.writeExhaustionToData)(data, {
191
+ role: this._roles.main
192
+ });
159
193
  return data;
160
194
  }
161
195
  _updateDataInHub(data) {
@@ -166,12 +200,20 @@ class SearchBasedWorkflow extends _base.default {
166
200
  const {
167
201
  request
168
202
  } = data;
169
- if (!request || !request.payload.start) {
170
- return data; // the initial state, or not from "more" request
203
+ if (!request) {
204
+ return data; // the initial state
205
+ }
206
+ const {
207
+ _meta
208
+ } = request.payload;
209
+ if (!_meta || !_meta.page) {
210
+ return data; // not from "more" request
171
211
  }
172
212
  // concat records if it's from "more" request
173
213
  const currentData = this._hub.states[_index.fields.data()];
174
- return (0, _processors.concatResults)(currentData, data);
214
+ return (0, _processors.concatItemsFromMoreResponse)(currentData, data, {
215
+ role: this._roles.main
216
+ });
175
217
  }
176
218
  }
177
219
  exports.default = SearchBasedWorkflow;
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.UseLinkMixin = void 0;
7
+ exports.enableUseLink = enableUseLink;
8
+ var _commons = require("@miso.ai/commons");
9
+ function enableUseLink(prototype) {
10
+ (0, _commons.mixin)(prototype, UseLinkMixin.prototype);
11
+ }
12
+ class UseLinkMixin {
13
+ useLink(fn) {
14
+ let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
15
+ if (typeof fn !== 'function' && fn !== false) {
16
+ throw new Error('useLink(fn) expects fn to be a function or false');
17
+ }
18
+ this._linkFn = fn ? [fn, options] : false;
19
+ return this;
20
+ }
21
+ _submitToPage() {
22
+ let args = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
23
+ if (!this._linkFn) {
24
+ return;
25
+ }
26
+ // TODO: we want a submit event
27
+ const url = this._getSubmitUrl(args);
28
+ const [_, options = {}] = this._linkFn;
29
+ if (options.open === false) {
30
+ window.location.href = url;
31
+ } else {
32
+ const target = options.target || '_blank';
33
+ const windowFeatures = options.windowFeatures || '';
34
+ window.open(url, target, windowFeatures);
35
+ }
36
+ }
37
+ _getSubmitUrl(args) {
38
+ if (!this._linkFn) {
39
+ throw new Error('useLink(fn) must be called before _getSubmitUrl()');
40
+ }
41
+ return this._linkFn[0](args.q);
42
+ }
43
+ }
44
+ exports.UseLinkMixin = UseLinkMixin;
package/package.json CHANGED
@@ -26,7 +26,7 @@
26
26
  "url": "https://github.com/MisoAI/miso-client-js-sdk/issues"
27
27
  },
28
28
  "dependencies": {
29
- "@miso.ai/commons": "1.11.0-beta.10"
29
+ "@miso.ai/commons": "1.11.0-beta.12"
30
30
  },
31
- "version": "1.11.0-beta.10"
31
+ "version": "1.11.0-beta.12"
32
32
  }
@@ -78,10 +78,7 @@ function product(layout, { product }) {
78
78
 
79
79
  function completionList(layout, type, items) {
80
80
  const { className, templates } = layout;
81
- return `
82
- <ul class="${className}__${type}-list" data-role="${type}-list">
83
- ${items.map(item => templates.completionItem(layout, type, item)).join('')}
84
- </ul>`.trim();
81
+ return `<ul class="${className}__${type}-list" data-role="${type}-list">${items.map(item => templates.completionItem(layout, type, item)).join('')}</ul>`;
85
82
  }
86
83
 
87
84
  function completionItem(layout, type, item) {
@@ -289,7 +286,7 @@ export default class SearchBoxLayout extends TemplateBasedLayout {
289
286
  }
290
287
 
291
288
  _handleKeyDown({ key, target, isComposing }) {
292
- if (!isComposing && key === 'Enter' && target.matches('input')) {
289
+ if (!isComposing && key === 'Enter' && target.matches('[data-role="input"]')) {
293
290
  this._submit(target.value);
294
291
  target.blur();
295
292
  }
@@ -8,7 +8,7 @@ function root(layout, state) {
8
8
  const { className, role, templates } = layout;
9
9
  const { status } = state;
10
10
  const roleAttr = role ? `data-role="${role}"` : '';
11
- return `<div class="${className} ${status}" ${roleAttr}>${status === STATUS.READY ? templates[status](layout, state) : ''}${templates.trigger(layout, state)}${templates.loadingBar(layout, state)}</div>`;
11
+ return `<div class="${className} ${status}" ${roleAttr}>${status === STATUS.READY ? templates[status](layout, state) : ''}${templates.trigger(layout, state)}${templates.loading(layout, state)}</div>`;
12
12
  }
13
13
 
14
14
  function ready(layout, state) {
@@ -48,9 +48,8 @@ function trigger(layout, state) {
48
48
  return `<div class="${className}__trigger" data-role="trigger"></div>`;
49
49
  }
50
50
 
51
- function loadingBar(layout, state) {
52
- const { className } = layout;
53
- return `<div class="${className}__loading-bar"></div>`;
51
+ function loading() {
52
+ return `<div class="miso-loading" aria-label="Loading" data-role="loading"></div>`;
54
53
  }
55
54
 
56
55
  // TODO: let templates.js control what to be included here
@@ -79,7 +78,7 @@ const DEFAULT_TEMPLATES = Object.freeze({
79
78
  imageBlock,
80
79
  indexBlock,
81
80
  trigger,
82
- loadingBar,
81
+ loading,
83
82
  });
84
83
 
85
84
  export default class CollectionLayout extends TemplateBasedLayout {
package/src/version.js CHANGED
@@ -1 +1 @@
1
- export default '1.11.0-beta.10';
1
+ export default '1.11.0-beta.12';
@@ -6,6 +6,7 @@ import { SearchBoxLayout, TextLayout, ListLayout, TypewriterLayout, FeedbackLayo
6
6
  import { processData as processAffiliationData } from '../affiliation/index.js';
7
7
  import { mergeRolesOptions } from './options.js';
8
8
  import { writeAnswerStageToMeta, mergeInteraction } from './processors.js';
9
+ import { enableUseLink } from './use-link.js';
9
10
 
10
11
  const DEFAULT_API_OPTIONS = Object.freeze({
11
12
  ...Workflow.DEFAULT_API_OPTIONS,
@@ -80,15 +81,6 @@ export default class AnswerBasedWorkflow extends Workflow {
80
81
  ];
81
82
  }
82
83
 
83
- // configuration //
84
- useLink(fn) {
85
- if (typeof fn !== 'function' && fn !== false) {
86
- throw new Error('useLink(fn) expects fn to be a function or false');
87
- }
88
- this._linkFn = fn;
89
- return this;
90
- }
91
-
92
84
  // lifecycle //
93
85
  restart() {
94
86
  // clear question id from previous session
@@ -154,7 +146,7 @@ export default class AnswerBasedWorkflow extends Workflow {
154
146
 
155
147
  _query(args = {}) {
156
148
  if (this._linkFn) {
157
- this._submit(args);
149
+ this._submitToPage(args);
158
150
  return;
159
151
  }
160
152
  // start a new session
@@ -168,15 +160,6 @@ export default class AnswerBasedWorkflow extends Workflow {
168
160
  this._request({ payload });
169
161
  }
170
162
 
171
- _submit(args) {
172
- if (!this._linkFn) {
173
- return;
174
- }
175
- // TODO: we want a submit event, but only with linkFn
176
- const url = this._linkFn(args.q);
177
- window.open(url, '_blank');
178
- }
179
-
180
163
  _writeQuestionSourceToSession(args) {
181
164
  this.session.meta.question_source = args.qs || ORGANIC_QUESTION_SOURCE; // might be null, not undefined
182
165
  }
@@ -294,6 +277,8 @@ export default class AnswerBasedWorkflow extends Workflow {
294
277
 
295
278
  }
296
279
 
280
+ enableUseLink(AnswerBasedWorkflow.prototype);
281
+
297
282
  Object.assign(AnswerBasedWorkflow, {
298
283
  DEFAULT_API_OPTIONS,
299
284
  DEFAULT_AUTOCOMPLETE_OPTIONS,
@@ -4,6 +4,7 @@ import { fields } from '../actor/index.js';
4
4
  import { ROLE, EVENT_TYPE } from '../constants.js';
5
5
  import { ListLayout, SearchBoxLayout } from '../layout/index.js';
6
6
  import { mergeRolesOptions } from './options.js';
7
+ import { enableUseLink, UseLinkMixin } from './use-link.js';
7
8
 
8
9
  const DEFAULT_API_OPTIONS = Object.freeze({
9
10
  ...Workflow.DEFAULT_API_OPTIONS,
@@ -61,7 +62,7 @@ export default class Explore extends Workflow {
61
62
  this._unsubscribes = [
62
63
  ...this._unsubscribes,
63
64
  this._views.get(ROLE.RELATED_QUESTIONS).on('click', event => this._handleRelatedQuestionClick(event)),
64
- this._hub.on(fields.query(), args => this._submit(args)),
65
+ this._hub.on(fields.query(), args => this._query(args)),
65
66
  ];
66
67
  }
67
68
 
@@ -81,18 +82,10 @@ export default class Explore extends Workflow {
81
82
  return super.useApi(options);
82
83
  }
83
84
 
84
- useLink(fn) {
85
- if (typeof fn !== 'function' && fn !== false) {
86
- throw new Error('useLink(fn) expects fn to be a function or false');
87
- }
88
- this._linkFn = fn;
89
- return this;
90
- }
91
-
92
85
  // lifecycle //
93
86
  start({ relatedQuestions = true } = {}) {
94
87
  if (this._linkFn === undefined) {
95
- throw new Error('Define link mapping function before calling start()');
88
+ throw new Error('Must define link mapping function with useLink(fn) before calling start()');
96
89
  }
97
90
  // in explore workflow, start() triggers query
98
91
  // TODO: we should still make the query lifecycle
@@ -113,12 +106,17 @@ export default class Explore extends Workflow {
113
106
 
114
107
  _defaultProcessData(data) {
115
108
  data = super._defaultProcessData(data);
116
- let { value } = data;
117
- if (!value) {
109
+ data = this._addUrlToRelatedQuestions(data);
110
+ return data;
111
+ }
112
+
113
+ _addUrlToRelatedQuestions(data) {
114
+ const { value } = data;
115
+ if (!value || !value.related_questions) {
118
116
  return data;
119
117
  }
120
- // patch value with links
121
- const related_questions = value.related_questions.map(this._linkFn ? (text => ({ text, url: this._getAnswersUrl(text, true) })) : (text => ({ text })));
118
+ const linkFn = this._linkFn && this._linkFn[0];
119
+ const related_questions = value.related_questions.map(linkFn ? (text => ({ text, url: this._getSubmitUrl({ q: text }, { generated: true }) })) : (text => ({ text })));
122
120
  return {
123
121
  ...data,
124
122
  value: {
@@ -139,19 +137,12 @@ export default class Explore extends Workflow {
139
137
  this._hub.update(fields.query(), args);
140
138
  }
141
139
 
142
- _submit({ q } = {}) {
143
- if (!this._linkFn) {
144
- return;
145
- }
146
- const url = this._getAnswersUrl(q);
147
- window.open(url, '_blank');
140
+ _query(args) {
141
+ this._submitToPage(args);
148
142
  }
149
143
 
150
- _getAnswersUrl(text, generated = false) {
151
- if (!this._linkFn) {
152
- return;
153
- }
154
- const url = this._linkFn(text);
144
+ _getSubmitUrl(args, { generated = false } = {}) {
145
+ let url = UseLinkMixin.prototype._getSubmitUrl.call(this, args);
155
146
  if (!generated || !this._productId) {
156
147
  return url;
157
148
  }
@@ -159,3 +150,5 @@ export default class Explore extends Workflow {
159
150
  }
160
151
 
161
152
  }
153
+
154
+ enableUseLink(Explore.prototype);
@@ -3,7 +3,6 @@ import SearchBasedWorkflow from './search-based.js';
3
3
  import { fields } from '../actor/index.js';
4
4
  import { ROLE } from '../constants.js';
5
5
  import { mergeRolesOptions } from './options.js';
6
- import { writeFiltersToPayload } from './processors.js';
7
6
 
8
7
  const ROLES_OPTIONS = mergeRolesOptions(SearchBasedWorkflow.ROLES_OPTIONS, {
9
8
  members: [ROLE.PRODUCTS, ROLE.KEYWORDS, ROLE.TOTAL, ROLE.FACETS, ROLE.MORE],
@@ -45,7 +44,7 @@ export default class HybridSearchResults extends SearchBasedWorkflow {
45
44
  _buildPayload({ filters, ...payload } = {}) {
46
45
  // borrow the work from the sibling
47
46
  payload = this._superworkflow._answer._buildPayload(payload);
48
- payload = writeFiltersToPayload(payload, filters);
47
+ payload = SearchBasedWorkflow.prototype._buildPayload.call(this, payload);
49
48
  payload = this._writeQuestionIdToPayload(payload);
50
49
  return { ...payload, answer: false };
51
50
  }
@@ -111,8 +111,8 @@ export default class HybridSearch extends Workflow {
111
111
  }
112
112
 
113
113
  // configuration //
114
- useLink(fn) {
115
- this._answer.useLink(fn);
114
+ useLink(fn, options) {
115
+ this._answer.useLink(fn, options);
116
116
  return this;
117
117
  }
118
118
 
@@ -113,7 +113,7 @@ function composeFacetFilters(facets) {
113
113
  return Object.keys(filters).length ? filters : undefined;
114
114
  }
115
115
 
116
- export function retainFacetCounts(data, currentFacetCounts) {
116
+ export function retainFacetCountsInData(data, currentFacetCounts) {
117
117
  if (!currentFacetCounts) {
118
118
  return data;
119
119
  }
@@ -137,16 +137,20 @@ export function retainFacetCounts(data, currentFacetCounts) {
137
137
  };
138
138
  }
139
139
 
140
- export function concatResults(oldData, newData, { role = ROLE.PRODUCTS } = {}) {
140
+ export function concatItemsFromMoreResponse(oldData, newData, { role = ROLE.PRODUCTS } = {}) {
141
141
  if (!newData.value) {
142
142
  return oldData;
143
143
  }
144
- const { [role]: oldProducts = [] } = oldData.value;
144
+ const { [role]: oldProducts = [], facet_counts, total } = oldData.value;
145
145
  const { [role]: newProducts = [] } = newData.value;
146
- // keep exhaustion flag
146
+ // keep old exhaustion flag, just in case
147
147
  // TODO
148
+
149
+ // use old values of facet_counts and total, for they are affected by product_id exclusion
148
150
  return {
149
151
  ...newData,
152
+ facet_counts,
153
+ total,
150
154
  value: {
151
155
  ...newData.value,
152
156
  [role]: [...oldProducts, ...newProducts],
@@ -154,18 +158,18 @@ export function concatResults(oldData, newData, { role = ROLE.PRODUCTS } = {}) {
154
158
  }
155
159
  }
156
160
 
157
- export function markExhaustion(data, { role = ROLE.PRODUCTS } = {}) {
161
+ export function writeExhaustionToData(data, { role = ROLE.PRODUCTS } = {}) {
158
162
  const { status, request, value } = data;
159
163
  // keep track of the exhaustion state
160
164
  if (status !== STATUS.READY || !request || !value) {
161
165
  return data;
162
166
  }
163
- const { [role]: products } = value;
164
- if (!products) {
167
+ const { [role]: items } = value;
168
+ if (!items) {
165
169
  return data;
166
170
  }
167
171
  const { rows } = request.payload;
168
- const { length } = products;
172
+ const { length } = items;
169
173
  const exhausted = length < rows;
170
174
  if (!exhausted) {
171
175
  return data;
@@ -1,9 +1,9 @@
1
1
  import { API } from '@miso.ai/commons';
2
2
  import Workflow from './base.js';
3
3
  import { fields } from '../actor/index.js';
4
- import { ROLE } from '../constants.js';
4
+ import { STATUS, ROLE } from '../constants.js';
5
5
  import { SearchBoxLayout, ListLayout, TextLayout, FacetsLayout, MoreButtonLayout } from '../layout/index.js';
6
- import { writeKeywordsToData, writeFiltersToPayload, retainFacetCounts, markExhaustion, concatResults } from './processors.js';
6
+ import { writeKeywordsToData, writeFiltersToPayload, retainFacetCountsInData, writeExhaustionToData, concatItemsFromMoreResponse } from './processors.js';
7
7
  import { mergeRolesOptions, makeConfigurable } from './options.js';
8
8
 
9
9
  const DEFAULT_ROWS = 10;
@@ -79,6 +79,10 @@ export default class SearchBasedWorkflow extends Workflow {
79
79
  return !!(data && data.meta && data.meta.exhausted);
80
80
  }
81
81
 
82
+ get page() {
83
+ return this._page;
84
+ }
85
+
82
86
  // query //
83
87
  query(args) {
84
88
  if (!args.q) {
@@ -116,23 +120,29 @@ export default class SearchBasedWorkflow extends Workflow {
116
120
  }
117
121
 
118
122
  _more() {
123
+ // if still loading, complain
124
+ if (this.status !== STATUS.READY) {
125
+ throw new Error(`Can not call more() while loading.`);
126
+ }
127
+ // no more pages, ignore
119
128
  if (this.exhausted || this._page >= DEFAULT_PAGE_LIMIT - 1) {
120
- return; // ignore
129
+ return;
121
130
  }
122
131
  // don't create a new session!
123
132
 
133
+ this._page++;
134
+
124
135
  // payload
125
136
  const query = this._getQuery();
126
137
  const filters = this._hub.states[fields.filters()]; // get stored filters
127
- const start = (this._page + 1) * this._getPageSize();
128
- const payload = this._buildPayload({ ...query, filters, start });
138
+ let payload = this._buildPayload({ ...query, filters });
139
+ payload = this._processPayloadForMoreRequest(payload);
129
140
 
130
141
  this._request({ payload });
131
-
132
- this._page++;
133
142
  }
134
143
 
135
144
  _getQuery() {
145
+ // TODO: just get from the hub
136
146
  return this._queryArgs;
137
147
  }
138
148
 
@@ -142,15 +152,40 @@ export default class SearchBasedWorkflow extends Workflow {
142
152
 
143
153
  _buildPayload({ filters, ...payload } = {}) {
144
154
  payload = writeFiltersToPayload(payload, filters);
155
+ payload = this._writePageInfoToPayload(payload);
145
156
  return payload;
146
157
  }
147
158
 
159
+ _writePageInfoToPayload(payload) {
160
+ return {
161
+ ...payload,
162
+ _meta: {
163
+ ...payload._meta,
164
+ page: this._page,
165
+ },
166
+ };
167
+ }
168
+
169
+ _processPayloadForMoreRequest(payload = {}) {
170
+ const data = this._hub.states[fields.data()];
171
+ const { [this._roles.main]: items = [] } = (data && data.value) || {};
172
+ const productIds = items.map(item => item.product_id).filter(v => v);
173
+ const exclude = [...(payload.exclude || []), ...productIds];
174
+ if (exclude.length === 0) {
175
+ return payload;
176
+ }
177
+ return {
178
+ ...payload,
179
+ exclude,
180
+ };
181
+ }
182
+
148
183
  // data //
149
184
  _defaultProcessData(data) {
150
185
  data = super._defaultProcessData(data);
151
186
  data = writeKeywordsToData(data);
152
- data = retainFacetCounts(data, this._currentFacetCounts);
153
- data = markExhaustion(data);
187
+ data = retainFacetCountsInData(data, this._currentFacetCounts);
188
+ data = writeExhaustionToData(data, { role: this._roles.main });
154
189
  return data;
155
190
  }
156
191
 
@@ -161,12 +196,16 @@ export default class SearchBasedWorkflow extends Workflow {
161
196
 
162
197
  _appendResultsFromMoreRequest(data) {
163
198
  const { request } = data;
164
- if (!request || !request.payload.start) {
165
- return data; // the initial state, or not from "more" request
199
+ if (!request) {
200
+ return data; // the initial state
201
+ }
202
+ const { _meta } = request.payload;
203
+ if (!_meta || !_meta.page) {
204
+ return data; // not from "more" request
166
205
  }
167
206
  // concat records if it's from "more" request
168
207
  const currentData = this._hub.states[fields.data()];
169
- return concatResults(currentData, data);
208
+ return concatItemsFromMoreResponse(currentData, data, { role: this._roles.main });
170
209
  }
171
210
 
172
211
  }
@@ -0,0 +1,40 @@
1
+ import { mixin } from '@miso.ai/commons';
2
+
3
+ export function enableUseLink(prototype) {
4
+ mixin(prototype, UseLinkMixin.prototype);
5
+ }
6
+
7
+ export class UseLinkMixin {
8
+
9
+ useLink(fn, options = {}) {
10
+ if (typeof fn !== 'function' && fn !== false) {
11
+ throw new Error('useLink(fn) expects fn to be a function or false');
12
+ }
13
+ this._linkFn = fn ? [fn, options] : false;
14
+ return this;
15
+ }
16
+
17
+ _submitToPage(args = {}) {
18
+ if (!this._linkFn) {
19
+ return;
20
+ }
21
+ // TODO: we want a submit event
22
+ const url = this._getSubmitUrl(args);
23
+ const [_, options = {}] = this._linkFn;
24
+ if (options.open === false) {
25
+ window.location.href = url;
26
+ } else {
27
+ const target = options.target || '_blank';
28
+ const windowFeatures = options.windowFeatures || '';
29
+ window.open(url, target, windowFeatures);
30
+ }
31
+ }
32
+
33
+ _getSubmitUrl(args) {
34
+ if (!this._linkFn) {
35
+ throw new Error('useLink(fn) must be called before _getSubmitUrl()');
36
+ }
37
+ return this._linkFn[0](args.q);
38
+ }
39
+
40
+ }