182 lines
4.8 KiB
Vue
182 lines
4.8 KiB
Vue
|
<script>
|
||
|
import { debounce } from 'lodash';
|
||
|
import { GlDeprecatedButton, GlSearchBoxByType } from '@gitlab/ui';
|
||
|
import axios from '~/lib/utils/axios_utils';
|
||
|
import { __ } from '~/locale';
|
||
|
import { deprecatedCreateFlash as createFlash } from '~/flash';
|
||
|
|
||
|
/**
|
||
|
* Creates a searchable input for environments.
|
||
|
*
|
||
|
* When given a value, it will render it as selected value
|
||
|
* Otherwise it will render a placeholder for the search input.
|
||
|
* It will fetch the available environments on focus.
|
||
|
*
|
||
|
* When the user types, it will trigger an event to allow
|
||
|
* for API queries outside of the component.
|
||
|
*
|
||
|
* When results are returned, it renders a selectable
|
||
|
* list with the suggestions
|
||
|
*
|
||
|
* When no results are returned, it will render a
|
||
|
* button with a `Create` label. When clicked, it will
|
||
|
* emit an event to allow for the creation of a new
|
||
|
* record.
|
||
|
*
|
||
|
*/
|
||
|
|
||
|
export default {
|
||
|
name: 'EnvironmentsSearchableInput',
|
||
|
components: {
|
||
|
GlDeprecatedButton,
|
||
|
GlSearchBoxByType,
|
||
|
},
|
||
|
props: {
|
||
|
value: {
|
||
|
type: String,
|
||
|
required: false,
|
||
|
default: '',
|
||
|
},
|
||
|
placeholder: {
|
||
|
type: String,
|
||
|
required: false,
|
||
|
default: __('Search an environment spec'),
|
||
|
},
|
||
|
createButtonLabel: {
|
||
|
type: String,
|
||
|
required: false,
|
||
|
default: __('Create'),
|
||
|
},
|
||
|
disabled: {
|
||
|
type: Boolean,
|
||
|
default: false,
|
||
|
required: false,
|
||
|
},
|
||
|
},
|
||
|
inject: ['environmentsEndpoint'],
|
||
|
data() {
|
||
|
return {
|
||
|
environmentSearch: this.value,
|
||
|
results: [],
|
||
|
showSuggestions: false,
|
||
|
isLoading: false,
|
||
|
};
|
||
|
},
|
||
|
computed: {
|
||
|
/**
|
||
|
* Creates a label with the value of the filter
|
||
|
* @returns {String}
|
||
|
*/
|
||
|
composedCreateButtonLabel() {
|
||
|
return `${this.createButtonLabel} ${this.environmentSearch}`;
|
||
|
},
|
||
|
shouldRenderCreateButton() {
|
||
|
return !this.isLoading && !this.results.length;
|
||
|
},
|
||
|
},
|
||
|
methods: {
|
||
|
fetchEnvironments: debounce(function debouncedFetchEnvironments() {
|
||
|
this.isLoading = true;
|
||
|
this.openSuggestions();
|
||
|
axios
|
||
|
.get(this.environmentsEndpoint, { params: { query: this.environmentSearch } })
|
||
|
.then(({ data }) => {
|
||
|
this.results = data || [];
|
||
|
this.isLoading = false;
|
||
|
})
|
||
|
.catch(() => {
|
||
|
this.isLoading = false;
|
||
|
this.closeSuggestions();
|
||
|
createFlash(__('Something went wrong on our end. Please try again.'));
|
||
|
});
|
||
|
}, 250),
|
||
|
/**
|
||
|
* Opens the list of suggestions
|
||
|
*/
|
||
|
openSuggestions() {
|
||
|
this.showSuggestions = true;
|
||
|
},
|
||
|
/**
|
||
|
* Closes the list of suggestions and cleans the results
|
||
|
*/
|
||
|
closeSuggestions() {
|
||
|
this.showSuggestions = false;
|
||
|
this.environmentSearch = '';
|
||
|
},
|
||
|
/**
|
||
|
* On click, it will:
|
||
|
* 1. clear the input value
|
||
|
* 2. close the list of suggestions
|
||
|
* 3. emit an event
|
||
|
*/
|
||
|
clearInput() {
|
||
|
this.closeSuggestions();
|
||
|
this.$emit('clearInput');
|
||
|
},
|
||
|
/**
|
||
|
* When the user selects a value from the list of suggestions
|
||
|
*
|
||
|
* It emits an event with the selected value
|
||
|
* Clears the filter
|
||
|
* and closes the list of suggestions
|
||
|
*
|
||
|
* @param {String} selected
|
||
|
*/
|
||
|
selectEnvironment(selected) {
|
||
|
this.$emit('selectEnvironment', selected);
|
||
|
this.results = [];
|
||
|
this.closeSuggestions();
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* When the user clicks the create button
|
||
|
* it emits an event with the filter value
|
||
|
*/
|
||
|
createClicked() {
|
||
|
this.$emit('createClicked', this.environmentSearch);
|
||
|
this.closeSuggestions();
|
||
|
},
|
||
|
},
|
||
|
};
|
||
|
</script>
|
||
|
<template>
|
||
|
<div>
|
||
|
<div class="dropdown position-relative">
|
||
|
<gl-search-box-by-type
|
||
|
v-model.trim="environmentSearch"
|
||
|
class="js-env-search"
|
||
|
:aria-label="placeholder"
|
||
|
:placeholder="placeholder"
|
||
|
:disabled="disabled"
|
||
|
:is-loading="isLoading"
|
||
|
@focus="fetchEnvironments"
|
||
|
@keyup="fetchEnvironments"
|
||
|
/>
|
||
|
<div
|
||
|
v-if="showSuggestions"
|
||
|
class="dropdown-menu d-block dropdown-menu-selectable dropdown-menu-full-width"
|
||
|
>
|
||
|
<div class="dropdown-content">
|
||
|
<ul v-if="results.length">
|
||
|
<li v-for="(result, i) in results" :key="i">
|
||
|
<gl-deprecated-button class="btn-transparent" @click="selectEnvironment(result)">{{
|
||
|
result
|
||
|
}}</gl-deprecated-button>
|
||
|
</li>
|
||
|
</ul>
|
||
|
<div v-else-if="!results.length" class="text-secondary gl-p-3">
|
||
|
{{ __('No matching results') }}
|
||
|
</div>
|
||
|
<div v-if="shouldRenderCreateButton" class="dropdown-footer">
|
||
|
<gl-deprecated-button
|
||
|
class="js-create-button btn-blank dropdown-item"
|
||
|
@click="createClicked"
|
||
|
>{{ composedCreateButtonLabel }}</gl-deprecated-button
|
||
|
>
|
||
|
</div>
|
||
|
</div>
|
||
|
</div>
|
||
|
</div>
|
||
|
</div>
|
||
|
</template>
|