@miso.ai/client-sdk-ui 1.9.10 → 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.
Files changed (37) hide show
  1. package/dist/cjs/actor/autocomplete.js +157 -0
  2. package/dist/cjs/actor/data.js +3 -11
  3. package/dist/cjs/actor/fields.js +8 -0
  4. package/dist/cjs/actor/index.js +7 -0
  5. package/dist/cjs/actor/utils.js +10 -0
  6. package/dist/cjs/defaults/ask/templates.js +21 -0
  7. package/dist/cjs/layout/input/search-box.js +213 -127
  8. package/dist/cjs/layout/list/collection.js +6 -1
  9. package/dist/cjs/layout/templates.js +50 -35
  10. package/dist/cjs/layout/text/typewriter/index.js +8 -53
  11. package/dist/cjs/source/index.js +20 -8
  12. package/dist/cjs/styles.js +4 -83
  13. package/dist/cjs/version.js +1 -1
  14. package/dist/cjs/workflow/ask.js +48 -16
  15. package/dist/cjs/workflow/base.js +1 -2
  16. package/dist/cjs/workflow/explore.js +4 -1
  17. package/dist/cjs/workflow/options.js +54 -13
  18. package/dist/cjs/workflow/search.js +35 -7
  19. package/package.json +2 -2
  20. package/src/actor/autocomplete.js +131 -0
  21. package/src/actor/data.js +3 -10
  22. package/src/actor/fields.js +8 -0
  23. package/src/actor/index.js +1 -0
  24. package/src/actor/utils.js +8 -0
  25. package/src/defaults/ask/templates.js +21 -0
  26. package/src/layout/input/search-box.js +186 -94
  27. package/src/layout/list/collection.js +6 -1
  28. package/src/layout/templates.js +31 -25
  29. package/src/layout/text/typewriter/index.js +6 -46
  30. package/src/source/index.js +14 -1
  31. package/src/styles.js +4 -86
  32. package/src/version.js +1 -1
  33. package/src/workflow/ask.js +31 -1
  34. package/src/workflow/base.js +1 -2
  35. package/src/workflow/explore.js +1 -1
  36. package/src/workflow/options.js +49 -12
  37. package/src/workflow/search.js +33 -1
