Commit 985a972c authored by Igor Drozdov's avatar Igor Drozdov

Merge branch 'feature/add-arbitrary-commit-api-frontend' into 'master'

Modal for add/remove post review (context) commits

See merge request gitlab-org/gitlab!27402
parents b4665a80 a176d249
<script>
import { GlButton } from '@gitlab/ui';
import { s__ } from '~/locale';
import eventHub from '../event_hub';
export default {
components: {
GlButton,
},
props: {
commitsEmpty: {
type: Boolean,
required: false,
default: false,
},
contextCommitsEmpty: {
type: Boolean,
required: true,
},
},
computed: {
buttonText() {
return this.contextCommitsEmpty || this.commitsEmpty
? s__('AddContextCommits|Add previously merged commits')
: s__('AddContextCommits|Add/remove');
},
},
methods: {
openModal() {
eventHub.$emit('openModal');
},
},
};
</script>
<template>
<gl-button
:class="[
{
'ml-3': !contextCommitsEmpty,
'mt-3': !commitsEmpty && contextCommitsEmpty,
},
]"
:variant="commitsEmpty ? 'info' : 'default'"
@click="openModal"
>
{{ buttonText }}
</gl-button>
</template>
<script>
import { mapState, mapActions } from 'vuex';
import { GlModal, GlTabs, GlTab, GlSearchBoxByType, GlSprintf } from '@gitlab/ui';
import ReviewTabContainer from '~/add_context_commits_modal/components/review_tab_container.vue';
import { s__ } from '~/locale';
import eventHub from '../event_hub';
import createFlash from '~/flash';
import {
findCommitIndex,
setCommitStatus,
removeIfReadyToBeRemoved,
removeIfPresent,
} from '../utils';
export default {
components: {
GlModal,
GlTabs,
GlTab,
ReviewTabContainer,
GlSearchBoxByType,
GlSprintf,
},
props: {
contextCommitsPath: {
type: String,
required: true,
},
targetBranch: {
type: String,
required: true,
},
mergeRequestIid: {
type: Number,
required: true,
},
projectId: {
type: Number,
required: true,
},
},
computed: {
...mapState([
'tabIndex',
'isLoadingCommits',
'commits',
'commitsLoadingError',
'isLoadingContextCommits',
'contextCommits',
'contextCommitsLoadingError',
'selectedCommits',
'searchText',
'toRemoveCommits',
]),
currentTabIndex: {
get() {
return this.tabIndex;
},
set(newTabIndex) {
this.setTabIndex(newTabIndex);
},
},
selectedCommitsCount() {
return this.selectedCommits.filter(selectedCommit => selectedCommit.isSelected).length;
},
shouldPurge() {
return this.selectedCommitsCount !== this.selectedCommits.length;
},
uniqueCommits() {
return this.selectedCommits.filter(
selectedCommit =>
selectedCommit.isSelected &&
findCommitIndex(this.contextCommits, selectedCommit.short_id) === -1,
);
},
disableSaveButton() {
// We should have a minimum of one commit selected and that should not be in the context commits list or we should have a context commit to delete
return (
(this.selectedCommitsCount.length === 0 || this.uniqueCommits.length === 0) &&
this.toRemoveCommits.length === 0
);
},
},
watch: {
tabIndex(newTabIndex) {
this.handleTabChange(newTabIndex);
},
},
mounted() {
eventHub.$on('openModal', this.openModal);
this.setBaseConfig({
contextCommitsPath: this.contextCommitsPath,
mergeRequestIid: this.mergeRequestIid,
projectId: this.projectId,
});
},
beforeDestroy() {
eventHub.$off('openModal', this.openModal);
clearTimeout(this.timeout);
this.timeout = null;
},
methods: {
...mapActions([
'setBaseConfig',
'setTabIndex',
'searchCommits',
'setCommits',
'createContextCommits',
'fetchContextCommits',
'removeContextCommits',
'setSelectedCommits',
'setSearchText',
'setToRemoveCommits',
'resetModalState',
]),
focusSearch() {
this.$refs.searchInput.focusInput();
},
openModal() {
this.searchCommits();
this.fetchContextCommits();
this.$root.$emit('bv::show::modal', 'add-review-item');
},
handleTabChange(tabIndex) {
if (tabIndex === 0) {
this.focusSearch();
if (this.shouldPurge) {
this.setSelectedCommits(
[...this.commits, ...this.selectedCommits].filter(commit => commit.isSelected),
);
}
}
},
handleSearchCommits(value) {
// We only call the service, if we have 3 characters or we don't have any characters
if (value.length >= 3) {
clearTimeout(this.timeout);
this.timeout = setTimeout(() => {
this.searchCommits(value);
}, 500);
} else if (value.length === 0) {
this.searchCommits();
}
this.setSearchText(value);
},
handleCommitRowSelect(event) {
const index = event[0];
const selected = event[1];
const tempCommit = this.tabIndex === 0 ? this.commits[index] : this.selectedCommits[index];
const commitIndex = findCommitIndex(this.commits, tempCommit.short_id);
const tempCommits = setCommitStatus(this.commits, commitIndex, selected);
const selectedCommitIndex = findCommitIndex(this.selectedCommits, tempCommit.short_id);
let tempSelectedCommits = setCommitStatus(
this.selectedCommits,
selectedCommitIndex,
selected,
);
if (selected) {
// If user deselects a commit which is already present in previously merged commits, then user adds it again.
// Then the state is neutral, so we remove it from the list
this.setToRemoveCommits(
removeIfReadyToBeRemoved(this.toRemoveCommits, tempCommit.short_id),
);
} else {
// If user is present in first tab and deselects a commit, remove it directly
if (this.tabIndex === 0) {
tempSelectedCommits = removeIfPresent(tempSelectedCommits, tempCommit.short_id);
}
// If user deselects a commit which is already present in previously merged commits, we keep track of it in a list to remove
const contextCommitsIndex = findCommitIndex(this.contextCommits, tempCommit.short_id);
if (contextCommitsIndex !== -1) {
this.setToRemoveCommits([...this.toRemoveCommits, tempCommit.short_id]);
}
}
this.setCommits({ commits: tempCommits });
this.setSelectedCommits([
...tempSelectedCommits,
...tempCommits.filter(commit => commit.isSelected),
]);
},
handleCreateContextCommits() {
if (this.uniqueCommits.length > 0 && this.toRemoveCommits.length > 0) {
return Promise.all([
this.createContextCommits({ commits: this.uniqueCommits }),
this.removeContextCommits(),
]).then(values => {
if (values[0] || values[1]) {
window.location.reload();
}
if (!values[0] && !values[1]) {
createFlash(
s__('ContextCommits|Failed to create/remove context commits. Please try again.'),
);
}
});
} else if (this.uniqueCommits.length > 0) {
return this.createContextCommits({ commits: this.uniqueCommits, forceReload: true });
}
return this.removeContextCommits(true);
},
handleModalClose() {
this.resetModalState();
clearTimeout(this.timeout);
},
handleModalHide() {
this.resetModalState();
clearTimeout(this.timeout);
},
},
};
</script>
<template>
<gl-modal
ref="modal"
cancel-variant="light"
size="md"
body-class="add-review-item pt-0"
:scrollable="true"
:ok-title="__('Save changes')"
modal-id="add-review-item"
:title="__('Add or remove previously merged commits')"
:ok-disabled="disableSaveButton"
@shown="focusSearch"
@ok="handleCreateContextCommits"
@cancel="handleModalClose"
@close="handleModalClose"
@hide="handleModalHide"
>
<gl-tabs v-model="currentTabIndex" content-class="pt-0">
<gl-tab>
<template #title>
<gl-sprintf :message="__(`Commits in %{codeStart}${targetBranch}%{codeEnd}`)">
<template #code="{ content }">
<code>{{ content }}</code>
</template>
</gl-sprintf>
</template>
<div class="mt-2">
<gl-search-box-by-type
ref="searchInput"
:placeholder="__(`Search by commit title or SHA`)"
@input="handleSearchCommits"
/>
<review-tab-container
:is-loading="isLoadingCommits"
:loading-error="commitsLoadingError"
:loading-failed-text="__('Unable to load commits. Try again later.')"
:commits="commits"
:empty-list-text="__('Your search didn\'t match any commits. Try a different query.')"
@handleCommitSelect="handleCommitRowSelect"
/>
</div>
</gl-tab>
<gl-tab>
<template #title>
{{ __('Selected commits') }}
<span class="badge badge-pill">{{ selectedCommitsCount }}</span>
</template>
<review-tab-container
:is-loading="isLoadingContextCommits"
:loading-error="contextCommitsLoadingError"
:loading-failed-text="__('Unable to load commits. Try again later.')"
:commits="selectedCommits"
:empty-list-text="
__(
'Commits you select appear here. Go to the first tab and select commits to add to this merge request.',
)
"
@handleCommitSelect="handleCommitRowSelect"
/>
</gl-tab>
</gl-tabs>
</gl-modal>
</template>
<script>
import { GlLoadingIcon, GlAlert } from '@gitlab/ui';
import CommitItem from '~/diffs/components/commit_item.vue';
import { __ } from '~/locale';
export default {
components: {
GlLoadingIcon,
GlAlert,
CommitItem,
},
props: {
isLoading: {
type: Boolean,
required: true,
},
loadingError: {
type: Boolean,
required: true,
},
loadingFailedText: {
type: String,
required: true,
},
commits: {
type: Array,
required: true,
},
emptyListText: {
type: String,
required: false,
default: __('No commits present here'),
},
},
};
</script>
<template>
<gl-loading-icon v-if="isLoading" size="lg" class="mt-3" />
<gl-alert v-else-if="loadingError" variant="danger" :dismissible="false" class="mt-3">
{{ loadingFailedText }}
</gl-alert>
<div v-else-if="commits.length === 0" class="text-center mt-4">
<span>{{ emptyListText }}</span>
</div>
<div v-else>
<ul class="content-list commit-list flex-list">
<commit-item
v-for="(commit, index) in commits"
:key="commit.id"
:is-selectable="true"
:commit="commit"
:checked="commit.isSelected"
@handleCheckboxChange="$emit('handleCommitSelect', [index, $event])"
/>
</ul>
</div>
</template>
import createEventHub from '~/helpers/event_hub_factory';
export default createEventHub();
import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils';
import createStore from './store';
import AddContextCommitsModalTrigger from './components/add_context_commits_modal_trigger.vue';
import AddContextCommitsModalWrapper from './components/add_context_commits_modal_wrapper.vue';
export default function initAddContextCommitsTriggers() {
const addContextCommitsModalTriggerEl = document.querySelector('.add-review-item-modal-trigger');
const addContextCommitsModalWrapperEl = document.querySelector('.add-review-item-modal-wrapper');
if (addContextCommitsModalTriggerEl || addContextCommitsModalWrapperEl) {
// eslint-disable-next-line no-new
new Vue({
el: addContextCommitsModalTriggerEl,
data() {
const { commitsEmpty, contextCommitsEmpty } = this.$options.el.dataset;
return {
commitsEmpty: parseBoolean(commitsEmpty),
contextCommitsEmpty: parseBoolean(contextCommitsEmpty),
};
},
render(createElement) {
return createElement(AddContextCommitsModalTrigger, {
props: {
commitsEmpty: this.commitsEmpty,
contextCommitsEmpty: this.contextCommitsEmpty,
},
});
},
});
const store = createStore();
// eslint-disable-next-line no-new
new Vue({
el: addContextCommitsModalWrapperEl,
store,
data() {
const {
contextCommitsPath,
targetBranch,
mergeRequestIid,
projectId,
} = this.$options.el.dataset;
return {
contextCommitsPath,
targetBranch,
mergeRequestIid: Number(mergeRequestIid),
projectId: Number(projectId),
};
},
render(createElement) {
return createElement(AddContextCommitsModalWrapper, {
props: {
contextCommitsPath: this.contextCommitsPath,
targetBranch: this.targetBranch,
mergeRequestIid: this.mergeRequestIid,
projectId: this.projectId,
},
});
},
});
}
}
import _ from 'lodash';
import axios from '~/lib/utils/axios_utils';
import createFlash from '~/flash';
import { s__ } from '~/locale';
import Api from '~/api';
import * as types from './mutation_types';
export const setBaseConfig = ({ commit }, options) => {
commit(types.SET_BASE_CONFIG, options);
};
export const setTabIndex = ({ commit }, tabIndex) => commit(types.SET_TABINDEX, tabIndex);
export const searchCommits = ({ dispatch, commit, state }, searchText) => {
commit(types.FETCH_COMMITS);
let params = {};
if (searchText) {
params = {
params: {
search: searchText,
per_page: 40,
},
};
}
return axios
.get(state.contextCommitsPath, params)
.then(({ data }) => {
let commits = data.map(o => ({ ...o, isSelected: false }));
commits = commits.map(c => {
const isPresent = state.selectedCommits.find(
selectedCommit => selectedCommit.short_id === c.short_id && selectedCommit.isSelected,
);
if (isPresent) {
return { ...c, isSelected: true };
}
return c;
});
if (!searchText) {
dispatch('setCommits', { commits: [...commits, ...state.contextCommits] });
} else {
dispatch('setCommits', { commits });
}
})
.catch(() => {
commit(types.FETCH_COMMITS_ERROR);
});
};
export const setCommits = ({ commit }, { commits: data, silentAddition = false }) => {
let commits = _.uniqBy(data, 'short_id');
commits = _.orderBy(data, c => new Date(c.committed_date), ['desc']);
if (silentAddition) {
commit(types.SET_COMMITS_SILENT, commits);
} else {
commit(types.SET_COMMITS, commits);
}
};
export const createContextCommits = ({ state }, { commits, forceReload = false }) =>
Api.createContextCommits(state.projectId, state.mergeRequestIid, {
commits: commits.map(commit => commit.short_id),
})
.then(() => {
if (forceReload) {
window.location.reload();
}
return true;
})
.catch(() => {
if (forceReload) {
createFlash(s__('ContextCommits|Failed to create context commits. Please try again.'));
}
return false;
});
export const fetchContextCommits = ({ dispatch, commit, state }) => {
commit(types.FETCH_CONTEXT_COMMITS);
return Api.allContextCommits(state.projectId, state.mergeRequestIid)
.then(({ data }) => {
const contextCommits = data.map(o => ({ ...o, isSelected: true }));
dispatch('setContextCommits', contextCommits);
dispatch('setCommits', {
commits: [...state.commits, ...contextCommits],
silentAddition: true,
});
dispatch('setSelectedCommits', contextCommits);
})
.catch(() => {
commit(types.FETCH_CONTEXT_COMMITS_ERROR);
});
};
export const setContextCommits = ({ commit }, data) => {
commit(types.SET_CONTEXT_COMMITS, data);
};
export const removeContextCommits = ({ state }, forceReload = false) =>
Api.removeContextCommits(state.projectId, state.mergeRequestIid, {
commits: state.toRemoveCommits,
})
.then(() => {
if (forceReload) {
window.location.reload();
}
return true;
})
.catch(() => {
if (forceReload) {
createFlash(s__('ContextCommits|Failed to delete context commits. Please try again.'));
}
return false;
});
export const setSelectedCommits = ({ commit }, selected) => {
let selectedCommits = _.uniqBy(selected, 'short_id');
selectedCommits = _.orderBy(
selectedCommits,
selectedCommit => new Date(selectedCommit.committed_date),
['desc'],
);
commit(types.SET_SELECTED_COMMITS, selectedCommits);
};
export const setSearchText = ({ commit }, searchText) => commit(types.SET_SEARCH_TEXT, searchText);
export const setToRemoveCommits = ({ commit }, data) => commit(types.SET_TO_REMOVE_COMMITS, data);
export const resetModalState = ({ commit }) => commit(types.RESET_MODAL_STATE);
import Vue from 'vue';
import Vuex from 'vuex';
import * as actions from './actions';
import mutations from './mutations';
import state from './state';
Vue.use(Vuex);
export default () =>
new Vuex.Store({
namespaced: true,
state: state(),
actions,
mutations,
});
export const SET_BASE_CONFIG = 'SET_BASE_CONFIG';
export const SET_TABINDEX = 'SET_TABINDEX';
export const FETCH_COMMITS = 'FETCH_COMMITS';
export const SET_COMMITS = 'SET_COMMITS';
export const SET_COMMITS_SILENT = 'SET_COMMITS_SILENT';
export const FETCH_COMMITS_ERROR = 'FETCH_COMMITS_ERROR';
export const FETCH_CONTEXT_COMMITS = 'FETCH_CONTEXT_COMMITS';
export const SET_CONTEXT_COMMITS = 'SET_CONTEXT_COMMITS';
export const FETCH_CONTEXT_COMMITS_ERROR = 'FETCH_CONTEXT_COMMITS_ERROR';
export const SET_SELECTED_COMMITS = 'SET_SELECTED_COMMITS';
export const SET_SEARCH_TEXT = 'SET_SEARCH_TEXT';
export const SET_TO_REMOVE_COMMITS = 'SET_TO_REMOVE_COMMITS';
export const RESET_MODAL_STATE = 'RESET_MODAL_STATE';
import * as types from './mutation_types';
export default {
[types.SET_BASE_CONFIG](state, options) {
Object.assign(state, { ...options });
},
[types.SET_TABINDEX](state, tabIndex) {
state.tabIndex = tabIndex;
},
[types.FETCH_COMMITS](state) {
state.isLoadingCommits = true;
state.commitsLoadingError = false;
},
[types.SET_COMMITS](state, commits) {
state.commits = commits;
state.isLoadingCommits = false;
state.commitsLoadingError = false;
},
[types.SET_COMMITS_SILENT](state, commits) {
state.commits = commits;
},
[types.FETCH_COMMITS_ERROR](state) {
state.commitsLoadingError = true;
state.isLoadingCommits = false;
},
[types.FETCH_CONTEXT_COMMITS](state) {
state.isLoadingContextCommits = true;
state.contextCommitsLoadingError = false;
},
[types.SET_CONTEXT_COMMITS](state, contextCommits) {
state.contextCommits = contextCommits;
state.isLoadingContextCommits = false;
state.contextCommitsLoadingError = false;
},
[types.FETCH_CONTEXT_COMMITS_ERROR](state) {
state.contextCommitsLoadingError = true;
state.isLoadingContextCommits = false;
},
[types.SET_SELECTED_COMMITS](state, commits) {
state.selectedCommits = commits;
},
[types.SET_SEARCH_TEXT](state, searchText) {
state.searchText = searchText;
},
[types.SET_TO_REMOVE_COMMITS](state, commits) {
state.toRemoveCommits = commits;
},
[types.RESET_MODAL_STATE](state) {
state.tabIndex = 0;
state.commits = [];
state.contextCommits = [];
state.selectedCommits = [];
state.toRemoveCommits = [];
state.searchText = '';
},
};
export default () => ({
contextCommitsPath: '',
tabIndex: 0,
isLoadingCommits: false,
commits: [],
commitsLoadingError: false,
selectedCommits: [],
isLoadingContextCommits: false,
contextCommits: [],
contextCommitsLoadingError: false,
searchText: '',
toRemoveCommits: [],
});
export const findCommitIndex = (commits, commitShortId) => {
return commits.findIndex(commit => commit.short_id === commitShortId);
};
export const setCommitStatus = (commits, commitIndex, selected) => {
const tempCommits = [...commits];
tempCommits[commitIndex] = {
...tempCommits[commitIndex],
isSelected: selected,
};
return tempCommits;
};
export const removeIfReadyToBeRemoved = (toRemoveCommits, commitShortId) => {
const tempToRemoveCommits = [...toRemoveCommits];
const isPresentInToRemove = tempToRemoveCommits.indexOf(commitShortId);
if (isPresentInToRemove !== -1) {
tempToRemoveCommits.splice(isPresentInToRemove, 1);
}
return tempToRemoveCommits;
};
export const removeIfPresent = (selectedCommits, commitShortId) => {
const tempSelectedCommits = [...selectedCommits];
const selectedCommitsIndex = findCommitIndex(tempSelectedCommits, commitShortId);
if (selectedCommitsIndex !== -1) {
tempSelectedCommits.splice(selectedCommitsIndex, 1);
}
return tempSelectedCommits;
};
......@@ -57,6 +57,8 @@ const Api = {
pipelinesPath: '/api/:version/projects/:id/pipelines/',
createPipelinePath: '/api/:version/projects/:id/pipeline',
environmentsPath: '/api/:version/projects/:id/environments',
contextCommitsPath:
'/api/:version/projects/:id/merge_requests/:merge_request_iid/context_commits',
rawFilePath: '/api/:version/projects/:id/repository/files/:path/raw',
issuePath: '/api/:version/projects/:id/issues/:issue_iid',
tagsPath: '/api/:version/projects/:id/repository/tags',
......@@ -598,6 +600,30 @@ const Api = {
return axios.get(url);
},
createContextCommits(id, mergeRequestIid, data) {
const url = Api.buildUrl(this.contextCommitsPath)
.replace(':id', encodeURIComponent(id))
.replace(':merge_request_iid', mergeRequestIid);
return axios.post(url, data);
},
allContextCommits(id, mergeRequestIid) {
const url = Api.buildUrl(this.contextCommitsPath)
.replace(':id', encodeURIComponent(id))
.replace(':merge_request_iid', mergeRequestIid);
return axios.get(url);
},
removeContextCommits(id, mergeRequestIid, data) {
const url = Api.buildUrl(this.contextCommitsPath)
.replace(':id', id)
.replace(':merge_request_iid', mergeRequestIid);
return axios.delete(url, { data });
},
getRawFile(id, path, params = { ref: 'master' }) {
const url = Api.buildUrl(this.rawFilePath)
.replace(':id', encodeURIComponent(id))
......
......@@ -52,10 +52,20 @@ export default {
},
mixins: [glFeatureFlagsMixin()],
props: {
isSelectable: {
type: Boolean,
required: false,
default: false,
},
commit: {
type: Object,
required: true,
},
checked: {
type: Boolean,
required: false,
default: false,
},
collapsible: {
type: Boolean,
required: false,
......@@ -83,6 +93,10 @@ export default {
authorAvatar() {
return this.author.avatar_url || this.commit.author_gravatar_url;
},
commitDescription() {
// Strip the newline at the beginning
return this.commit.description_html.replace(/^&#x000A;/, '');
},
nextCommitUrl() {
return this.commit.next_commit_id
? setUrlParams({ commit_id: this.commit.next_commit_id })
......@@ -110,13 +124,22 @@ export default {
<template>
<li :class="{ 'js-toggle-container': collapsible }" class="commit flex-row">
<user-avatar-link
:link-href="authorUrl"
:img-src="authorAvatar"
:img-alt="authorName"
:img-size="40"
class="avatar-cell d-none d-sm-block"
/>
<div class="d-flex align-items-center align-self-start">
<input
v-if="isSelectable"
class="mr-2"
type="checkbox"
:checked="checked"
@change="$emit('handleCheckboxChange', $event.target.checked)"
/>
<user-avatar-link
:link-href="authorUrl"
:img-src="authorAvatar"
:img-alt="authorName"
:img-size="40"
class="avatar-cell d-none d-sm-block"
/>
</div>
<div class="commit-detail flex-list">
<div class="commit-content qa-commit-content">
<a
......@@ -151,7 +174,7 @@ export default {
v-if="commit.description_html"
:class="{ 'js-toggle-content': collapsible, 'd-block': !collapsible }"
class="commit-row-description gl-mb-3 text-dark"
v-html="commit.description_html"
v-html="commitDescription"
></pre>
</div>
<div class="commit-actions flex-row d-none d-sm-flex">
......
......@@ -21,6 +21,7 @@ import { localTimeAgo } from './lib/utils/datetime_utility';
import syntaxHighlight from './syntax_highlight';
import Notes from './notes';
import { polyfillSticky } from './lib/utils/sticky';
import initAddContextCommitsTriggers from './add_context_commits_modal';
import { __ } from './locale';
// MergeRequestTabs
......@@ -340,6 +341,7 @@ export default class MergeRequestTabs {
this.scrollToElement('#commits');
this.toggleLoading(false);
initAddContextCommitsTriggers();
})
.catch(() => {
this.toggleLoading(false);
......
......@@ -388,3 +388,9 @@
display: block;
color: $link-color;
}
.add-review-item {
.gl-tab-nav-item {
height: 100%;
}
}
......@@ -82,7 +82,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
@note = @project.notes.new(noteable: @merge_request)
@noteable = @merge_request
@commits_count = @merge_request.commits_count
@commits_count = @merge_request.commits_count + @merge_request.context_commits_count
@issuable_sidebar = serializer.represent(@merge_request, serializer: 'sidebar')
@current_user_data = UserSerializer.new(project: @project).represent(current_user, {}, MergeRequestUserEntity).to_json
@show_whitespace_default = current_user.nil? || current_user.show_whitespace_in_diffs
......@@ -116,6 +116,12 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
end
def commits
# Get context commits from repository
@context_commits =
set_commits_for_rendering(
@merge_request.recent_context_commits
)
# Get commits from repository
# or from cache if already merged
@commits =
......
......@@ -25,7 +25,7 @@ class ContextCommitsFinder
if search.present?
search_commits
else
project.repository.commits(merge_request.source_branch, { limit: limit, offset: offset })
project.repository.commits(merge_request.target_branch, { limit: limit, offset: offset })
end
commits
......@@ -47,7 +47,7 @@ class ContextCommitsFinder
commits = [commit_by_sha] if commit_by_sha
end
else
commits = project.repository.find_commits_by_message(search, nil, nil, 20)
commits = project.repository.find_commits_by_message(search, merge_request.target_branch, nil, 20)
end
commits
......
......@@ -40,7 +40,7 @@ class MergeRequest < ApplicationRecord
has_internal_id :iid, scope: :target_project, track_if: -> { !importing? }, init: ->(s) { s&.target_project&.merge_requests&.maximum(:iid) }
has_many :merge_request_diffs
has_many :merge_request_context_commits
has_many :merge_request_context_commits, inverse_of: :merge_request
has_many :merge_request_context_commit_diff_files, through: :merge_request_context_commits, source: :diff_files
has_one :merge_request_diff,
......@@ -427,7 +427,7 @@ class MergeRequest < ApplicationRecord
end
def context_commits(limit: nil)
@context_commits ||= merge_request_context_commits.limit(limit).map(&:to_commit)
@context_commits ||= merge_request_context_commits.order_by_committed_date_desc.limit(limit).map(&:to_commit)
end
def recent_context_commits
......
......@@ -12,6 +12,9 @@ class MergeRequestContextCommit < ApplicationRecord
validates :sha, presence: true
validates :sha, uniqueness: { message: 'has already been added' }
# Sort by committed date in descending order to ensure latest commits comes on the top
scope :order_by_committed_date_desc, -> { order('committed_date DESC') }
# delete all MergeRequestContextCommit & MergeRequestContextCommitDiffFile for given merge_request & commit SHAs
def self.delete_bulk(merge_request, commits)
commit_ids = commits.map(&:sha)
......
- merge_request = local_assigns.fetch(:merge_request, nil)
- project = local_assigns.fetch(:project) { merge_request&.project }
- ref = local_assigns.fetch(:ref) { merge_request&.source_branch }
- can_update_merge_request = can?(current_user, :update_merge_request, @merge_request)
- commits = @commits
- context_commits = @context_commits
- hidden = @hidden_commit_count
- commits.chunk { |c| c.committed_date.in_time_zone.to_date }.each do |day, daily_commits|
......@@ -14,11 +16,26 @@
%ul.content-list.commit-list.flex-list
= render partial: 'projects/commits/commit', collection: daily_commits, locals: { project: project, ref: ref, merge_request: merge_request }
- if context_commits.present?
%li.commit-header.js-commit-header
%span.font-weight-bold= n_("%d previously merged commit", "%d previously merged commits", context_commits.count) % context_commits.count
- if project.context_commits_enabled? && can_update_merge_request
%button.btn.btn-default.ml-3.add-review-item-modal-trigger{ type: "button", data: { context_commits_empty: 'false' } }
= _('Add/remove')
%li.commits-row
%ul.content-list.commit-list.flex-list
= render partial: 'projects/commits/commit', collection: context_commits, locals: { project: project, ref: ref, merge_request: merge_request }
- if hidden > 0
%li.alert.alert-warning
= n_('%s additional commit has been omitted to prevent performance issues.', '%s additional commits have been omitted to prevent performance issues.', hidden) % number_with_delimiter(hidden)
- if commits.size == 0
- if project.context_commits_enabled? && can_update_merge_request && context_commits&.empty?
%button.btn.btn-default.mt-3.add-review-item-modal-trigger{ type: "button", data: { context_commits_empty: 'true' } }
= _('Add previously merged commits')
- if commits.size == 0 && context_commits.nil?
.mt-4.text-center
.bold
= _('Your search didn\'t match any commits.')
......
- if @commits.empty?
.commits-empty
%h4
There are no commits yet.
- can_update_merge_request = can?(current_user, :update_merge_request, @merge_request)
- if @commits.empty? && @context_commits.empty?
.commits-empty.mt-5
= custom_icon ('illustration_no_commits')
%h4
= _('There are no commits yet.')
- if @project&.context_commits_enabled? && can_update_merge_request
%p
= _('Push commits to the source branch or add previously merged commits to review them.')
%button.btn.btn-primary.add-review-item-modal-trigger{ type: "button", data: { commits_empty: 'true', context_commits_empty: 'true' } }
= _('Add previously merged commits')
- else
%ol#commits-list.list-unstyled
= render "projects/commits/commits", merge_request: @merge_request
- if @project&.context_commits_enabled? && can_update_merge_request && @merge_request.iid
.add-review-item-modal-wrapper{ data: { context_commits_path: context_commits_project_json_merge_request_url(@merge_request&.project, @merge_request, :json), target_branch: @merge_request.target_branch, merge_request_iid: @merge_request.iid, project_id: @merge_request.project.id } }
......@@ -239,6 +239,11 @@ msgid_plural "%d personal projects will be removed and cannot be restored."
msgstr[0] ""
msgstr[1] ""
msgid "%d previously merged commit"
msgid_plural "%d previously merged commits"
msgstr[0] ""
msgstr[1] ""
msgid "%d project"
msgid_plural "%d projects"
msgstr[0] ""
......@@ -1528,9 +1533,15 @@ msgstr ""
msgid "Add new directory"
msgstr ""
msgid "Add or remove previously merged commits"
msgstr ""
msgid "Add or subtract spent time"
msgstr ""
msgid "Add previously merged commits"
msgstr ""
msgid "Add reaction"
msgstr ""
......@@ -1576,6 +1587,15 @@ msgstr ""
msgid "Add webhook"
msgstr ""
msgid "Add/remove"
msgstr ""
msgid "AddContextCommits|Add previously merged commits"
msgstr ""
msgid "AddContextCommits|Add/remove"
msgstr ""
msgid "AddMember|No users specified."
msgstr ""
......@@ -6140,6 +6160,9 @@ msgstr ""
msgid "Commits to"
msgstr ""
msgid "Commits you select appear here. Go to the first tab and select commits to add to this merge request."
msgstr ""
msgid "Commits|An error occurred while fetching merge requests data."
msgstr ""
......@@ -6641,6 +6664,15 @@ msgstr ""
msgid "Contents of .gitlab-ci.yml"
msgstr ""
msgid "ContextCommits|Failed to create context commits. Please try again."
msgstr ""
msgid "ContextCommits|Failed to create/remove context commits. Please try again."
msgstr ""
msgid "ContextCommits|Failed to delete context commits. Please try again."
msgstr ""
msgid "Continue"
msgstr ""
......@@ -16040,6 +16072,9 @@ msgstr ""
msgid "No child epics match applied filters"
msgstr ""
msgid "No commits present here"
msgstr ""
msgid "No connection could be made to a Gitaly Server, please check your logs!"
msgstr ""
......@@ -19567,6 +19602,9 @@ msgstr ""
msgid "Push an existing folder"
msgstr ""
msgid "Push commits to the source branch or add previously merged commits to review them."
msgstr ""
msgid "Push events"
msgstr ""
......@@ -20935,6 +20973,9 @@ msgstr ""
msgid "Search by author"
msgstr ""
msgid "Search by commit title or SHA"
msgstr ""
msgid "Search by message"
msgstr ""
......@@ -21585,6 +21626,9 @@ msgstr ""
msgid "Select user"
msgstr ""
msgid "Selected commits"
msgstr ""
msgid "Selected levels cannot be used by non-admin users for groups, projects or snippets. If the public level is restricted, user profiles are only visible to logged in users."
msgstr ""
......@@ -24152,6 +24196,9 @@ msgstr ""
msgid "There are no closed merge requests"
msgstr ""
msgid "There are no commits yet."
msgstr ""
msgid "There are no custom project templates set up for this GitLab instance. They are enabled from GitLab's Admin Area. Contact your GitLab instance administrator to setup custom project templates."
msgstr ""
......@@ -25698,6 +25745,9 @@ msgstr ""
msgid "Unable to generate new instance ID"
msgstr ""
msgid "Unable to load commits. Try again later."
msgstr ""
msgid "Unable to load file contents. Try again later."
msgstr ""
......@@ -28044,6 +28094,9 @@ msgstr ""
msgid "Your search didn't match any commits."
msgstr ""
msgid "Your search didn't match any commits. Try a different query."
msgstr ""
msgid "Your subscription expired!"
msgstr ""
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AddContextCommitsModal renders modal with 2 tabs 1`] = `
<gl-modal-stub
body-class="add-review-item pt-0"
cancel-variant="light"
modalclass=""
modalid="add-review-item"
ok-disabled="true"
ok-title="Save changes"
scrollable="true"
size="md"
title="Add or remove previously merged commits"
titletag="h4"
>
<gl-tabs-stub
contentclass="pt-0"
theme="indigo"
value="0"
>
<gl-tab-stub>
<div
class="mt-2"
>
<gl-search-box-by-type-stub
clearbuttontitle="Clear"
placeholder="Search by commit title or SHA"
value=""
/>
<review-tab-container-stub
commits=""
emptylisttext="Your search didn't match any commits. Try a different query."
loadingfailedtext="Unable to load commits. Try again later."
/>
</div>
</gl-tab-stub>
<gl-tab-stub>
<review-tab-container-stub
commits=""
emptylisttext="Commits you select appear here. Go to the first tab and select commits to add to this merge request."
loadingfailedtext="Unable to load commits. Try again later."
/>
</gl-tab-stub>
</gl-tabs-stub>
</gl-modal-stub>
`;
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import { GlModal, GlSearchBoxByType } from '@gitlab/ui';
import AddReviewItemsModal from '~/add_context_commits_modal/components/add_context_commits_modal_wrapper.vue';
import getDiffWithCommit from '../../diffs/mock_data/diff_with_commit';
import defaultState from '~/add_context_commits_modal/store/state';
import mutations from '~/add_context_commits_modal/store/mutations';
import * as actions from '~/add_context_commits_modal/store/actions';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('AddContextCommitsModal', () => {
let wrapper;
let store;
const createContextCommits = jest.fn();
const removeContextCommits = jest.fn();
const resetModalState = jest.fn();
const searchCommits = jest.fn();
const { commit } = getDiffWithCommit();
const createWrapper = (props = {}) => {
store = new Vuex.Store({
mutations,
state: {
...defaultState(),
},
actions: {
...actions,
searchCommits,
createContextCommits,
removeContextCommits,
resetModalState,
},
});
wrapper = shallowMount(AddReviewItemsModal, {
localVue,
store,
propsData: {
contextCommitsPath: '',
targetBranch: 'master',
mergeRequestIid: 1,
projectId: 1,
...props,
},
});
return wrapper;
};
const findModal = () => wrapper.find(GlModal);
const findSearch = () => wrapper.find(GlSearchBoxByType);
beforeEach(() => {
wrapper = createWrapper();
});
afterEach(() => {
wrapper.destroy();
});
it('renders modal with 2 tabs', () => {
expect(wrapper.element).toMatchSnapshot();
});
it('an ok button labeled "Save changes"', () => {
expect(findModal().attributes('ok-title')).toEqual('Save changes');
});
describe('when in first tab, renders a modal with', () => {
it('renders the search box component', () => {
expect(findSearch().exists()).toBe(true);
});
it('when user starts entering text in search box, it calls action "searchCommits" after waiting for 500s', () => {
const searchText = 'abcd';
findSearch().vm.$emit('input', searchText);
expect(searchCommits).not.toBeCalled();
jest.advanceTimersByTime(500);
expect(searchCommits).toHaveBeenCalledWith(expect.anything(), searchText, undefined);
});
it('disabled ok button when no row is selected', () => {
expect(findModal().attributes('ok-disabled')).toBe('true');
});
it('enabled ok button when atleast one row is selected', () => {
wrapper.vm.$store.state.selectedCommits = [{ ...commit, isSelected: true }];
return wrapper.vm.$nextTick().then(() => {
expect(findModal().attributes('ok-disabled')).toBeFalsy();
});
});
});
describe('when in second tab, renders a modal with', () => {
beforeEach(() => {
wrapper.vm.$store.state.tabIndex = 1;
});
it('a disabled ok button when no row is selected', () => {
expect(findModal().attributes('ok-disabled')).toBe('true');
});
it('an enabled ok button when atleast one row is selected', () => {
wrapper.vm.$store.state.selectedCommits = [{ ...commit, isSelected: true }];
return wrapper.vm.$nextTick().then(() => {
expect(findModal().attributes('ok-disabled')).toBeFalsy();
});
});
it('a disabled ok button in first tab, when row is selected in second tab', () => {
createWrapper({ selectedContextCommits: [commit] });
expect(wrapper.find(GlModal).attributes('ok-disabled')).toBe('true');
});
});
describe('has an ok button when clicked calls action', () => {
it('"createContextCommits" when only new commits to be added ', () => {
wrapper.vm.$store.state.selectedCommits = [{ ...commit, isSelected: true }];
findModal().vm.$emit('ok');
return wrapper.vm.$nextTick().then(() => {
expect(createContextCommits).toHaveBeenCalledWith(
expect.anything(),
{ commits: [{ ...commit, isSelected: true }], forceReload: true },
undefined,
);
});
});
it('"removeContextCommits" when only added commits are to be removed ', () => {
wrapper.vm.$store.state.toRemoveCommits = [commit.short_id];
findModal().vm.$emit('ok');
return wrapper.vm.$nextTick().then(() => {
expect(removeContextCommits).toHaveBeenCalledWith(expect.anything(), true, undefined);
});
});
it('"createContextCommits" and "removeContextCommits" when new commits are to be added and old commits are to be removed', () => {
wrapper.vm.$store.state.selectedCommits = [{ ...commit, isSelected: true }];
wrapper.vm.$store.state.toRemoveCommits = [commit.short_id];
findModal().vm.$emit('ok');
return wrapper.vm.$nextTick().then(() => {
expect(createContextCommits).toHaveBeenCalledWith(
expect.anything(),
{ commits: [{ ...commit, isSelected: true }] },
undefined,
);
expect(removeContextCommits).toHaveBeenCalledWith(expect.anything(), undefined, undefined);
});
});
});
describe('has a cancel button when clicked', () => {
it('does not call "createContextCommits" or "removeContextCommits"', () => {
findModal().vm.$emit('cancel');
expect(createContextCommits).not.toHaveBeenCalled();
expect(removeContextCommits).not.toHaveBeenCalled();
});
it('"resetModalState" to reset all the modal state', () => {
findModal().vm.$emit('cancel');
expect(resetModalState).toHaveBeenCalledWith(expect.anything(), undefined, undefined);
});
});
describe('when model is closed by clicking the "X" button or by pressing "ESC" key', () => {
it('does not call "createContextCommits" or "removeContextCommits"', () => {
findModal().vm.$emit('close');
expect(createContextCommits).not.toHaveBeenCalled();
expect(removeContextCommits).not.toHaveBeenCalled();
});
it('"resetModalState" to reset all the modal state', () => {
findModal().vm.$emit('close');
expect(resetModalState).toHaveBeenCalledWith(expect.anything(), undefined, undefined);
});
});
});
import { shallowMount } from '@vue/test-utils';
import { GlLoadingIcon } from '@gitlab/ui';
import ReviewTabContainer from '~/add_context_commits_modal/components/review_tab_container.vue';
import CommitItem from '~/diffs/components/commit_item.vue';
import getDiffWithCommit from '../../diffs/mock_data/diff_with_commit';
describe('ReviewTabContainer', () => {
let wrapper;
const { commit } = getDiffWithCommit();
const createWrapper = (props = {}) => {
wrapper = shallowMount(ReviewTabContainer, {
propsData: {
tab: 'commits',
isLoading: false,
loadingError: false,
loadingFailedText: 'Failed to load commits',
commits: [],
selectedRow: [],
...props,
},
});
};
beforeEach(() => {
createWrapper();
});
afterEach(() => {
wrapper.destroy();
});
it('shows loading icon when commits are being loaded', () => {
createWrapper({ isLoading: true });
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
});
it('shows loading error text when API call fails', () => {
createWrapper({ loadingError: true });
expect(wrapper.text()).toContain('Failed to load commits');
});
it('shows "No commits present here" when commits are not present', () => {
expect(wrapper.text()).toContain('No commits present here');
});
it('renders all passed commits as list', () => {
createWrapper({ commits: [commit] });
expect(wrapper.findAll(CommitItem).length).toBe(1);
});
});
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import {
setBaseConfig,
setTabIndex,
setCommits,
createContextCommits,
fetchContextCommits,
setContextCommits,
removeContextCommits,
setSelectedCommits,
setSearchText,
setToRemoveCommits,
resetModalState,
} from '~/add_context_commits_modal/store/actions';
import * as types from '~/add_context_commits_modal/store/mutation_types';
import { TEST_HOST } from 'helpers/test_constants';
import testAction from '../../helpers/vuex_action_helper';
describe('AddContextCommitsModalStoreActions', () => {
const contextCommitEndpoint =
'/api/v4/projects/gitlab-org%2fgitlab/merge_requests/1/context_commits';
const mergeRequestIid = 1;
const projectId = 1;
const projectPath = 'gitlab-org/gitlab';
const contextCommitsPath = `${TEST_HOST}/gitlab-org/gitlab/-/merge_requests/1/context_commits.json`;
const dummyCommit = {
id: 1,
title: 'dummy commit',
short_id: 'abcdef',
committed_date: '2020-06-12',
};
gon.api_version = 'v4';
let mock;
beforeEach(() => {
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.restore();
});
describe('setBaseConfig', () => {
it('commits SET_BASE_CONFIG', done => {
const options = { contextCommitsPath, mergeRequestIid, projectId };
testAction(
setBaseConfig,
options,
{
contextCommitsPath: '',
mergeRequestIid,
projectId,
},
[
{
type: types.SET_BASE_CONFIG,
payload: options,
},
],
[],
done,
);
});
});
describe('setTabIndex', () => {
it('commits SET_TABINDEX', done => {
testAction(
setTabIndex,
{ tabIndex: 1 },
{ tabIndex: 0 },
[{ type: types.SET_TABINDEX, payload: { tabIndex: 1 } }],
[],
done,
);
});
});
describe('setCommits', () => {
it('commits SET_COMMITS', done => {
testAction(
setCommits,
{ commits: [], silentAddition: false },
{ isLoadingCommits: false, commits: [] },
[{ type: types.SET_COMMITS, payload: [] }],
[],
done,
);
});
it('commits SET_COMMITS_SILENT', done => {
testAction(
setCommits,
{ commits: [], silentAddition: true },
{ isLoadingCommits: true, commits: [] },
[{ type: types.SET_COMMITS_SILENT, payload: [] }],
[],
done,
);
});
});
describe('createContextCommits', () => {
it('calls API to create context commits', done => {
mock.onPost(contextCommitEndpoint).reply(200, {});
testAction(createContextCommits, { commits: [] }, {}, [], [], done);
createContextCommits(
{ state: { projectId, mergeRequestIid }, commit: () => null },
{ commits: [] },
)
.then(() => {
done();
})
.catch(done.fail);
});
});
describe('fetchContextCommits', () => {
beforeEach(() => {
mock
.onGet(
`/api/${gon.api_version}/projects/gitlab-org%2Fgitlab/merge_requests/1/context_commits`,
)
.reply(200, [dummyCommit]);
});
it('commits FETCH_CONTEXT_COMMITS', done => {
const contextCommit = { ...dummyCommit, isSelected: true };
testAction(
fetchContextCommits,
null,
{
mergeRequestIid,
projectId: projectPath,
isLoadingContextCommits: false,
contextCommitsLoadingError: false,
commits: [],
},
[{ type: types.FETCH_CONTEXT_COMMITS }],
[
{ type: 'setContextCommits', payload: [contextCommit] },
{ type: 'setCommits', payload: { commits: [contextCommit], silentAddition: true } },
{ type: 'setSelectedCommits', payload: [contextCommit] },
],
done,
);
});
});
describe('setContextCommits', () => {
it('commits SET_CONTEXT_COMMITS', done => {
testAction(
setContextCommits,
{ data: [] },
{ contextCommits: [], isLoadingContextCommits: false },
[{ type: types.SET_CONTEXT_COMMITS, payload: { data: [] } }],
[],
done,
);
});
});
describe('removeContextCommits', () => {
beforeEach(() => {
mock
.onDelete('/api/v4/projects/gitlab-org%2Fgitlab/merge_requests/1/context_commits')
.reply(204);
});
it('calls API to remove context commits', done => {
testAction(
removeContextCommits,
{ forceReload: false },
{ mergeRequestIid, projectId, toRemoveCommits: [] },
[],
[],
done,
);
});
});
describe('setSelectedCommits', () => {
it('commits SET_SELECTED_COMMITS', done => {
testAction(
setSelectedCommits,
[dummyCommit],
{ selectedCommits: [] },
[{ type: types.SET_SELECTED_COMMITS, payload: [dummyCommit] }],
[],
done,
);
});
});
describe('setSearchText', () => {
it('commits SET_SEARCH_TEXT', done => {
const searchText = 'Dummy Text';
testAction(
setSearchText,
searchText,
{ searchText: '' },
[{ type: types.SET_SEARCH_TEXT, payload: searchText }],
[],
done,
);
});
});
describe('setToRemoveCommits', () => {
it('commits SET_TO_REMOVE_COMMITS', done => {
const commitId = 'abcde';
testAction(
setToRemoveCommits,
[commitId],
{ toRemoveCommits: [] },
[{ type: types.SET_TO_REMOVE_COMMITS, payload: [commitId] }],
[],
done,
);
});
});
describe('resetModalState', () => {
it('commits RESET_MODAL_STATE', done => {
const commitId = 'abcde';
testAction(
resetModalState,
null,
{ toRemoveCommits: [commitId] },
[{ type: types.RESET_MODAL_STATE }],
[],
done,
);
});
});
});
import mutations from '~/add_context_commits_modal/store/mutations';
import * as types from '~/add_context_commits_modal/store/mutation_types';
import { TEST_HOST } from 'helpers/test_constants';
import getDiffWithCommit from '../../diffs/mock_data/diff_with_commit';
describe('AddContextCommitsModalStoreMutations', () => {
const { commit } = getDiffWithCommit();
describe('SET_BASE_CONFIG', () => {
it('should set contextCommitsPath, mergeRequestIid and projectId', () => {
const state = {};
const contextCommitsPath = `${TEST_HOST}/gitlab-org/gitlab/-/merge_requests/1/context_commits.json`;
const mergeRequestIid = 1;
const projectId = 1;
mutations[types.SET_BASE_CONFIG](state, { contextCommitsPath, mergeRequestIid, projectId });
expect(state.contextCommitsPath).toEqual(contextCommitsPath);
expect(state.mergeRequestIid).toEqual(mergeRequestIid);
expect(state.projectId).toEqual(projectId);
});
});
describe('SET_TABINDEX', () => {
it('sets tabIndex to specific index', () => {
const state = { tabIndex: 0 };
mutations[types.SET_TABINDEX](state, 1);
expect(state.tabIndex).toBe(1);
});
});
describe('FETCH_COMMITS', () => {
it('sets isLoadingCommits to true', () => {
const state = { isLoadingCommits: false };
mutations[types.FETCH_COMMITS](state);
expect(state.isLoadingCommits).toBe(true);
});
});
describe('SET_COMMITS', () => {
it('sets commits to passed data and stop loading', () => {
const state = { commits: [], isLoadingCommits: true };
mutations[types.SET_COMMITS](state, [commit]);
expect(state.commits).toStrictEqual([commit]);
expect(state.isLoadingCommits).toBe(false);
});
});
describe('SET_COMMITS_SILENT', () => {
it('sets commits to passed data and loading continues', () => {
const state = { commits: [], isLoadingCommits: true };
mutations[types.SET_COMMITS_SILENT](state, [commit]);
expect(state.commits).toStrictEqual([commit]);
expect(state.isLoadingCommits).toBe(true);
});
});
describe('FETCH_COMMITS_ERROR', () => {
it('sets commitsLoadingError to true', () => {
const state = { commitsLoadingError: false };
mutations[types.FETCH_COMMITS_ERROR](state);
expect(state.commitsLoadingError).toBe(true);
});
});
describe('FETCH_CONTEXT_COMMITS', () => {
it('sets isLoadingContextCommits to true', () => {
const state = { isLoadingContextCommits: false };
mutations[types.FETCH_CONTEXT_COMMITS](state);
expect(state.isLoadingContextCommits).toBe(true);
});
});
describe('SET_CONTEXT_COMMITS', () => {
it('sets contextCommit to passed data and stop loading', () => {
const state = { contextCommits: [], isLoadingContextCommits: true };
mutations[types.SET_CONTEXT_COMMITS](state, [commit]);
expect(state.contextCommits).toStrictEqual([commit]);
expect(state.isLoadingContextCommits).toBe(false);
});
});
describe('FETCH_CONTEXT_COMMITS_ERROR', () => {
it('sets contextCommitsLoadingError to true', () => {
const state = { contextCommitsLoadingError: false };
mutations[types.FETCH_CONTEXT_COMMITS_ERROR](state);
expect(state.contextCommitsLoadingError).toBe(true);
});
});
describe('SET_SELECTED_COMMITS', () => {
it('sets selectedCommits to specified value', () => {
const state = { selectedCommits: [] };
mutations[types.SET_SELECTED_COMMITS](state, [commit]);
expect(state.selectedCommits).toStrictEqual([commit]);
});
});
describe('SET_SEARCH_TEXT', () => {
it('sets searchText to specified value', () => {
const searchText = 'Test';
const state = { searchText: '' };
mutations[types.SET_SEARCH_TEXT](state, searchText);
expect(state.searchText).toBe(searchText);
});
});
describe('SET_TO_REMOVE_COMMITS', () => {
it('sets searchText to specified value', () => {
const state = { toRemoveCommits: [] };
mutations[types.SET_TO_REMOVE_COMMITS](state, [commit.short_id]);
expect(state.toRemoveCommits).toStrictEqual([commit.short_id]);
});
});
describe('RESET_MODAL_STATE', () => {
it('sets searchText to specified value', () => {
const state = {
commits: [commit],
contextCommits: [commit],
selectedCommits: [commit],
toRemoveCommits: [commit.short_id],
searchText: 'Test',
};
mutations[types.RESET_MODAL_STATE](state);
expect(state.commits).toStrictEqual([]);
expect(state.contextCommits).toStrictEqual([]);
expect(state.selectedCommits).toStrictEqual([]);
expect(state.toRemoveCommits).toStrictEqual([]);
expect(state.searchText).toBe('');
});
});
});
......@@ -667,6 +667,79 @@ describe('Api', () => {
});
});
describe('createContextCommits', () => {
it('creates a new context commit', done => {
const projectPath = 'abc';
const mergeRequestId = '123456';
const commitsData = ['abcdefg'];
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectPath}/merge_requests/${mergeRequestId}/context_commits`;
const expectedData = {
commits: commitsData,
};
jest.spyOn(axios, 'post');
mock.onPost(expectedUrl).replyOnce(200, [
{
id: 'abcdefghijklmnop',
short_id: 'abcdefg',
title: 'Dummy commit',
},
]);
Api.createContextCommits(projectPath, mergeRequestId, expectedData)
.then(({ data }) => {
expect(data[0].title).toBe('Dummy commit');
})
.then(done)
.catch(done.fail);
});
});
describe('allContextCommits', () => {
it('gets all context commits', done => {
const projectPath = 'abc';
const mergeRequestId = '123456';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectPath}/merge_requests/${mergeRequestId}/context_commits`;
jest.spyOn(axios, 'get');
mock
.onGet(expectedUrl)
.replyOnce(200, [{ id: 'abcdef', short_id: 'abcdefghi', title: 'Dummy commit title' }]);
Api.allContextCommits(projectPath, mergeRequestId)
.then(({ data }) => {
expect(data[0].title).toBe('Dummy commit title');
})
.then(done)
.catch(done.fail);
});
});
describe('removeContextCommits', () => {
it('removes context commits', done => {
const projectPath = 'abc';
const mergeRequestId = '123456';
const commitsData = ['abcdefg'];
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectPath}/merge_requests/${mergeRequestId}/context_commits`;
const expectedData = {
commits: commitsData,
};
jest.spyOn(axios, 'delete');
mock.onDelete(expectedUrl).replyOnce(204);
Api.removeContextCommits(projectPath, mergeRequestId, expectedData)
.then(() => {
expect(axios.delete).toHaveBeenCalledWith(expectedUrl, { data: expectedData });
})
.then(done)
.catch(done.fail);
});
});
describe('release-related methods', () => {
const dummyProjectPath = 'gitlab-org/gitlab';
const dummyTagName = 'v1.3';
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment