Commit b0020a4f authored by Andrew Fontaine's avatar Andrew Fontaine Committed by Natalia Tepluhina

Migrate MR Widget Deployments to GlDropdown

Filtered Search Dropdown is not used anywhere else either, so it can be
deleted.
parent 219327ac
<script>
import { GlLink } from '@gitlab/ui';
import FilteredSearchDropdown from '~/vue_shared/components/filtered_search_dropdown.vue';
import { GlButtonGroup, GlDropdown, GlDropdownItem, GlLink, GlSearchBoxByType } from '@gitlab/ui';
import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
import ReviewAppLink from '../review_app_link.vue';
export default {
name: 'DeploymentViewButton',
components: {
FilteredSearchDropdown,
GlButtonGroup,
GlDropdown,
GlDropdownItem,
GlLink,
GlSearchBoxByType,
ReviewAppLink,
VisualReviewAppLink: () =>
import('ee_component/vue_merge_request_widget/components/visual_review_app_link.vue'),
},
directives: {
autofocusonshow,
},
props: {
appButtonText: {
type: Object,
......@@ -37,6 +43,9 @@ export default {
}),
},
},
data() {
return { searchTerm: '' };
},
computed: {
deploymentExternalUrl() {
if (this.deployment.changes && this.deployment.changes.length === 1) {
......@@ -47,44 +56,52 @@ export default {
shouldRenderDropdown() {
return this.deployment.changes && this.deployment.changes.length > 1;
},
filteredChanges() {
return this.deployment?.changes?.filter(change => change.path.includes(this.searchTerm));
},
},
};
</script>
<template>
<span>
<filtered-search-dropdown
v-if="shouldRenderDropdown"
class="js-mr-wigdet-deployment-dropdown inline"
:items="deployment.changes"
:main-action-link="deploymentExternalUrl"
filter-key="path"
>
<template #mainAction="{ className }">
<review-app-link
:display="appButtonText"
:link="deploymentExternalUrl"
:css-class="`deploy-link js-deploy-url inline ${className}`"
<gl-button-group v-if="shouldRenderDropdown" size="small">
<review-app-link
:display="appButtonText"
:link="deploymentExternalUrl"
size="small"
css-class="deploy-link js-deploy-url inline"
/>
<gl-dropdown size="small" class="js-mr-wigdet-deployment-dropdown">
<gl-search-box-by-type
v-model.trim="searchTerm"
v-autofocusonshow
autofocus
class="gl-m-3"
/>
</template>
<template #result="{ result }">
<gl-link
:href="result.external_url"
target="_blank"
rel="noopener noreferrer nofollow"
class="js-deploy-url-menu-item menu-item"
<gl-dropdown-item
v-for="change in filteredChanges"
:key="change.path"
class="js-filtered-dropdown-result"
>
<strong class="str-truncated-100 gl-mb-0 d-block">{{ result.path }}</strong>
<p class="text-secondary str-truncated-100 gl-mb-0 d-block">{{ result.external_url }}</p>
</gl-link>
</template>
</filtered-search-dropdown>
<gl-link
:href="change.external_url"
target="_blank"
rel="noopener noreferrer nofollow"
class="js-deploy-url-menu-item menu-item"
>
<strong class="str-truncated-100 gl-mb-0 gl-display-block">{{ change.path }}</strong>
<p class="text-secondary str-truncated-100 gl-mb-0 d-block">
{{ change.external_url }}
</p>
</gl-link>
</gl-dropdown-item>
</gl-dropdown>
</gl-button-group>
<review-app-link
v-else
:display="appButtonText"
:link="deploymentExternalUrl"
size="small"
css-class="js-deploy-url deploy-link btn btn-default btn-sm inline"
/>
<visual-review-app-link
......
<script>
import { GlTooltipDirective, GlIcon } from '@gitlab/ui';
import { GlButton, GlTooltipDirective, GlIcon } from '@gitlab/ui';
export default {
components: {
GlButton,
GlIcon,
},
directives: {
......@@ -21,14 +22,20 @@ export default {
type: String,
required: true,
},
size: {
type: String,
required: false,
default: 'medium',
},
},
};
</script>
<template>
<a
<gl-button
v-gl-tooltip
:title="display.tooltip"
:href="link"
:size="size"
target="_blank"
rel="noopener noreferrer nofollow"
:class="cssClass"
......@@ -36,5 +43,5 @@ export default {
data-track-label="review_app"
>
{{ display.text }} <gl-icon class="fgray" name="external-link" />
</a>
</gl-button>
</template>
<script>
import $ from 'jquery';
import { GlDeprecatedButton, GlIcon } from '@gitlab/ui';
import { __ } from '~/locale';
/**
* Renders a split dropdown with
* an input that allows to search through the given
* array of options.
*
* When there are no results and `showCreateMode` is true
* it renders a create button with the value typed.
*/
export default {
name: 'FilteredSearchDropdown',
components: {
GlIcon,
GlDeprecatedButton,
},
props: {
title: {
type: String,
required: false,
default: '',
},
buttonType: {
required: false,
validator: value =>
['primary', 'default', 'secondary', 'success', 'info', 'warning', 'danger'].indexOf(
value,
) !== -1,
default: 'default',
},
size: {
required: false,
type: String,
default: 'sm',
},
items: {
type: Array,
required: true,
},
visibleItems: {
type: Number,
required: false,
default: 5,
},
filterKey: {
type: String,
required: true,
},
showCreateMode: {
type: Boolean,
required: false,
default: false,
},
createButtonText: {
type: String,
required: false,
default: __('Create'),
},
},
data() {
return {
filter: '',
};
},
computed: {
className() {
return `btn btn-${this.buttonType} btn-${this.size}`;
},
filteredResults() {
if (this.filter !== '') {
return this.items.filter(
item =>
item[this.filterKey] &&
item[this.filterKey].toLowerCase().includes(this.filter.toLowerCase()),
);
}
return this.items.slice(0, this.visibleItems);
},
computedCreateButtonText() {
return `${this.createButtonText} ${this.filter}`;
},
shouldRenderCreateButton() {
return this.showCreateMode && this.filteredResults.length === 0 && this.filter !== '';
},
},
mounted() {
/**
* Resets the filter every time the user closes the dropdown
*/
$(this.$el)
.on('shown.bs.dropdown', () => {
this.$nextTick(() => this.$refs.searchInput.focus());
})
.on('hidden.bs.dropdown', () => {
this.filter = '';
});
},
};
</script>
<template>
<div class="dropdown">
<div class="btn-group">
<slot name="mainAction" :class-name="className">
<button type="button" :class="className">{{ title }}</button>
</slot>
<button
type="button"
:class="className"
class="dropdown-toggle dropdown-toggle-split"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false"
:aria-label="__('Expand dropdown')"
>
<gl-icon name="angle-down" :size="12" />
</button>
<div class="dropdown-menu dropdown-menu-right">
<div class="dropdown-input">
<input
ref="searchInput"
v-model="filter"
type="search"
:placeholder="__('Filter')"
class="js-filtered-dropdown-input dropdown-input-field"
/>
<gl-icon class="dropdown-input-search" name="search" />
</div>
<div class="dropdown-content">
<ul>
<li v-for="(result, i) in filteredResults" :key="i" class="js-filtered-dropdown-result">
<slot name="result" :result="result">{{ result[filterKey] }}</slot>
</li>
</ul>
</div>
<div v-if="shouldRenderCreateButton" class="dropdown-footer">
<slot name="footer" :filter="filter">
<gl-deprecated-button
class="js-dropdown-create-button btn-transparent"
@click="$emit('createItem', filter)"
>{{ computedCreateButtonText }}</gl-deprecated-button
>
</slot>
</div>
</div>
</div>
</div>
</template>
---
title: Migrate MR Deployment Widget to GlDropdown
merge_request: 42004
author:
type: changed
......@@ -26,6 +26,7 @@ exports[`Environment Header has a failed pipeline matches the snapshot 1`] = `
cssclass="btn btn-default btn-sm"
display="[object Object]"
link="http://example.com"
size="medium"
/>
</div>
`;
......@@ -56,6 +57,7 @@ exports[`Environment Header has errors matches the snapshot 1`] = `
cssclass="btn btn-default btn-sm"
display="[object Object]"
link="http://example.com"
size="medium"
/>
</div>
`;
......@@ -83,7 +85,7 @@ exports[`Environment Header renders name and link to app matches the snapshot 1`
</div>
<a
class="btn btn-default btn-sm"
class="btn btn-default btn-md gl-button btn btn-default btn-sm"
data-track-event="open_review_app"
data-track-label="review_app"
href="http://example.com"
......@@ -91,16 +93,24 @@ exports[`Environment Header renders name and link to app matches the snapshot 1`
target="_blank"
title=""
>
View app
<svg
class="fgray gl-icon s16"
data-testid="external-link-icon"
<!---->
<!---->
<span
class="gl-button-text"
>
<use
href="#external-link"
/>
</svg>
View app
<svg
class="fgray gl-icon s16"
data-testid="external-link-icon"
>
<use
href="#external-link"
/>
</svg>
</span>
</a>
</div>
`;
......
......@@ -10357,9 +10357,6 @@ msgstr ""
msgid "Expand approvers"
msgstr ""
msgid "Expand dropdown"
msgstr ""
msgid "Expand file"
msgstr ""
......
......@@ -30,7 +30,7 @@ describe('review app link', () => {
});
it('renders provided cssClass as class attribute', () => {
expect(el.getAttribute('class')).toEqual(props.cssClass);
expect(el.getAttribute('class')).toContain(props.cssClass);
});
it('renders View app text', () => {
......
import Vue from 'vue';
import mountComponent from 'helpers/vue_mount_component_helper';
import component from '~/vue_shared/components/filtered_search_dropdown.vue';
describe('Filtered search dropdown', () => {
const Component = Vue.extend(component);
let vm;
afterEach(() => {
vm.$destroy();
});
describe('with an empty array of items', () => {
beforeEach(() => {
vm = mountComponent(Component, {
items: [],
filterKey: '',
});
});
it('renders empty list', () => {
expect(vm.$el.querySelectorAll('.js-filtered-dropdown-result').length).toEqual(0);
});
it('renders filter input', () => {
expect(vm.$el.querySelector('.js-filtered-dropdown-input')).not.toBeNull();
});
});
describe('when visible numbers is less than the items length', () => {
beforeEach(() => {
vm = mountComponent(Component, {
items: [{ title: 'One' }, { title: 'Two' }, { title: 'Three' }],
visibleItems: 2,
filterKey: 'title',
});
});
it('it renders only the maximum number provided', () => {
expect(vm.$el.querySelectorAll('.js-filtered-dropdown-result').length).toEqual(2);
});
});
describe('when visible number is bigger than the items length', () => {
beforeEach(() => {
vm = mountComponent(Component, {
items: [{ title: 'One' }, { title: 'Two' }, { title: 'Three' }],
filterKey: 'title',
});
});
it('it renders the full list of items the maximum number provided', () => {
expect(vm.$el.querySelectorAll('.js-filtered-dropdown-result').length).toEqual(3);
});
});
describe('while filtering', () => {
beforeEach(() => {
vm = mountComponent(Component, {
items: [
{ title: 'One' },
{ title: 'Two/three' },
{ title: 'Three four' },
{ title: 'Five' },
],
filterKey: 'title',
});
});
it('updates the results to match the typed value', done => {
vm.$el.querySelector('.js-filtered-dropdown-input').value = 'three';
vm.$el.querySelector('.js-filtered-dropdown-input').dispatchEvent(new Event('input'));
vm.$nextTick(() => {
expect(vm.$el.querySelectorAll('.js-filtered-dropdown-result').length).toEqual(2);
done();
});
});
describe('when no value matches the typed one', () => {
it('does not render any result', done => {
vm.$el.querySelector('.js-filtered-dropdown-input').value = 'six';
vm.$el.querySelector('.js-filtered-dropdown-input').dispatchEvent(new Event('input'));
vm.$nextTick(() => {
expect(vm.$el.querySelectorAll('.js-filtered-dropdown-result').length).toEqual(0);
done();
});
});
});
});
describe('with create mode enabled', () => {
describe('when there are no matches', () => {
beforeEach(() => {
vm = mountComponent(Component, {
items: [
{ title: 'One' },
{ title: 'Two/three' },
{ title: 'Three four' },
{ title: 'Five' },
],
filterKey: 'title',
showCreateMode: true,
});
vm.$el.querySelector('.js-filtered-dropdown-input').value = 'eleven';
vm.$el.querySelector('.js-filtered-dropdown-input').dispatchEvent(new Event('input'));
});
it('renders a create button', done => {
vm.$nextTick(() => {
expect(vm.$el.querySelector('.js-dropdown-create-button')).not.toBeNull();
done();
});
});
it('renders computed button text', done => {
vm.$nextTick(() => {
expect(vm.$el.querySelector('.js-dropdown-create-button').textContent.trim()).toEqual(
'Create eleven',
);
done();
});
});
describe('on click create button', () => {
it('emits createItem event with the filter', done => {
jest.spyOn(vm, '$emit').mockImplementation(() => {});
vm.$nextTick(() => {
vm.$el.querySelector('.js-dropdown-create-button').click();
expect(vm.$emit).toHaveBeenCalledWith('createItem', 'eleven');
done();
});
});
});
});
describe('when there are matches', () => {
beforeEach(() => {
vm = mountComponent(Component, {
items: [
{ title: 'One' },
{ title: 'Two/three' },
{ title: 'Three four' },
{ title: 'Five' },
],
filterKey: 'title',
showCreateMode: true,
});
vm.$el.querySelector('.js-filtered-dropdown-input').value = 'one';
vm.$el.querySelector('.js-filtered-dropdown-input').dispatchEvent(new Event('input'));
});
it('does not render a create button', done => {
vm.$nextTick(() => {
expect(vm.$el.querySelector('.js-dropdown-create-button')).toBeNull();
done();
});
});
});
});
describe('with create mode disabled', () => {
describe('when there are no matches', () => {
beforeEach(() => {
vm = mountComponent(Component, {
items: [
{ title: 'One' },
{ title: 'Two/three' },
{ title: 'Three four' },
{ title: 'Five' },
],
filterKey: 'title',
});
vm.$el.querySelector('.js-filtered-dropdown-input').value = 'eleven';
vm.$el.querySelector('.js-filtered-dropdown-input').dispatchEvent(new Event('input'));
});
it('does not render a create button', done => {
vm.$nextTick(() => {
expect(vm.$el.querySelector('.js-dropdown-create-button')).toBeNull();
done();
});
});
});
});
});
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment