Commit 0d6e50d5 authored by Paul Slaughter's avatar Paul Slaughter Committed by Phil Hughes

Create Web IDE MR and branch picker

parent 0e90f27f
...@@ -244,6 +244,18 @@ const Api = { ...@@ -244,6 +244,18 @@ const Api = {
}); });
}, },
branches(id, query = '', options = {}) {
const url = Api.buildUrl(this.createBranchPath).replace(':id', encodeURIComponent(id));
return axios.get(url, {
params: {
search: query,
per_page: 20,
...options,
},
});
},
createBranch(id, { ref, branch }) { createBranch(id, { ref, branch }) {
const url = Api.buildUrl(this.createBranchPath).replace(':id', encodeURIComponent(id)); const url = Api.buildUrl(this.createBranchPath).replace(':id', encodeURIComponent(id));
......
<script>
import Icon from '~/vue_shared/components/icon.vue';
import Timeago from '~/vue_shared/components/time_ago_tooltip.vue';
import router from '../../ide_router';
export default {
components: {
Icon,
Timeago,
},
props: {
item: {
type: Object,
required: true,
},
projectId: {
type: String,
required: true,
},
isActive: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
branchHref() {
return router.resolve(`/project/${this.projectId}/edit/${this.item.name}`).href;
},
},
};
</script>
<template>
<a
:href="branchHref"
class="btn-link d-flex align-items-center"
>
<span class="d-flex append-right-default ide-search-list-current-icon">
<icon
v-if="isActive"
:size="18"
name="mobile-issue-close"
/>
</span>
<span>
<strong>
{{ item.name }}
</strong>
<span
class="ide-merge-request-project-path d-block mt-1"
>
Updated
<timeago
:time="item.committedDate || ''"
/>
</span>
</span>
</a>
</template>
<script>
import { mapActions, mapState } from 'vuex';
import _ from 'underscore';
import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
import Icon from '~/vue_shared/components/icon.vue';
import Item from './item.vue';
export default {
components: {
LoadingIcon,
Item,
Icon,
},
data() {
return {
search: '',
};
},
computed: {
...mapState('branches', ['branches', 'isLoading']),
...mapState(['currentBranchId', 'currentProjectId']),
hasBranches() {
return this.branches.length !== 0;
},
hasNoSearchResults() {
return this.search !== '' && !this.hasBranches;
},
},
watch: {
isLoading: {
handler: 'focusSearch',
},
},
mounted() {
this.loadBranches();
},
methods: {
...mapActions('branches', ['fetchBranches']),
loadBranches() {
this.fetchBranches({ search: this.search });
},
searchBranches: _.debounce(function debounceSearch() {
this.loadBranches();
}, 250),
focusSearch() {
if (!this.isLoading) {
this.$nextTick(() => {
this.$refs.searchInput.focus();
});
}
},
isActiveBranch(item) {
return item.name === this.currentBranchId;
},
},
};
</script>
<template>
<div>
<div class="dropdown-input mt-3 pb-3 mb-0 border-bottom">
<div class="position-relative">
<input
ref="searchInput"
:placeholder="__('Search branches')"
v-model="search"
type="search"
class="form-control dropdown-input-field"
@input="searchBranches"
/>
<icon
:size="18"
name="search"
class="input-icon"
/>
</div>
</div>
<div class="dropdown-content ide-merge-requests-dropdown-content d-flex">
<loading-icon
v-if="isLoading"
class="mt-3 mb-3 align-self-center ml-auto mr-auto"
size="2"
/>
<ul
v-else
class="mb-3 w-100"
>
<template v-if="hasBranches">
<li
v-for="item in branches"
:key="item.name"
>
<item
:item="item"
:project-id="currentProjectId"
:is-active="isActiveBranch(item)"
/>
</li>
</template>
<li
v-else
class="ide-search-list-empty d-flex align-items-center justify-content-center"
>
<template v-if="hasNoSearchResults">
{{ __('No branches found') }}
</template>
</li>
</ul>
</div>
</div>
</template>
...@@ -41,7 +41,7 @@ export default { ...@@ -41,7 +41,7 @@ export default {
slot="header" slot="header"
> >
{{ __('Edit') }} {{ __('Edit') }}
<div class="ml-auto d-flex"> <div class="ide-tree-actions ml-auto d-flex">
<new-entry-button <new-entry-button
:label="__('New file')" :label="__('New file')"
:show-label="false" :show-label="false"
......
...@@ -3,14 +3,14 @@ import { mapActions, mapGetters, mapState } from 'vuex'; ...@@ -3,14 +3,14 @@ import { mapActions, mapGetters, mapState } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue'; import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
import RepoFile from './repo_file.vue'; import RepoFile from './repo_file.vue';
import NewDropdown from './new_dropdown/index.vue'; import NavDropdown from './nav_dropdown.vue';
export default { export default {
components: { components: {
Icon, Icon,
RepoFile, RepoFile,
SkeletonLoadingContainer, SkeletonLoadingContainer,
NewDropdown, NavDropdown,
}, },
props: { props: {
viewerType: { viewerType: {
...@@ -57,6 +57,7 @@ export default { ...@@ -57,6 +57,7 @@ export default {
:class="headerClass" :class="headerClass"
class="ide-tree-header" class="ide-tree-header"
> >
<nav-dropdown />
<slot name="header"></slot> <slot name="header"></slot>
</header> </header>
<div <div
......
<script>
import { mapGetters } from 'vuex';
import Tabs from '../../../vue_shared/components/tabs/tabs';
import Tab from '../../../vue_shared/components/tabs/tab.vue';
import List from './list.vue';
export default {
components: {
Tabs,
Tab,
List,
},
props: {
show: {
type: Boolean,
required: true,
},
},
computed: {
...mapGetters('mergeRequests', ['assignedData', 'createdData']),
createdMergeRequestLength() {
return this.createdData.mergeRequests.length;
},
assignedMergeRequestLength() {
return this.assignedData.mergeRequests.length;
},
},
};
</script>
<template>
<div class="dropdown-menu ide-merge-requests-dropdown p-0">
<tabs
v-if="show"
stop-propagation
>
<tab active>
<template slot="title">
{{ __('Created by me') }}
<span class="badge badge-pill">
{{ createdMergeRequestLength }}
</span>
</template>
<list
:empty-text="__('You have not created any merge requests')"
type="created"
/>
</tab>
<tab>
<template slot="title">
{{ __('Assigned to me') }}
<span class="badge badge-pill">
{{ assignedMergeRequestLength }}
</span>
</template>
<list
:empty-text="__('You do not have any assigned merge requests')"
type="assigned"
/>
</tab>
</tabs>
</div>
</template>
<script> <script>
import Icon from '../../../vue_shared/components/icon.vue'; import Icon from '../../../vue_shared/components/icon.vue';
import router from '../../ide_router';
export default { export default {
components: { components: {
...@@ -29,22 +30,21 @@ export default { ...@@ -29,22 +30,21 @@ export default {
pathWithID() { pathWithID() {
return `${this.item.projectPathWithNamespace}!${this.item.iid}`; return `${this.item.projectPathWithNamespace}!${this.item.iid}`;
}, },
}, mergeRequestHref() {
methods: { const path = `/project/${this.item.projectPathWithNamespace}/merge_requests/${this.item.iid}`;
clickItem() {
this.$emit('click', this.item); return router.resolve(path).href;
}, },
}, },
}; };
</script> </script>
<template> <template>
<button <a
type="button" :href="mergeRequestHref"
class="btn-link d-flex align-items-center" class="btn-link d-flex align-items-center"
@click="clickItem"
> >
<span class="d-flex append-right-default ide-merge-request-current-icon"> <span class="d-flex append-right-default ide-search-list-current-icon">
<icon <icon
v-if="isActive" v-if="isActive"
:size="18" :size="18"
...@@ -59,5 +59,5 @@ export default { ...@@ -59,5 +59,5 @@ export default {
{{ pathWithID }} {{ pathWithID }}
</span> </span>
</span> </span>
</button> </a>
</template> </template>
<script> <script>
import { mapActions, mapGetters, mapState } from 'vuex'; import { mapActions, mapState } from 'vuex';
import _ from 'underscore'; import _ from 'underscore';
import LoadingIcon from '../../../vue_shared/components/loading_icon.vue'; import { __ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
import Item from './item.vue'; import Item from './item.vue';
import TokenedInput from '../shared/tokened_input.vue';
const SEARCH_TYPES = [
{ type: 'created', label: __('Created by me') },
{ type: 'assigned', label: __('Assigned to me') },
];
export default { export default {
components: { components: {
LoadingIcon, LoadingIcon,
TokenedInput,
Item, Item,
}, Icon,
props: {
type: {
type: String,
required: true,
},
emptyText: {
type: String,
required: true,
},
}, },
data() { data() {
return { return {
search: '', search: '',
currentSearchType: null,
hasSearchFocus: false,
}; };
}, },
computed: { computed: {
...mapGetters('mergeRequests', ['getData']), ...mapState('mergeRequests', ['mergeRequests', 'isLoading']),
...mapState(['currentMergeRequestId', 'currentProjectId']), ...mapState(['currentMergeRequestId', 'currentProjectId']),
data() {
return this.getData(this.type);
},
isLoading() {
return this.data.isLoading;
},
mergeRequests() {
return this.data.mergeRequests;
},
hasMergeRequests() { hasMergeRequests() {
return this.mergeRequests.length !== 0; return this.mergeRequests.length !== 0;
}, },
hasNoSearchResults() { hasNoSearchResults() {
return this.search !== '' && !this.hasMergeRequests; return this.search !== '' && !this.hasMergeRequests;
}, },
showSearchTypes() {
return this.hasSearchFocus && !this.search && !this.currentSearchType;
},
type() {
return this.currentSearchType
? this.currentSearchType.type
: '';
},
searchTokens() {
return this.currentSearchType
? [this.currentSearchType]
: [];
},
}, },
watch: { watch: {
isLoading: { search() {
handler: 'focusSearch', // When the search is updated, let's turn off this flag to hide the search types
this.hasSearchFocus = false;
}, },
}, },
mounted() { mounted() {
this.loadMergeRequests(); this.loadMergeRequests();
}, },
methods: { methods: {
...mapActions('mergeRequests', ['fetchMergeRequests', 'openMergeRequest']), ...mapActions('mergeRequests', ['fetchMergeRequests']),
loadMergeRequests() { loadMergeRequests() {
this.fetchMergeRequests({ type: this.type, search: this.search }); this.fetchMergeRequests({ type: this.type, search: this.search });
}, },
viewMergeRequest(item) {
this.openMergeRequest({
projectPath: item.projectPathWithNamespace,
id: item.iid,
});
},
searchMergeRequests: _.debounce(function debounceSearch() { searchMergeRequests: _.debounce(function debounceSearch() {
this.loadMergeRequests(); this.loadMergeRequests();
}, 250), }, 250),
focusSearch() { onSearchFocus() {
if (!this.isLoading) { this.hasSearchFocus = true;
this.$nextTick(() => { },
this.$refs.searchInput.focus(); setSearchType(searchType) {
}); this.currentSearchType = searchType;
} this.loadMergeRequests();
}, },
}, },
searchTypes: SEARCH_TYPES,
}; };
</script> </script>
<template> <template>
<div> <div>
<div class="dropdown-input mt-3 pb-3 mb-0 border-bottom"> <div class="dropdown-input mt-3 pb-3 mb-0 border-bottom">
<input <div class="position-relative">
ref="searchInput" <tokened-input
:placeholder="__('Search merge requests')" v-model="search"
v-model="search" :tokens="searchTokens"
type="search" :placeholder="__('Search merge requests')"
class="dropdown-input-field" @focus="onSearchFocus"
@input="searchMergeRequests" @input="searchMergeRequests"
/> @removeToken="setSearchType(null)"
<i />
aria-hidden="true" <icon
class="fa fa-search dropdown-input-search" :size="18"
></i> name="search"
class="input-icon"
/>
</div>
</div> </div>
<div class="dropdown-content ide-merge-requests-dropdown-content d-flex"> <div class="dropdown-content ide-merge-requests-dropdown-content d-flex">
<loading-icon <loading-icon
...@@ -98,35 +103,52 @@ export default { ...@@ -98,35 +103,52 @@ export default {
class="mt-3 mb-3 align-self-center ml-auto mr-auto" class="mt-3 mb-3 align-self-center ml-auto mr-auto"
size="2" size="2"
/> />
<ul <template v-else>
v-else <ul
class="mb-3 w-100" class="mb-3 w-100"
>
<template v-if="hasMergeRequests">
<li
v-for="item in mergeRequests"
:key="item.id"
>
<item
:item="item"
:current-id="currentMergeRequestId"
:current-project-id="currentProjectId"
@click="viewMergeRequest"
/>
</li>
</template>
<li
v-else
class="ide-merge-requests-empty d-flex align-items-center justify-content-center"
> >
<template v-if="hasNoSearchResults"> <template v-if="showSearchTypes">
{{ __('No merge requests found') }} <li
v-for="searchType in $options.searchTypes"
:key="searchType.type"
>
<button
type="button"
class="btn-link d-flex align-items-center"
@click.stop="setSearchType(searchType)"
>
<span class="d-flex append-right-default ide-search-list-current-icon">
<icon
:size="18"
name="search"
/>
</span>
<span>
{{ searchType.label }}
</span>
</button>
</li>
</template> </template>
<template v-else> <template v-else-if="hasMergeRequests">
{{ emptyText }} <li
v-for="item in mergeRequests"
:key="item.id"
>
<item
:item="item"
:current-id="currentMergeRequestId"
:current-project-id="currentProjectId"
/>
</li>
</template> </template>
</li> <li
</ul> v-else
class="ide-search-list-empty d-flex align-items-center justify-content-center"
>
{{ __('No merge requests found') }}
</li>
</ul>
</template>
</div> </div>
</div> </div>
</template> </template>
<script>
import $ from 'jquery';
import Icon from '~/vue_shared/components/icon.vue';
import NavForm from './nav_form.vue';
import NavDropdownButton from './nav_dropdown_button.vue';
export default {
components: {
Icon,
NavDropdownButton,
NavForm,
},
data() {
return {
isVisibleDropdown: false,
};
},
mounted() {
this.addDropdownListeners();
},
beforeDestroy() {
this.removeDropdownListeners();
},
methods: {
addDropdownListeners() {
$(this.$refs.dropdown)
.on('show.bs.dropdown', () => this.showDropdown())
.on('hide.bs.dropdown', () => this.hideDropdown());
},
removeDropdownListeners() {
$(this.$refs.dropdown)
.off('show.bs.dropdown')
.off('hide.bs.dropdown');
},
showDropdown() {
this.isVisibleDropdown = true;
},
hideDropdown() {
this.isVisibleDropdown = false;
},
},
};
</script>
<template>
<div
ref="dropdown"
class="btn-group ide-nav-dropdown dropdown"
>
<nav-dropdown-button />
<div
class="dropdown-menu dropdown-menu-left p-0"
>
<nav-form
v-if="isVisibleDropdown"
/>
</div>
</div>
</template>
<script>
import { mapState } from 'vuex';
import DropdownButton from '~/vue_shared/components/dropdown/dropdown_button.vue';
import Icon from '~/vue_shared/components/icon.vue';
const EMPTY_LABEL = '-';
export default {
components: {
Icon,
DropdownButton,
},
computed: {
...mapState(['currentBranchId', 'currentMergeRequestId']),
mergeRequestLabel() {
return this.currentMergeRequestId
? `!${this.currentMergeRequestId}`
: EMPTY_LABEL;
},
branchLabel() {
return this.currentBranchId || EMPTY_LABEL;
},
},
};
</script>
<template>
<dropdown-button>
<span
class="row"
>
<span
class="col-7 text-truncate"
>
<icon
:size="16"
:aria-label="__('Current Branch')"
name="branch"
/>
{{ branchLabel }}
</span>
<span
class="col-5 pl-0 text-truncate"
>
<icon
:size="16"
:aria-label="__('Merge Request')"
name="merge-request"
/>
{{ mergeRequestLabel }}
</span>
</span>
</dropdown-button>
</template>
<script>
import Tabs from '~/vue_shared/components/tabs/tabs';
import Tab from '~/vue_shared/components/tabs/tab.vue';
import BranchesSearchList from './branches/search_list.vue';
import MergeRequestSearchList from './merge_requests/list.vue';
export default {
components: {
Tabs,
Tab,
BranchesSearchList,
MergeRequestSearchList,
},
};
</script>
<template>
<div
class="ide-nav-form p-0"
>
<tabs
stop-propagation
>
<tab
active
>
<template slot="title">
{{ __('Merge Requests') }}
</template>
<merge-request-search-list />
</tab>
<tab>
<template slot="title">
{{ __('Branches') }}
</template>
<branches-search-list />
</tab>
</tabs>
</div>
</template>
<script>
import { __ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
export default {
components: {
Icon,
},
props: {
placeholder: {
type: String,
required: false,
default: __('Search'),
},
tokens: {
type: Array,
required: false,
default: () => [],
},
value: {
type: String,
required: false,
default: '',
},
},
data() {
return {
backspaceCount: 0,
};
},
computed: {
placeholderText() {
return this.tokens.length
? ''
: this.placeholder;
},
},
watch: {
tokens() {
this.$refs.input.focus();
},
},
methods: {
onFocus() {
this.$emit('focus');
},
onBlur() {
this.$emit('blur');
},
onInput(evt) {
this.$emit('input', evt.target.value);
},
onBackspace() {
if (!this.value && this.tokens.length) {
this.backspaceCount += 1;
} else {
this.backspaceCount = 0;
return;
}
if (this.backspaceCount > 1) {
this.removeToken(this.tokens[this.tokens.length - 1]);
this.backspaceCount = 0;
}
},
removeToken(token) {
this.$emit('removeToken', token);
},
},
};
</script>
<template>
<div class="filtered-search-wrapper">
<div class="filtered-search-box">
<div class="tokens-container list-unstyled">
<div
v-for="token in tokens"
:key="token.label"
class="filtered-search-token"
>
<button
class="selectable btn-blank"
type="button"
@click.stop="removeToken(token)"
@keyup.delete="removeToken(token)"
>
<div
class="value-container rounded"
>
<div
class="value"
>{{ token.label }}</div>
<div
class="remove-token inverted"
>
<icon
:size="10"
name="close"
/>
</div>
</div>
</button>
</div>
<div class="input-token">
<input
ref="input"
:placeholder="placeholderText"
:value="value"
type="search"
class="form-control filtered-search"
@input="onInput"
@focus="onFocus"
@blur="onBlur"
@keyup.delete="onBackspace"
/>
</div>
</div>
</div>
</div>
</template>
...@@ -7,6 +7,7 @@ import mutations from './mutations'; ...@@ -7,6 +7,7 @@ import mutations from './mutations';
import commitModule from './modules/commit'; import commitModule from './modules/commit';
import pipelines from './modules/pipelines'; import pipelines from './modules/pipelines';
import mergeRequests from './modules/merge_requests'; import mergeRequests from './modules/merge_requests';
import branches from './modules/branches';
Vue.use(Vuex); Vue.use(Vuex);
...@@ -20,6 +21,7 @@ export const createStore = () => ...@@ -20,6 +21,7 @@ export const createStore = () =>
commit: commitModule, commit: commitModule,
pipelines, pipelines,
mergeRequests, mergeRequests,
branches,
}, },
}); });
......
import { __ } from '~/locale';
import Api from '~/api';
import * as types from './mutation_types';
export const requestBranches = ({ commit }) => commit(types.REQUEST_BRANCHES);
export const receiveBranchesError = ({ commit, dispatch }, { search }) => {
dispatch(
'setErrorMessage',
{
text: __('Error loading branches.'),
action: payload =>
dispatch('fetchBranches', payload).then(() =>
dispatch('setErrorMessage', null, { root: true }),
),
actionText: __('Please try again'),
actionPayload: { search },
},
{ root: true },
);
commit(types.RECEIVE_BRANCHES_ERROR);
};
export const receiveBranchesSuccess = ({ commit }, data) =>
commit(types.RECEIVE_BRANCHES_SUCCESS, data);
export const fetchBranches = ({ dispatch, rootGetters }, { search = '' }) => {
dispatch('requestBranches');
dispatch('resetBranches');
return Api.branches(rootGetters.currentProject.id, search, { sort: 'updated_desc' })
.then(({ data }) => dispatch('receiveBranchesSuccess', data))
.catch(() => dispatch('receiveBranchesError', { search }));
};
export const resetBranches = ({ commit }) => commit(types.RESET_BRANCHES);
export const openBranch = ({ rootState, dispatch }, id) =>
dispatch('goToRoute', `/project/${rootState.currentProjectId}/edit/${id}`, { root: true });
export default () => {};
import state from './state';
import * as actions from './actions';
import mutations from './mutations';
export default {
namespaced: true,
state: state(),
actions,
mutations,
};
export const REQUEST_BRANCHES = 'REQUEST_BRANCHES';
export const RECEIVE_BRANCHES_ERROR = 'RECEIVE_BRANCHES_ERROR';
export const RECEIVE_BRANCHES_SUCCESS = 'RECEIVE_BRANCHES_SUCCESS';
export const RESET_BRANCHES = 'RESET_BRANCHES';
/* eslint-disable no-param-reassign */
import * as types from './mutation_types';
export default {
[types.REQUEST_BRANCHES](state) {
state.isLoading = true;
},
[types.RECEIVE_BRANCHES_ERROR](state) {
state.isLoading = false;
},
[types.RECEIVE_BRANCHES_SUCCESS](state, data) {
state.isLoading = false;
state.branches = data.map(branch => ({
name: branch.name,
committedDate: branch.commit.committed_date,
}));
},
[types.RESET_BRANCHES](state) {
state.branches = [];
},
};
export default () => ({
isLoading: false,
branches: [],
});
import { __ } from '../../../../locale'; import { __ } from '../../../../locale';
import Api from '../../../../api'; import Api from '../../../../api';
import router from '../../../ide_router';
import { scopes } from './constants'; import { scopes } from './constants';
import * as types from './mutation_types'; import * as types from './mutation_types';
import * as rootTypes from '../../mutation_types';
export const requestMergeRequests = ({ commit }, type) => export const requestMergeRequests = ({ commit }) =>
commit(types.REQUEST_MERGE_REQUESTS, type); commit(types.REQUEST_MERGE_REQUESTS);
export const receiveMergeRequestsError = ({ commit, dispatch }, { type, search }) => { export const receiveMergeRequestsError = ({ commit, dispatch }, { type, search }) => {
dispatch( dispatch(
'setErrorMessage', 'setErrorMessage',
...@@ -21,39 +19,22 @@ export const receiveMergeRequestsError = ({ commit, dispatch }, { type, search } ...@@ -21,39 +19,22 @@ export const receiveMergeRequestsError = ({ commit, dispatch }, { type, search }
}, },
{ root: true }, { root: true },
); );
commit(types.RECEIVE_MERGE_REQUESTS_ERROR, type); commit(types.RECEIVE_MERGE_REQUESTS_ERROR);
}; };
export const receiveMergeRequestsSuccess = ({ commit }, { type, data }) => export const receiveMergeRequestsSuccess = ({ commit }, data) =>
commit(types.RECEIVE_MERGE_REQUESTS_SUCCESS, { type, data }); commit(types.RECEIVE_MERGE_REQUESTS_SUCCESS, data);
export const fetchMergeRequests = ({ dispatch, state: { state } }, { type, search = '' }) => { export const fetchMergeRequests = ({ dispatch, state: { state } }, { type, search = '' }) => {
const scope = scopes[type]; dispatch('requestMergeRequests');
dispatch('requestMergeRequests', type); dispatch('resetMergeRequests');
dispatch('resetMergeRequests', type);
const scope = type ? scopes[type] : 'all';
return Api.mergeRequests({ scope, state, search }) return Api.mergeRequests({ scope, state, search })
.then(({ data }) => dispatch('receiveMergeRequestsSuccess', { type, data })) .then(({ data }) => dispatch('receiveMergeRequestsSuccess', data))
.catch(() => dispatch('receiveMergeRequestsError', { type, search })); .catch(() => dispatch('receiveMergeRequestsError', { type, search }));
}; };
export const resetMergeRequests = ({ commit }, type) => commit(types.RESET_MERGE_REQUESTS, type); export const resetMergeRequests = ({ commit }) => commit(types.RESET_MERGE_REQUESTS);
export const openMergeRequest = ({ commit, dispatch }, { projectPath, id }) => {
commit(rootTypes.CLEAR_PROJECTS, null, { root: true });
commit(rootTypes.SET_CURRENT_MERGE_REQUEST, `${id}`, { root: true });
commit(rootTypes.RESET_OPEN_FILES, null, { root: true });
dispatch('setCurrentBranchId', '', { root: true });
dispatch('pipelines/stopPipelinePolling', null, { root: true })
.then(() => {
dispatch('pipelines/resetLatestPipeline', null, { root: true });
dispatch('pipelines/clearEtagPoll', null, { root: true });
})
.catch(e => {
throw e;
});
dispatch('setRightPane', null, { root: true });
router.push(`/project/${projectPath}/merge_requests/${id}`);
};
export default () => {}; export default () => {};
export const getData = state => type => state[type];
export const assignedData = state => state.assigned;
export const createdData = state => state.created;
import state from './state'; import state from './state';
import * as actions from './actions'; import * as actions from './actions';
import * as getters from './getters';
import mutations from './mutations'; import mutations from './mutations';
export default { export default {
...@@ -8,5 +7,4 @@ export default { ...@@ -8,5 +7,4 @@ export default {
state: state(), state: state(),
actions, actions,
mutations, mutations,
getters,
}; };
...@@ -2,15 +2,15 @@ ...@@ -2,15 +2,15 @@
import * as types from './mutation_types'; import * as types from './mutation_types';
export default { export default {
[types.REQUEST_MERGE_REQUESTS](state, type) { [types.REQUEST_MERGE_REQUESTS](state) {
state[type].isLoading = true; state.isLoading = true;
}, },
[types.RECEIVE_MERGE_REQUESTS_ERROR](state, type) { [types.RECEIVE_MERGE_REQUESTS_ERROR](state) {
state[type].isLoading = false; state.isLoading = false;
}, },
[types.RECEIVE_MERGE_REQUESTS_SUCCESS](state, { type, data }) { [types.RECEIVE_MERGE_REQUESTS_SUCCESS](state, data) {
state[type].isLoading = false; state.isLoading = false;
state[type].mergeRequests = data.map(mergeRequest => ({ state.mergeRequests = data.map(mergeRequest => ({
id: mergeRequest.id, id: mergeRequest.id,
iid: mergeRequest.iid, iid: mergeRequest.iid,
title: mergeRequest.title, title: mergeRequest.title,
...@@ -20,7 +20,7 @@ export default { ...@@ -20,7 +20,7 @@ export default {
.replace(`/merge_requests/${mergeRequest.iid}`, ''), .replace(`/merge_requests/${mergeRequest.iid}`, ''),
})); }));
}, },
[types.RESET_MERGE_REQUESTS](state, type) { [types.RESET_MERGE_REQUESTS](state) {
state[type].mergeRequests = []; state.mergeRequests = [];
}, },
}; };
import { states } from './constants'; import { states } from './constants';
export default () => ({ export default () => ({
created: { isLoading: false,
isLoading: false, mergeRequests: [],
mergeRequests: [],
},
assigned: {
isLoading: false,
mergeRequests: [],
},
state: states.opened, state: states.opened,
}); });
...@@ -38,9 +38,17 @@ export default { ...@@ -38,9 +38,17 @@ export default {
v-show="isLoading" v-show="isLoading"
:inline="true" :inline="true"
/> />
<span class="dropdown-toggle-text"> <template>
{{ toggleText }} <slot
</span> v-if="$slots.default"
></slot>
<span
v-else
class="dropdown-toggle-text"
>
{{ toggleText }}
</span>
</template>
<span <span
v-show="!isLoading" v-show="!isLoading"
class="dropdown-toggle-icon" class="dropdown-toggle-icon"
......
<script> <script>
// only allow classes in images.scss e.g. s12 // only allow classes in images.scss e.g. s12
const validSizes = [8, 12, 16, 18, 24, 32, 48, 72]; const validSizes = [8, 10, 12, 16, 18, 24, 32, 48, 72];
let iconValidator = () => true; let iconValidator = () => true;
/* /*
......
...@@ -571,7 +571,8 @@ ...@@ -571,7 +571,8 @@
margin-bottom: 10px; margin-bottom: 10px;
padding: 0 10px; padding: 0 10px;
.fa { .fa,
.input-icon {
position: absolute; position: absolute;
top: 10px; top: 10px;
right: 20px; right: 20px;
......
...@@ -39,7 +39,7 @@ ...@@ -39,7 +39,7 @@
svg { svg {
fill: currentColor; fill: currentColor;
$svg-sizes: 8 12 16 18 24 32 48 72; $svg-sizes: 8 10 12 16 18 24 32 48 72;
@each $svg-size in $svg-sizes { @each $svg-size in $svg-sizes {
&.s#{$svg-size} { &.s#{$svg-size} {
@include svg-size(#{$svg-size}px); @include svg-size(#{$svg-size}px);
......
@import 'framework/variables'; @import 'framework/variables';
@import 'framework/mixins'; @import 'framework/mixins';
$search-list-icon-width: 18px;
$ide-activity-bar-width: 60px; $ide-activity-bar-width: 60px;
$ide-context-header-padding: 10px; $ide-context-header-padding: 10px;
$ide-project-avatar-end: $ide-context-header-padding + 48px; $ide-project-avatar-end: $ide-context-header-padding + 48px;
...@@ -49,7 +50,7 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding; ...@@ -49,7 +50,7 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
flex: 1; flex: 1;
overflow: hidden; min-height: 0;
.file { .file {
height: 32px; height: 32px;
...@@ -541,11 +542,11 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding; ...@@ -541,11 +542,11 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding;
display: flex; display: flex;
flex: 1; flex: 1;
flex-direction: column; flex-direction: column;
overflow: hidden;
background-color: $white-light; background-color: $white-light;
border-left: 1px solid $white-dark; border-left: 1px solid $white-dark;
border-top: 1px solid $white-dark; border-top: 1px solid $white-dark;
border-top-left-radius: $border-radius-small; border-top-left-radius: $border-radius-small;
min-height: 0;
} }
} }
...@@ -1057,6 +1058,7 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding; ...@@ -1057,6 +1058,7 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding;
flex: 0 0 auto; flex: 0 0 auto;
display: flex; display: flex;
align-items: center; align-items: center;
flex-wrap: wrap;
padding: 12px 0; padding: 12px 0;
margin-left: $ide-tree-padding; margin-left: $ide-tree-padding;
margin-right: $ide-tree-padding; margin-right: $ide-tree-padding;
...@@ -1066,6 +1068,32 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding; ...@@ -1066,6 +1068,32 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding;
margin-left: auto; margin-left: auto;
} }
.ide-nav-dropdown {
width: 100%;
margin-bottom: 12px;
.dropdown-menu {
width: 385px;
max-height: initial;
}
.dropdown-menu-toggle {
svg {
vertical-align: middle;
}
&:hover {
background-color: $white-normal;
}
}
&.show {
.dropdown-menu-toggle {
background-color: $white-dark;
}
}
}
button { button {
color: $gl-text-color; color: $gl-text-color;
} }
...@@ -1181,7 +1209,7 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding; ...@@ -1181,7 +1209,7 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding;
} }
.ide-context-body { .ide-context-body {
overflow: hidden; min-height: 0;
} }
.ide-sidebar-project-title { .ide-sidebar-project-title {
...@@ -1331,7 +1359,7 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding; ...@@ -1331,7 +1359,7 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding;
min-height: 60px; min-height: 60px;
} }
.ide-merge-requests-dropdown { .ide-nav-form {
.nav-links li { .nav-links li {
width: 50%; width: 50%;
padding-left: 0; padding-left: 0;
...@@ -1350,22 +1378,36 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding; ...@@ -1350,22 +1378,36 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding;
padding-left: $gl-padding; padding-left: $gl-padding;
padding-right: $gl-padding; padding-right: $gl-padding;
.fa { .input-icon {
right: 26px; right: auto;
left: 10px;
top: 50%;
transform: translateY(-50%);
} }
} }
.dropdown-input-field {
padding-left: $search-list-icon-width + $gl-padding;
padding-top: 2px;
padding-bottom: 2px;
}
.tokens-container {
padding-left: $search-list-icon-width + $gl-padding;
overflow-x: hidden;
}
.btn-link { .btn-link {
padding-top: $gl-padding; padding-top: $gl-padding;
padding-bottom: $gl-padding; padding-bottom: $gl-padding;
} }
} }
.ide-merge-request-current-icon { .ide-search-list-current-icon {
min-width: 18px; min-width: $search-list-icon-width;
} }
.ide-merge-requests-empty { .ide-search-list-empty {
height: 230px; height: 230px;
} }
......
---
title: Create branch and MR picker for Web IDE
merge_request: 20978
author:
type: changed
...@@ -59,9 +59,18 @@ left. ...@@ -59,9 +59,18 @@ left.
> [Introduced in](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/19318) [GitLab Core][ce] 11.0. > [Introduced in](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/19318) [GitLab Core][ce] 11.0.
Switching between your authored and assigned merge requests can be done without Switching between your authored and assigned merge requests can be done without
leaving the Web IDE. Click the project name in the top left to open a list of leaving the Web IDE. Click the dropdown in the top of the sidebar to open a list
merge requests. You will need to commit or discard all your changes before of merge requests. You will need to commit or discard all your changes before
switching to a different merge request. switching to a different merge request.
## Switching branches
> [Introduced in](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/20850) [GitLab Core][ce] 11.2.
Switching between branches of the current project repository can be done without
leaving the Web IDE. Click the dropdown in the top of the sidebar to open a list
of branches. You will need to commit or discard all your changes before
switching to a different branch.
[ce]: https://about.gitlab.com/pricing/ [ce]: https://about.gitlab.com/pricing/
[ee]: https://about.gitlab.com/pricing/ [ee]: https://about.gitlab.com/pricing/
...@@ -19,6 +19,7 @@ module API ...@@ -19,6 +19,7 @@ module API
params :filter_params do params :filter_params do
optional :search, type: String, desc: 'Return list of branches matching the search criteria' optional :search, type: String, desc: 'Return list of branches matching the search criteria'
optional :sort, type: String, desc: 'Return list of branches sorted by the given field'
end end
end end
......
...@@ -1930,6 +1930,9 @@ msgstr "" ...@@ -1930,6 +1930,9 @@ msgstr ""
msgid "Cron syntax" msgid "Cron syntax"
msgstr "" msgstr ""
msgid "Current Branch"
msgstr ""
msgid "CurrentUser|Profile" msgid "CurrentUser|Profile"
msgstr "" msgstr ""
...@@ -2409,6 +2412,9 @@ msgstr "" ...@@ -2409,6 +2412,9 @@ msgstr ""
msgid "Error loading branch data. Please try again." msgid "Error loading branch data. Please try again."
msgstr "" msgstr ""
msgid "Error loading branches."
msgstr ""
msgid "Error loading last commit." msgid "Error loading last commit."
msgstr "" msgstr ""
...@@ -3605,6 +3611,9 @@ msgstr "" ...@@ -3605,6 +3611,9 @@ msgstr ""
msgid "No assignee" msgid "No assignee"
msgstr "" msgstr ""
msgid "No branches found"
msgstr ""
msgid "No changes" msgid "No changes"
msgstr "" msgstr ""
...@@ -6045,9 +6054,6 @@ msgstr "" ...@@ -6045,9 +6054,6 @@ msgstr ""
msgid "You cannot write to this read-only GitLab instance." msgid "You cannot write to this read-only GitLab instance."
msgstr "" msgstr ""
msgid "You do not have any assigned merge requests"
msgstr ""
msgid "You don't have any applications" msgid "You don't have any applications"
msgstr "" msgstr ""
...@@ -6057,9 +6063,6 @@ msgstr "" ...@@ -6057,9 +6063,6 @@ msgstr ""
msgid "You have no permissions" msgid "You have no permissions"
msgstr "" msgstr ""
msgid "You have not created any merge requests"
msgstr ""
msgid "You have reached your project limit" msgid "You have reached your project limit"
msgstr "" msgstr ""
......
...@@ -22,7 +22,7 @@ describe 'Multi-file editor new directory', :js do ...@@ -22,7 +22,7 @@ describe 'Multi-file editor new directory', :js do
end end
it 'creates directory in current directory' do it 'creates directory in current directory' do
all('.ide-tree-header button').last.click all('.ide-tree-actions button').last.click
page.within('.modal') do page.within('.modal') do
find('.form-control').set('folder name') find('.form-control').set('folder name')
...@@ -30,7 +30,7 @@ describe 'Multi-file editor new directory', :js do ...@@ -30,7 +30,7 @@ describe 'Multi-file editor new directory', :js do
click_button('Create directory') click_button('Create directory')
end end
first('.ide-tree-header button').click first('.ide-tree-actions button').click
page.within('.modal-dialog') do page.within('.modal-dialog') do
find('.form-control').set('file name') find('.form-control').set('file name')
......
...@@ -22,7 +22,7 @@ describe 'Multi-file editor new file', :js do ...@@ -22,7 +22,7 @@ describe 'Multi-file editor new file', :js do
end end
it 'creates file in current directory' do it 'creates file in current directory' do
first('.ide-tree-header button').click first('.ide-tree-actions button').click
page.within('.modal') do page.within('.modal') do
find('.form-control').set('file name') find('.form-control').set('file name')
......
...@@ -84,7 +84,7 @@ export default ( ...@@ -84,7 +84,7 @@ export default (
done(); done();
}; };
const result = action({ commit, state, dispatch, rootState: state }, payload); const result = action({ commit, state, dispatch, rootState: state, rootGetters: state }, payload);
return new Promise(resolve => { return new Promise(resolve => {
setImmediate(resolve); setImmediate(resolve);
......
import Vue from 'vue';
import mountCompontent from 'spec/helpers/vue_mount_component_helper';
import router from '~/ide/ide_router';
import Item from '~/ide/components/branches/item.vue';
import { getTimeago } from '~/lib/utils/datetime_utility';
import { projectData } from '../../mock_data';
const TEST_BRANCH = {
name: 'master',
committedDate: '2018-01-05T05:50Z',
};
const TEST_PROJECT_ID = projectData.name_with_namespace;
describe('IDE branch item', () => {
const Component = Vue.extend(Item);
let vm;
beforeEach(() => {
vm = mountCompontent(Component, {
item: { ...TEST_BRANCH },
projectId: TEST_PROJECT_ID,
isActive: false,
});
});
afterEach(() => {
vm.$destroy();
});
it('renders branch name and timeago', () => {
const timeText = getTimeago().format(TEST_BRANCH.committedDate);
expect(vm.$el).toContainText(TEST_BRANCH.name);
expect(vm.$el.querySelector('time')).toHaveText(timeText);
expect(vm.$el.querySelector('.ic-mobile-issue-close')).toBe(null);
});
it('renders link to branch', () => {
const expectedHref = router.resolve(`/project/${TEST_PROJECT_ID}/edit/${TEST_BRANCH.name}`).href;
expect(vm.$el).toMatch('a');
expect(vm.$el).toHaveAttr('href', expectedHref);
});
it('renders icon if isActive', done => {
vm.isActive = true;
vm.$nextTick()
.then(() => {
expect(vm.$el.querySelector('.ic-mobile-issue-close')).not.toBe(null);
})
.then(done)
.catch(done.fail);
});
});
import Vue from 'vue';
import store from '~/ide/stores';
import * as types from '~/ide/stores/modules/branches/mutation_types';
import List from '~/ide/components/branches/search_list.vue';
import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper';
import { branches as testBranches } from '../../mock_data';
import { resetStore } from '../../helpers';
describe('IDE branches search list', () => {
const Component = Vue.extend(List);
let vm;
beforeEach(() => {
vm = createComponentWithStore(Component, store, {});
spyOn(vm, 'fetchBranches');
vm.$mount();
});
afterEach(() => {
vm.$destroy();
resetStore(store);
});
it('calls fetch on mounted', () => {
expect(vm.fetchBranches).toHaveBeenCalledWith({
search: '',
});
});
it('renders loading icon', done => {
vm.$store.state.branches.isLoading = true;
vm.$nextTick()
.then(() => {
expect(vm.$el).toContainElement('.loading-container');
})
.then(done)
.catch(done.fail);
});
it('renders branches not found when search is not empty', done => {
vm.search = 'testing';
vm.$nextTick(() => {
expect(vm.$el).toContainText('No branches found');
done();
});
});
describe('with branches', () => {
const currentBranch = testBranches[1];
beforeEach(done => {
vm.$store.state.currentBranchId = currentBranch.name;
vm.$store.commit(`branches/${types.RECEIVE_BRANCHES_SUCCESS}`, testBranches);
vm.$nextTick(done);
});
it('renders list', () => {
const elementText = Array.from(vm.$el.querySelectorAll('li strong'))
.map(x => x.textContent.trim());
expect(elementText).toEqual(testBranches.map(x => x.name));
});
it('renders check next to active branch', () => {
const checkedText = Array.from(vm.$el.querySelectorAll('li'))
.filter(x => x.querySelector('.ide-search-list-current-icon svg'))
.map(x => x.querySelector('strong').textContent.trim());
expect(checkedText).toEqual([currentBranch.name]);
});
});
});
import Vue from 'vue';
import { createStore } from '~/ide/stores';
import Dropdown from '~/ide/components/merge_requests/dropdown.vue';
import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper';
import { mergeRequests } from '../../mock_data';
describe('IDE merge requests dropdown', () => {
const Component = Vue.extend(Dropdown);
let vm;
beforeEach(() => {
const store = createStore();
vm = createComponentWithStore(Component, store, { show: false }).$mount();
});
afterEach(() => {
vm.$destroy();
});
it('does not render tabs when show is false', () => {
expect(vm.$el.querySelector('.nav-links')).toBe(null);
});
describe('when show is true', () => {
beforeEach(done => {
vm.show = true;
vm.$store.state.mergeRequests.assigned.mergeRequests.push(mergeRequests[0]);
vm.$nextTick(done);
});
it('renders tabs', () => {
expect(vm.$el.querySelector('.nav-links')).not.toBe(null);
});
it('renders count for assigned & created data', () => {
expect(vm.$el.querySelector('.nav-links a').textContent).toContain('Created by me');
expect(vm.$el.querySelector('.nav-links a .badge').textContent).toContain('0');
expect(vm.$el.querySelectorAll('.nav-links a')[1].textContent).toContain('Assigned to me');
expect(
vm.$el.querySelectorAll('.nav-links a')[1].querySelector('.badge').textContent,
).toContain('1');
});
});
});
import Vue from 'vue'; import Vue from 'vue';
import router from '~/ide/ide_router';
import Item from '~/ide/components/merge_requests/item.vue'; import Item from '~/ide/components/merge_requests/item.vue';
import mountCompontent from '../../../helpers/vue_mount_component_helper'; import mountCompontent from '../../../helpers/vue_mount_component_helper';
...@@ -27,6 +28,12 @@ describe('IDE merge request item', () => { ...@@ -27,6 +28,12 @@ describe('IDE merge request item', () => {
expect(vm.$el.textContent).toContain('gitlab-org/gitlab-ce!1'); expect(vm.$el.textContent).toContain('gitlab-org/gitlab-ce!1');
}); });
it('renders link with href', () => {
const expectedHref = router.resolve(`/project/${vm.item.projectPathWithNamespace}/merge_requests/${vm.item.iid}`).href;
expect(vm.$el).toMatch('a');
expect(vm.$el).toHaveAttr('href', expectedHref);
});
it('renders icon if ID matches currentId', () => { it('renders icon if ID matches currentId', () => {
expect(vm.$el.querySelector('.ic-mobile-issue-close')).not.toBe(null); expect(vm.$el.querySelector('.ic-mobile-issue-close')).not.toBe(null);
}); });
...@@ -50,12 +57,4 @@ describe('IDE merge request item', () => { ...@@ -50,12 +57,4 @@ describe('IDE merge request item', () => {
done(); done();
}); });
}); });
it('emits click event on click', () => {
spyOn(vm, '$emit');
vm.$el.click();
expect(vm.$emit).toHaveBeenCalledWith('click', vm.item);
});
}); });
...@@ -10,10 +10,7 @@ describe('IDE merge requests list', () => { ...@@ -10,10 +10,7 @@ describe('IDE merge requests list', () => {
let vm; let vm;
beforeEach(() => { beforeEach(() => {
vm = createComponentWithStore(Component, store, { vm = createComponentWithStore(Component, store, {});
type: 'created',
emptyText: 'empty text',
});
spyOn(vm, 'fetchMergeRequests'); spyOn(vm, 'fetchMergeRequests');
...@@ -28,13 +25,13 @@ describe('IDE merge requests list', () => { ...@@ -28,13 +25,13 @@ describe('IDE merge requests list', () => {
it('calls fetch on mounted', () => { it('calls fetch on mounted', () => {
expect(vm.fetchMergeRequests).toHaveBeenCalledWith({ expect(vm.fetchMergeRequests).toHaveBeenCalledWith({
type: 'created',
search: '', search: '',
type: '',
}); });
}); });
it('renders loading icon', done => { it('renders loading icon', done => {
vm.$store.state.mergeRequests.created.isLoading = true; vm.$store.state.mergeRequests.isLoading = true;
vm.$nextTick(() => { vm.$nextTick(() => {
expect(vm.$el.querySelector('.loading-container')).not.toBe(null); expect(vm.$el.querySelector('.loading-container')).not.toBe(null);
...@@ -43,10 +40,6 @@ describe('IDE merge requests list', () => { ...@@ -43,10 +40,6 @@ describe('IDE merge requests list', () => {
}); });
}); });
it('renders empty text when no merge requests exist', () => {
expect(vm.$el.textContent).toContain('empty text');
});
it('renders no search results text when search is not empty', done => { it('renders no search results text when search is not empty', done => {
vm.search = 'testing'; vm.search = 'testing';
...@@ -57,9 +50,29 @@ describe('IDE merge requests list', () => { ...@@ -57,9 +50,29 @@ describe('IDE merge requests list', () => {
}); });
}); });
it('clicking on search type, sets currentSearchType and loads merge requests', done => {
vm.onSearchFocus();
vm.$nextTick()
.then(() => {
vm.$el.querySelector('li button').click();
return vm.$nextTick();
})
.then(() => {
expect(vm.currentSearchType).toEqual(vm.$options.searchTypes[0]);
expect(vm.fetchMergeRequests).toHaveBeenCalledWith({
type: vm.currentSearchType.type,
search: '',
});
})
.then(done)
.catch(done.fail);
});
describe('with merge requests', () => { describe('with merge requests', () => {
beforeEach(done => { beforeEach(done => {
vm.$store.state.mergeRequests.created.mergeRequests.push({ vm.$store.state.mergeRequests.mergeRequests.push({
...mergeRequests[0], ...mergeRequests[0],
projectPathWithNamespace: 'gitlab-org/gitlab-ce', projectPathWithNamespace: 'gitlab-org/gitlab-ce',
}); });
...@@ -71,35 +84,6 @@ describe('IDE merge requests list', () => { ...@@ -71,35 +84,6 @@ describe('IDE merge requests list', () => {
expect(vm.$el.querySelectorAll('li').length).toBe(1); expect(vm.$el.querySelectorAll('li').length).toBe(1);
expect(vm.$el.querySelector('li').textContent).toContain(mergeRequests[0].title); expect(vm.$el.querySelector('li').textContent).toContain(mergeRequests[0].title);
}); });
it('calls openMergeRequest when clicking merge request', done => {
spyOn(vm, 'openMergeRequest');
vm.$el.querySelector('li button').click();
vm.$nextTick(() => {
expect(vm.openMergeRequest).toHaveBeenCalledWith({
projectPath: 'gitlab-org/gitlab-ce',
id: 1,
});
done();
});
});
});
describe('focusSearch', () => {
it('focuses search input when loading is false', done => {
spyOn(vm.$refs.searchInput, 'focus');
vm.$store.state.mergeRequests.created.isLoading = false;
vm.focusSearch();
vm.$nextTick(() => {
expect(vm.$refs.searchInput.focus).toHaveBeenCalled();
done();
});
});
}); });
describe('searchMergeRequests', () => { describe('searchMergeRequests', () => {
...@@ -123,4 +107,52 @@ describe('IDE merge requests list', () => { ...@@ -123,4 +107,52 @@ describe('IDE merge requests list', () => {
expect(vm.loadMergeRequests).toHaveBeenCalled(); expect(vm.loadMergeRequests).toHaveBeenCalled();
}); });
}); });
describe('onSearchFocus', () => {
it('shows search types', done => {
vm.$el.querySelector('input').dispatchEvent(new Event('focus'));
expect(vm.hasSearchFocus).toBe(true);
expect(vm.showSearchTypes).toBe(true);
vm.$nextTick()
.then(() => {
const expectedSearchTypes = vm.$options.searchTypes.map(x => x.label);
const renderedSearchTypes = Array.from(vm.$el.querySelectorAll('li'))
.map(x => x.textContent.trim());
expect(renderedSearchTypes).toEqual(expectedSearchTypes);
})
.then(done)
.catch(done.fail);
});
it('does not show search types, if already has search value', () => {
vm.search = 'lorem ipsum';
vm.$el.querySelector('input').dispatchEvent(new Event('focus'));
expect(vm.hasSearchFocus).toBe(true);
expect(vm.showSearchTypes).toBe(false);
});
it('does not show search types, if already has a search type', () => {
vm.currentSearchType = {};
vm.$el.querySelector('input').dispatchEvent(new Event('focus'));
expect(vm.hasSearchFocus).toBe(true);
expect(vm.showSearchTypes).toBe(false);
});
it('resets hasSearchFocus when search changes', done => {
vm.hasSearchFocus = true;
vm.search = 'something else';
vm.$nextTick()
.then(() => {
expect(vm.hasSearchFocus).toBe(false);
})
.then(done)
.catch(done.fail);
});
});
}); });
import Vue from 'vue';
import NavDropdownButton from '~/ide/components/nav_dropdown_button.vue';
import store from '~/ide/stores';
import { trimText } from 'spec/helpers/vue_component_helper';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { resetStore } from '../helpers';
describe('NavDropdown', () => {
const TEST_BRANCH_ID = 'lorem-ipsum-dolar';
const TEST_MR_ID = '12345';
const Component = Vue.extend(NavDropdownButton);
let vm;
beforeEach(() => {
vm = mountComponentWithStore(Component, { store });
vm.$mount();
});
afterEach(() => {
vm.$destroy();
resetStore(store);
});
it('renders empty placeholders, if state is falsey', () => {
expect(trimText(vm.$el.textContent)).toEqual('- -');
});
it('renders branch name, if state has currentBranchId', done => {
vm.$store.state.currentBranchId = TEST_BRANCH_ID;
vm.$nextTick()
.then(() => {
expect(trimText(vm.$el.textContent)).toEqual(`${TEST_BRANCH_ID} -`);
})
.then(done)
.catch(done.fail);
});
it('renders mr id, if state has currentMergeRequestId', done => {
vm.$store.state.currentMergeRequestId = TEST_MR_ID;
vm.$nextTick()
.then(() => {
expect(trimText(vm.$el.textContent)).toEqual(`- !${TEST_MR_ID}`);
})
.then(done)
.catch(done.fail);
});
it('renders branch and mr, if state has both', done => {
vm.$store.state.currentBranchId = TEST_BRANCH_ID;
vm.$store.state.currentMergeRequestId = TEST_MR_ID;
vm.$nextTick()
.then(() => {
expect(trimText(vm.$el.textContent)).toEqual(`${TEST_BRANCH_ID} !${TEST_MR_ID}`);
})
.then(done)
.catch(done.fail);
});
});
import $ from 'jquery';
import Vue from 'vue';
import store from '~/ide/stores';
import NavDropdown from '~/ide/components/nav_dropdown.vue';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
describe('IDE NavDropdown', () => {
const Component = Vue.extend(NavDropdown);
let vm;
let $dropdown;
beforeEach(() => {
vm = mountComponentWithStore(Component, { store });
$dropdown = $(vm.$el);
// block dispatch from doing anything
spyOn(vm.$store, 'dispatch');
});
afterEach(() => {
vm.$destroy();
});
it('renders nothing initially', () => {
expect(vm.$el).not.toContainElement('.ide-nav-form');
});
it('renders nav form when show.bs.dropdown', done => {
$dropdown.trigger('show.bs.dropdown');
vm.$nextTick()
.then(() => {
expect(vm.$el).toContainElement('.ide-nav-form');
})
.then(done)
.catch(done.fail);
});
it('destroys nav form when closed', done => {
$dropdown.trigger('show.bs.dropdown');
$dropdown.trigger('hide.bs.dropdown');
vm.$nextTick()
.then(() => {
expect(vm.$el).not.toContainElement('.ide-nav-form');
})
.then(done)
.catch(done.fail);
});
});
import Vue from 'vue';
import TokenedInput from '~/ide/components/shared/tokened_input.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
const TEST_PLACEHOLDER = 'Searching in test';
const TEST_TOKENS = [
{ label: 'lorem', id: 1 },
{ label: 'ipsum', id: 2 },
{ label: 'dolar', id: 3 },
];
const TEST_VALUE = 'lorem';
function getTokenElements(vm) {
return Array.from(vm.$el.querySelectorAll('.filtered-search-token button'));
}
function createBackspaceEvent() {
const e = new Event('keyup');
e.keyCode = 8;
e.which = e.keyCode;
e.altKey = false;
e.ctrlKey = true;
e.shiftKey = false;
e.metaKey = false;
return e;
}
describe('IDE shared/TokenedInput', () => {
const Component = Vue.extend(TokenedInput);
let vm;
beforeEach(() => {
vm = mountComponent(Component, {
tokens: TEST_TOKENS,
placeholder: TEST_PLACEHOLDER,
value: TEST_VALUE,
});
spyOn(vm, '$emit');
});
afterEach(() => {
vm.$destroy();
});
it('renders tokens', () => {
const renderedTokens = getTokenElements(vm)
.map(x => x.textContent.trim());
expect(renderedTokens).toEqual(TEST_TOKENS.map(x => x.label));
});
it('renders input', () => {
expect(vm.$refs.input).toBeTruthy();
expect(vm.$refs.input).toHaveValue(TEST_VALUE);
});
it('renders placeholder, when tokens are empty', done => {
vm.tokens = [];
vm.$nextTick()
.then(() => {
expect(vm.$refs.input).toHaveAttr('placeholder', TEST_PLACEHOLDER);
})
.then(done)
.catch(done.fail);
});
it('triggers "removeToken" on token click', () => {
getTokenElements(vm)[0].click();
expect(vm.$emit).toHaveBeenCalledWith('removeToken', TEST_TOKENS[0]);
});
it('when input triggers backspace event, it calls "onBackspace"', () => {
spyOn(vm, 'onBackspace');
vm.$refs.input.dispatchEvent(createBackspaceEvent());
vm.$refs.input.dispatchEvent(createBackspaceEvent());
expect(vm.onBackspace).toHaveBeenCalledTimes(2);
});
it('triggers "removeToken" on backspaces when value is empty', () => {
vm.value = '';
vm.onBackspace();
expect(vm.$emit).not.toHaveBeenCalled();
expect(vm.backspaceCount).toEqual(1);
vm.onBackspace();
expect(vm.$emit).toHaveBeenCalledWith('removeToken', TEST_TOKENS[TEST_TOKENS.length - 1]);
expect(vm.backspaceCount).toEqual(0);
});
it('does not trigger "removeToken" on backspaces when value is not empty', () => {
vm.onBackspace();
vm.onBackspace();
expect(vm.backspaceCount).toEqual(0);
expect(vm.$emit).not.toHaveBeenCalled();
});
it('does not trigger "removeToken" on backspaces when tokens are empty', () => {
vm.tokens = [];
vm.onBackspace();
vm.onBackspace();
expect(vm.backspaceCount).toEqual(0);
expect(vm.$emit).not.toHaveBeenCalled();
});
it('triggers "focus" on input focus', () => {
vm.$refs.input.dispatchEvent(new Event('focus'));
expect(vm.$emit).toHaveBeenCalledWith('focus');
});
it('triggers "blur" on input blur', () => {
vm.$refs.input.dispatchEvent(new Event('blur'));
expect(vm.$emit).toHaveBeenCalledWith('blur');
});
it('triggers "input" with value on input change', () => {
vm.$refs.input.value = 'something-else';
vm.$refs.input.dispatchEvent(new Event('input'));
expect(vm.$emit).toHaveBeenCalledWith('input', 'something-else');
});
});
...@@ -4,6 +4,7 @@ import state from '~/ide/stores/state'; ...@@ -4,6 +4,7 @@ import state from '~/ide/stores/state';
import commitState from '~/ide/stores/modules/commit/state'; import commitState from '~/ide/stores/modules/commit/state';
import mergeRequestsState from '~/ide/stores/modules/merge_requests/state'; import mergeRequestsState from '~/ide/stores/modules/merge_requests/state';
import pipelinesState from '~/ide/stores/modules/pipelines/state'; import pipelinesState from '~/ide/stores/modules/pipelines/state';
import branchesState from '~/ide/stores/modules/branches/state';
export const resetStore = store => { export const resetStore = store => {
const newState = { const newState = {
...@@ -11,6 +12,7 @@ export const resetStore = store => { ...@@ -11,6 +12,7 @@ export const resetStore = store => {
commit: commitState(), commit: commitState(),
mergeRequests: mergeRequestsState(), mergeRequests: mergeRequestsState(),
pipelines: pipelinesState(), pipelines: pipelinesState(),
branches: branchesState(),
}; };
store.replaceState(newState); store.replaceState(newState);
}; };
......
...@@ -165,3 +165,33 @@ export const mergeRequests = [ ...@@ -165,3 +165,33 @@ export const mergeRequests = [
web_url: `${gl.TEST_HOST}/namespace/project-path/merge_requests/1`, web_url: `${gl.TEST_HOST}/namespace/project-path/merge_requests/1`,
}, },
]; ];
export const branches = [
{
id: 1,
name: 'master',
commit: {
message: 'Update master branch',
committed_date: '2018-08-01T00:20:05Z',
},
can_push: true,
},
{
id: 2,
name: 'feature/lorem-ipsum',
commit: {
message: 'Update some stuff',
committed_date: '2018-08-02T00:00:05Z',
},
can_push: true,
},
{
id: 3,
name: 'feature/dolar-amit',
commit: {
message: 'Update some more stuff',
committed_date: '2018-06-30T00:20:05Z',
},
can_push: true,
},
];
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import state from '~/ide/stores/modules/branches/state';
import * as types from '~/ide/stores/modules/branches/mutation_types';
import testAction from 'spec/helpers/vuex_action_helper';
import {
requestBranches,
receiveBranchesError,
receiveBranchesSuccess,
fetchBranches,
resetBranches,
openBranch,
} from '~/ide/stores/modules/branches/actions';
import { branches, projectData } from '../../../mock_data';
describe('IDE branches actions', () => {
const TEST_SEARCH = 'foosearch';
let mockedContext;
let mockedState;
let mock;
beforeEach(() => {
mockedContext = {
dispatch() {},
rootState: {
currentProjectId: projectData.name_with_namespace,
},
rootGetters: {
currentProject: projectData,
},
state: state(),
};
// testAction looks for rootGetters in state,
// so they need to be concatenated here.
mockedState = {
...mockedContext.state,
...mockedContext.rootGetters,
...mockedContext.rootState,
};
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.restore();
});
describe('requestBranches', () => {
it('should commit request', done => {
testAction(
requestBranches,
null,
mockedContext.state,
[{ type: types.REQUEST_BRANCHES }],
[],
done,
);
});
});
describe('receiveBranchesError', () => {
it('should should commit error', done => {
testAction(
receiveBranchesError,
{ search: TEST_SEARCH },
mockedContext.state,
[{ type: types.RECEIVE_BRANCHES_ERROR }],
[
{
type: 'setErrorMessage',
payload: {
text: 'Error loading branches.',
action: jasmine.any(Function),
actionText: 'Please try again',
actionPayload: { search: TEST_SEARCH },
},
},
],
done,
);
});
});
describe('receiveBranchesSuccess', () => {
it('should commit received data', done => {
testAction(
receiveBranchesSuccess,
branches,
mockedContext.state,
[{ type: types.RECEIVE_BRANCHES_SUCCESS, payload: branches }],
[],
done,
);
});
});
describe('fetchBranches', () => {
beforeEach(() => {
gon.api_version = 'v4';
});
describe('success', () => {
beforeEach(() => {
mock.onGet(/\/api\/v4\/projects\/\d+\/repository\/branches(.*)$/).replyOnce(200, branches);
});
it('calls API with params', () => {
const apiSpy = spyOn(axios, 'get').and.callThrough();
fetchBranches(mockedContext, { search: TEST_SEARCH });
expect(apiSpy).toHaveBeenCalledWith(jasmine.anything(), {
params: jasmine.objectContaining({
search: TEST_SEARCH,
sort: 'updated_desc',
}),
});
});
it('dispatches success with received data', done => {
testAction(
fetchBranches,
{ search: TEST_SEARCH },
mockedState,
[],
[
{ type: 'requestBranches' },
{ type: 'resetBranches' },
{
type: 'receiveBranchesSuccess',
payload: branches,
},
],
done,
);
});
});
describe('error', () => {
beforeEach(() => {
mock.onGet(/\/api\/v4\/projects\/\d+\/repository\/branches(.*)$/).replyOnce(500);
});
it('dispatches error', done => {
testAction(
fetchBranches,
{ search: TEST_SEARCH },
mockedState,
[],
[
{ type: 'requestBranches' },
{ type: 'resetBranches' },
{
type: 'receiveBranchesError',
payload: { search: TEST_SEARCH },
},
],
done,
);
});
});
describe('resetBranches', () => {
it('commits reset', done => {
testAction(
resetBranches,
null,
mockedContext.state,
[{ type: types.RESET_BRANCHES }],
[],
done,
);
});
});
describe('openBranch', () => {
it('dispatches goToRoute action with path', done => {
const branchId = branches[0].name;
const expectedPath = `/project/${projectData.name_with_namespace}/edit/${branchId}`;
testAction(
openBranch,
branchId,
mockedState,
[],
[{ type: 'goToRoute', payload: expectedPath }],
done,
);
});
});
});
});
import state from '~/ide/stores/modules/branches/state';
import mutations from '~/ide/stores/modules/branches/mutations';
import * as types from '~/ide/stores/modules/branches/mutation_types';
import { branches } from '../../../mock_data';
describe('IDE branches mutations', () => {
let mockedState;
beforeEach(() => {
mockedState = state();
});
describe(types.REQUEST_BRANCHES, () => {
it('sets loading to true', () => {
mutations[types.REQUEST_BRANCHES](mockedState);
expect(mockedState.isLoading).toBe(true);
});
});
describe(types.RECEIVE_BRANCHES_ERROR, () => {
it('sets loading to false', () => {
mutations[types.RECEIVE_BRANCHES_ERROR](mockedState);
expect(mockedState.isLoading).toBe(false);
});
});
describe(types.RECEIVE_BRANCHES_SUCCESS, () => {
it('sets branches', () => {
const expectedBranches = branches.map(branch => ({
name: branch.name,
committedDate: branch.commit.committed_date,
}));
mutations[types.RECEIVE_BRANCHES_SUCCESS](mockedState, branches);
expect(mockedState.branches).toEqual(expectedBranches);
});
});
describe(types.RESET_BRANCHES, () => {
it('clears branches array', () => {
mockedState.branches = ['test'];
mutations[types.RESET_BRANCHES](mockedState);
expect(mockedState.branches).toEqual([]);
});
});
});
...@@ -8,9 +8,7 @@ import { ...@@ -8,9 +8,7 @@ import {
receiveMergeRequestsSuccess, receiveMergeRequestsSuccess,
fetchMergeRequests, fetchMergeRequests,
resetMergeRequests, resetMergeRequests,
openMergeRequest,
} from '~/ide/stores/modules/merge_requests/actions'; } from '~/ide/stores/modules/merge_requests/actions';
import router from '~/ide/ide_router';
import { mergeRequests } from '../../../mock_data'; import { mergeRequests } from '../../../mock_data';
import testAction from '../../../../helpers/vuex_action_helper'; import testAction from '../../../../helpers/vuex_action_helper';
...@@ -28,12 +26,12 @@ describe('IDE merge requests actions', () => { ...@@ -28,12 +26,12 @@ describe('IDE merge requests actions', () => {
}); });
describe('requestMergeRequests', () => { describe('requestMergeRequests', () => {
it('should should commit request', done => { it('should commit request', done => {
testAction( testAction(
requestMergeRequests, requestMergeRequests,
'created', null,
mockedState, mockedState,
[{ type: types.REQUEST_MERGE_REQUESTS, payload: 'created' }], [{ type: types.REQUEST_MERGE_REQUESTS }],
[], [],
done, done,
); );
...@@ -46,7 +44,7 @@ describe('IDE merge requests actions', () => { ...@@ -46,7 +44,7 @@ describe('IDE merge requests actions', () => {
receiveMergeRequestsError, receiveMergeRequestsError,
{ type: 'created', search: '' }, { type: 'created', search: '' },
mockedState, mockedState,
[{ type: types.RECEIVE_MERGE_REQUESTS_ERROR, payload: 'created' }], [{ type: types.RECEIVE_MERGE_REQUESTS_ERROR }],
[ [
{ {
type: 'setErrorMessage', type: 'setErrorMessage',
...@@ -67,12 +65,12 @@ describe('IDE merge requests actions', () => { ...@@ -67,12 +65,12 @@ describe('IDE merge requests actions', () => {
it('should commit received data', done => { it('should commit received data', done => {
testAction( testAction(
receiveMergeRequestsSuccess, receiveMergeRequestsSuccess,
{ type: 'created', data: 'data' }, mergeRequests,
mockedState, mockedState,
[ [
{ {
type: types.RECEIVE_MERGE_REQUESTS_SUCCESS, type: types.RECEIVE_MERGE_REQUESTS_SUCCESS,
payload: { type: 'created', data: 'data' }, payload: mergeRequests,
}, },
], ],
[], [],
...@@ -129,11 +127,11 @@ describe('IDE merge requests actions', () => { ...@@ -129,11 +127,11 @@ describe('IDE merge requests actions', () => {
mockedState, mockedState,
[], [],
[ [
{ type: 'requestMergeRequests', payload: 'created' }, { type: 'requestMergeRequests' },
{ type: 'resetMergeRequests', payload: 'created' }, { type: 'resetMergeRequests' },
{ {
type: 'receiveMergeRequestsSuccess', type: 'receiveMergeRequestsSuccess',
payload: { type: 'created', data: mergeRequests }, payload: mergeRequests,
}, },
], ],
done, done,
...@@ -149,12 +147,12 @@ describe('IDE merge requests actions', () => { ...@@ -149,12 +147,12 @@ describe('IDE merge requests actions', () => {
it('dispatches error', done => { it('dispatches error', done => {
testAction( testAction(
fetchMergeRequests, fetchMergeRequests,
{ type: 'created' }, { type: 'created', search: '' },
mockedState, mockedState,
[], [],
[ [
{ type: 'requestMergeRequests', payload: 'created' }, { type: 'requestMergeRequests' },
{ type: 'resetMergeRequests', payload: 'created' }, { type: 'resetMergeRequests' },
{ type: 'receiveMergeRequestsError', payload: { type: 'created', search: '' } }, { type: 'receiveMergeRequestsError', payload: { type: 'created', search: '' } },
], ],
done, done,
...@@ -167,59 +165,12 @@ describe('IDE merge requests actions', () => { ...@@ -167,59 +165,12 @@ describe('IDE merge requests actions', () => {
it('commits reset', done => { it('commits reset', done => {
testAction( testAction(
resetMergeRequests, resetMergeRequests,
'created', null,
mockedState, mockedState,
[{ type: types.RESET_MERGE_REQUESTS, payload: 'created' }], [{ type: types.RESET_MERGE_REQUESTS }],
[], [],
done, done,
); );
}); });
}); });
describe('openMergeRequest', () => {
beforeEach(() => {
spyOn(router, 'push');
});
it('commits reset mutations and actions', done => {
const commit = jasmine.createSpy();
const dispatch = jasmine.createSpy().and.returnValue(Promise.resolve());
openMergeRequest({ commit, dispatch }, { projectPath: 'gitlab-org/gitlab-ce', id: '1' });
setTimeout(() => {
expect(commit.calls.argsFor(0)).toEqual(['CLEAR_PROJECTS', null, { root: true }]);
expect(commit.calls.argsFor(1)).toEqual(['SET_CURRENT_MERGE_REQUEST', '1', { root: true }]);
expect(commit.calls.argsFor(2)).toEqual(['RESET_OPEN_FILES', null, { root: true }]);
expect(dispatch.calls.argsFor(0)).toEqual(['setCurrentBranchId', '', { root: true }]);
expect(dispatch.calls.argsFor(1)).toEqual([
'pipelines/stopPipelinePolling',
null,
{ root: true },
]);
expect(dispatch.calls.argsFor(2)).toEqual(['setRightPane', null, { root: true }]);
expect(dispatch.calls.argsFor(3)).toEqual([
'pipelines/resetLatestPipeline',
null,
{ root: true },
]);
expect(dispatch.calls.argsFor(4)).toEqual([
'pipelines/clearEtagPoll',
null,
{ root: true },
]);
done();
});
});
it('pushes new route', () => {
openMergeRequest(
{ commit() {}, dispatch: () => Promise.resolve() },
{ projectPath: 'gitlab-org/gitlab-ce', id: '1' },
);
expect(router.push).toHaveBeenCalledWith('/project/gitlab-org/gitlab-ce/merge_requests/1');
});
});
}); });
...@@ -12,29 +12,26 @@ describe('IDE merge requests mutations', () => { ...@@ -12,29 +12,26 @@ describe('IDE merge requests mutations', () => {
describe(types.REQUEST_MERGE_REQUESTS, () => { describe(types.REQUEST_MERGE_REQUESTS, () => {
it('sets loading to true', () => { it('sets loading to true', () => {
mutations[types.REQUEST_MERGE_REQUESTS](mockedState, 'created'); mutations[types.REQUEST_MERGE_REQUESTS](mockedState);
expect(mockedState.created.isLoading).toBe(true); expect(mockedState.isLoading).toBe(true);
}); });
}); });
describe(types.RECEIVE_MERGE_REQUESTS_ERROR, () => { describe(types.RECEIVE_MERGE_REQUESTS_ERROR, () => {
it('sets loading to false', () => { it('sets loading to false', () => {
mutations[types.RECEIVE_MERGE_REQUESTS_ERROR](mockedState, 'created'); mutations[types.RECEIVE_MERGE_REQUESTS_ERROR](mockedState);
expect(mockedState.created.isLoading).toBe(false); expect(mockedState.isLoading).toBe(false);
}); });
}); });
describe(types.RECEIVE_MERGE_REQUESTS_SUCCESS, () => { describe(types.RECEIVE_MERGE_REQUESTS_SUCCESS, () => {
it('sets merge requests', () => { it('sets merge requests', () => {
gon.gitlab_url = gl.TEST_HOST; gon.gitlab_url = gl.TEST_HOST;
mutations[types.RECEIVE_MERGE_REQUESTS_SUCCESS](mockedState, { mutations[types.RECEIVE_MERGE_REQUESTS_SUCCESS](mockedState, mergeRequests);
type: 'created',
data: mergeRequests,
});
expect(mockedState.created.mergeRequests).toEqual([ expect(mockedState.mergeRequests).toEqual([
{ {
id: 1, id: 1,
iid: 1, iid: 1,
...@@ -50,9 +47,9 @@ describe('IDE merge requests mutations', () => { ...@@ -50,9 +47,9 @@ describe('IDE merge requests mutations', () => {
it('clears merge request array', () => { it('clears merge request array', () => {
mockedState.mergeRequests = ['test']; mockedState.mergeRequests = ['test'];
mutations[types.RESET_MERGE_REQUESTS](mockedState, 'created'); mutations[types.RESET_MERGE_REQUESTS](mockedState);
expect(mockedState.created.mergeRequests).toEqual([]); expect(mockedState.mergeRequests).toEqual([]);
}); });
}); });
}); });
...@@ -2,15 +2,15 @@ import Vue from 'vue'; ...@@ -2,15 +2,15 @@ import Vue from 'vue';
import dropdownButtonComponent from '~/vue_shared/components/dropdown/dropdown_button.vue'; import dropdownButtonComponent from '~/vue_shared/components/dropdown/dropdown_button.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper'; import { mountComponentWithSlots } from 'spec/helpers/vue_mount_component_helper';
const defaultLabel = 'Select'; const defaultLabel = 'Select';
const customLabel = 'Select project'; const customLabel = 'Select project';
const createComponent = config => { const createComponent = (props, slots = {}) => {
const Component = Vue.extend(dropdownButtonComponent); const Component = Vue.extend(dropdownButtonComponent);
return mountComponent(Component, config); return mountComponentWithSlots(Component, { props, slots });
}; };
describe('DropdownButtonComponent', () => { describe('DropdownButtonComponent', () => {
...@@ -65,5 +65,14 @@ describe('DropdownButtonComponent', () => { ...@@ -65,5 +65,14 @@ describe('DropdownButtonComponent', () => {
expect(dropdownIconEl).not.toBeNull(); expect(dropdownIconEl).not.toBeNull();
expect(dropdownIconEl.classList.contains('fa-chevron-down')).toBe(true); expect(dropdownIconEl.classList.contains('fa-chevron-down')).toBe(true);
}); });
it('renders slot, if default slot exists', () => {
vm = createComponent({}, {
default: ['Lorem Ipsum Dolar'],
});
expect(vm.$el).not.toContainElement('.dropdown-toggle-text');
expect(vm.$el).toHaveText('Lorem Ipsum Dolar');
});
}); });
}); });
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