diff options
author | Filipa Lacerda <filipa@gitlab.com> | 2018-06-06 15:49:06 +0000 |
---|---|---|
committer | Filipa Lacerda <filipa@gitlab.com> | 2018-06-06 15:49:06 +0000 |
commit | 0dd7563b7c91141f545432e9082906ebb196a38d (patch) | |
tree | 251414680a4278bbe79835b828ec8783be133854 | |
parent | 6c5c383361f0e1cd8cfb9ca671a58abcdf4a6c0b (diff) | |
parent | 4223beff2539df96f5dcba56c6b57504d925ab6a (diff) | |
download | gitlab-ce-0dd7563b7c91141f545432e9082906ebb196a38d.tar.gz |
Merge branch 'ide-merge-requests-components' into 'master'
Show merge requests in IDE
Closes #45184
See merge request gitlab-org/gitlab-ce!19318
26 files changed, 821 insertions, 98 deletions
diff --git a/app/assets/javascripts/ide/components/ide_review.vue b/app/assets/javascripts/ide/components/ide_review.vue index 0c9ec3b00f0..99fa2465a84 100644 --- a/app/assets/javascripts/ide/components/ide_review.vue +++ b/app/assets/javascripts/ide/components/ide_review.vue @@ -11,17 +11,20 @@ export default { }, computed: { ...mapGetters(['currentMergeRequest']), - ...mapState(['viewer']), + ...mapState(['viewer', 'currentMergeRequestId']), showLatestChangesText() { - return !this.currentMergeRequest || this.viewer === viewerTypes.diff; + return !this.currentMergeRequestId || this.viewer === viewerTypes.diff; }, showMergeRequestText() { - return this.currentMergeRequest && this.viewer === viewerTypes.mr; + return this.currentMergeRequestId && this.viewer === viewerTypes.mr; + }, + mergeRequestId() { + return `!${this.currentMergeRequest.iid}`; }, }, mounted() { this.$nextTick(() => { - this.updateViewer(this.currentMergeRequest ? viewerTypes.mr : viewerTypes.diff); + this.updateViewer(this.currentMergeRequestId ? viewerTypes.mr : viewerTypes.diff); }); }, methods: { @@ -54,7 +57,11 @@ export default { </template> <template v-else-if="showMergeRequestText"> {{ __('Merge request') }} - (<a :href="currentMergeRequest.web_url">!{{ currentMergeRequest.iid }}</a>) + (<a + v-if="currentMergeRequest" + :href="currentMergeRequest.web_url" + v-text="mergeRequestId" + ></a>) </template> </div> </template> diff --git a/app/assets/javascripts/ide/components/ide_side_bar.vue b/app/assets/javascripts/ide/components/ide_side_bar.vue index 3f980203911..1dc2170edde 100644 --- a/app/assets/javascripts/ide/components/ide_side_bar.vue +++ b/app/assets/javascripts/ide/components/ide_side_bar.vue @@ -1,4 +1,5 @@ <script> +import $ from 'jquery'; import { mapState, mapGetters } from 'vuex'; import ProjectAvatarImage from '~/vue_shared/components/project_avatar/image.vue'; import Icon from '~/vue_shared/components/icon.vue'; @@ -13,6 +14,7 @@ import CommitSection from './repo_commit_section.vue'; import CommitForm from './commit_sidebar/form.vue'; import IdeReview from './ide_review.vue'; import SuccessMessage from './commit_sidebar/success_message.vue'; +import MergeRequestDropdown from './merge_requests/dropdown.vue'; import { activityBarViews } from '../constants'; export default { @@ -32,10 +34,12 @@ export default { CommitForm, IdeReview, SuccessMessage, + MergeRequestDropdown, }, data() { return { showTooltip: false, + showMergeRequestsDropdown: false, }; }, computed: { @@ -46,6 +50,7 @@ export default { 'changedFiles', 'stagedFiles', 'lastCommitMsg', + 'currentMergeRequestId', ]), ...mapGetters(['currentProject', 'someUncommitedChanges']), showSuccessMessage() { @@ -61,9 +66,39 @@ export default { watch: { currentBranchId() { this.$nextTick(() => { + if (!this.$refs.branchId) return; + this.showTooltip = this.$refs.branchId.scrollWidth > this.$refs.branchId.offsetWidth; }); }, + loading() { + this.$nextTick(() => { + this.addDropdownListeners(); + }); + }, + }, + mounted() { + this.addDropdownListeners(); + }, + beforeDestroy() { + $(this.$refs.mergeRequestDropdown) + .off('show.bs.dropdown') + .off('hide.bs.dropdown'); + }, + methods: { + addDropdownListeners() { + if (!this.$refs.mergeRequestDropdown) return; + + $(this.$refs.mergeRequestDropdown) + .on('show.bs.dropdown', () => { + this.toggleMergeRequestDropdown(); + }).on('hide.bs.dropdown', () => { + this.toggleMergeRequestDropdown(); + }); + }, + toggleMergeRequestDropdown() { + this.showMergeRequestsDropdown = !this.showMergeRequestsDropdown; + }, }, }; </script> @@ -88,9 +123,13 @@ export default { </div> </template> <template v-else> - <div class="context-header ide-context-header"> - <a - :href="currentProject.web_url" + <div + class="context-header ide-context-header dropdown" + ref="mergeRequestDropdown" + > + <button + type="button" + data-toggle="dropdown" > <div v-if="currentProject.avatar_url" @@ -114,19 +153,41 @@ export default { <div class="sidebar-context-title"> {{ currentProject.name }} </div> - <div - class="sidebar-context-title ide-sidebar-branch-title" - ref="branchId" - v-tooltip - :title="branchTooltipTitle" - > - <icon - name="branch" - css-classes="append-right-5" - />{{ currentBranchId }} + <div class="d-flex"> + <div + v-if="currentBranchId" + class="sidebar-context-title ide-sidebar-branch-title" + ref="branchId" + v-tooltip + :title="branchTooltipTitle" + > + <icon + name="branch" + css-classes="append-right-5" + />{{ currentBranchId }} + </div> + <div + v-if="currentMergeRequestId" + class="sidebar-context-title ide-sidebar-branch-title" + :class="{ + 'prepend-left-8': currentBranchId + }" + > + <icon + name="git-merge" + css-classes="append-right-5" + />!{{ currentMergeRequestId }} + </div> </div> </div> - </a> + <icon + class="ml-auto" + name="chevron-down" + /> + </button> + <merge-request-dropdown + :show="showMergeRequestsDropdown" + /> </div> <div class="multi-file-commit-panel-inner-scroll"> <component diff --git a/app/assets/javascripts/ide/components/ide_status_bar.vue b/app/assets/javascripts/ide/components/ide_status_bar.vue index 368a2995ed9..e40f137d998 100644 --- a/app/assets/javascripts/ide/components/ide_status_bar.vue +++ b/app/assets/javascripts/ide/components/ide_status_bar.vue @@ -35,9 +35,7 @@ export default { }, watch: { lastCommit() { - if (!this.isPollingInitialized) { - this.initPipelinePolling(); - } + this.initPipelinePolling(); }, }, mounted() { @@ -47,9 +45,8 @@ export default { if (this.intervalId) { clearInterval(this.intervalId); } - if (this.isPollingInitialized) { - this.stopPipelinePolling(); - } + + this.stopPipelinePolling(); }, methods: { ...mapActions('pipelines', ['fetchLatestPipeline', 'stopPipelinePolling']), @@ -59,8 +56,9 @@ export default { }, 1000); }, initPipelinePolling() { - this.fetchLatestPipeline(); - this.isPollingInitialized = true; + if (this.lastCommit) { + this.fetchLatestPipeline(); + } }, commitAgeUpdate() { if (this.lastCommit) { diff --git a/app/assets/javascripts/ide/components/merge_requests/dropdown.vue b/app/assets/javascripts/ide/components/merge_requests/dropdown.vue new file mode 100644 index 00000000000..8cc8345db2e --- /dev/null +++ b/app/assets/javascripts/ide/components/merge_requests/dropdown.vue @@ -0,0 +1,63 @@ +<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 + type="created" + :empty-text="__('You have not created any merge requests')" + /> + </tab> + <tab> + <template slot="title"> + {{ __('Assigned to me') }} + <span class="badge badge-pill"> + {{ assignedMergeRequestLength }} + </span> + </template> + <list + type="assigned" + :empty-text="__('You do not have any assigned merge requests')" + /> + </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 new file mode 100644 index 00000000000..b50fc8a3dbb --- /dev/null +++ b/app/assets/javascripts/ide/components/merge_requests/item.vue @@ -0,0 +1,63 @@ +<script> +import Icon from '../../../vue_shared/components/icon.vue'; + +export default { + components: { + Icon, + }, + props: { + item: { + type: Object, + required: true, + }, + currentId: { + type: String, + required: true, + }, + currentProjectId: { + type: String, + required: true, + }, + }, + computed: { + isActive() { + return ( + this.item.iid === parseInt(this.currentId, 10) && + this.currentProjectId === this.item.projectPathWithNamespace + ); + }, + pathWithID() { + return `${this.item.projectPathWithNamespace}!${this.item.iid}`; + }, + }, + methods: { + clickItem() { + this.$emit('click', this.item); + }, + }, +}; +</script> + +<template> + <button + type="button" + class="btn-link d-flex align-items-center" + @click="clickItem" + > + <span class="d-flex append-right-default ide-merge-request-current-icon"> + <icon + v-if="isActive" + name="mobile-issue-close" + :size="18" + /> + </span> + <span> + <strong> + {{ item.title }} + </strong> + <span class="ide-merge-request-project-path d-block mt-1"> + {{ pathWithID }} + </span> + </span> + </button> +</template> diff --git a/app/assets/javascripts/ide/components/merge_requests/list.vue b/app/assets/javascripts/ide/components/merge_requests/list.vue new file mode 100644 index 00000000000..5896e3a147d --- /dev/null +++ b/app/assets/javascripts/ide/components/merge_requests/list.vue @@ -0,0 +1,132 @@ +<script> +import { mapActions, mapGetters, mapState } from 'vuex'; +import _ from 'underscore'; +import LoadingIcon from '../../../vue_shared/components/loading_icon.vue'; +import Item from './item.vue'; + +export default { + components: { + LoadingIcon, + Item, + }, + props: { + type: { + type: String, + required: true, + }, + emptyText: { + type: String, + required: true, + }, + }, + data() { + return { + search: '', + }; + }, + computed: { + ...mapGetters('mergeRequests', ['getData']), + ...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; + }, + }, + watch: { + isLoading: { + handler: 'focusSearch', + }, + }, + mounted() { + this.loadMergeRequests(); + }, + methods: { + ...mapActions('mergeRequests', ['fetchMergeRequests', 'openMergeRequest']), + 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(); + }); + } + }, + }, +}; +</script> + +<template> + <div> + <div class="dropdown-input mt-3 pb-3 mb-0 border-bottom"> + <input + type="search" + class="dropdown-input-field" + :placeholder="__('Search merge requests')" + v-model="search" + @input="searchMergeRequests" + ref="searchInput" + /> + <i + aria-hidden="true" + class="fa fa-search dropdown-input-search" + ></i> + </div> + <div class="dropdown-content ide-merge-requests-dropdown-content d-flex"> + <loading-icon + class="mt-3 mb-3 align-self-center ml-auto mr-auto" + v-if="isLoading" + 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-if="hasNoSearchResults"> + {{ __('No merge requests found') }} + </template> + <template v-else> + {{ emptyText }} + </template> + </li> + </ul> + </div> + </div> +</template> diff --git a/app/assets/javascripts/ide/stores/actions/merge_request.js b/app/assets/javascripts/ide/stores/actions/merge_request.js index 5ec9bd661bb..edb20ff96fc 100644 --- a/app/assets/javascripts/ide/stores/actions/merge_request.js +++ b/app/assets/javascripts/ide/stores/actions/merge_request.js @@ -17,9 +17,7 @@ export const getMergeRequestData = ( mergeRequestId, mergeRequest: data, }); - if (!state.currentMergeRequestId) { - commit(types.SET_CURRENT_MERGE_REQUEST, mergeRequestId); - } + commit(types.SET_CURRENT_MERGE_REQUEST, mergeRequestId); resolve(data); }) .catch(() => { diff --git a/app/assets/javascripts/ide/stores/actions/project.js b/app/assets/javascripts/ide/stores/actions/project.js index 46af47d2f81..0b99bce4a8e 100644 --- a/app/assets/javascripts/ide/stores/actions/project.js +++ b/app/assets/javascripts/ide/stores/actions/project.js @@ -13,8 +13,7 @@ export const getProjectData = ({ commit, state }, { namespace, projectId, force .then(data => { commit(types.TOGGLE_LOADING, { entry: state }); commit(types.SET_PROJECT, { projectPath: `${namespace}/${projectId}`, project: data }); - if (!state.currentProjectId) - commit(types.SET_CURRENT_PROJECT, `${namespace}/${projectId}`); + commit(types.SET_CURRENT_PROJECT, `${namespace}/${projectId}`); resolve(data); }) .catch(() => { 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 d3050183bd3..5beb8fac71f 100644 --- a/app/assets/javascripts/ide/stores/modules/merge_requests/actions.js +++ b/app/assets/javascripts/ide/stores/modules/merge_requests/actions.js @@ -1,25 +1,42 @@ import { __ } from '../../../../locale'; import Api from '../../../../api'; import flash from '../../../../flash'; +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 }) => commit(types.REQUEST_MERGE_REQUESTS); -export const receiveMergeRequestsError = ({ commit }) => { +export const requestMergeRequests = ({ commit }, type) => + commit(types.REQUEST_MERGE_REQUESTS, type); +export const receiveMergeRequestsError = ({ commit }, type) => { flash(__('Error loading merge requests.')); - commit(types.RECEIVE_MERGE_REQUESTS_ERROR); + commit(types.RECEIVE_MERGE_REQUESTS_ERROR, type); }; -export const receiveMergeRequestsSuccess = ({ commit }, data) => - commit(types.RECEIVE_MERGE_REQUESTS_SUCCESS, data); +export const receiveMergeRequestsSuccess = ({ commit }, { type, data }) => + commit(types.RECEIVE_MERGE_REQUESTS_SUCCESS, { type, data }); -export const fetchMergeRequests = ({ dispatch, state: { scope, state } }, search = '') => { - dispatch('requestMergeRequests'); - dispatch('resetMergeRequests'); +export const fetchMergeRequests = ({ dispatch, state: { state } }, { type, search = '' }) => { + const scope = scopes[type]; + dispatch('requestMergeRequests', type); + dispatch('resetMergeRequests', type); Api.mergeRequests({ scope, state, search }) - .then(({ data }) => dispatch('receiveMergeRequestsSuccess', data)) - .catch(() => dispatch('receiveMergeRequestsError')); + .then(({ data }) => dispatch('receiveMergeRequestsSuccess', { type, data })) + .catch(() => dispatch('receiveMergeRequestsError', type)); }; -export const resetMergeRequests = ({ commit }) => commit(types.RESET_MERGE_REQUESTS); +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('pipelines/stopPipelinePolling', null, { root: true }); + dispatch('pipelines/clearEtagPoll', null, { root: true }); + dispatch('pipelines/resetLatestPipeline', null, { root: true }); + dispatch('setCurrentBranchId', '', { root: true }); + + router.push(`/project/${projectPath}/merge_requests/${id}`); +}; export default () => {}; diff --git a/app/assets/javascripts/ide/stores/modules/merge_requests/constants.js b/app/assets/javascripts/ide/stores/modules/merge_requests/constants.js index 64b7763f257..a7085c7d04c 100644 --- a/app/assets/javascripts/ide/stores/modules/merge_requests/constants.js +++ b/app/assets/javascripts/ide/stores/modules/merge_requests/constants.js @@ -1,6 +1,6 @@ export const scopes = { - assignedToMe: 'assigned-to-me', - createdByMe: 'created-by-me', + assigned: 'assigned-to-me', + created: 'created-by-me', }; export const states = { diff --git a/app/assets/javascripts/ide/stores/modules/merge_requests/getters.js b/app/assets/javascripts/ide/stores/modules/merge_requests/getters.js new file mode 100644 index 00000000000..8e2b234be8d --- /dev/null +++ b/app/assets/javascripts/ide/stores/modules/merge_requests/getters.js @@ -0,0 +1,4 @@ +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 04e7e0f08f1..2e6dfb420f4 100644 --- a/app/assets/javascripts/ide/stores/modules/merge_requests/index.js +++ b/app/assets/javascripts/ide/stores/modules/merge_requests/index.js @@ -1,5 +1,6 @@ import state from './state'; import * as actions from './actions'; +import * as getters from './getters'; import mutations from './mutations'; export default { @@ -7,4 +8,5 @@ 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 98102a68e08..971da0806bd 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) { - state.isLoading = true; + [types.REQUEST_MERGE_REQUESTS](state, type) { + state[type].isLoading = true; }, - [types.RECEIVE_MERGE_REQUESTS_ERROR](state) { - state.isLoading = false; + [types.RECEIVE_MERGE_REQUESTS_ERROR](state, type) { + state[type].isLoading = false; }, - [types.RECEIVE_MERGE_REQUESTS_SUCCESS](state, data) { - state.isLoading = false; - state.mergeRequests = data.map(mergeRequest => ({ + [types.RECEIVE_MERGE_REQUESTS_SUCCESS](state, { type, data }) { + state[type].isLoading = false; + state[type].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) { - state.mergeRequests = []; + [types.RESET_MERGE_REQUESTS](state, type) { + state[type].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 2947b686c1c..57eb6b04283 100644 --- a/app/assets/javascripts/ide/stores/modules/merge_requests/state.js +++ b/app/assets/javascripts/ide/stores/modules/merge_requests/state.js @@ -1,8 +1,13 @@ -import { scopes, states } from './constants'; +import { states } from './constants'; export default () => ({ - isLoading: false, - mergeRequests: [], - scope: scopes.assignedToMe, + created: { + isLoading: false, + mergeRequests: [], + }, + assigned: { + isLoading: false, + mergeRequests: [], + }, state: states.opened, }); diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/actions.js b/app/assets/javascripts/ide/stores/modules/pipelines/actions.js index 3de3e6d3376..0a4ea80c4c1 100644 --- a/app/assets/javascripts/ide/stores/modules/pipelines/actions.js +++ b/app/assets/javascripts/ide/stores/modules/pipelines/actions.js @@ -102,4 +102,7 @@ export const fetchJobTrace = ({ dispatch, state }) => { .catch(() => dispatch('receiveJobTraceError')); }; +export const resetLatestPipeline = ({ commit }) => + commit(types.RECEIVE_LASTEST_PIPELINE_SUCCESS, null); + export default () => {}; diff --git a/app/assets/javascripts/ide/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js index fbfb92105d6..99b315ac4db 100644 --- a/app/assets/javascripts/ide/stores/mutation_types.js +++ b/app/assets/javascripts/ide/stores/mutation_types.js @@ -68,3 +68,6 @@ export const TOGGLE_FILE_FINDER = 'TOGGLE_FILE_FINDER'; export const BURST_UNUSED_SEAL = 'BURST_UNUSED_SEAL'; export const SET_RIGHT_PANE = 'SET_RIGHT_PANE'; + +export const CLEAR_PROJECTS = 'CLEAR_PROJECTS'; +export const RESET_OPEN_FILES = 'RESET_OPEN_FILES'; diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js index eeaa7cb0ec3..48f1da4eccf 100644 --- a/app/assets/javascripts/ide/stores/mutations.js +++ b/app/assets/javascripts/ide/stores/mutations.js @@ -157,6 +157,12 @@ export default { [types.SET_LINKS](state, links) { Object.assign(state, { links }); }, + [types.CLEAR_PROJECTS](state) { + Object.assign(state, { projects: {}, trees: {} }); + }, + [types.RESET_OPEN_FILES](state) { + Object.assign(state, { openFiles: [] }); + }, ...projectMutations, ...mergeRequestMutation, ...fileMutations, diff --git a/app/assets/javascripts/vue_shared/components/tabs/tab.vue b/app/assets/javascripts/vue_shared/components/tabs/tab.vue index 2a35d6bc151..9b2f46186ac 100644 --- a/app/assets/javascripts/vue_shared/components/tabs/tab.vue +++ b/app/assets/javascripts/vue_shared/components/tabs/tab.vue @@ -26,6 +26,11 @@ export default { created() { this.isTab = true; }, + updated() { + if (this.$parent) { + this.$parent.$forceUpdate(); + } + }, }; </script> diff --git a/app/assets/javascripts/vue_shared/components/tabs/tabs.js b/app/assets/javascripts/vue_shared/components/tabs/tabs.js index 4362264caa5..9b9e4bb47bd 100644 --- a/app/assets/javascripts/vue_shared/components/tabs/tabs.js +++ b/app/assets/javascripts/vue_shared/components/tabs/tabs.js @@ -1,4 +1,11 @@ export default { + props: { + stopPropagation: { + type: Boolean, + required: false, + default: false, + }, + }, data() { return { currentIndex: 0, @@ -13,7 +20,12 @@ export default { this.tabs = this.$children.filter(child => child.isTab); this.currentIndex = this.tabs.findIndex(tab => tab.localActive); }, - setTab(index) { + setTab(e, index) { + if (this.stopPropagation) { + e.stopPropagation(); + e.preventDefault(); + } + this.tabs[this.currentIndex].localActive = false; this.tabs[index].localActive = true; @@ -36,7 +48,7 @@ export default { href: '#', }, on: { - click: () => this.setTab(i), + click: e => this.setTab(e, i), }, }, tab.$slots.title || tab.title, diff --git a/app/assets/stylesheets/framework/contextual_sidebar.scss b/app/assets/stylesheets/framework/contextual_sidebar.scss index 1a415e1b852..9cbaaa5dc8d 100644 --- a/app/assets/stylesheets/framework/contextual_sidebar.scss +++ b/app/assets/stylesheets/framework/contextual_sidebar.scss @@ -26,19 +26,25 @@ margin-right: 2px; width: $contextual-sidebar-width; - a { + > a, + > button { transition: padding $sidebar-transition-duration; font-weight: $gl-font-weight-bold; display: flex; + width: 100%; align-items: center; padding: 10px 16px 10px 10px; color: $gl-text-color; - } + background-color: transparent; + border: 0; + text-align: left; - &:hover, - a:hover { - background-color: $link-hover-background; - color: $gl-text-color; + &:hover, + &:focus { + background-color: $link-hover-background; + color: $gl-text-color; + outline: 0; + } } .avatar-container { diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss index ffa8d13b09c..3c7edb0d4bb 100644 --- a/app/assets/stylesheets/pages/repo.scss +++ b/app/assets/stylesheets/pages/repo.scss @@ -458,14 +458,10 @@ width: auto; margin-right: 0; - a { + > a, + > button { height: 60px; } - - a:hover, - a:focus { - text-decoration: none; - } } .projects-sidebar { @@ -1135,6 +1131,11 @@ .avatar { flex: 0 0 40px; } + + .ide-merge-requests-dropdown.dropdown-menu { + width: 385px; + max-height: initial; + } } .ide-sidebar-project-title { @@ -1143,6 +1144,10 @@ .sidebar-context-title { white-space: nowrap; } + + .ide-sidebar-branch-title { + min-width: 50px; + } } .ide-external-link { @@ -1274,3 +1279,52 @@ .ide-job-header { min-height: 60px; } + +.ide-merge-requests-dropdown { + .nav-links li { + width: 50%; + padding-left: 0; + padding-right: 0; + + a { + text-align: center; + + &:not(.active) { + background-color: $gray-light; + } + } + } + + .dropdown-input { + padding-left: $gl-padding; + padding-right: $gl-padding; + + .fa { + right: 26px; + } + } + + .btn-link { + padding-top: $gl-padding; + padding-bottom: $gl-padding; + } +} + +.ide-merge-request-current-icon { + min-width: 18px; +} + +.ide-merge-requests-empty { + height: 230px; +} + +.ide-merge-requests-dropdown-content { + min-height: 230px; + max-height: 470px; +} + +.ide-merge-request-project-path { + font-size: 12px; + line-height: 16px; + color: $gl-text-color-secondary; +} diff --git a/spec/javascripts/ide/components/merge_requests/dropdown_spec.js b/spec/javascripts/ide/components/merge_requests/dropdown_spec.js new file mode 100644 index 00000000000..74884c9a362 --- /dev/null +++ b/spec/javascripts/ide/components/merge_requests/dropdown_spec.js @@ -0,0 +1,47 @@ +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 new file mode 100644 index 00000000000..51c4cddef2f --- /dev/null +++ b/spec/javascripts/ide/components/merge_requests/item_spec.js @@ -0,0 +1,61 @@ +import Vue from 'vue'; +import Item from '~/ide/components/merge_requests/item.vue'; +import mountCompontent from '../../../helpers/vue_mount_component_helper'; + +describe('IDE merge request item', () => { + const Component = Vue.extend(Item); + let vm; + + beforeEach(() => { + vm = mountCompontent(Component, { + item: { + iid: 1, + projectPathWithNamespace: 'gitlab-org/gitlab-ce', + title: 'Merge request title', + }, + currentId: '1', + currentProjectId: 'gitlab-org/gitlab-ce', + }); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('renders merge requests data', () => { + expect(vm.$el.textContent).toContain('Merge request title'); + expect(vm.$el.textContent).toContain('gitlab-org/gitlab-ce!1'); + }); + + it('renders icon if ID matches currentId', () => { + expect(vm.$el.querySelector('.ic-mobile-issue-close')).not.toBe(null); + }); + + it('does not render icon if ID does not match currentId', done => { + vm.currentId = '2'; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.ic-mobile-issue-close')).toBe(null); + + done(); + }); + }); + + it('does not render icon if project ID does not match', done => { + vm.currentProjectId = 'test/test'; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.ic-mobile-issue-close')).toBe(null); + + 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 new file mode 100644 index 00000000000..f4b393778dc --- /dev/null +++ b/spec/javascripts/ide/components/merge_requests/list_spec.js @@ -0,0 +1,126 @@ +import Vue from 'vue'; +import store from '~/ide/stores'; +import List from '~/ide/components/merge_requests/list.vue'; +import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper'; +import { mergeRequests } from '../../mock_data'; +import { resetStore } from '../../helpers'; + +describe('IDE merge requests list', () => { + const Component = Vue.extend(List); + let vm; + + beforeEach(() => { + vm = createComponentWithStore(Component, store, { + type: 'created', + emptyText: 'empty text', + }); + + spyOn(vm, 'fetchMergeRequests'); + + vm.$mount(); + }); + + afterEach(() => { + vm.$destroy(); + + resetStore(vm.$store); + }); + + it('calls fetch on mounted', () => { + expect(vm.fetchMergeRequests).toHaveBeenCalledWith({ + type: 'created', + search: '', + }); + }); + + it('renders loading icon', done => { + vm.$store.state.mergeRequests.created.isLoading = true; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.loading-container')).not.toBe(null); + + done(); + }); + }); + + 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'; + + vm.$nextTick(() => { + expect(vm.$el.textContent).toContain('No merge requests found'); + + done(); + }); + }); + + describe('with merge requests', () => { + beforeEach(done => { + vm.$store.state.mergeRequests.created.mergeRequests.push({ + ...mergeRequests[0], + projectPathWithNamespace: 'gitlab-org/gitlab-ce', + }); + + vm.$nextTick(done); + }); + + it('renders 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', () => { + beforeEach(() => { + spyOn(vm, 'loadMergeRequests'); + + jasmine.clock().install(); + }); + + afterEach(() => { + jasmine.clock().uninstall(); + }); + + it('calls loadMergeRequests on input in search field', () => { + const event = new Event('input'); + + vm.$el.querySelector('input').dispatchEvent(event); + + jasmine.clock().tick(300); + + expect(vm.loadMergeRequests).toHaveBeenCalled(); + }); + }); +}); 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 b571cfb963a..d178a44b76a 100644 --- a/spec/javascripts/ide/stores/modules/merge_requests/actions_spec.js +++ b/spec/javascripts/ide/stores/modules/merge_requests/actions_spec.js @@ -8,7 +8,9 @@ import actions, { 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'; @@ -29,9 +31,9 @@ describe('IDE merge requests actions', () => { it('should should commit request', done => { testAction( requestMergeRequests, - null, + 'created', mockedState, - [{ type: types.REQUEST_MERGE_REQUESTS }], + [{ type: types.REQUEST_MERGE_REQUESTS, payload: 'created' }], [], done, ); @@ -48,16 +50,16 @@ describe('IDE merge requests actions', () => { it('should should commit error', done => { testAction( receiveMergeRequestsError, - null, + 'created', mockedState, - [{ type: types.RECEIVE_MERGE_REQUESTS_ERROR }], + [{ type: types.RECEIVE_MERGE_REQUESTS_ERROR, payload: 'created' }], [], done, ); }); it('creates flash message', () => { - receiveMergeRequestsError({ commit() {} }); + receiveMergeRequestsError({ commit() {} }, 'created'); expect(flashSpy).toHaveBeenCalled(); }); @@ -67,9 +69,14 @@ describe('IDE merge requests actions', () => { it('should commit received data', done => { testAction( receiveMergeRequestsSuccess, - 'data', + { type: 'created', data: 'data' }, mockedState, - [{ type: types.RECEIVE_MERGE_REQUESTS_SUCCESS, payload: 'data' }], + [ + { + type: types.RECEIVE_MERGE_REQUESTS_SUCCESS, + payload: { type: 'created', data: 'data' }, + }, + ], [], done, ); @@ -86,14 +93,14 @@ describe('IDE merge requests actions', () => { mock.onGet(/\/api\/v4\/merge_requests(.*)$/).replyOnce(200, mergeRequests); }); - it('calls API with params from state', () => { + it('calls API with params', () => { const apiSpy = spyOn(axios, 'get').and.callThrough(); - fetchMergeRequests({ dispatch() {}, state: mockedState }); + fetchMergeRequests({ dispatch() {}, state: mockedState }, { type: 'created' }); expect(apiSpy).toHaveBeenCalledWith(jasmine.anything(), { params: { - scope: 'assigned-to-me', + scope: 'created-by-me', state: 'opened', search: '', }, @@ -103,11 +110,14 @@ describe('IDE merge requests actions', () => { it('calls API with search', () => { const apiSpy = spyOn(axios, 'get').and.callThrough(); - fetchMergeRequests({ dispatch() {}, state: mockedState }, 'testing search'); + fetchMergeRequests( + { dispatch() {}, state: mockedState }, + { type: 'created', search: 'testing search' }, + ); expect(apiSpy).toHaveBeenCalledWith(jasmine.anything(), { params: { - scope: 'assigned-to-me', + scope: 'created-by-me', state: 'opened', search: 'testing search', }, @@ -117,7 +127,7 @@ describe('IDE merge requests actions', () => { it('dispatches request', done => { testAction( fetchMergeRequests, - null, + { type: 'created' }, mockedState, [], [ @@ -132,13 +142,16 @@ describe('IDE merge requests actions', () => { it('dispatches success with received data', done => { testAction( fetchMergeRequests, - null, + { type: 'created' }, mockedState, [], [ { type: 'requestMergeRequests' }, { type: 'resetMergeRequests' }, - { type: 'receiveMergeRequestsSuccess', payload: mergeRequests }, + { + type: 'receiveMergeRequestsSuccess', + payload: { type: 'created', data: mergeRequests }, + }, ], done, ); @@ -153,7 +166,7 @@ describe('IDE merge requests actions', () => { it('dispatches error', done => { testAction( fetchMergeRequests, - null, + { type: 'created' }, mockedState, [], [ @@ -171,12 +184,47 @@ describe('IDE merge requests actions', () => { it('commits reset', done => { testAction( resetMergeRequests, - null, + 'created', mockedState, - [{ type: types.RESET_MERGE_REQUESTS }], + [{ type: types.RESET_MERGE_REQUESTS, payload: 'created' }], [], done, ); }); }); + + describe('openMergeRequest', () => { + beforeEach(() => { + spyOn(router, 'push'); + }); + + it('commits reset mutations and actions', done => { + testAction( + openMergeRequest, + { projectPath: 'gitlab-org/gitlab-ce', id: '1' }, + mockedState, + [ + { type: 'CLEAR_PROJECTS' }, + { type: 'SET_CURRENT_MERGE_REQUEST', payload: '1' }, + { type: 'RESET_OPEN_FILES' }, + ], + [ + { type: 'pipelines/stopPipelinePolling' }, + { type: 'pipelines/clearEtagPoll' }, + { type: 'pipelines/resetLatestPipeline' }, + { type: 'setCurrentBranchId', payload: '' }, + ], + done, + ); + }); + + it('pushes new route', () => { + openMergeRequest( + { commit() {}, dispatch() {} }, + { 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 664d3914564..ea03131d90d 100644 --- a/spec/javascripts/ide/stores/modules/merge_requests/mutations_spec.js +++ b/spec/javascripts/ide/stores/modules/merge_requests/mutations_spec.js @@ -12,26 +12,29 @@ describe('IDE merge requests mutations', () => { describe(types.REQUEST_MERGE_REQUESTS, () => { it('sets loading to true', () => { - mutations[types.REQUEST_MERGE_REQUESTS](mockedState); + mutations[types.REQUEST_MERGE_REQUESTS](mockedState, 'created'); - expect(mockedState.isLoading).toBe(true); + expect(mockedState.created.isLoading).toBe(true); }); }); describe(types.RECEIVE_MERGE_REQUESTS_ERROR, () => { it('sets loading to false', () => { - mutations[types.RECEIVE_MERGE_REQUESTS_ERROR](mockedState); + mutations[types.RECEIVE_MERGE_REQUESTS_ERROR](mockedState, 'created'); - expect(mockedState.isLoading).toBe(false); + expect(mockedState.created.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, mergeRequests); + mutations[types.RECEIVE_MERGE_REQUESTS_SUCCESS](mockedState, { + type: 'created', + data: mergeRequests, + }); - expect(mockedState.mergeRequests).toEqual([ + expect(mockedState.created.mergeRequests).toEqual([ { id: 1, iid: 1, @@ -47,9 +50,9 @@ describe('IDE merge requests mutations', () => { it('clears merge request array', () => { mockedState.mergeRequests = ['test']; - mutations[types.RESET_MERGE_REQUESTS](mockedState); + mutations[types.RESET_MERGE_REQUESTS](mockedState, 'created'); - expect(mockedState.mergeRequests).toEqual([]); + expect(mockedState.created.mergeRequests).toEqual([]); }); }); }); |