diff options
author | Paul Slaughter <pslaughter@gitlab.com> | 2018-07-20 17:04:57 -0500 |
---|---|---|
committer | Paul Slaughter <pslaughter@gitlab.com> | 2018-07-20 17:04:57 -0500 |
commit | c4b16c7ec9611da7b4388f6a989c59e1a1735f7c (patch) | |
tree | 529babb78dd30a6bb8bbf2e3f31b4268f6ca6e64 | |
parent | 133377147b893319db1353dd78322261ce92b59d (diff) | |
download | gitlab-ce-46165-web-ide-branch-picker.tar.gz |
wip: mock implementation46165-web-ide-branch-picker
16 files changed, 412 insertions, 104 deletions
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index 422becb7db8..01ed2473c3a 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -25,6 +25,7 @@ const Api = { commitPipelinesPath: '/:project_id/commit/:sha/pipelines', branchSinglePath: '/api/:version/projects/:id/repository/branches/:branch', createBranchPath: '/api/:version/projects/:id/repository/branches', + refsPath: '/:project_id/refs', group(groupId, callback) { const url = Api.buildUrl(Api.groupPath).replace(':id', groupId); @@ -260,6 +261,25 @@ const Api = { } return urlRoot + url.replace(':version', gon.api_version); }, + + projectRefs(projectId, search = '') { + const encodedProjectId = projectId + .split('/') + .map(fragment => encodeURIComponent(fragment)) + .join('/'); + + const url = Api.buildUrl(this.refsPath).replace(':project_id', encodedProjectId); + const sort = 'updated_desc'; + + return axios.get(url, { + params: { + sort, + search, + ref: 'master', + }, + }); + }, + }; export default Api; 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..01b55b05cc5 --- /dev/null +++ b/app/assets/javascripts/ide/components/branches/item.vue @@ -0,0 +1,41 @@ +<script> +import Icon from '~/vue_shared/components/icon.vue'; + +export default { + components: { + Icon, + }, + props: { + item: { + type: Object, + required: true, + }, + }, + computed: { + isActive() { + return false; + }, + }, + methods: { + clickItem() { + this.$emit('click', this.item); + }, + }, +}; +</script> + +<template> + <button + type="button" + class="btn-link d-flex align-items-center" + @click="clickItem" + > + <icon + name="branch" + css-classes="append-right-5" + /> + <span> + {{ item.name }} + </span> + </button> +</template> diff --git a/app/assets/javascripts/ide/components/branches/list.vue b/app/assets/javascripts/ide/components/branches/list.vue new file mode 100644 index 00000000000..2d987053fef --- /dev/null +++ b/app/assets/javascripts/ide/components/branches/list.vue @@ -0,0 +1,111 @@ +<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: { + + }, + 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', 'openBranch']), + loadBranches() { + this.fetchBranches({ type: this.type, search: this.search }); + }, + viewBranch(item) { + this.openBranch({ + projectPath: item.projectPathWithNamespace, + id: item.iid, + }); + }, + searchBranches: _.debounce(function debounceSearch() { + this.loadBranches(); + }, 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 + ref="searchInput" + :placeholder="__('Search branches')" + v-model="search" + type="search" + class="dropdown-input-field" + @input="searchBranches" + /> + <i + aria-hidden="true" + class="fa fa-search dropdown-input-search" + ></i> + </div> + <div class="dropdown-content ide-branches-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="branchName in branches" + :key="branchName" + > + <item + :item="{ name: branchName }" + :current-id="currentBranchId" + :current-project-id="currentProjectId" + @click="viewBranch" + /> + </li> + </template> + <li + v-else + class="ide-nav-list-empty d-flex align-items-center justify-content-center" + > + {{ __('No branches found') }} + </li> + </ul> + </div> + </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 21906674c4b..6561779e013 100644 --- a/app/assets/javascripts/ide/components/ide_side_bar.vue +++ b/app/assets/javascripts/ide/components/ide_side_bar.vue @@ -14,7 +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 NavDropdown from './nav_dropdown/dropdown.vue'; import { activityBarViews } from '../constants'; export default { @@ -34,12 +34,12 @@ export default { CommitForm, IdeReview, SuccessMessage, - MergeRequestDropdown, + NavDropdown, }, data() { return { showTooltip: false, - showMergeRequestsDropdown: false, + showNavDropdown: false, }; }, computed: { @@ -91,13 +91,13 @@ export default { $(this.$refs.mergeRequestDropdown) .on('show.bs.dropdown', () => { - this.toggleMergeRequestDropdown(); + this.toggleNavDropdown(); }).on('hide.bs.dropdown', () => { - this.toggleMergeRequestDropdown(); + this.toggleNavDropdown(); }); }, - toggleMergeRequestDropdown() { - this.showMergeRequestsDropdown = !this.showMergeRequestsDropdown; + toggleNavDropdown() { + this.showNavDropdown = !this.showNavDropdown; }, }, }; @@ -185,8 +185,8 @@ export default { name="chevron-down" /> </button> - <merge-request-dropdown - :show="showMergeRequestsDropdown" + <nav-dropdown + :show="showNavDropdown" /> </div> <div class="multi-file-commit-panel-inner-scroll"> 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/list.vue b/app/assets/javascripts/ide/components/merge_requests/list.vue index 19d3e48ee10..9e99e5578e6 100644 --- a/app/assets/javascripts/ide/components/merge_requests/list.vue +++ b/app/assets/javascripts/ide/components/merge_requests/list.vue @@ -1,31 +1,26 @@ <script> import { mapActions, mapGetters, mapState } from 'vuex'; import _ from 'underscore'; +import { __ } from '~/locale'; import LoadingIcon from '../../../vue_shared/components/loading_icon.vue'; import Item from './item.vue'; +const TYPE_CREATED = 'created'; +const TYPE_ASSIGNED = 'assigned'; + export default { components: { LoadingIcon, Item, }, - props: { - type: { - type: String, - required: true, - }, - emptyText: { - type: String, - required: true, - }, - }, data() { return { search: '', + type: TYPE_CREATED, }; }, computed: { - ...mapGetters('mergeRequests', ['getData']), + ...mapGetters('mergeRequests', ['getData', 'assignedData', 'createdData']), ...mapState(['currentMergeRequestId', 'currentProjectId']), data() { return this.getData(this.type); @@ -39,9 +34,28 @@ export default { hasMergeRequests() { return this.mergeRequests.length !== 0; }, + hasAssignedActive() { + return this.type === TYPE_ASSIGNED; + }, + hasCreatedActive() { + return this.type === TYPE_CREATED; + }, hasNoSearchResults() { return this.search !== '' && !this.hasMergeRequests; }, + emptyText() { + return this.type === TYPE_CREATED + ? __('You have not created any merge requests') + : this.type === TYPE_ASSIGNED + ? __('You are not assigned any merge requests') + : __('No merge requests could be found'); + }, + createdMergeRequestLength() { + return this.createdData.mergeRequests.length; + }, + assignedMergeRequestLength() { + return this.assignedData.mergeRequests.length; + }, }, watch: { isLoading: { @@ -54,7 +68,17 @@ export default { methods: { ...mapActions('mergeRequests', ['fetchMergeRequests', 'openMergeRequest']), loadMergeRequests() { - this.fetchMergeRequests({ type: this.type, search: this.search }); + this.fetchMergeRequests({ type: TYPE_ASSIGNED, search: this.search }); + this.fetchMergeRequests({ type: TYPE_CREATED, search: this.search }); + }, + setType(mrType) { + this.type = mrType; + }, + onCreatedButtonClick() { + this.setType(TYPE_CREATED); + }, + onAssignedButtonClick() { + this.setType(TYPE_ASSIGNED); }, viewMergeRequest(item) { this.openMergeRequest({ @@ -78,19 +102,49 @@ export default { <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> + <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="btn-group d-flex" + role="group" + style="margin-top: 1rem;" + > + <button + type="button" + class="btn col-6" + :class="{'btn-primary': hasCreatedActive}" + @click.prevent.stop="onCreatedButtonClick" + > + {{ __('Created by me') }} + <span class="badge badge-pill"> + {{ createdMergeRequestLength }} + </span> + </button> + <button + type="button" + class="btn col-6" + :class="{'btn-primary': hasAssignedActive}" + @click.prevent.stop="onAssignedButtonClick" + > + {{ __('Assigned to me') }} + <span class="badge badge-pill"> + {{ assignedMergeRequestLength }} + </span> + </button> + </div> + </div> </div> <div class="dropdown-content ide-merge-requests-dropdown-content d-flex"> <loading-icon @@ -117,7 +171,7 @@ export default { </template> <li v-else - class="ide-merge-requests-empty d-flex align-items-center justify-content-center" + class="ide-nav-list-empty d-flex align-items-center justify-content-center" > <template v-if="hasNoSearchResults"> {{ __('No merge requests found') }} diff --git a/app/assets/javascripts/ide/components/nav_dropdown/dropdown.vue b/app/assets/javascripts/ide/components/nav_dropdown/dropdown.vue new file mode 100644 index 00000000000..d215a782a74 --- /dev/null +++ b/app/assets/javascripts/ide/components/nav_dropdown/dropdown.vue @@ -0,0 +1,46 @@ +<script> +import { mapGetters } from 'vuex'; +import Tabs from '../../../vue_shared/components/tabs/tabs'; +import Tab from '../../../vue_shared/components/tabs/tab.vue'; +import MergeRequestsList from '../merge_requests/list.vue'; +import BranchesList from '../branches/list.vue'; + +export default { + components: { + Tabs, + Tab, + MergeRequestsList, + BranchesList, + }, + props: { + show: { + type: Boolean, + required: true, + }, + }, + computed: { + }, +}; +</script> + +<template> + <div class="dropdown-menu ide-nav-dropdown p-0"> + <tabs + v-if="show" + stop-propagation + > + <tab active> + <template slot="title"> + {{ __('Branches') }} + </template> + <branches-list /> + </tab> + <tab> + <template slot="title"> + {{ __('Merge Requests') }} + </template> + <merge-requests-list /> + </tab> + </tabs> + </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..0da2902b620 --- /dev/null +++ b/app/assets/javascripts/ide/stores/modules/branches/actions.js @@ -0,0 +1,55 @@ +import { __ } from '~/locale'; +import Api from '~/api'; +import router from '../../../ide_router'; +import * as types from './mutation_types'; +import * as rootTypes 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, rootState }, { search = '' }) => { + dispatch('requestBranches'); + dispatch('resetBranches'); + + return Api.projectRefs(rootState.currentProjectId, search) + .then(({ data }) => dispatch('receiveBranchesSuccess', data.Branches)) + .catch(() => dispatch('receiveBranchesError', { search })); +}; + +export const resetBranches = ({ commit }) => commit(types.RESET_BRANCHES); + +export const openBranch = ({ commit, dispatch }, { projectPath, id }) => { + commit(rootTypes.CLEAR_PROJECTS, null, { root: true }); + commit(rootTypes.RESET_OPEN_FILES, null, { root: true }); + dispatch('setCurrentBranchId', id, { 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}/edit/${id}`); +}; + +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..2475b115f2b --- /dev/null +++ b/app/assets/javascripts/ide/stores/modules/branches/mutations.js @@ -0,0 +1,18 @@ +/* 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; + }, + [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..9a572a14161 --- /dev/null +++ b/app/assets/javascripts/ide/stores/modules/branches/state.js @@ -0,0 +1,5 @@ +export default () => ({ + isLoading: false, + branches: [], + page: -1, +}); diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index 523fcb05a87..2ecce4b1e35 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -88,6 +88,10 @@ border-color: $border-dark; color: $color; } + + .badge.badge-pill { + color: $color; + } } @mixin btn-green { diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss index 8b1227b9131..7a2ca4f8db8 100644 --- a/app/assets/stylesheets/pages/repo.scss +++ b/app/assets/stylesheets/pages/repo.scss @@ -1108,8 +1108,8 @@ flex: 0 0 38px; } - .ide-merge-requests-dropdown.dropdown-menu { - width: 385px; + .ide-nav-dropdown.dropdown-menu { + width: 400px; max-height: initial; } } @@ -1256,7 +1256,7 @@ min-height: 60px; } -.ide-merge-requests-dropdown { +.ide-nav-dropdown { .nav-links li { width: 50%; padding-left: 0; @@ -1290,7 +1290,7 @@ min-width: 18px; } -.ide-merge-requests-empty { +.ide-nav-list-empty { height: 230px; } diff --git a/spec/javascripts/ide/components/merge_requests/dropdown_spec.js b/spec/javascripts/ide/components/nav_dropdown/dropdown_spec.js index 74884c9a362..93ea2ac106e 100644 --- a/spec/javascripts/ide/components/merge_requests/dropdown_spec.js +++ b/spec/javascripts/ide/components/nav_dropdown/dropdown_spec.js @@ -1,10 +1,10 @@ import Vue from 'vue'; import { createStore } from '~/ide/stores'; -import Dropdown from '~/ide/components/merge_requests/dropdown.vue'; +import Dropdown from '~/ide/components/nav_dropdown/dropdown.vue'; import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper'; import { mergeRequests } from '../../mock_data'; -describe('IDE merge requests dropdown', () => { +describe('IDE nav dropdown', () => { const Component = Vue.extend(Dropdown); let vm; |