@miso.ai/client-sdk-ui 1.10.0-beta.0 → 1.10.0-beta.1

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.
@@ -1,53 +1,104 @@
1
- import { escapeHtml } from '@miso.ai/commons';
2
- import { LAYOUT_CATEGORY } from '../../constants.js';
1
+ import { defineValues, escapeHtml, findInAncestors, debounce } from '@miso.ai/commons';
2
+ import { LAYOUT_CATEGORY, STATUS } from '../../constants.js';
3
3
  import { fields } from '../../actor/index.js';
4
4
  import TemplateBasedLayout from '../template.js';
5
+ import { helpers, imageBlock, productInfoBlock } from '../templates.js';
5
6
  import { SEND, SEARCH } from '../../asset/svgs.js';
6
7
 
7
8
  const TYPE = 'search-box';
8
9
  const DEFAULT_CLASSNAME = 'miso-search-box';
10
+ const DEFAULT_AUTOCOMPLETE_CLASSNAME = 'miso-autocomplete';
9
11
 
10
12
  function root(layout) {
11
- const { className, role, templates, options, workflow } = layout;
12
- const { autocomplete, placeholder, buttonText } = options;
13
+ const { className, role, options, workflow, autocomplete } = layout;
14
+ const { placeholder, buttonText } = options;
13
15
  const roleAttr = role ? `data-role="${role}"` : '';
14
16
  const buttonContent = buttonText || workflow === 'search' ? SEARCH : SEND;
15
17
  return `
16
18
  <div class="${className}" ${roleAttr}>
17
19
  <div class="${className}__input-group">
18
20
  <input class="${className}__input" type="text" data-role="input" ${placeholder ? `placeholder="${placeholder}"` : ''}>
19
- <button class="${className}__button" type="submit" data-role="button">${buttonContent}</button>
21
+ <button class="${className}__button" type="submit" data-role="submit">${buttonContent}</button>
20
22
  </div>
21
- ${autocomplete ? templates.autocomplete(layout) : ''}
22
- </div>
23
- `;
23
+ <div class="${autocomplete.className}" data-role="autocomplete"></div>
24
+ </div>`.trim();
24
25
  }
25
26
 
26
- function autocomplete(layout) {
27
- const { className, templates } = layout;
28
- return `
29
- <div class="${className}__autocomplete" data-role="autocomplete">
30
- ${templates.suggestionList(layout)}
31
- </div>
32
- `;
27
+ function completions(layout, items) {
28
+ const { templates, workflow, autocomplete } = layout;
29
+ // use options under layout.autocomplete from now on
30
+ layout = {
31
+ templates,
32
+ workflow,
33
+ ...autocomplete,
34
+ };
35
+
36
+ const products = [], queries = [];
37
+ for (const item of items) {
38
+ (item.product ? products : queries).push(item);
39
+ }
40
+
41
+ return templates.queries(layout, queries) + (queries.length > 0 && products.length > 0 ? `<hr>` : '') + templates.products(layout, products);
42
+ }
43
+
44
+ function queries(layout, items) {
45
+ const { templates } = layout;
46
+ return templates.completionList(layout, 'query', items);
47
+ }
48
+
49
+ function products(layout, items) {
50
+ const { templates } = layout;
51
+ return templates.completionList(layout, 'product', items);
33
52
  }
34
53
 
35
- function suggestionList(layout) {
36
- const { className } = layout;
37
- return `<ol class="${className}__suggestion-list" data-role="suggestion-list"></ol>`;
54
+ function query(layout, { text, text_with_inverted_markups } = {}) {
55
+ return text_with_inverted_markups || escapeHtml(text);
38
56
  }
39
57
 
40
- function suggestionItem(layout, value) {
41
- const { className } = layout;
42
- const text = escapeHtml(value.text || value);
43
- return `<li class="${className}__suggestion-item" data-role="suggestion-item" tabindex="0">${text}</li>`;
58
+ function product(layout, { product }) {
59
+ const { templates } = layout;
60
+ const [openTag, closeTag] = helpers.tagPair(layout, product, { role: false });
61
+ return [
62
+ openTag,
63
+ templates.imageBlock(layout, product),
64
+ templates.productInfoBlock(layout, product),
65
+ closeTag,
66
+ ].join('');
67
+ }
68
+
69
+ function completionList(layout, type, items) {
70
+ const { className, templates } = layout;
71
+ return `
72
+ <ul class="${className}__${type}-list" data-role="${type}-list">
73
+ ${items.map(item => templates.completionItem(layout, type, item)).join('')}
74
+ </ul>`.trim();
75
+ }
76
+
77
+ function completionItem(layout, type, item) {
78
+ const { className, templates } = layout;
79
+ return `
80
+ <li class="${className}__${type}-item" data-role="${type}-item" data-index="${item._index}" tabindex="0">
81
+ ${templates[type](layout, item)}
82
+ </li>`.trim();
44
83
  }
45
84
 
46
85
  const DEFAULT_TEMPLATES = Object.freeze({
47
86
  root,
48
- autocomplete,
49
- suggestionList,
50
- suggestionItem,
87
+ completions,
88
+ completionList,
89
+ completionItem,
90
+ queries,
91
+ products,
92
+ query,
93
+ product,
94
+ imageBlock,
95
+ productInfoBlock,
96
+ helpers,
97
+ });
98
+
99
+ const DEFAULT_AUTOCOMPLETE_OPTIONS = Object.freeze({
100
+ className: DEFAULT_AUTOCOMPLETE_CLASSNAME,
101
+ debounce: 300,
51
102
  });
52
103
 
53
104
  export default class SearchBoxLayout extends TemplateBasedLayout {
@@ -68,31 +119,36 @@ export default class SearchBoxLayout extends TemplateBasedLayout {
68
119
  return DEFAULT_CLASSNAME;
69
120
  }
70
121
 
71
- constructor({ className = DEFAULT_CLASSNAME, templates, ...options } = {}) {
122
+ constructor({ className = DEFAULT_CLASSNAME, templates, autocomplete, ...options } = {}) {
72
123
  super({
73
124
  className,
74
125
  templates: { ...DEFAULT_TEMPLATES, ...templates },
75
126
  ...options,
76
127
  });
128
+ defineValues(this, {
129
+ autocomplete: { ...DEFAULT_AUTOCOMPLETE_OPTIONS, ...autocomplete },
130
+ });
77
131
  this._contexts = new WeakMap();
78
- this._suggestionItems = new WeakMap(); // TODO: put them on elements as layout can be potentially replaced
132
+ this._prevInput = undefined;
133
+ this._debounce = debounce(this.autocomplete.debounce);
79
134
  }
80
135
 
81
136
  initialize(view) {
82
- const { autocomplete } = this.options;
83
137
  this._view = view;
84
138
  const { proxyElement, hub } = view;
85
139
  this._unsubscribes = [
86
140
  ...this._unsubscribes,
141
+ proxyElement.on('input', (e) => this._syncInput(e)),
87
142
  proxyElement.on('keydown', (e) => this._handleKeyDown(e)),
88
143
  proxyElement.on('click', (e) => this._handleClick(e)),
144
+ proxyElement.on('focusin', (e) => this._handleFocusIn(e)),
145
+ proxyElement.on('focusout', (e) => this._handleFocusOut(e)),
89
146
  ];
90
- if (autocomplete) {
91
- this._unsubscribes.push(hub.on(fields.suggestions(), () => this._handleInput()));
92
- }
147
+ this._unsubscribes.push(hub.on(fields.completions(), () => view.refresh({ force: true })));
93
148
  }
94
149
 
95
150
  focus() {
151
+ // TODO: support blur as well
96
152
  if (this._element) {
97
153
  this._context().focus();
98
154
  } else {
@@ -101,6 +157,14 @@ export default class SearchBoxLayout extends TemplateBasedLayout {
101
157
  }
102
158
  }
103
159
 
160
+ open() {
161
+ this._element.classList.add('open');
162
+ }
163
+
164
+ close() {
165
+ this._element.classList.remove('open');
166
+ }
167
+
104
168
  set value(value) {
105
169
  if (this._element) {
106
170
  this._context().value = value;
@@ -111,12 +175,10 @@ export default class SearchBoxLayout extends TemplateBasedLayout {
111
175
 
112
176
  _preprocess({ state }) {
113
177
  state = super._preprocess({ state });
114
- const { autocomplete } = this.options;
115
- if (!autocomplete) {
116
- return state;
117
- }
118
- const suggestions = this._view.hub.states[fields.suggestions()];
119
- return { ...state, suggestions };
178
+ return {
179
+ ...state,
180
+ completions: this._view.hub.states[fields.completions()],
181
+ };
120
182
  }
121
183
 
122
184
  _render(element, data, controls) {
@@ -124,42 +186,32 @@ export default class SearchBoxLayout extends TemplateBasedLayout {
124
186
  // only render the template once, and also skip if there are children already to allow customized DOM
125
187
  super._render(element, data, controls);
126
188
  }
127
- this._setupAutocomplete(element);
128
- this._renderSuggestions(element, data, controls);
189
+ this._updateCompletions(element, data, controls);
129
190
  this._fullfillFocusRequest(element);
130
191
  this._fullfillValueRequest(element);
131
192
  }
132
193
 
133
- _setupAutocomplete(element) {
134
- const { autocomplete } = this.options;
135
- if (!autocomplete) {
194
+ _updateCompletions(element, { state, rendered }, { writeToState }) {
195
+ if (!state.completions) {
136
196
  return;
137
197
  }
138
- this._context(element).setupAutocomplete();
139
- }
140
-
141
- _renderSuggestions(element, { state }, { writeToState } = {}) {
142
- const { autocomplete } = this.options;
143
- if (!autocomplete) {
198
+ const { status = STATUS.INITIAL, index, value: completions } = state.completions;
199
+ if (rendered && rendered.completions.index === index && rendered.completions.status === status) {
144
200
  return;
145
201
  }
146
-
147
- const { suggestionListElement, autocompleteElement } = this._context(element);
148
- if (!suggestionListElement) {
149
- // not actually rendered
150
- writeToState && writeToState({ suggestions: { value: [] } });
202
+ const context = this._context(element);
203
+ const { autocompleteElement } = context;
204
+ if (!autocompleteElement) {
151
205
  return;
152
206
  }
153
207
 
154
- // TODO: incremental
155
- const { suggestions = {} } = state;
156
- const suggestionItems = suggestions.value || [];
157
- autocompleteElement.classList[suggestionItems.length === 0 ? 'add' : 'remove']('empty');
158
- suggestionListElement.innerHTML = suggestionItems.map((suggestion) => this.templates.suggestionItem(this, suggestion)).join('');
159
- let i = 0;
160
- for (const itemElement of suggestionListElement.children) {
161
- this._suggestionItems.set(itemElement, suggestionItems[i++]);
162
- }
208
+ const items = context.trackItems(completions);
209
+
210
+ element.classList[items.length > 0 ? 'add' : 'remove']('nonempty');
211
+ autocompleteElement.setAttribute('data-status', status);
212
+ autocompleteElement.innerHTML = this.templates.completions(this, items);
213
+
214
+ writeToState({ completions: { index, status } });
163
215
  }
164
216
 
165
217
  _fullfillFocusRequest(element) {
@@ -190,39 +242,75 @@ export default class SearchBoxLayout extends TemplateBasedLayout {
190
242
  return context;
191
243
  }
192
244
 
245
+ _syncInput() {
246
+ const { inputElement } = this._context();
247
+ if (!inputElement) {
248
+ return;
249
+ }
250
+ const value = inputElement.value.trim();
251
+
252
+ if (this._prevInput === value) {
253
+ return;
254
+ }
255
+ this._prevInput = value;
256
+
257
+ const self = this;
258
+ this._debounce(() => self._view.hub.update(fields.input(), { value }));
259
+ }
260
+
261
+ _handleFocusIn({ target }) {
262
+ if (!this._element) {
263
+ return;
264
+ }
265
+ if (target.matches('[data-role="input"]')) {
266
+ this._syncInput();
267
+ this.open();
268
+ }
269
+ }
270
+
271
+ _handleFocusOut({ relatedTarget }) {
272
+ if (!this._element) {
273
+ return;
274
+ }
275
+ if (!relatedTarget || !findInAncestors(relatedTarget, element => element === this._element || undefined)) {
276
+ this.close();
277
+ }
278
+ }
279
+
193
280
  _handleKeyDown({ key, target, isComposing }) {
194
281
  if (!isComposing && key === 'Enter' && target.matches('input')) {
195
282
  this._submit(target.value);
196
283
  target.blur();
197
284
  }
285
+ // TODO: select completion items with arrow keys
198
286
  }
199
287
 
200
288
  _handleClick({ target }) {
201
289
  const { inputElement } = this._context();
202
- for (let element = target; element && element !== this._view.element; element = element.parentElement) {
203
- if (element.matches('[type="submit"]') || element.matches('[data-role="button"]')) {
290
+ for (let element = target; element && element !== this._element; element = element.parentElement) {
291
+ if (element.matches('[data-role="submit"]') || element.matches('[data-role="button"]') || element.matches('[type="submit"]')) {
204
292
  if (inputElement) {
205
293
  inputElement.blur();
206
294
  this._submit(inputElement.value);
207
295
  }
208
296
  return;
209
297
  }
210
- }
211
- const { autocomplete } = this.options;
212
- if (!autocomplete) {
213
- return;
214
- }
215
- for (let element = target; element && element !== this._view.element; element = element.parentElement) {
216
- const suggestion = this._suggestionItems.get(element);
217
- if (!suggestion) {
218
- continue;
298
+ if (element.matches('[data-role="query-item"]')) {
299
+ const index = element.getAttribute('data-index');
300
+ const item = this._context().data.get(parseInt(index));
301
+ const value = item && item.text;
302
+ if (value) {
303
+ if (inputElement) {
304
+ inputElement.value = value;
305
+ }
306
+ this._submit(value);
307
+ }
308
+ this.close();
309
+ return;
219
310
  }
220
- const value = suggestion.text || suggestion;
221
- if (inputElement) {
222
- inputElement.value = value;
311
+ if (element.matches('[data-role="product-item"]')) {
312
+ return;
223
313
  }
224
- this._submit(value);
225
- return;
226
314
  }
227
315
  }
228
316
 
@@ -230,6 +318,7 @@ export default class SearchBoxLayout extends TemplateBasedLayout {
230
318
  if (!value) {
231
319
  return;
232
320
  }
321
+ this.close();
233
322
  // TODO: q -> value
234
323
  this._view.hub.trigger(fields.query(), { q: value });
235
324
  }
@@ -245,6 +334,7 @@ class Context {
245
334
  constructor(element) {
246
335
  this._element = element;
247
336
  this._cache = {};
337
+ this._data = new Map();
248
338
  }
249
339
 
250
340
  get inputElement() {
@@ -255,25 +345,27 @@ class Context {
255
345
  return this._get('autocomplete');
256
346
  }
257
347
 
258
- get suggestionListElement() {
259
- return this._get('suggestion-list');
348
+ get data() {
349
+ return this._data;
260
350
  }
261
351
 
262
- setupAutocomplete() {
263
- if (!this._element || this._autocompleteSetup) {
264
- return;
265
- }
266
- this._autocompleteSetup = true;
267
- const { inputElement, autocompleteElement } = this;
268
- if (inputElement && autocompleteElement) {
269
- // TODO: should we clean up
270
- inputElement.addEventListener('focus', () => {
271
- autocompleteElement.classList.add('open');
272
- });
273
- inputElement.addEventListener('blur', () => {
274
- autocompleteElement.classList.remove('open');
275
- });
352
+ trackItems(completions) {
353
+ let index = 0;
354
+ const data = this._data = new Map();
355
+ const items = [];
356
+ for (const field in completions) {
357
+ for (const item of completions[field]) {
358
+ if (item.product && !item.product.url) {
359
+ continue; // product without URL is not clickable
360
+ }
361
+ item._index = index;
362
+ item._field = field;
363
+ data.set(index, item);
364
+ items.push(item);
365
+ index++;
366
+ }
276
367
  }
368
+ return items;
277
369
  }
278
370
 
279
371
  focus() {
@@ -1,7 +1,7 @@
1
1
  import { defineValues } from '@miso.ai/commons';
2
2
  import { STATUS, LAYOUT_CATEGORY } from '../../constants.js';
3
3
  import TemplateBasedLayout from '../template.js';
4
- import { product, article, question } from '../templates.js';
4
+ import { product, article, question, productInfoBlock, articleInfoBlock, imageBlock, indexBlock, helpers } from '../templates.js';
5
5
 
6
6
  const VALUE = Symbol.for('miso.value');
7
7
 
@@ -57,6 +57,11 @@ const DEFAULT_TEMPLATES = Object.freeze({
57
57
  ordered: false,
58
58
  items,
59
59
  item,
60
+ productInfoBlock,
61
+ articleInfoBlock,
62
+ imageBlock,
63
+ indexBlock,
64
+ helpers,
60
65
  });
61
66
 
62
67
  export default class CollectionLayout extends TemplateBasedLayout {
@@ -16,34 +16,33 @@ export function unimplemented(name) {
16
16
 
17
17
  export function product(layout, state, data, meta) {
18
18
  const { templates } = layout;
19
- const [openTag, closeTag] = renderTagPair(layout, data);
19
+ const [openTag, closeTag] = tagPair(layout, data);
20
20
  return [
21
21
  openTag,
22
- (templates.imageBlock || renderImageBlock)(layout, data, meta),
23
- (templates.infoBlock || productInfoBlock)(layout, data, meta),
24
- (templates.indexBlock || renderIndexBlock)(layout, data, meta),
22
+ (templates.imageBlock || imageBlock)(layout, data, meta),
23
+ (templates.infoBlock || templates.productInfoBlock || productInfoBlock)(layout, data, meta),
25
24
  closeTag,
26
25
  ].join('');
27
26
  }
28
27
 
29
28
  export function question(layout, state, data) {
30
- const [openTag, closeTag] = renderTagPair(layout, data);
29
+ const [openTag, closeTag] = tagPair(layout, data);
31
30
  return `${openTag}${data.value || data.text || data}${closeTag}`;
32
31
  }
33
32
 
34
33
  export function article(layout, state, data, meta) {
35
34
  const { templates } = layout;
36
- const [openTag, closeTag] = renderTagPair(layout, data);
35
+ const [openTag, closeTag] = tagPair(layout, data);
37
36
  return [
38
37
  openTag,
39
- (templates.imageBlock || renderImageBlock)(layout, data, meta),
40
- (templates.infoBlock || articleInfoBlock)(layout, data, meta),
41
- (templates.indexBlock || renderIndexBlock)(layout, data, meta),
38
+ (templates.imageBlock || imageBlock)(layout, data, meta),
39
+ (templates.infoBlock || templates.articleInfoBlock || articleInfoBlock)(layout, data, meta),
40
+ (templates.indexBlock || indexBlock)(layout, data, meta),
42
41
  closeTag,
43
42
  ].join('');
44
43
  }
45
44
 
46
- function productInfoBlock({ className }, { title, description, sale_price, original_price }) {
45
+ export function productInfoBlock({ className }, { title, description, sale_price, original_price }) {
47
46
  let content = '';
48
47
  if (title) {
49
48
  content += `<div class="${className}__item-title">${escapeHtml(title)}</div>`;
@@ -59,14 +58,14 @@ function productInfoBlock({ className }, { title, description, sale_price, origi
59
58
  return `<div class="${className}__item-info-container">${content}</div>`;
60
59
  }
61
60
 
62
- function articleInfoBlock({ className, templates }, { title, snippet, description, created_at, updated_at, published_at }) {
61
+ export function articleInfoBlock({ className, templates }, { title, snippet, description, created_at, updated_at, published_at }) {
63
62
  const date = published_at || created_at || updated_at;
64
63
  let content = '';
65
64
  if (title) {
66
65
  content += `<div class="${className}__item-title">${escapeHtml(title)}</div>`;
67
66
  }
68
67
  if (date) {
69
- content += `<div class="${className}__item-date">${renderDate(date, templates.date)}</div>`;
68
+ content += `<div class="${className}__item-date">${formatDate(date, templates.date)}</div>`;
70
69
  }
71
70
  if (snippet) {
72
71
  content += `<div class="${className}__item-snippet">${snippet}</div>`;
@@ -76,17 +75,7 @@ function articleInfoBlock({ className, templates }, { title, snippet, descriptio
76
75
  return `<div class="${className}__item-info-container">${content}</div>`;
77
76
  }
78
77
 
79
- // helpers //
80
- function renderTagPair({ className, options = {} }, { product_id, url }) {
81
- const { link = {} } = options;
82
- const { target = '_blank', rel = 'noopener' } = link; // TODO: other properties
83
- const tag = url ? 'a' : 'div';
84
- const urlAttrs = url ? `href="${url}" target="${target}" rel="${rel}"` : '';
85
- const productAttrs = product_id ? `${ATTR_DATA_MISO_PRODUCT_ID}="${product_id}"` : '';
86
- return [`<${tag} class="${className}__item-body" data-role="item" ${productAttrs} ${urlAttrs}>`, `</${tag}>`];
87
- }
88
-
89
- function renderImageBlock({ className }, { cover_image }) {
78
+ export function imageBlock({ className }, { cover_image }) {
90
79
  if (!cover_image) {
91
80
  return '';
92
81
  }
@@ -94,14 +83,25 @@ function renderImageBlock({ className }, { cover_image }) {
94
83
  return `<div class="${className}__item-cover-image-container">${img}</div>`;
95
84
  }
96
85
 
97
- function renderIndexBlock({ className }, data, { index }) {
86
+ export function indexBlock({ className }, data, { index }) {
98
87
  const i = index + 1;
99
88
  return `<div class="${className}__item-index-container"><span class="${className}__item-index miso-citation-index" data-index="${i}"></span></div>`;
100
89
  }
101
90
 
91
+ // helpers //
92
+ function tagPair({ className, options = {} }, { product_id, url }, { classSuffix = 'item-body', role = 'item' } = {}) {
93
+ const { link = {} } = options;
94
+ const { target = '_blank', rel = 'noopener' } = link; // TODO: other properties
95
+ const tag = url ? 'a' : 'div';
96
+ const urlAttrs = url ? `href="${url}" target="${target}" rel="${rel}"` : '';
97
+ const productAttrs = product_id ? `${ATTR_DATA_MISO_PRODUCT_ID}="${product_id}"` : '';
98
+ const roleAttrs = role ? `data-role="${role}"` : '';
99
+ return [`<${tag} class="${className}__${classSuffix}" ${roleAttrs} ${productAttrs} ${urlAttrs}>`, `</${tag}>`];
100
+ }
101
+
102
102
  const DEFAULT_DATE_OPTIONS = Object.freeze({ locale: 'en-US', year: 'numeric', month: 'short', day: 'numeric' });
103
103
 
104
- function renderDate(date, fn = DEFAULT_DATE_OPTIONS) {
104
+ function formatDate(date, fn = DEFAULT_DATE_OPTIONS) {
105
105
  switch (typeof fn) {
106
106
  case 'function':
107
107
  return fn(date);
@@ -113,3 +113,9 @@ function renderDate(date, fn = DEFAULT_DATE_OPTIONS) {
113
113
  throw new Error(`Invalid date format: ${fn}`);
114
114
  }
115
115
  }
116
+
117
+ export const helpers = Object.freeze({
118
+ tagPair,
119
+ formatDate,
120
+ escapeHtml,
121
+ });
@@ -1 +1,14 @@
1
- export { default as api } from './api.js';
1
+ import { API } from '@miso.ai/commons';
2
+
3
+ //export { default as api } from './api.js';
4
+
5
+ export function api(client) {
6
+ // TODO: send uuid & unit id
7
+ return async ({ group, name, payload, options }) => {
8
+ if (group === API.GROUP.ASK && name === API.NAME.QUESTIONS) {
9
+ return client.api.ask.questions(payload, options);
10
+ }
11
+ // because name is in snake case
12
+ return client.api[group]._run(name, payload, options);
13
+ };
14
+ }
package/src/version.js CHANGED
@@ -1 +1 @@
1
- export default '1.10.0-beta.0';
1
+ export default '1.10.0-beta.1';
@@ -1,9 +1,11 @@
1
1
  import { defineValues, trimObj, API } from '@miso.ai/commons';
2
2
  import Workflow from './base.js';
3
3
  import { mergeApiOptions } from './options.js';
4
- import { fields, FeedbackActor } from '../actor/index.js';
4
+ import * as sources from '../source/index.js';
5
+ import { fields, FeedbackActor, AutocompleteActor } from '../actor/index.js';
5
6
  import { ROLE, STATUS, ORGANIC_QUESTION_SOURCE } from '../constants.js';
6
7
  import { SearchBoxLayout, OptionListLayout, ListLayout, TextLayout, TypewriterLayout, FeedbackLayout } from '../layout/index.js';
8
+ import { makeConfigurable } from './options.js';
7
9
 
8
10
  const DEFAULT_API_OPTIONS = Object.freeze({
9
11
  group: API.GROUP.ASK,
@@ -17,6 +19,14 @@ const DEFAULT_API_OPTIONS = Object.freeze({
17
19
  },
18
20
  });
19
21
 
22
+ const DEFAULT_AUTOCOMPLETE_OPTIONS = Object.freeze({
23
+ actor: false,
24
+ api: {
25
+ group: API.GROUP.ASK,
26
+ name: API.NAME.AUTOCOMPLETE,
27
+ },
28
+ });
29
+
20
30
  const DEFAULT_LAYOUTS = Object.freeze({
21
31
  [ROLE.QUERY]: [SearchBoxLayout.type, { placeholder: 'Ask a question' }],
22
32
  [ROLE.QUESTION]: [TextLayout.type, { tag: 'h2' }],
@@ -40,6 +50,7 @@ const DEFAULT_TRACKERS = Object.freeze({
40
50
 
41
51
  const DEFAULT_OPTIONS = Object.freeze({
42
52
  api: DEFAULT_API_OPTIONS,
53
+ autocomplete: DEFAULT_AUTOCOMPLETE_OPTIONS,
43
54
  layouts: DEFAULT_LAYOUTS,
44
55
  trackers: DEFAULT_TRACKERS,
45
56
  });
@@ -60,6 +71,13 @@ export default class Ask extends Workflow {
60
71
  this._feedback = new FeedbackActor(this._hub);
61
72
  this._setSuggestedQuestions();
62
73
 
74
+ /*
75
+ this._autocomplete = new AutocompleteActor(this._hub, {
76
+ source: sources.api(this._client),
77
+ options: this._options,
78
+ });
79
+ */
80
+
63
81
  this._unsubscribes = [
64
82
  ...this._unsubscribes,
65
83
  this._hub.on(fields.query(), payload => this.query(payload)),
@@ -152,6 +170,17 @@ export default class Ask extends Workflow {
152
170
  super.restart();
153
171
  }
154
172
 
173
+ /*
174
+ updateCompletions(event) {
175
+ // TODO: verify
176
+ this._hub.update(fields.completions(), {
177
+ ...event,
178
+ source: 'manual',
179
+ });
180
+ return this;
181
+ }
182
+ */
183
+
155
184
  _defaultProcessData(data) {
156
185
  data = super._defaultProcessData(data);
157
186
  const { value, request } = data;
@@ -259,6 +288,7 @@ export default class Ask extends Workflow {
259
288
  this._context._byQid.delete(questionId);
260
289
  }
261
290
  this._feedback._destroy();
291
+ this._autocomplete._destroy();
262
292
  super._destroy(options);
263
293
  }
264
294
 
@@ -42,11 +42,10 @@ export default class Workflow extends Component {
42
42
  const extensions = this._extensions = plugin._getExtensions(client);
43
43
  const options = this._options = new WorkflowOptions(context && context._options, mergeDefaults(this, defaults));
44
44
  const hub = this._hub = injectLogger(new Hub(), (...args) => this._log(...args));
45
- const source = this._source = sources.api(client);
46
45
  const layouts = plugin.layouts;
47
46
 
48
47
  this._sessions = new SessionMaker(hub);
49
- this._data = new DataActor(hub, { source, options });
48
+ this._data = new DataActor(hub, { source: sources.api(client), options });
50
49
  const views = this._views = new ViewsActor(hub, { extensions, layouts, roles, options, workflow: name });
51
50
  this._interactions = new InteractionsActor(hub, { client, options });
52
51
  this._trackers = new Trackers(hub, { views, options });
@@ -11,7 +11,7 @@ const DEFAULT_API_OPTIONS = Object.freeze({
11
11
  });
12
12
 
13
13
  const DEFAULT_LAYOUTS = Object.freeze({
14
- [ROLE.RELATED_QUESTIONS]: [ListLayout.type, { itemType: 'question' }],
14
+ [ROLE.RELATED_QUESTIONS]: [ListLayout.type, { itemType: 'question', link: { rel: 'noopener nofollow' } }],
15
15
  [ROLE.QUERY]: [SearchBoxLayout.type, { placeholder: 'Ask a question' }],
16
16
  });
17
17