diff options
author | Sarah German <sgerman@gitlab.com> | 2022-06-16 12:54:51 +0300 |
---|---|---|
committer | David O'Regan <doregan@gitlab.com> | 2022-06-16 12:54:51 +0300 |
commit | 700448adb96fe2a19bfb0c31b751ae99d188d9d7 (patch) | |
tree | 8e37cf20cf8a67ab35284bc2b7384ab6d94e58b8 | |
parent | be62e2e61b36d9619ee368aa7d0156a5d1590b2a (diff) |
Use instantsearch-vue for the dedicated search page
-rw-r--r-- | content/assets/stylesheets/instantsearch.scss | 184 | ||||
-rw-r--r-- | content/frontend/search/components/search_page.vue | 77 | ||||
-rw-r--r-- | content/frontend/search/docsearch.js | 8 | ||||
-rw-r--r-- | content/frontend/search/index.js | 74 | ||||
-rw-r--r-- | content/frontend/search/instantsearch.js | 35 | ||||
-rw-r--r-- | content/frontend/search/search.js | 11 | ||||
-rw-r--r-- | content/search/index.md | 24 | ||||
-rw-r--r-- | jest.config.js | 13 | ||||
-rw-r--r-- | layouts/head.html | 1 | ||||
-rw-r--r-- | layouts/instantsearch-head.html | 4 | ||||
-rw-r--r-- | layouts/instantsearch.html | 12 | ||||
-rw-r--r-- | package.json | 6 | ||||
-rw-r--r-- | spec/frontend/__mocks__/style_mock.js | 1 | ||||
-rw-r--r-- | spec/frontend/search/search_spec.js | 24 | ||||
-rw-r--r-- | yarn.lock | 36 |
15 files changed, 205 insertions, 305 deletions
diff --git a/content/assets/stylesheets/instantsearch.scss b/content/assets/stylesheets/instantsearch.scss deleted file mode 100644 index f50c7f11..00000000 --- a/content/assets/stylesheets/instantsearch.scss +++ /dev/null @@ -1,184 +0,0 @@ ---- -version: 15 ---- - -@import 'variables'; - -// original CSS: https://github.com/algolia/examples/blob/master/instant-search/instantsearch.js/assets/style.css -// original CSS customized and converted to SCSS with http://sebastianpontow.de/css2compass/ - -//colors -$color0: rgba(180, 162, 231, 0.1); -$color1: rgba(180, 162, 231, 0.2); -$color2: rgba(180, 162, 231, 0.1); -$color3: rgba(79, 171, 225, 0.2); -$color4: #eee; -$color5: #ed5565; -$color6: #999; -$color7: #868686; -$color8: #fff; -$color9: #383838; -$color10: rgb(117, 117, 117); -$color11: transparent; -// link-color: rgb(32, 139, 196); -$color12: rgba(32, 139, 196, 0.2); //link color lighter - - -/* stylelint-disable-next-line selector-class-pattern */ -.ais-SearchBox { - width: 100%; - max-width: 100% !important; // was 300px -} - -.search-input { - border-radius: 2px; -} - -/* stylelint-disable-next-line selector-class-pattern */ -.ais-PoweredBy { - font-family: $gl-regular-font; - font-size: $body-font-size; - - svg { - display: inline; - } -} - -.hit-content { - border-top: 1px solid $color0; - padding: 1rem 0.5rem; - margin: 0; - font-size: 13px; - font-weight: 300; - width: 100%; // was 81% - position: relative; - color: $color10; - transition: 0.3s ease; - word-wrap: break-word; - - &:hover { - background-color: $color12; - - .hit-tag { - color: $link-color; - border-color: $color3; - } - } - - .lvl0 { - color: $gds-black; - } - - .lvl1 { - font-size: 18px !important; - } - - .lvl2 { - font-size: 0.875rem; - color: $color7; - } - - .hit-name { - font-weight: normal; - margin-top: 0; - } - - em { - font-style: normal; - background-color: $color3; - } - - p { - font-size: 13px; - } - - .hit-description { - margin: 1px 0 5px; - } - - .hit-tag { - display: block; - position: absolute; - padding: 3px; - border: 1px solid $color0; - border-top: 0; - border-bottom-left-radius: 2px; - border-bottom-right-radius: 2px; - right: 0; - top: 0; - width: 70px; - height: 25px; - font-size: 11px; - font-weight: 500; - text-align: center; - text-transform: uppercase; - color: $color6; - } - - .hit-text { - font-size: inherit; - color: $link-color; - } - - @media all and (max-width: $bp-sm) { - .hit-tag { - width: 60px; - height: 23px; - font-size: 10px; - } - - .lvl0 { - padding-top: 5px; - } - } -} - -#hits { - /* stylelint-disable-next-line selector-list-comma-newline-after */ - > h1, h2, h3, h4, h5, h6 { - border: 0; - } -} - -.algolia-docsearch-suggestion--highlight { - background-color: $color3; -} - -/* stylelint-disable selector-class-pattern */ -.ais-InfiniteHits { - .ais-InfiniteHits-item { - // https://github.com/algolia/instantsearch-specs/blob/v7.4.4/src/scss/themes/algolia.scss#L277-L305 - width: calc(100% - 1rem); - border: 0; - box-shadow: none; - } -} - -.ais-InfiniteHits-loadMore { - display: block; - width: 100%; - margin: 1rem 0.5rem; - - &:focus { - outline: none; - } -} - -.ais-Stats-text { - color: $body-color; -} - -.ais-RefinementList-item { - display: inline-block; - padding-right: 10px; -} - -.ais-RefinementList-label { - text-transform: uppercase; - color: $link-color; -} -/* stylelint-enable selector-class-pattern */ - -.search-results { - min-height: 50vh; -} diff --git a/content/frontend/search/components/search_page.vue b/content/frontend/search/components/search_page.vue new file mode 100644 index 00000000..621d2b9d --- /dev/null +++ b/content/frontend/search/components/search_page.vue @@ -0,0 +1,77 @@ +<script> +import algoliasearch from 'algoliasearch/lite'; +import 'instantsearch.css/themes/satellite-min.css'; +import { history as historyRouter } from 'instantsearch.js/es/lib/routers'; +import { singleIndex as singleIndexMapping } from 'instantsearch.js/es/lib/stateMappings'; +import { GlIcon } from '@gitlab/ui'; + +export default { + components: { + GlIcon, + }, + props: { + docsVersion: { + type: String, + required: true, + }, + }, + data() { + return { + searchClient: algoliasearch('3PNCFOU757', '89b85ffae982a7f1adeeed4a90bb0ab1'), + routing: { + router: historyRouter(), + stateMapping: singleIndexMapping('gitlab'), + }, + }; + }, +}; +</script> + +<template> + <ais-instant-search + :search-client="searchClient" + index-name="gitlab" + :routing="routing" + :stalled-search-delay="500" + class="gl-pb-8" + > + <h1 class="gl-mt-5!">Search</h1> + <ais-search-box + placeholder="Search GitLab Documentation" + show-loading-indicator + data-testid="docs-search" + /> + + <ais-state-results> + <template #default="{ results: { hits, query } }"> + <div class="gl-display-flex gl-align-items-bottom gl-mb-6"> + <ais-stats v-show="query.length > 0" class="gl-font-sm" /> + <ais-powered-by + :class-names="{ + 'ais-PoweredBy': 'gl-absolute gl-right-7 gl-mt-2', + 'ais-PoweredBy-link': 'no-attachment-icon gl-border-bottom-0!', + }" + /> + </div> + + <ais-infinite-hits v-if="query.length > 0 && hits.length > 0"> + <template #item="{ item }"> + <div> + <a :href="item.url" class="gl-font-lg">{{ item.hierarchy.lvl0 }}</a> + <p v-if="item.hierarchy.lvl2" class="gl-mt-2! gl-font-base!"> + <gl-icon name="chevron-right" :size="12" /> {{ item.hierarchy.lvl2 }} + </p> + </div> + </template> + </ais-infinite-hits> + + <div v-if="query.length > 0 && hits.length < 0"> + No results found for <em>{{ query }}</em + >. + </div> + </template> + </ais-state-results> + + <ais-configure :hits-per-page.camel="10" :facet-filters.camel="`version:` + docsVersion" /> + </ais-instant-search> +</template> diff --git a/content/frontend/search/docsearch.js b/content/frontend/search/docsearch.js index 0e12f398..e01ab696 100644 --- a/content/frontend/search/docsearch.js +++ b/content/frontend/search/docsearch.js @@ -1,11 +1,9 @@ import docsearch from '@docsearch/js'; import '@docsearch/css'; +import { getDocsVersion } from './search'; document.addEventListener('DOMContentLoaded', () => { - let version = 'main'; - if (document.querySelector('meta[name="docsearch:version"]').content.length > 0) { - version = document.querySelector('meta[name="docsearch:version"]').content; - } + const docsVersion = getDocsVersion(); // eslint-disable-next-line no-undef docsearch({ @@ -15,7 +13,7 @@ document.addEventListener('DOMContentLoaded', () => { appId: '3PNCFOU757', placeholder: 'Search the docs', searchParameters: { - facetFilters: [`version:${version}`], + facetFilters: [`version:${docsVersion}`], }, resultsFooterComponent({ state }) { return { diff --git a/content/frontend/search/index.js b/content/frontend/search/index.js deleted file mode 100644 index 1c658dc9..00000000 --- a/content/frontend/search/index.js +++ /dev/null @@ -1,74 +0,0 @@ -import instantsearch from 'instantsearch.js'; -import { singleIndex } from 'instantsearch.js/es/lib/stateMappings'; -import { - searchBox, - refinementList, - infiniteHits, - stats, - poweredBy, - configure, -} from 'instantsearch.js/es/widgets'; -import algoliasearch from 'algoliasearch'; - -document.addEventListener('DOMContentLoaded', () => { - const search = instantsearch({ - indexName: 'gitlab', - searchClient: algoliasearch('3PNCFOU757', '89b85ffae982a7f1adeeed4a90bb0ab1'), - algoliaOptions: { - // Filter by tags as described in https://github.com/algolia/docsearch-configs/blob/master/configs/gitlab.json - filters: 'tags:gitlab<score=3> OR tags:omnibus<score=2> OR tags:runner<score=1>', - }, - routing: { - stateMapping: singleIndex('gitlab'), - }, - searchFunction: (helper) => { - if (helper.state.query === '') { - return; - } - helper.search(); - }, - }); - - search.addWidgets([ - searchBox({ - container: '#searchbox', - placeholder: 'Search GitLab Documentation', - showReset: true, - showLoadingIndicator: true, - }), - - poweredBy({ - container: '#powered-by', - }), - - refinementList({ - container: '#refinement-list', - attribute: 'tags', - sortBy: ['name:asc', 'isRefined'], - templates: { - header: 'Refine your search:', - }, - }), - - infiniteHits({ - container: '#hits', - templates: { - item: document.getElementById('hit-template').innerHTML, - empty: 'We didn\'t find any results for the search <em>"{{query}}"</em>', - }, - escapeHits: true, - showMoreLabel: 'Load more results...', - }), - - stats({ - container: '#stats', - }), - - configure({ - // Number of results shown in the search dropdown - hitsPerPage: 10, - }), - ]); - - search.start(); -}); diff --git a/content/frontend/search/instantsearch.js b/content/frontend/search/instantsearch.js new file mode 100644 index 00000000..64bd9f9a --- /dev/null +++ b/content/frontend/search/instantsearch.js @@ -0,0 +1,35 @@ +import Vue from 'vue'; +import { + AisInstantSearch, + AisStateResults, + AisSearchBox, + AisStats, + AisPoweredBy, + AisInfiniteHits, + AisConfigure, +} from 'vue-instantsearch'; +import SearchPage from './components/search_page.vue'; +import { getDocsVersion } from './search'; + +Vue.component(AisInstantSearch.name, AisInstantSearch); +Vue.component(AisSearchBox.name, AisSearchBox); +Vue.component(AisStateResults.name, AisStateResults); +Vue.component(AisStats.name, AisStats); +Vue.component(AisPoweredBy.name, AisPoweredBy); +Vue.component(AisInfiniteHits.name, AisInfiniteHits); +Vue.component(AisConfigure.name, AisConfigure); + +const docsVersion = getDocsVersion(); + +document.addEventListener('DOMContentLoaded', () => { + return new Vue({ + el: '.js-instantsearch', + render(createElement) { + return createElement(SearchPage, { + props: { + docsVersion, + }, + }); + }, + }); +}); diff --git a/content/frontend/search/search.js b/content/frontend/search/search.js new file mode 100644 index 00000000..5ea8a87a --- /dev/null +++ b/content/frontend/search/search.js @@ -0,0 +1,11 @@ +/** + * Functions used by both DocSearch and InstantSearch. + */ + +export const getDocsVersion = () => { + let docsVersion = 'main'; + if (document.querySelector('meta[name="docsearch:version"]').content.length > 0) { + docsVersion = document.querySelector('meta[name="docsearch:version"]').content; + } + return docsVersion; +}; diff --git a/content/search/index.md b/content/search/index.md index 8869da3f..a4d484db 100644 --- a/content/search/index.md +++ b/content/search/index.md @@ -1,26 +1,4 @@ --- -title: Search through GitLab Documentation +title: Search GitLab Docs layout: instantsearch -feedback: nil --- -<header> - <div id="searchbox"></div> - <div id="powered-by"></div> -</header> -<main class="search-results"> - <div id="stats"></div> - <div id="refinement-list"></div> - <div id="hits"></div> - - <script type="text/html" id="hit-template"> - <a href="{{ url }}" class="hit"> - <div class="hit-content"> - <h3 class="hit-name lvl0">{{{_highlightResult.hierarchy.lvl0.value}}}</h3> - <h4 class="hit-description lvl1">{{{_highlightResult.hierarchy.lvl1.value}}}</h4> - <h5 class="hit-description lvl2">{{{_highlightResult.hierarchy.lvl2.value}}}</h5> - <div class="hit-text">{{{_highlightResult.content.value}}}</div> - <div class="hit-tag">{{ tags }}</div> - </div> - </a> - </script> -</main> diff --git a/jest.config.js b/jest.config.js index 929f6571..df2dfd9d 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,11 +1,10 @@ -const moduleNameMapper = { - '^~(/.*)$': '<rootDir>/content/frontend$1', -}; - module.exports = { testMatch: ['<rootDir>/spec/frontend/**/**/*_spec.js'], moduleFileExtensions: ['js', 'json', 'vue'], - moduleNameMapper, + moduleNameMapper: { + '^~(/.*)$': '<rootDir>/content/frontend$1', + '\\.(css|less|sass|scss)$': '<rootDir>/spec/frontend/__mocks__/style_mock.js', + }, cacheDirectory: '<rootDir>/tmp/cache/jest', restoreMocks: true, transform: { @@ -13,5 +12,7 @@ module.exports = { '^.+\\.vue$': '@vue/vue2-jest', '^.+\\.svg$': '@vue/vue2-jest', }, - transformIgnorePatterns: ['node_modules/(?!(@gitlab/(ui|svgs)|bootstrap-vue)/)'], + transformIgnorePatterns: [ + 'node_modules/(?!(@gitlab/(ui|svgs)|bootstrap-vue|vue-instantsearch|instantsearch.js)/)', + ], }; diff --git a/layouts/head.html b/layouts/head.html index faaf345c..9d101b64 100644 --- a/layouts/head.html +++ b/layouts/head.html @@ -22,6 +22,7 @@ <% else %> <meta name="docsearch:version" content="<%= ENV['CI_DEFAULT_BRANCH'] %>" /> <% end %> +<link crossorigin href="https://3PNCFOU757-dsn.algolia.net" rel="preconnect" /> <!-- Enable CSP headers --> <% unless ENV['DISABLE_CSP'] %> diff --git a/layouts/instantsearch-head.html b/layouts/instantsearch-head.html deleted file mode 100644 index d6af4e06..00000000 --- a/layouts/instantsearch-head.html +++ /dev/null @@ -1,4 +0,0 @@ -<!-- instantsearch --> -<link rel="stylesheet" href="<%= @items['/assets/stylesheets/instantsearch.*'].path %>"> -<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/instantsearch.css@7.4.5/themes/algolia-min.css"> -<!-- end of instantsearch --> diff --git a/layouts/instantsearch.html b/layouts/instantsearch.html index b9a349cf..514e7273 100644 --- a/layouts/instantsearch.html +++ b/layouts/instantsearch.html @@ -3,7 +3,7 @@ <head> <%= render '/head.*' %> <link rel="canonical" href="<%= @config[:base_url] %>/search/"> - <%= render '/instantsearch-head.*' %> + <link rel="stylesheet" href="/frontend/search/instantsearch.css"> </head> <body> <%= render '/gtm.*' %> @@ -11,17 +11,17 @@ <section class="gl-docs container pt-5"> <div class="row"> <div class="col-12 mt-5"> - <div class="main class pl-lg-4 instantsearch-input"> - <%= yield %> + <div class="main class pl-lg-4"> + <div class="js-instantsearch"></div> <%= render '/footer.*' %> </div> </div> </div> </section> - <script src="/frontend/shared/global_imports.js"></script> - <script type="application/javascript" src="<%= @items['/assets/javascripts/badges.*'].path %>"></script> + <script src="<%= @items['/frontend/shared/global_imports.*'].path %>"></script> <script src="<%= @items['/frontend/header/index.*'].path %>"></script> - <script src="<%= @items['/frontend/search/index.*'].path %>"></script> + <script src="<%= @items['/frontend/search/instantsearch.*'].path %>"></script> + <% if production? %> <%# Add analytics only in production %> <%= render '/analytics.*' %> diff --git a/package.json b/package.json index 0063c06c..68ba7040 100644 --- a/package.json +++ b/package.json @@ -45,14 +45,16 @@ "@gitlab/ui": "^41.5.0", "@popperjs/core": "^2.11.5", "@rollup/plugin-image": "^2.1.1", - "algoliasearch": "4.13.1", + "algoliasearch": "^4.13.1", "bootstrap": "^4.6.1", "compare-versions": "^4.1.3", "eslint-plugin-filenames": "^1.3.2", + "instantsearch.css": "^7.4.5", "instantsearch.js": "^4.41.0", "jquery": "^3.6.0", "pikaday": "^1.8.2", "rollup-plugin-import-css": "^3.0.3", - "vue": "^2.6.14" + "vue": "^2.6.14", + "vue-instantsearch": "^4.3.3" } } diff --git a/spec/frontend/__mocks__/style_mock.js b/spec/frontend/__mocks__/style_mock.js new file mode 100644 index 00000000..f053ebf7 --- /dev/null +++ b/spec/frontend/__mocks__/style_mock.js @@ -0,0 +1 @@ +module.exports = {}; diff --git a/spec/frontend/search/search_spec.js b/spec/frontend/search/search_spec.js new file mode 100644 index 00000000..e009fdf4 --- /dev/null +++ b/spec/frontend/search/search_spec.js @@ -0,0 +1,24 @@ +/** + * @jest-environment jsdom + */ + +import { mount } from '@vue/test-utils'; +import SearchPage from '../../../content/frontend/search/components/search_page.vue'; + +const propsData = { docsVersion: 'main' }; +const searchFormSelector = '[data-testid="docs-search"]'; + +describe('component: Search page', () => { + it('Search form renders', () => { + const wrapper = mount(SearchPage, { + propsData, + stubs: { + 'ais-instant-search': true, + 'ais-search-box': true, + 'ais-state-results': true, + 'ais-configure': true, + }, + }); + expect(wrapper.find(searchFormSelector).isVisible()).toBe(true); + }); +}); @@ -1860,7 +1860,7 @@ algoliasearch-helper@^3.8.2: dependencies: "@algolia/events" "^4.0.1" -algoliasearch@4.13.1, algoliasearch@^4.0.0: +algoliasearch@^4.0.0, algoliasearch@^4.13.1: version "4.13.1" resolved "https://registry.yarnpkg.com/algoliasearch/-/algoliasearch-4.13.1.tgz#54195c41c9e4bd13ed64982248cf49d4576974fe" integrity sha512-dtHUSE0caWTCE7liE1xaL+19AFf6kWEcyn76uhcitWpntqvicFHXKFoZe5JJcv9whQOTRM6+B8qJz6sFj+rDJA== @@ -3659,6 +3659,27 @@ ini@~2.0.0: resolved "https://registry.yarnpkg.com/ini/-/ini-2.0.0.tgz#e5fd556ecdd5726be978fa1001862eacb0a94bc5" integrity sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA== +instantsearch.css@^7.4.5: + version "7.4.5" + resolved "https://registry.yarnpkg.com/instantsearch.css/-/instantsearch.css-7.4.5.tgz#2a521aa634329bf1680f79adf87c79d67669ec8d" + integrity sha512-iIGBYjCokU93DDB8kbeztKtlu4qVEyTg1xvS6iSO1YvqRwkIZgf0tmsl/GytsLdZhuw8j4wEaeYsCzNbeJ/zEQ== + +instantsearch.js@^4.37.2: + version "4.41.1" + resolved "https://registry.yarnpkg.com/instantsearch.js/-/instantsearch.js-4.41.1.tgz#5d8947ab37769255931649dbe23d5ed25fc84970" + integrity sha512-uwl8voHcsvy+FJQPgT7Q4SCiwHGhtFnAFU8doWZZC1+o2KRPGHF6vtd+8c/UqXmFx9iHwGuJwQ5fve0G/zBjfA== + dependencies: + "@algolia/events" "^4.0.1" + "@types/google.maps" "^3.45.3" + "@types/hogan.js" "^3.0.0" + "@types/qs" "^6.5.3" + algoliasearch-helper "^3.8.2" + classnames "^2.2.5" + hogan.js "^3.0.2" + preact "^10.6.0" + qs "^6.5.1 < 6.10" + search-insights "^2.1.0" + instantsearch.js@^4.41.0: version "4.41.0" resolved "https://registry.yarnpkg.com/instantsearch.js/-/instantsearch.js-4.41.0.tgz#c879d7b892e3d9159a17a803e793e695aeb5431f" @@ -4793,6 +4814,11 @@ minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== +mitt@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mitt/-/mitt-2.1.0.tgz#f740577c23176c6205b121b2973514eade1b2230" + integrity sha512-ILj2TpLiysu2wkBbWjAmww7TkZb65aiQO+DkVdUTBpBXq+MHYiETENkKFMtsJZX1Lf4pe4QOrTSjIfUwN5lRdg== + mkdirp@0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.3.0.tgz#1bbf5ab1ba827af23575143490426455f481fe1e" @@ -6349,6 +6375,14 @@ vue-functional-data-merge@^3.1.0: resolved "https://registry.yarnpkg.com/vue-functional-data-merge/-/vue-functional-data-merge-3.1.0.tgz#08a7797583b7f35680587f8a1d51d729aa1dc657" integrity sha512-leT4kdJVQyeZNY1kmnS1xiUlQ9z1B/kdBFCILIjYYQDqZgLqCLa0UhjSSeRX6c3mUe6U5qYeM8LrEqkHJ1B4LA== +vue-instantsearch@^4.3.3: + version "4.3.3" + resolved "https://registry.yarnpkg.com/vue-instantsearch/-/vue-instantsearch-4.3.3.tgz#9cf5a28c34616aee17018efde4a7e440d3f7432c" + integrity sha512-iwaVS/EbFsCFmTU893FROwvMXYaYBriPzOni/5lINX5anL+f84YHvQuPa4UXkPNroc30O6ziarKd4Y0U+D2QhQ== + dependencies: + instantsearch.js "^4.37.2" + mitt "^2.1.0" + vue-runtime-helpers@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/vue-runtime-helpers/-/vue-runtime-helpers-1.1.2.tgz#446b7b820888ab0c5264d2c3a32468e72e4100f3" |