@@ -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
+ });
@@ -72,63 +72,23 @@ export default class TypewriterLayout extends ProgressiveLayout {
72
72
  async _setupForMarkdown() {
73
73
  const context = await this._view._views._extensions.require('markdown');
74
74
  const cursorClass = cursorClassName(this.className);
75
- const { onDebug } = this.options;
75
+ const { onDebug, onCitationLink } = this.options;
76
76
  // TODO: options
77
77
  this._renderer = context.createRenderer({
78
- onRefChange: (oldRef, newRef) => {
79
- oldRef && oldRef.classList.remove(cursorClass);
80
- newRef && newRef.classList.add(cursorClass);
81
- },
82
- onDone: (element) => {
83
- element.classList.add('done');
84
- },
78
+ cursorClass,
79
+ getSource: index => this._getSource(index),
80
+ onCitationLink,
85
81
  onDebug,
86
- onCitationLink: this._handleCitationLink.bind(this),
87
82
  });
88
83
  // capture citation link click if necessary
89
84
  this._unsubscribes.push(this._view.proxyElement.on('click', (e) => this._handleClick(e)));
90
85
  }
91
86
 
92
- _handleCitationLink(methods, index) {
93
- const { onCitationLink } = this.options;
94
- if (typeof onCitationLink !== 'function') {
95
- return;
96
- }
97
- const { data = {} } = this._view._data;
98
- const { sources = [] } = data;
99
- const source = sources[index];
100
- try {
101
- onCitationLink(methods, { source, index });
102
- } catch (e) {
103
- console.error(e);
104
- }
105
- }
106
-
107
- /*
108
- _handleCitationTooltip({ node, tooltip, index }) {
109
- const { tooltip: tooltipOptions } = this.options;
110
- if (!tooltipOptions || !tooltip || tooltip.properties['data-value']) {
111
- return;
112
- }
87
+ _getSource(index) {
113
88
  const { data = {} } = this._view._data;
114
89
  const { sources = [] } = data;
115
- const source = sources[index];
116
- if (!source) {
117
- return;
118
- }
119
- // TODO: allow HTML
120
- let value;
121
- try {
122
- value = typeof tooltipOptions === 'function' ? tooltipOptions(source) : source.title;
123
- } catch (e) {
124
- console.error(e);
125
- }
126
- if (!value) {
127
- return;
128
- }
129
- tooltip.properties['data-value'] = escapeHtml(value);
90
+ return sources[index];
130
91
  }
131
- */
132
92
 
133
93
  async _setupForPlaintext() {
134
94
  this._renderer = new PlaintextRenderer();
@@ -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/styles.js CHANGED
@@ -1,97 +1,15 @@
1
- import version from './version.js';
1
+ import { loadStyles, resolveCssUrl } from '@miso.ai/commons';
2
2
 
3
- const { currentScript } = document;
3
+ const NAME = 'ui';
4
4
 
5
5
  let cssUrl;
6
6
  let stylesLoaded;
7
7
 
8
8
  export async function loadStylesIfNecessary() {
9
- return stylesLoaded || (stylesLoaded = loadStyles());
10
- }
11
-
12
- export async function loadStyles() {
13
- try {
14
- let link = getExistedLinkElement();
15
- if (!link) {
16
- link = document.createElement('link');
17
- link.rel = 'stylesheet';
18
- link.href = getCssUrl();
19
- document.head.appendChild(link);
20
- }
21
- return waitForStylesApplied(link);
22
- } catch(e) {
23
- console.error(e);
24
- }
9
+ return stylesLoaded || (stylesLoaded = loadStyles(getCssUrl()));
25
10
  }
26
11
 
27
12
  // helpers //
28
13
  function getCssUrl() {
29
- return cssUrl || (cssUrl = resolveCssUrl());
30
- }
31
-
32
- function resolveCssUrl() {
33
- // TODO: refactor with getPluginScriptUrl() in core
34
- if (version === 'dev') {
35
- const src = currentScript.src;
36
- const searchIndex = src.indexOf('?');
37
- const k = searchIndex < 0 ? src.length : searchIndex;
38
- const i = src.lastIndexOf('/', k); // .../dist/umd/*.min.js
39
- return `${src.slice(0, i - 4)}/css/ui.css`;
40
- } else {
41
- return `https://cdn.jsdelivr.net/npm/@miso.ai/client-sdk@${version}/dist/css/ui.css`;
42
- }
43
- }
44
-
45
- function getExistedLinkElement() {
46
- return document.head.querySelector(`link[href="${getCssUrl()}"]`);
47
- }
48
-
49
- async function waitForStylesApplied(element) {
50
- await waitForSheetAvailable(element);
51
- await waitForSheetIncluded(element);
52
- }
53
-
54
- async function waitForSheetAvailable(element) {
55
- if (element.sheet) {
56
- return;
57
- }
58
- return new Promise((resolve, reject) => {
59
- let intervalId;
60
- element.onerror = () => {
61
- intervalId && clearInterval(intervalId);
62
- reject();
63
- };
64
- intervalId = setInterval(() => {
65
- if (!element.sheet) {
66
- return;
67
- }
68
- intervalId && clearInterval(intervalId);
69
- resolve();
70
- }, 100);
71
- });
72
- }
73
-
74
- async function waitForSheetIncluded(element) {
75
- if (isSheetIncluded(element)) {
76
- return;
77
- }
78
- return new Promise((resolve) => {
79
- let intervalId;
80
- intervalId = setInterval(() => {
81
- if (!isSheetIncluded(element)) {
82
- return;
83
- }
84
- intervalId && clearInterval(intervalId);
85
- resolve();
86
- }, 100);
87
- });
88
- }
89
-
90
- function isSheetIncluded(element) {
91
- for (const sheet of document.styleSheets) {
92
- if (sheet.ownerNode === element) {
93
- return true;
94
- }
95
- }
96
- return false;
14
+ return cssUrl || (cssUrl = resolveCssUrl(NAME));
97
15
  }
package/src/version.js CHANGED
@@ -1 +1 @@
1
- export default '1.9.10';
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
 
@@ -3,6 +3,7 @@ import { ROLE } from '../constants.js';
3
3
 
4
4
  // normalize //
5
5
  export function normalizeApiOptions([name, payload] = []) {
6
+ // TODO: take object form as well
6
7
  if (name === false) {
7
8
  return { actor: false };
8
9
  }
@@ -105,6 +106,17 @@ export function normalizeInteractionsOptions(options) {
105
106
  return { ...options, preprocess };
106
107
  }
107
108
 
109
+ export function normalizeAutocompleteOptions(options) {
110
+ if (options === undefined) {
111
+ throw new Error(`Expect autocomplete options to be an object or a boolean value: ${options}`);
112
+ }
113
+ if (typeof options === 'boolean') {
114
+ options = { actor: options };
115
+ }
116
+ const processData = normalizeDataProcessorOptions(options.processData);
117
+ return { ...options, processData };
118
+ }
119
+
108
120
  // merge //
109
121
  export function mergeApiOptions(...optionsList) {
110
122
  if (optionsList[optionsList.length - 1] === false) {
@@ -190,6 +202,17 @@ export function mergeInteractionsOptions(...optionsList) {
190
202
  }));
191
203
  }
192
204
 
205
+ export function mergeAutocompleteOptions(...optionsList) {
206
+ if (optionsList[optionsList.length - 1] === false) {
207
+ return false;
208
+ }
209
+ return mergeOptions(optionsList, (merged, options) => Object.assign(merged, {
210
+ ...options,
211
+ api: mergeApiOptions(merged.api, options.api),
212
+ processData: mergeDataProcessorOptions(merged.processData, options.processData),
213
+ }));
214
+ }
215
+
193
216
 
194
217
 
195
218
  // manifest //
@@ -219,6 +242,11 @@ const FEATURES = [
219
242
  normalize: normalizeInteractionsOptions,
220
243
  merge: mergeInteractionsOptions,
221
244
  },
245
+ {
246
+ key: 'autocomplete',
247
+ normalize: normalizeAutocompleteOptions,
248
+ merge: mergeAutocompleteOptions,
249
+ },
222
250
  ];
223
251
 
224
252
 
@@ -328,18 +356,27 @@ for (const { key, normalize, merge } of FEATURES) {
328
356
  });
