From 94daabc95c21d3bd2e6581eda961483d321d0dfe Mon Sep 17 00:00:00 2001 From: Haroen Viaene Date: Mon, 5 Mar 2018 09:41:00 +0100 Subject: [PATCH] feat(configure): add the Configure widget (#2698) * feat(configure): add the configure connector * chore: use jsHelper.make in tests * chore: format * refactor: add configure widget instead * test: use workaround method instead * refactor: remove connectConfigure * chore: fix imports * chore: use searchParameters key * chore: use searchParameters directly after all * chore(doc): support for unnnamed parameters in widgets * chore(doc): support for externals * chore: incorporate feedback --- .../layouts/mixins/documentationjs/type.pug | 4 +- .../mixins/documentationjs/widget-usage.pug | 42 ++++++++- docgen/layouts/widget.pug | 28 ++++-- docgen/plugins/documentationjs-data.js | 21 +++-- src/lib/main.js | 10 ++- .../configure/__tests__/configure-test.js | 85 +++++++++++++++++++ src/widgets/configure/configure.js | 51 +++++++++++ src/widgets/index.js | 1 + 8 files changed, 224 insertions(+), 18 deletions(-) create mode 100644 src/widgets/configure/__tests__/configure-test.js create mode 100644 src/widgets/configure/configure.js diff --git a/docgen/layouts/mixins/documentationjs/type.pug b/docgen/layouts/mixins/documentationjs/type.pug index 25c689be16..a85ecd724b 100644 --- a/docgen/layouts/mixins/documentationjs/type.pug +++ b/docgen/layouts/mixins/documentationjs/type.pug @@ -44,10 +44,10 @@ mixin type(t, relatedTypes) if t.name !== 'Object' && t.name !== 'Array' && t.name[0] === t.name[0].toUpperCase() - const symbol = relatedTypes && relatedTypes.find(t2 => t2 && t.name === t2.name) || {}; - const isExternal = symbol.kind === 'external' - if isExternal + if symbol.name && isExternal - const firstSee = symbol.tags.find(t => t.title === 'see') || {}; a.typed-link(href=firstSee.description, target='_blank')=t.name - else + else if symbol.name a.typed-link(href=`${navPath}#struct-${t.name}`)=t.name else | !{t.name} diff --git a/docgen/layouts/mixins/documentationjs/widget-usage.pug b/docgen/layouts/mixins/documentationjs/widget-usage.pug index 0f8302956b..d7df744dc1 100644 --- a/docgen/layouts/mixins/documentationjs/widget-usage.pug +++ b/docgen/layouts/mixins/documentationjs/widget-usage.pug @@ -12,8 +12,8 @@ mixin widgetUsage(fnSymbol) span.cm-variable search | = span.cm-def instantsearch - | ( - span.cm-comment /* parameters */ + | ( + span.cm-comment /* parameters */ | ); br br @@ -45,3 +45,41 @@ mixin widgetUsage(fnSymbol) | ( span.cm-variable widget | ); + else + .heading + | Usage + pre.CodeMirror.cm-s-mdn-like + code + span.cm-keyword const + span.cm-variable search + | = + span.cm-def instantsearch + | ( + span.cm-comment /* parameters */ + | ); + br + br + span.cm-keyword const + span.cm-def widget + span.cm-operator = + span.cm-variable instantsearch + | . + span.cm-variable widgets + | . + span.cm-variable=fnSymbol.name + | (!{' \n'} + for property in fnSymbol.params + span.cm-property=` ${property.name}` + | : + span.cm-def + +type(property.type, fnSymbol.relatedTypes) + | , !{'\n'} + | ); + | + br + span.cm-variable search + |. + span.cm-property addWidget + | ( + span.cm-variable widget + | ); diff --git a/docgen/layouts/widget.pug b/docgen/layouts/widget.pug index 870ea60f08..dc7aae2c48 100644 --- a/docgen/layouts/widget.pug +++ b/docgen/layouts/widget.pug @@ -30,11 +30,29 @@ block content a.anchor(href=`${navPath}#usage`) +widgetUsage(jsdoc) - h2#structure Options - a.anchor(href=`${navPath}#structure`) - each t in jsdoc.relatedTypes - if t - +struct(t, jsdoc.relatedTypes) + if jsdoc.params && jsdoc.params.length === 1 && jsdoc.params[0].name === '$0' + if jsdoc.relatedTypes && jsdoc.relatedTypes.length > 0 + h2#structure Options + a.anchor(href=`${navPath}#structure`) + each t in jsdoc.relatedTypes + if t + +struct(t, jsdoc.relatedTypes) + else if jsdoc.params + h2#structure Options + a.anchor(href=`${navPath}#structure`) + ul.struct-def + each property in jsdoc.params + li.type(id=`struct-${property.name}`) + strong=property.name + a.anchor(href=`${navPath}#param-${property.name}`) + code + +type(property.type, jsdoc.relatedTypes) + if tag && tag.default + div.default-value + | Default value: + code=tag.default + +description(property.description) + if jsdoc.examples && jsdoc.examples.length > 0 h2#example Example a.anchor(href=`${navPath}#example`) diff --git a/docgen/plugins/documentationjs-data.js b/docgen/plugins/documentationjs-data.js index 5bf761598e..d43c302d7e 100644 --- a/docgen/plugins/documentationjs-data.js +++ b/docgen/plugins/documentationjs-data.js @@ -186,16 +186,23 @@ function findRelatedTypes(functionSymbol, symbols) { const typeSymbol = find(symbols, {name: unnamedParameterType}); types = [...types, typeSymbol, ...findRelatedTypes(typeSymbol, symbols)] } else { - const currentTypeName = p.name; - const isCustomType = currentTypeName && currentTypeName !== 'Object' && currentTypeName[0] === currentTypeName[0].toUpperCase(); - if (isCustomType) { - const typeSymbol = find(symbols, {name: currentTypeName}); - if(!typeSymbol) console.warn('Undefined type: ', currentTypeName); + if (isCustomType(p.name)) { + const typeSymbol = find(symbols, {name: p.name}); + if(!typeSymbol) console.warn('Undefined type: ', p.name); else { types = [...types, typeSymbol]; // iterate over each property to get their types forEach(typeSymbol.properties, p => findParamsTypes({name: p.type.name, type: p.type})); } + } else if(isCustomType(p.type.name)){ + const typeSymbol = find(symbols, {name: p.type.name}); + if(!typeSymbol) console.warn('Undefined type: ', p.type.name); + else { + types = [...types, typeSymbol]; + // iterate over each property to get their types + if(typeSymbol.properties) + forEach(typeSymbol.properties, p2 => findParamsTypes({name: p2.type.name, type: p2.type})); + } } } }; @@ -206,3 +213,7 @@ function findRelatedTypes(functionSymbol, symbols) { return uniqBy(types, 'name'); } + +function isCustomType(name) { + return name && name !== 'Object' && name[0] === name[0].toUpperCase(); +} diff --git a/src/lib/main.js b/src/lib/main.js index afb2062d04..f061d53c07 100644 --- a/src/lib/main.js +++ b/src/lib/main.js @@ -1,8 +1,5 @@ /** @module module:instantsearch */ -/** - * @external SearchParameters - * @see https://community.algolia.com/algoliasearch-helper-js/reference.html#searchparameters - */ + // required for browsers not supporting Object.freeze (helper requirement) import '../shams/Object.freeze.js'; @@ -15,6 +12,11 @@ import version from './version.js'; import * as connectors from '../connectors/index.js'; import * as widgets from '../widgets/index.js'; +/** + * @external SearchParameters + * @see https://www.algolia.com/doc/api-reference/search-api-parameters/ + */ + /** * @typedef {Object} UrlSyncOptions * @property {Object} [mapping] Object used to define replacement query diff --git a/src/widgets/configure/__tests__/configure-test.js b/src/widgets/configure/__tests__/configure-test.js new file mode 100644 index 0000000000..0c6a79ca1d --- /dev/null +++ b/src/widgets/configure/__tests__/configure-test.js @@ -0,0 +1,85 @@ +import { SearchParameters } from 'algoliasearch-helper'; +import configure from '../configure'; + +describe('configure', () => { + it('throws when you pass it a non-plain object', () => { + [ + () => configure(new Date()), + () => configure(() => {}), + () => configure(/ok/), + ].forEach(widget => expect(widget).toThrowError(/Usage/)); + }); + + it('Applies searchParameters if nothing in configuration yet', () => { + const widget = configure({ analytics: true }); + const config = widget.getConfiguration(SearchParameters.make({})); + expect(config).toEqual({ + analytics: true, + }); + }); + + it('Applies searchParameters if nothing conflicting configuration', () => { + const widget = configure({ analytics: true }); + const config = widget.getConfiguration( + SearchParameters.make({ query: 'testing' }) + ); + expect(config).toEqual({ + analytics: true, + }); + }); + + it('Applies searchParameters with a higher priority', () => { + const widget = configure({ analytics: true }); + { + const config = widget.getConfiguration( + SearchParameters.make({ analytics: false }) + ); + expect(config).toEqual({ + analytics: true, + }); + } + { + const config = widget.getConfiguration( + SearchParameters.make({ analytics: false, extra: true }) + ); + expect(config).toEqual({ + analytics: true, + }); + } + }); + + it('disposes all of the state set by configure', () => { + const widget = configure({ analytics: true }); + + const nextState = widget.dispose({ + state: SearchParameters.make({ + analytics: true, + somethingElse: false, + }), + }); + + expect(nextState).toEqual( + SearchParameters.make({ + somethingElse: false, + }) + ); + }); + + it('disposes all of the state set by configure in case of a conflict', () => { + const widget = configure({ analytics: true }); + + const nextState = widget.dispose({ + state: SearchParameters.make({ + // even though it's different, it will be deleted + analytics: false, + somethingElse: false, + }), + }); + + expect(nextState).toEqual( + SearchParameters.make({ + somethingElse: false, + }) + ); + }); +}); diff --git a/src/widgets/configure/configure.js b/src/widgets/configure/configure.js new file mode 100644 index 0000000000..b0074448a0 --- /dev/null +++ b/src/widgets/configure/configure.js @@ -0,0 +1,51 @@ +import isPlainObject from 'lodash/isPlainObject'; +const usage = `Usage: +search.addWidget( + instantsearch.widgets.configure({ + // any searchParameter + }) +); +Full documentation available at https://community.algolia.com/instantsearch.js/v2/widgets/configure.html +`; + +/** + * The **configure** widget is a headless widget that let you configure the + * settings of your search using the parameters described by the + * [general Algolia documentation](https://www.algolia.com/doc/api-reference/search-api-parameters/) + * + * This widget has no visible UI, so you should only use it for search parameters + * users shouldn't expect to change. + * + * @type {WidgetFactory} + * @category filter + * @param {SearchParameters} searchParameters The Configure widget options are search parameters + * @returns {Object} A new Configure widget instance. + * @example + * search.addWidget( + * instantsearch.widgets.configure({ + * analytics: true, + * ruleContexts: ['desktop', 'cool-users'], + * distinct: 3, + * }) + * ); + */ +export default function configure(searchParameters = {}) { + if (!isPlainObject(searchParameters)) { + throw new Error(usage); + } + return { + getConfiguration() { + return searchParameters; + }, + init() {}, + dispose({ state }) { + return state.mutateMe(mutableState => { + // widgetParams are assumed 'controlled', + // so they override whatever other widgets give the state + Object.keys(searchParameters).forEach(key => { + delete mutableState[key]; + }); + }); + }, + }; +} diff --git a/src/widgets/index.js b/src/widgets/index.js index f3ca2d30ca..943bc5d560 100644 --- a/src/widgets/index.js +++ b/src/widgets/index.js @@ -6,6 +6,7 @@ */ export { default as clearAll } from '../widgets/clear-all/clear-all.js'; +export { default as configure } from '../widgets/configure/configure.js'; export { default as currentRefinedValues, } from '../widgets/current-refined-values/current-refined-values.js';