Skip to content

feat(filtered_search): Implement filtered search

Illya Klymov requested to merge filtered-search into master

This MR is for pajamasbuild part of the filtered search.

Target branch will be set to master when !981 (merged) will be closed

Related issue: gitlab-org/gitlab-services/design.gitlab.com#272 (closed)

Road signs through this description:

  • This behavior is different from current non-Vue implementation
  • Implementation details
  • Things to be addressed in follow-up requests

Glossary:

  • term - free-text value within filter (becomes input with suggestions when activated), used for creating other tokens
  • token - current tokens, consisting of "kind" and "value" used in existing filtered searches

Overview

This MR introduces multiple elements:

  • filtered_search component for token-agnostic filtering
  • filtered_search_term component for text input + creating other tokens via suggestions dropdown
  • filtered_search_binary_token component for implementing existing "two-parts" tokens
  • Suggestions module - helper components for suggestions implementation
    • filtered_search_suggestion_list - wrapper component, responsible for handling suggestions logic (moving to next/previous suggestion, highlighting suggestions, registering and unregistering suggestion elements)
    • filtered_search_suggestion - wrapper around gl-dropdown-item, which registers search suggestion in top-level suggestion_list
    • filtered_search_suggestions_control_mixin - helper mixin to abstract suggestions logic and reuse it for tokens implementation

filtered_search_suggestion theoretically can transparently wrap any element, but this requires abstract: true, which is an undocumented feature in Vue 2.x

suggestions module might be abstracted to a general autocomplete component (neither bootstrap nor bootstrap-vue do not provide one, but this is out of the scope of this MR)

Requirements

There are no design specs for filters for now, so these requirements were gathered based on existing filtered search implementations (used in issues):

General requirements

  • User can type free text in the search field. This input is used to filter possible filter suggestions of possible tokens.

old implementation allows to manually create a token by typing, for example, "author:" in the input. This also captures invalid tokens according to gitlab#19092 (closed) and is not implemented in this version - the only way to create token is suggestion selection

old implementation allows pasting string which will be converted to tokens. This behavior is buggy (see gitlab#18013) and is considered low priority. This could be added as follow-up request if needed

  • Creating token is possible either by pressing it in suggestion list or selecting it using arrow keys + clicking Enter
  • Some tokens (like "confidential: yes/no") should not appear in filters more than once, some tokens are ok and allowed to appear more than once (for example label token, allowing us to filter issues by more than one label simultaneously)
  • Component should be easily extended to support "not" filtering and other possible "complex" tokens in future
  • Any time there should be at least X px of extra space to the right (highlighted in pink), allowing user to add new tokens any time image
  • For existing tokens user should be able to edit token by clicking it
  • For existing tokens user should be able to delete token by clicking the close icon
  • When the user hits Backspace when existing token value is empty - it should be converted to term with the title of the current token

Space handling

Honestly, this is an ugliest part of "requirements" and I really feel UX needs to be revised in these parts. This section basically documenting how our current filter work, so don't kill the messenger

  • Tokens can't contain spaces, Space is used to proceed to new token
  • If token has unclosed quote " - space is allowed in token, till matching quote is found
  • Pressing Space at the beginning of any token has no effect and is ignored
  • Pressing Space in the middle of existing token value "completes" token with the value before space and creates new term with second word. This word is immediately activated
  • Pasting a string containing spaces applies same logic - each word will be converted to term

Public API

<gl-filtered-search> usage

Original intent is to allow people to use filtered tokens in the following way:

const tokens = [
  { type: 'label', icon: 'label', title: 'static:token', token: labelToken  },
  // ...
  { type: 'private', icon: 'rocket', title: 'dynamic:~token', token: privacyToken },
];

and in Vue template:

<gl-filtered-search v-model="value" :available-tokens="tokens" />

v-model of filtered search is updated in real-time. This allows us to dynamically calculate available tokens, so we can implement complex logic, for example:

  • ensure specific token is used no more than once
  • adds the possibility to add "mutually exclusive tokens" - for example, either first or second should be available

submit event provides slightly different normalized tokens structure 0 for example multiple terms in a row are merged, trailing empty term is removed.

Each token mentioned in token field inside available-tokens array is Vue component, which will be rendered inside filtered search. For now, we introduce two types of tokens:

  • term token - just a plain input with autocomplete, used for creating other tokens
  • binary-token token - current tokens, consisting of "kind" and "value" used in existing filtered searches

Tokens implementation

Each token is unaware of other tokens and is fully responsible for:

  • rendering it's unactive state
  • rendering input and focusing it when the token becomes active
  • rendering suggestions and navigating through them (with help of provided mixin, if needed)
  • handling any token-specific logic (for example label token might remove labels, already selected in filters from suggestions
  • requesting self-destruction or "replacement" (for example binary-token when it has empty value and Backspace is pressed replaces itself with term token with value of binary-token title)

Implementation nuances

Portal

🔵 🔴

We want each token to be solely responsible for its suggestions image

Token (yellow) wants to render suggestions (red). Unfortunately, tokens should be located inside the scrollable container (green) with overflow-x, which makes us unable to render it properly (theoretically, we can use fixed, but that creates even more problems). Bootstrap-vue also uses portals underneath (portals will be part of Vue3 API), so actually we're not adding something really new to our code.

So filtered-search provides <portal-target> allowing any its descendant to "push" content into suggestions. Name of this <portal-target> is published via context (see next section), aside with function alignSuggestions in order to position suggestions correctly

Theoretically, it's possible to use popper.js to implement this one. Unfortunately, positioning of popper.js elements relative to nodes inside scrollable containers is partially broken in 1.x and I feel like a huge overkill to introduce popper.js with complex logic just to align item on one axis.

Context usage

bootstrap-vue relies solely on CSS selectors and native browser focus + :focus to display focused state and implement keyboard navigation for b-dropdown. Unfortunately, our focus needs to remain inside the input, so we need to use a different approach. One approach is just to pass list items as data and make <suggestions-list> component to render them

<gl-filtered-search-suggestions :items="items"> 
  template for each item
</gl-filtered-search-suggestions> 

Problems of such approach:

  • requires hacks to display, for example, for separators between different suggestion groups
  • quickly bloats suggestions component for common use cases, for example some of the suggestions should always be available, some of them are loaded dynamically based on user input
  • requires hacks if certain list items require different styling (example: None, and Any in Assignee dropdown do not have avatars in existing solution)

Context API (provide/inject) is used instead. By using this API, we can nest specially crafted gl-filtered-search-suggestion any deep inside gl-filtered-search-suggestion-list. Since we're gathering available suggestions based on created/beforeDestroy hooks we can easily ensure that keyboard navigation will be up-to-date with token state

Known issues

  • a11y of this component is very bad. We need at least allow user either tab-jump from input to suggestions and select suggestion via keyboard or use aria-autocomplete in a proper way
Edited by 🤖 GitLab Bot 🤖

Merge request reports