329
357
  }
330
358
 
331
- export function makeConfigurable(prototype) {
332
- Object.assign(prototype, {
333
- useApi(...args) {
334
- this._options.api.merge(args);
335
- return this;
336
- },
337
- clearApi() {
338
- this._options.api.set();
339
- return this;
340
- },
341
- });
342
- for (const feature of ['layouts', 'dataProcessor', 'trackers', 'interactions']) {
359
+ const DEFAULT_FEATURES = ['api', 'layouts', 'dataProcessor', 'trackers', 'interactions'];
360
+
361
+ export function makeConfigurable(prototype, features = DEFAULT_FEATURES) {
362
+ for (const feature of features) {
363
+ injectConfigurableFeature(prototype, feature);
364
+ }
365
+ }
366
+
367
+ function injectConfigurableFeature(prototype, feature) {
368
+ if (feature === 'api') {
369
+ Object.assign(prototype, {
370
+ useApi(...args) {
371
+ this._options.api.merge(args);
372
+ return this;
373
+ },
374
+ clearApi() {
375
+ this._options.api.set();
376
+ return this;
377
+ },
378
+ });
379
+ } else {
343
380
  const upperCased = upperCase(feature);
344
381
  Object.assign(prototype, {
345
382
  [`use${upperCased}`](value) {
@@ -1,9 +1,11 @@
1
1
  import { API } from '@miso.ai/commons';
2
2
  import Workflow from './base.js';
3
3
  import { mergeApiOptions } from './options.js';
4
- import { fields } from '../actor/index.js';
4
+ import * as sources from '../source/index.js';
5
+ import { fields, AutocompleteActor } from '../actor/index.js';
5
6
  import { ROLE } from '../constants.js';
6
7
  import { ListLayout, SearchBoxLayout } 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.SEARCH,
@@ -13,6 +15,19 @@ const DEFAULT_API_OPTIONS = Object.freeze({
13
15
  },
14
16
  });
15
17
 
18
+ const DEFAULT_AUTOCOMPLETE_OPTIONS = Object.freeze({
19
+ actor: false,
20
+ api: {
21
+ group: API.GROUP.SEARCH,
22
+ name: API.NAME.AUTOCOMPLETE,
23
+ payload: {
24
+ completion_fields: ['suggested_queries', 'title'],
25
+ fl: ['title', 'url', 'cover_image'],
26
+ },
27
+ },
28
+ throttle: 300,
29
+ });
30
+
16
31
  const DEFAULT_LAYOUTS = Object.freeze({
17
32
  [ROLE.QUERY]: SearchBoxLayout.type,
18
33
  [ROLE.PRODUCTS]: ListLayout.type,
@@ -24,6 +39,7 @@ const DEFAULT_TRACKERS = Object.freeze({
24
39
 
25
40
  const DEFAULT_OPTIONS = Object.freeze({
26
41
  api: DEFAULT_API_OPTIONS,
42
+ autocomplete: DEFAULT_AUTOCOMPLETE_OPTIONS,
27
43
  layouts: DEFAULT_LAYOUTS,
28
44
  trackers: DEFAULT_TRACKERS,
29
45
  });
@@ -39,6 +55,11 @@ export default class Search extends Workflow {
39
55
  defaults: DEFAULT_OPTIONS,
40
56
  });
41
57
 
58
+ this._autocomplete = new AutocompleteActor(this._hub, {
59
+ source: sources.api(this._client),
60
+ options: this._options,
61
+ });
62
+
42
63
  this._unsubscribes.push(this._hub.on(fields.query(), payload => this.query(payload)));
43
64
  }
44
65
 
@@ -66,9 +87,20 @@ export default class Search extends Workflow {
66
87
  return this;
67
88
  }
68
89
 
90
+ updateCompletions(event) {
91
+ // TODO: verify
92
+ this._hub.update(fields.completions(), {
93
+ ...event,
94
+ source: 'manual',
95
+ });
96
+ return this;
97
+ }
98
+
69
99
  notifyViewUpdate(role = ROLE.PRODUCTS, ...args) {
70
100
  super.notifyViewUpdate(role, ...args);
71
101
  return this;
72
102
  }
73
103
 
74
104
  }
105
+
106
+ makeConfigurable(Search.prototype, ['autocomplete']);