diff options
author | Phil Hughes <me@iamphill.com> | 2018-08-07 15:15:56 +0000 |
---|---|---|
committer | Phil Hughes <me@iamphill.com> | 2018-08-07 15:15:56 +0000 |
commit | d0f03ed9b34e626141c5a55073d282cce6ea3d10 (patch) | |
tree | 9bf41acf27d039f673f45520187daff9d47cb04f | |
parent | 0e90f27ff79d1743d8ec5e49e003d4c68a689f78 (diff) | |
parent | 0d6e50d54270a973647f828047828b80fdf8d013 (diff) | |
download | gitlab-ce-d0f03ed9b34e626141c5a55073d282cce6ea3d10.tar.gz |
Merge branch '46165-web-ide-branch-picker' into 'master'
Create Web IDE MR and branch picker
Closes #46165
See merge request gitlab-org/gitlab-ce!20978
50 files changed, 1532 insertions, 402 deletions
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index 422becb7db8..25fe2ae553e 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -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 }) { const url = Api.buildUrl(this.createBranchPath).replace(':id', encodeURIComponent(id)); diff --git a/app/assets/javascripts/ide/components/branches/item.vue b/app/assets/javascripts/ide/components/branches/item.vue new file mode 100644 index 00000000000..cc3e84e3f77 --- /dev/null +++ b/app/assets/javascripts/ide/components/branches/item.vue @@ -0,0 +1,60 @@ +<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> diff --git a/app/assets/javascripts/ide/components/branches/search_list.vue b/app/assets/javascripts/ide/components/branches/search_list.vue new file mode 100644 index 00000000000..6db7b9d6b0e --- /dev/null +++ b/app/assets/javascripts/ide/components/branches/search_list.vue @@ -0,0 +1,111 @@ +<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> diff --git a/app/assets/javascripts/ide/components/ide_tree.vue b/app/assets/javascripts/ide/components/ide_tree.vue index 33f1179a234..39d46a91731 100644 --- a/app/assets/javascripts/ide/components/ide_tree.vue +++ b/app/assets/javascripts/ide/components/ide_tree.vue @@ -41,7 +41,7 @@ export default { slot="header" > {{ __('Edit') }} - <div class="ml-auto d-flex"> + <div class="ide-tree-actions ml-auto d-flex"> <new-entry-button :label="__('New file')" :show-label="false" diff --git a/app/assets/javascripts/ide/components/ide_tree_list.vue b/app/assets/javascripts/ide/components/ide_tree_list.vue index e303ff6ea8f..5611b37be7c 100644 --- a/app/assets/javascripts/ide/components/ide_tree_list.vue +++ b/app/assets/javascripts/ide/components/ide_tree_list.vue @@ -3,14 +3,14 @@ import { mapActions, mapGetters, mapState } from 'vuex'; import Icon from '~/vue_shared/components/icon.vue'; import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue'; import RepoFile from './repo_file.vue'; -import NewDropdown from './new_dropdown/index.vue'; +import NavDropdown from './nav_dropdown.vue'; export default { components: { Icon, RepoFile, SkeletonLoadingContainer, - NewDropdown, + NavDropdown, }, props: { viewerType: { @@ -57,6 +57,7 @@ export default { :class="headerClass" class="ide-tree-header" > + <nav-dropdown /> <slot name="header"></slot> </header> <div diff --git a/app/assets/javascripts/ide/components/merge_requests/dropdown.vue b/app/assets/javascripts/ide/components/merge_requests/dropdown.vue deleted file mode 100644 index 4b9824bf04b..00000000000 --- a/app/assets/javascripts/ide/components/merge_requests/dropdown.vue +++ /dev/null @@ -1,63 +0,0 @@ -<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> diff --git a/app/assets/javascripts/ide/components/merge_requests/item.vue b/app/assets/javascripts/ide/components/merge_requests/item.vue index 4e18376bd48..0c4ea80ba08 100644 --- a/app/assets/javascripts/ide/components/merge_requests/item.vue +++ b/app/assets/javascripts/ide/components/merge_requests/item.vue @@ -1,5 +1,6 @@ <script> import Icon from '../../../vue_shared/components/icon.vue'; +import router from '../../ide_router'; export default { components: { @@ -29,22 +30,21 @@ export default { pathWithID() { return `${this.item.projectPathWithNamespace}!${this.item.iid}`; }, - }, - methods: { - clickItem() { - this.$emit('click', this.item); + mergeRequestHref() { + const path = `/project/${this.item.projectPathWithNamespace}/merge_requests/${this.item.iid}`; + + return router.resolve(path).href; }, }, }; </script> <template> - <button - type="button" + <a + :href="mergeRequestHref" 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 v-if="isActive" :size="18" @@ -59,5 +59,5 @@ export default { {{ pathWithID }} </span> </span> - </button> + </a> </template> diff --git a/app/assets/javascripts/ide/components/merge_requests/list.vue b/app/assets/javascripts/ide/components/merge_requests/list.vue index 19d3e48ee10..fc612956688 100644 --- a/app/assets/javascripts/ide/components/merge_requests/list.vue +++ b/app/assets/javascripts/ide/components/merge_requests/list.vue @@ -1,96 +1,101 @@ <script> -import { mapActions, mapGetters, mapState } from 'vuex'; +import { mapActions, mapState } from 'vuex'; 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 TokenedInput from '../shared/tokened_input.vue'; + +const SEARCH_TYPES = [ + { type: 'created', label: __('Created by me') }, + { type: 'assigned', label: __('Assigned to me') }, +]; export default { components: { LoadingIcon, + TokenedInput, Item, - }, - props: { - type: { - type: String, - required: true, - }, - emptyText: { - type: String, - required: true, - }, + Icon, }, data() { return { search: '', + currentSearchType: null, + hasSearchFocus: false, }; }, computed: { - ...mapGetters('mergeRequests', ['getData']), + ...mapState('mergeRequests', ['mergeRequests', 'isLoading']), ...mapState(['currentMergeRequestId', 'currentProjectId']), - data() { - return this.getData(this.type); - }, - isLoading() { - return this.data.isLoading; - }, - mergeRequests() { - return this.data.mergeRequests; - }, hasMergeRequests() { return this.mergeRequests.length !== 0; }, hasNoSearchResults() { 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: { - isLoading: { - handler: 'focusSearch', + search() { + // When the search is updated, let's turn off this flag to hide the search types + this.hasSearchFocus = false; }, }, mounted() { this.loadMergeRequests(); }, methods: { - ...mapActions('mergeRequests', ['fetchMergeRequests', 'openMergeRequest']), + ...mapActions('mergeRequests', ['fetchMergeRequests']), loadMergeRequests() { this.fetchMergeRequests({ type: this.type, search: this.search }); }, - viewMergeRequest(item) { - this.openMergeRequest({ - projectPath: item.projectPathWithNamespace, - id: item.iid, - }); - }, searchMergeRequests: _.debounce(function debounceSearch() { this.loadMergeRequests(); }, 250), - focusSearch() { - if (!this.isLoading) { - this.$nextTick(() => { - this.$refs.searchInput.focus(); - }); - } + onSearchFocus() { + this.hasSearchFocus = true; + }, + setSearchType(searchType) { + this.currentSearchType = searchType; + this.loadMergeRequests(); }, }, + searchTypes: SEARCH_TYPES, }; </script> <template> <div> <div class="dropdown-input mt-3 pb-3 mb-0 border-bottom"> - <input - ref="searchInput" - :placeholder="__('Search merge requests')" - v-model="search" - type="search" - class="dropdown-input-field" - @input="searchMergeRequests" - /> - <i - aria-hidden="true" - class="fa fa-search dropdown-input-search" - ></i> + <div class="position-relative"> + <tokened-input + v-model="search" + :tokens="searchTokens" + :placeholder="__('Search merge requests')" + @focus="onSearchFocus" + @input="searchMergeRequests" + @removeToken="setSearchType(null)" + /> + <icon + :size="18" + name="search" + class="input-icon" + /> + </div> </div> <div class="dropdown-content ide-merge-requests-dropdown-content d-flex"> <loading-icon @@ -98,35 +103,52 @@ export default { 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="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-else> + <ul + class="mb-3 w-100" > - <template v-if="hasNoSearchResults"> - {{ __('No merge requests found') }} + <template v-if="showSearchTypes"> + <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 v-else> - {{ emptyText }} + <template v-else-if="hasMergeRequests"> + <li + v-for="item in mergeRequests" + :key="item.id" + > + <item + :item="item" + :current-id="currentMergeRequestId" + :current-project-id="currentProjectId" + /> + </li> </template> - </li> - </ul> + <li + v-else + class="ide-search-list-empty d-flex align-items-center justify-content-center" + > + {{ __('No merge requests found') }} + </li> + </ul> + </template> </div> </div> </template> diff --git a/app/assets/javascripts/ide/components/nav_dropdown.vue b/app/assets/javascripts/ide/components/nav_dropdown.vue new file mode 100644 index 00000000000..db36779c395 --- /dev/null +++ b/app/assets/javascripts/ide/components/nav_dropdown.vue @@ -0,0 +1,59 @@ +<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> diff --git a/app/assets/javascripts/ide/components/nav_dropdown_button.vue b/app/assets/javascripts/ide/components/nav_dropdown_button.vue new file mode 100644 index 00000000000..7f98769d484 --- /dev/null +++ b/app/assets/javascripts/ide/components/nav_dropdown_button.vue @@ -0,0 +1,54 @@ +<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> diff --git a/app/assets/javascripts/ide/components/nav_form.vue b/app/assets/javascripts/ide/components/nav_form.vue new file mode 100644 index 00000000000..718b836e11c --- /dev/null +++ b/app/assets/javascripts/ide/components/nav_form.vue @@ -0,0 +1,40 @@ +<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> diff --git a/app/assets/javascripts/ide/components/shared/tokened_input.vue b/app/assets/javascripts/ide/components/shared/tokened_input.vue new file mode 100644 index 00000000000..a7a12f6785d --- /dev/null +++ b/app/assets/javascripts/ide/components/shared/tokened_input.vue @@ -0,0 +1,121 @@ +<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> diff --git a/app/assets/javascripts/ide/stores/index.js b/app/assets/javascripts/ide/stores/index.js index f8ce8a67ec0..a601dc8f5a0 100644 --- a/app/assets/javascripts/ide/stores/index.js +++ b/app/assets/javascripts/ide/stores/index.js @@ -7,6 +7,7 @@ import mutations from './mutations'; import commitModule from './modules/commit'; import pipelines from './modules/pipelines'; import mergeRequests from './modules/merge_requests'; +import branches from './modules/branches'; Vue.use(Vuex); @@ -20,6 +21,7 @@ export const createStore = () => commit: commitModule, pipelines, mergeRequests, + branches, }, }); diff --git a/app/assets/javascripts/ide/stores/modules/branches/actions.js b/app/assets/javascripts/ide/stores/modules/branches/actions.js new file mode 100644 index 00000000000..74aa98ef9f9 --- /dev/null +++ b/app/assets/javascripts/ide/stores/modules/branches/actions.js @@ -0,0 +1,39 @@ +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 () => {}; diff --git a/app/assets/javascripts/ide/stores/modules/branches/index.js b/app/assets/javascripts/ide/stores/modules/branches/index.js new file mode 100644 index 00000000000..04e7e0f08f1 --- /dev/null +++ b/app/assets/javascripts/ide/stores/modules/branches/index.js @@ -0,0 +1,10 @@ +import state from './state'; +import * as actions from './actions'; +import mutations from './mutations'; + +export default { + namespaced: true, + state: state(), + actions, + mutations, +}; diff --git a/app/assets/javascripts/ide/stores/modules/branches/mutation_types.js b/app/assets/javascripts/ide/stores/modules/branches/mutation_types.js new file mode 100644 index 00000000000..2272f7b9531 --- /dev/null +++ b/app/assets/javascripts/ide/stores/modules/branches/mutation_types.js @@ -0,0 +1,5 @@ +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'; diff --git a/app/assets/javascripts/ide/stores/modules/branches/mutations.js b/app/assets/javascripts/ide/stores/modules/branches/mutations.js new file mode 100644 index 00000000000..081ec2d4c28 --- /dev/null +++ b/app/assets/javascripts/ide/stores/modules/branches/mutations.js @@ -0,0 +1,21 @@ +/* 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 = []; + }, +}; diff --git a/app/assets/javascripts/ide/stores/modules/branches/state.js b/app/assets/javascripts/ide/stores/modules/branches/state.js new file mode 100644 index 00000000000..89bf220c45f --- /dev/null +++ b/app/assets/javascripts/ide/stores/modules/branches/state.js @@ -0,0 +1,4 @@ +export default () => ({ + isLoading: false, + branches: [], +}); diff --git a/app/assets/javascripts/ide/stores/modules/merge_requests/actions.js b/app/assets/javascripts/ide/stores/modules/merge_requests/actions.js index 6ef938b0ae2..baa2497ec5b 100644 --- a/app/assets/javascripts/ide/stores/modules/merge_requests/actions.js +++ b/app/assets/javascripts/ide/stores/modules/merge_requests/actions.js @@ -1,12 +1,10 @@ import { __ } from '../../../../locale'; import Api from '../../../../api'; -import router from '../../../ide_router'; import { scopes } from './constants'; import * as types from './mutation_types'; -import * as rootTypes from '../../mutation_types'; -export const requestMergeRequests = ({ commit }, type) => - commit(types.REQUEST_MERGE_REQUESTS, type); +export const requestMergeRequests = ({ commit }) => + commit(types.REQUEST_MERGE_REQUESTS); export const receiveMergeRequestsError = ({ commit, dispatch }, { type, search }) => { dispatch( 'setErrorMessage', @@ -21,39 +19,22 @@ export const receiveMergeRequestsError = ({ commit, dispatch }, { type, search } }, { root: true }, ); - commit(types.RECEIVE_MERGE_REQUESTS_ERROR, type); + commit(types.RECEIVE_MERGE_REQUESTS_ERROR); }; -export const receiveMergeRequestsSuccess = ({ commit }, { type, data }) => - commit(types.RECEIVE_MERGE_REQUESTS_SUCCESS, { type, data }); +export const receiveMergeRequestsSuccess = ({ commit }, data) => + commit(types.RECEIVE_MERGE_REQUESTS_SUCCESS, data); export const fetchMergeRequests = ({ dispatch, state: { state } }, { type, search = '' }) => { - const scope = scopes[type]; - dispatch('requestMergeRequests', type); - dispatch('resetMergeRequests', type); + dispatch('requestMergeRequests'); + dispatch('resetMergeRequests'); + + const scope = type ? scopes[type] : 'all'; return Api.mergeRequests({ scope, state, search }) - .then(({ data }) => dispatch('receiveMergeRequestsSuccess', { type, data })) + .then(({ data }) => dispatch('receiveMergeRequestsSuccess', data)) .catch(() => dispatch('receiveMergeRequestsError', { type, search })); }; -export const resetMergeRequests = ({ commit }, type) => commit(types.RESET_MERGE_REQUESTS, type); - -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 const resetMergeRequests = ({ commit }) => commit(types.RESET_MERGE_REQUESTS); export default () => {}; diff --git a/app/assets/javascripts/ide/stores/modules/merge_requests/getters.js b/app/assets/javascripts/ide/stores/modules/merge_requests/getters.js deleted file mode 100644 index 8e2b234be8d..00000000000 --- a/app/assets/javascripts/ide/stores/modules/merge_requests/getters.js +++ /dev/null @@ -1,4 +0,0 @@ -export const getData = state => type => state[type]; - -export const assignedData = state => state.assigned; -export const createdData = state => state.created; diff --git a/app/assets/javascripts/ide/stores/modules/merge_requests/index.js b/app/assets/javascripts/ide/stores/modules/merge_requests/index.js index 2e6dfb420f4..04e7e0f08f1 100644 --- a/app/assets/javascripts/ide/stores/modules/merge_requests/index.js +++ b/app/assets/javascripts/ide/stores/modules/merge_requests/index.js @@ -1,6 +1,5 @@ import state from './state'; import * as actions from './actions'; -import * as getters from './getters'; import mutations from './mutations'; export default { @@ -8,5 +7,4 @@ export default { state: state(), actions, mutations, - getters, }; diff --git a/app/assets/javascripts/ide/stores/modules/merge_requests/mutations.js b/app/assets/javascripts/ide/stores/modules/merge_requests/mutations.js index 971da0806bd..98102a68e08 100644 --- a/app/assets/javascripts/ide/stores/modules/merge_requests/mutations.js +++ b/app/assets/javascripts/ide/stores/modules/merge_requests/mutations.js @@ -2,15 +2,15 @@ import * as types from './mutation_types'; export default { - [types.REQUEST_MERGE_REQUESTS](state, type) { - state[type].isLoading = true; + [types.REQUEST_MERGE_REQUESTS](state) { + state.isLoading = true; }, - [types.RECEIVE_MERGE_REQUESTS_ERROR](state, type) { - state[type].isLoading = false; + [types.RECEIVE_MERGE_REQUESTS_ERROR](state) { + state.isLoading = false; }, - [types.RECEIVE_MERGE_REQUESTS_SUCCESS](state, { type, data }) { - state[type].isLoading = false; - state[type].mergeRequests = data.map(mergeRequest => ({ + [types.RECEIVE_MERGE_REQUESTS_SUCCESS](state, data) { + state.isLoading = false; + state.mergeRequests = data.map(mergeRequest => ({ id: mergeRequest.id, iid: mergeRequest.iid, title: mergeRequest.title, @@ -20,7 +20,7 @@ export default { .replace(`/merge_requests/${mergeRequest.iid}`, ''), })); }, - [types.RESET_MERGE_REQUESTS](state, type) { - state[type].mergeRequests = []; + [types.RESET_MERGE_REQUESTS](state) { + state.mergeRequests = []; }, }; diff --git a/app/assets/javascripts/ide/stores/modules/merge_requests/state.js b/app/assets/javascripts/ide/stores/modules/merge_requests/state.js index 57eb6b04283..4748ccfa2e6 100644 --- a/app/assets/javascripts/ide/stores/modules/merge_requests/state.js +++ b/app/assets/javascripts/ide/stores/modules/merge_requests/state.js @@ -1,13 +1,7 @@ import { states } from './constants'; export default () => ({ - created: { - isLoading: false, - mergeRequests: [], - }, - assigned: { - isLoading: false, - mergeRequests: [], - }, + isLoading: false, + mergeRequests: [], state: states.opened, }); diff --git a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue index 3cba0c5e633..af5ebcdc40a 100644 --- a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue +++ b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue @@ -38,9 +38,17 @@ export default { v-show="isLoading" :inline="true" /> - <span class="dropdown-toggle-text"> - {{ toggleText }} - </span> + <template> + <slot + v-if="$slots.default" + ></slot> + <span + v-else + class="dropdown-toggle-text" + > + {{ toggleText }} + </span> + </template> <span v-show="!isLoading" class="dropdown-toggle-icon" diff --git a/app/assets/javascripts/vue_shared/components/icon.vue b/app/assets/javascripts/vue_shared/components/icon.vue index 3cf90b45a97..5e0e7315e99 100644 --- a/app/assets/javascripts/vue_shared/components/icon.vue +++ b/app/assets/javascripts/vue_shared/components/icon.vue @@ -1,7 +1,7 @@ <script> // 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; /* diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index ec4a0f378d0..eebce8b9011 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -571,7 +571,8 @@ margin-bottom: 10px; padding: 0 10px; - .fa { + .fa, + .input-icon { position: absolute; top: 10px; right: 20px; diff --git a/app/assets/stylesheets/framework/images.scss b/app/assets/stylesheets/framework/images.scss index ab3cceceae9..f878ec1ca91 100644 --- a/app/assets/stylesheets/framework/images.scss +++ b/app/assets/stylesheets/framework/images.scss @@ -39,7 +39,7 @@ svg { 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 { &.s#{$svg-size} { @include svg-size(#{$svg-size}px); diff --git a/app/assets/stylesheets/page_bundles/ide.scss b/app/assets/stylesheets/page_bundles/ide.scss index 37ad6a717d9..c3381e06c30 100644 --- a/app/assets/stylesheets/page_bundles/ide.scss +++ b/app/assets/stylesheets/page_bundles/ide.scss @@ -1,6 +1,7 @@ @import 'framework/variables'; @import 'framework/mixins'; +$search-list-icon-width: 18px; $ide-activity-bar-width: 60px; $ide-context-header-padding: 10px; $ide-project-avatar-end: $ide-context-header-padding + 48px; @@ -49,7 +50,7 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding; display: flex; flex-direction: column; flex: 1; - overflow: hidden; + min-height: 0; .file { height: 32px; @@ -541,11 +542,11 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding; display: flex; flex: 1; flex-direction: column; - overflow: hidden; background-color: $white-light; border-left: 1px solid $white-dark; border-top: 1px solid $white-dark; 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; flex: 0 0 auto; display: flex; align-items: center; + flex-wrap: wrap; padding: 12px 0; margin-left: $ide-tree-padding; margin-right: $ide-tree-padding; @@ -1066,6 +1068,32 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding; 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 { color: $gl-text-color; } @@ -1181,7 +1209,7 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding; } .ide-context-body { - overflow: hidden; + min-height: 0; } .ide-sidebar-project-title { @@ -1331,7 +1359,7 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding; min-height: 60px; } -.ide-merge-requests-dropdown { +.ide-nav-form { .nav-links li { width: 50%; padding-left: 0; @@ -1350,22 +1378,36 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding; padding-left: $gl-padding; padding-right: $gl-padding; - .fa { - right: 26px; + .input-icon { + 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 { padding-top: $gl-padding; padding-bottom: $gl-padding; } } -.ide-merge-request-current-icon { - min-width: 18px; +.ide-search-list-current-icon { + min-width: $search-list-icon-width; } -.ide-merge-requests-empty { +.ide-search-list-empty { height: 230px; } diff --git a/changelogs/unreleased/46165-web-ide-branch-picker.yml b/changelogs/unreleased/46165-web-ide-branch-picker.yml new file mode 100644 index 00000000000..ff879cb3d37 --- /dev/null +++ b/changelogs/unreleased/46165-web-ide-branch-picker.yml @@ -0,0 +1,5 @@ +--- +title: Create branch and MR picker for Web IDE +merge_request: 20978 +author: +type: changed diff --git a/doc/user/project/web_ide/index.md b/doc/user/project/web_ide/index.md index b0143e45ab6..511ac2d7e79 100644 --- a/doc/user/project/web_ide/index.md +++ b/doc/user/project/web_ide/index.md @@ -59,9 +59,18 @@ left. > [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 -leaving the Web IDE. Click the project name in the top left to open a list of -merge requests. You will need to commit or discard all your changes before +leaving the Web IDE. Click the dropdown in the top of the sidebar to open a list +of merge requests. You will need to commit or discard all your changes before 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/ [ee]: https://about.gitlab.com/pricing/ diff --git a/lib/api/branches.rb b/lib/api/branches.rb index 4b223a391ae..3e445e6b1fa 100644 --- a/lib/api/branches.rb +++ b/lib/api/branches.rb @@ -19,6 +19,7 @@ module API params :filter_params do 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 diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 6c89186beaa..8bd46dedb7f 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -1930,6 +1930,9 @@ msgstr "" msgid "Cron syntax" msgstr "" +msgid "Current Branch" +msgstr "" + msgid "CurrentUser|Profile" msgstr "" @@ -2409,6 +2412,9 @@ msgstr "" msgid "Error loading branch data. Please try again." msgstr "" +msgid "Error loading branches." +msgstr "" + msgid "Error loading last commit." msgstr "" @@ -3605,6 +3611,9 @@ msgstr "" msgid "No assignee" msgstr "" +msgid "No branches found" +msgstr "" + msgid "No changes" msgstr "" @@ -6045,9 +6054,6 @@ msgstr "" msgid "You cannot write to this read-only GitLab instance." msgstr "" -msgid "You do not have any assigned merge requests" -msgstr "" - msgid "You don't have any applications" msgstr "" @@ -6057,9 +6063,6 @@ msgstr "" msgid "You have no permissions" msgstr "" -msgid "You have not created any merge requests" -msgstr "" - msgid "You have reached your project limit" msgstr "" diff --git a/spec/features/projects/tree/create_directory_spec.rb b/spec/features/projects/tree/create_directory_spec.rb index d3aa4912099..9e58280b868 100644 --- a/spec/features/projects/tree/create_directory_spec.rb +++ b/spec/features/projects/tree/create_directory_spec.rb @@ -22,7 +22,7 @@ describe 'Multi-file editor new directory', :js do end 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 find('.form-control').set('folder name') @@ -30,7 +30,7 @@ describe 'Multi-file editor new directory', :js do click_button('Create directory') end - first('.ide-tree-header button').click + first('.ide-tree-actions button').click page.within('.modal-dialog') do find('.form-control').set('file name') diff --git a/spec/features/projects/tree/create_file_spec.rb b/spec/features/projects/tree/create_file_spec.rb index f836783cbff..a04d3566a7e 100644 --- a/spec/features/projects/tree/create_file_spec.rb +++ b/spec/features/projects/tree/create_file_spec.rb @@ -22,7 +22,7 @@ describe 'Multi-file editor new file', :js do end it 'creates file in current directory' do - first('.ide-tree-header button').click + first('.ide-tree-actions button').click page.within('.modal') do find('.form-control').set('file name') diff --git a/spec/javascripts/helpers/vuex_action_helper.js b/spec/javascripts/helpers/vuex_action_helper.js index dd9174194a1..1972408356e 100644 --- a/spec/javascripts/helpers/vuex_action_helper.js +++ b/spec/javascripts/helpers/vuex_action_helper.js @@ -84,7 +84,7 @@ export default ( 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 => { setImmediate(resolve); diff --git a/spec/javascripts/ide/components/branches/item_spec.js b/spec/javascripts/ide/components/branches/item_spec.js new file mode 100644 index 00000000000..8b756c8f168 --- /dev/null +++ b/spec/javascripts/ide/components/branches/item_spec.js @@ -0,0 +1,53 @@ +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); + }); +}); diff --git a/spec/javascripts/ide/components/branches/search_list_spec.js b/spec/javascripts/ide/components/branches/search_list_spec.js new file mode 100644 index 00000000000..c3f84ba1c24 --- /dev/null +++ b/spec/javascripts/ide/components/branches/search_list_spec.js @@ -0,0 +1,79 @@ +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]); + }); + }); +}); diff --git a/spec/javascripts/ide/components/merge_requests/dropdown_spec.js b/spec/javascripts/ide/components/merge_requests/dropdown_spec.js deleted file mode 100644 index 74884c9a362..00000000000 --- a/spec/javascripts/ide/components/merge_requests/dropdown_spec.js +++ /dev/null @@ -1,47 +0,0 @@ -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'); - }); - }); -}); diff --git a/spec/javascripts/ide/components/merge_requests/item_spec.js b/spec/javascripts/ide/components/merge_requests/item_spec.js index 51c4cddef2f..750948cae3c 100644 --- a/spec/javascripts/ide/components/merge_requests/item_spec.js +++ b/spec/javascripts/ide/components/merge_requests/item_spec.js @@ -1,4 +1,5 @@ import Vue from 'vue'; +import router from '~/ide/ide_router'; import Item from '~/ide/components/merge_requests/item.vue'; import mountCompontent from '../../../helpers/vue_mount_component_helper'; @@ -27,6 +28,12 @@ describe('IDE merge request item', () => { 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', () => { expect(vm.$el.querySelector('.ic-mobile-issue-close')).not.toBe(null); }); @@ -50,12 +57,4 @@ describe('IDE merge request item', () => { done(); }); }); - - it('emits click event on click', () => { - spyOn(vm, '$emit'); - - vm.$el.click(); - - expect(vm.$emit).toHaveBeenCalledWith('click', vm.item); - }); }); diff --git a/spec/javascripts/ide/components/merge_requests/list_spec.js b/spec/javascripts/ide/components/merge_requests/list_spec.js index f4b393778dc..c761315444c 100644 --- a/spec/javascripts/ide/components/merge_requests/list_spec.js +++ b/spec/javascripts/ide/components/merge_requests/list_spec.js @@ -10,10 +10,7 @@ describe('IDE merge requests list', () => { let vm; beforeEach(() => { - vm = createComponentWithStore(Component, store, { - type: 'created', - emptyText: 'empty text', - }); + vm = createComponentWithStore(Component, store, {}); spyOn(vm, 'fetchMergeRequests'); @@ -28,13 +25,13 @@ describe('IDE merge requests list', () => { it('calls fetch on mounted', () => { expect(vm.fetchMergeRequests).toHaveBeenCalledWith({ - type: 'created', search: '', + type: '', }); }); it('renders loading icon', done => { - vm.$store.state.mergeRequests.created.isLoading = true; + vm.$store.state.mergeRequests.isLoading = true; vm.$nextTick(() => { expect(vm.$el.querySelector('.loading-container')).not.toBe(null); @@ -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 => { vm.search = 'testing'; @@ -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', () => { beforeEach(done => { - vm.$store.state.mergeRequests.created.mergeRequests.push({ + vm.$store.state.mergeRequests.mergeRequests.push({ ...mergeRequests[0], projectPathWithNamespace: 'gitlab-org/gitlab-ce', }); @@ -71,35 +84,6 @@ describe('IDE merge requests list', () => { expect(vm.$el.querySelectorAll('li').length).toBe(1); 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', () => { @@ -123,4 +107,52 @@ describe('IDE merge requests list', () => { 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); + }); + }); }); diff --git a/spec/javascripts/ide/components/nav_dropdown_button_spec.js b/spec/javascripts/ide/components/nav_dropdown_button_spec.js new file mode 100644 index 00000000000..0a58e260280 --- /dev/null +++ b/spec/javascripts/ide/components/nav_dropdown_button_spec.js @@ -0,0 +1,63 @@ +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); + }); +}); diff --git a/spec/javascripts/ide/components/nav_dropdown_spec.js b/spec/javascripts/ide/components/nav_dropdown_spec.js new file mode 100644 index 00000000000..af6665bcd62 --- /dev/null +++ b/spec/javascripts/ide/components/nav_dropdown_spec.js @@ -0,0 +1,50 @@ +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); + }); +}); diff --git a/spec/javascripts/ide/components/shared/tokened_input_spec.js b/spec/javascripts/ide/components/shared/tokened_input_spec.js new file mode 100644 index 00000000000..09940fe8c6a --- /dev/null +++ b/spec/javascripts/ide/components/shared/tokened_input_spec.js @@ -0,0 +1,132 @@ +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'); + }); +}); diff --git a/spec/javascripts/ide/helpers.js b/spec/javascripts/ide/helpers.js index 569fa5c7aae..c11c482fef8 100644 --- a/spec/javascripts/ide/helpers.js +++ b/spec/javascripts/ide/helpers.js @@ -4,6 +4,7 @@ import state from '~/ide/stores/state'; import commitState from '~/ide/stores/modules/commit/state'; import mergeRequestsState from '~/ide/stores/modules/merge_requests/state'; import pipelinesState from '~/ide/stores/modules/pipelines/state'; +import branchesState from '~/ide/stores/modules/branches/state'; export const resetStore = store => { const newState = { @@ -11,6 +12,7 @@ export const resetStore = store => { commit: commitState(), mergeRequests: mergeRequestsState(), pipelines: pipelinesState(), + branches: branchesState(), }; store.replaceState(newState); }; diff --git a/spec/javascripts/ide/mock_data.js b/spec/javascripts/ide/mock_data.js index 7be450a0df7..4fe826943b2 100644 --- a/spec/javascripts/ide/mock_data.js +++ b/spec/javascripts/ide/mock_data.js @@ -165,3 +165,33 @@ export const mergeRequests = [ 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, + }, +]; diff --git a/spec/javascripts/ide/stores/modules/branches/actions_spec.js b/spec/javascripts/ide/stores/modules/branches/actions_spec.js new file mode 100644 index 00000000000..a0fce578958 --- /dev/null +++ b/spec/javascripts/ide/stores/modules/branches/actions_spec.js @@ -0,0 +1,193 @@ +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, + ); + }); + }); + }); +}); diff --git a/spec/javascripts/ide/stores/modules/branches/mutations_spec.js b/spec/javascripts/ide/stores/modules/branches/mutations_spec.js new file mode 100644 index 00000000000..be91440f119 --- /dev/null +++ b/spec/javascripts/ide/stores/modules/branches/mutations_spec.js @@ -0,0 +1,51 @@ +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([]); + }); + }); +}); diff --git a/spec/javascripts/ide/stores/modules/merge_requests/actions_spec.js b/spec/javascripts/ide/stores/modules/merge_requests/actions_spec.js index d063f1ea860..62699143a91 100644 --- a/spec/javascripts/ide/stores/modules/merge_requests/actions_spec.js +++ b/spec/javascripts/ide/stores/modules/merge_requests/actions_spec.js @@ -8,9 +8,7 @@ import { receiveMergeRequestsSuccess, fetchMergeRequests, resetMergeRequests, - openMergeRequest, } from '~/ide/stores/modules/merge_requests/actions'; -import router from '~/ide/ide_router'; import { mergeRequests } from '../../../mock_data'; import testAction from '../../../../helpers/vuex_action_helper'; @@ -28,12 +26,12 @@ describe('IDE merge requests actions', () => { }); describe('requestMergeRequests', () => { - it('should should commit request', done => { + it('should commit request', done => { testAction( requestMergeRequests, - 'created', + null, mockedState, - [{ type: types.REQUEST_MERGE_REQUESTS, payload: 'created' }], + [{ type: types.REQUEST_MERGE_REQUESTS }], [], done, ); @@ -46,7 +44,7 @@ describe('IDE merge requests actions', () => { receiveMergeRequestsError, { type: 'created', search: '' }, mockedState, - [{ type: types.RECEIVE_MERGE_REQUESTS_ERROR, payload: 'created' }], + [{ type: types.RECEIVE_MERGE_REQUESTS_ERROR }], [ { type: 'setErrorMessage', @@ -67,12 +65,12 @@ describe('IDE merge requests actions', () => { it('should commit received data', done => { testAction( receiveMergeRequestsSuccess, - { type: 'created', data: 'data' }, + mergeRequests, mockedState, [ { type: types.RECEIVE_MERGE_REQUESTS_SUCCESS, - payload: { type: 'created', data: 'data' }, + payload: mergeRequests, }, ], [], @@ -129,11 +127,11 @@ describe('IDE merge requests actions', () => { mockedState, [], [ - { type: 'requestMergeRequests', payload: 'created' }, - { type: 'resetMergeRequests', payload: 'created' }, + { type: 'requestMergeRequests' }, + { type: 'resetMergeRequests' }, { type: 'receiveMergeRequestsSuccess', - payload: { type: 'created', data: mergeRequests }, + payload: mergeRequests, }, ], done, @@ -149,12 +147,12 @@ describe('IDE merge requests actions', () => { it('dispatches error', done => { testAction( fetchMergeRequests, - { type: 'created' }, + { type: 'created', search: '' }, mockedState, [], [ - { type: 'requestMergeRequests', payload: 'created' }, - { type: 'resetMergeRequests', payload: 'created' }, + { type: 'requestMergeRequests' }, + { type: 'resetMergeRequests' }, { type: 'receiveMergeRequestsError', payload: { type: 'created', search: '' } }, ], done, @@ -167,59 +165,12 @@ describe('IDE merge requests actions', () => { it('commits reset', done => { testAction( resetMergeRequests, - 'created', + null, mockedState, - [{ type: types.RESET_MERGE_REQUESTS, payload: 'created' }], + [{ type: types.RESET_MERGE_REQUESTS }], [], 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'); - }); - }); }); diff --git a/spec/javascripts/ide/stores/modules/merge_requests/mutations_spec.js b/spec/javascripts/ide/stores/modules/merge_requests/mutations_spec.js index ea03131d90d..664d3914564 100644 --- a/spec/javascripts/ide/stores/modules/merge_requests/mutations_spec.js +++ b/spec/javascripts/ide/stores/modules/merge_requests/mutations_spec.js @@ -12,29 +12,26 @@ describe('IDE merge requests mutations', () => { describe(types.REQUEST_MERGE_REQUESTS, () => { 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, () => { 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, () => { it('sets merge requests', () => { gon.gitlab_url = gl.TEST_HOST; - mutations[types.RECEIVE_MERGE_REQUESTS_SUCCESS](mockedState, { - type: 'created', - data: mergeRequests, - }); + mutations[types.RECEIVE_MERGE_REQUESTS_SUCCESS](mockedState, mergeRequests); - expect(mockedState.created.mergeRequests).toEqual([ + expect(mockedState.mergeRequests).toEqual([ { id: 1, iid: 1, @@ -50,9 +47,9 @@ describe('IDE merge requests mutations', () => { it('clears merge request array', () => { 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([]); }); }); }); diff --git a/spec/javascripts/vue_shared/components/dropdown/dropdown_button_spec.js b/spec/javascripts/vue_shared/components/dropdown/dropdown_button_spec.js index ba897f4660d..2796cd088c6 100644 --- a/spec/javascripts/vue_shared/components/dropdown/dropdown_button_spec.js +++ b/spec/javascripts/vue_shared/components/dropdown/dropdown_button_spec.js @@ -2,15 +2,15 @@ import Vue from '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 customLabel = 'Select project'; -const createComponent = config => { +const createComponent = (props, slots = {}) => { const Component = Vue.extend(dropdownButtonComponent); - return mountComponent(Component, config); + return mountComponentWithSlots(Component, { props, slots }); }; describe('DropdownButtonComponent', () => { @@ -65,5 +65,14 @@ describe('DropdownButtonComponent', () => { expect(dropdownIconEl).not.toBeNull(); 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'); + }); }); }); |