Create Web IDE MR and branch picker
This commit is contained in:
parent
0e90f27ff7
commit
0d6e50d542
|
@ -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));
|
||||
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
<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-if="hasMergeRequests">
|
||||
<li
|
||||
v-for="item in mergeRequests"
|
||||
:key="item.id"
|
||||
>
|
||||
<item
|
||||
:item="item"
|
||||
:current-id="currentMergeRequestId"
|
||||
:current-project-id="currentProjectId"
|
||||
/>
|
||||
</li>
|
||||
</template>
|
||||
<li
|
||||
v-else
|
||||
class="ide-search-list-empty d-flex align-items-center justify-content-center"
|
||||
>
|
||||
{{ __('No merge requests found') }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ emptyText }}
|
||||
</template>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -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 () => {};
|
|
@ -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,
|
||||
};
|
|
@ -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';
|
|
@ -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 = [];
|
||||
},
|
||||
};
|
|
@ -0,0 +1,4 @@
|
|||
export default () => ({
|
||||
isLoading: false,
|
||||
branches: [],
|
||||
});
|
|
@ -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 () => {};
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
export const getData = state => type => state[type];
|
||||
|
||||
export const assignedData = state => state.assigned;
|
||||
export const createdData = state => state.created;
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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 = [];
|
||||
},
|
||||
};
|
||||
|
|
|
@ -1,13 +1,7 @@
|
|||
import { states } from './constants';
|
||||
|
||||
export default () => ({
|
||||
created: {
|
||||
isLoading: false,
|
||||
mergeRequests: [],
|
||||
},
|
||||
assigned: {
|
||||
isLoading: false,
|
||||
mergeRequests: [],
|
||||
},
|
||||
isLoading: false,
|
||||
mergeRequests: [],
|
||||
state: states.opened,
|
||||
});
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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;
|
||||
|
||||
/*
|
||||
|
|
|
@ -571,7 +571,8 @@
|
|||
margin-bottom: 10px;
|
||||
padding: 0 10px;
|
||||
|
||||
.fa {
|
||||
.fa,
|
||||
.input-icon {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 20px;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Create branch and MR picker for Web IDE
|
||||
merge_request: 20978
|
||||
author:
|
||||
type: changed
|
|
@ -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/
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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 ""
|
||||
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
];
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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([]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue