mirror of
https://gitlab.com/gitlab-org/gitlab-foss.git
synced 2025-07-29 12:00:32 +00:00
Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
@ -1,31 +1,28 @@
|
|||||||
import Vue from 'vue';
|
import axios from '~/lib/utils/axios_utils';
|
||||||
import VueResource from 'vue-resource';
|
|
||||||
import * as constants from '../constants';
|
import * as constants from '../constants';
|
||||||
|
|
||||||
Vue.use(VueResource);
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
fetchDiscussions(endpoint, filter, persistFilter = true) {
|
fetchDiscussions(endpoint, filter, persistFilter = true) {
|
||||||
const config =
|
const config =
|
||||||
filter !== undefined
|
filter !== undefined
|
||||||
? { params: { notes_filter: filter, persist_filter: persistFilter } }
|
? { params: { notes_filter: filter, persist_filter: persistFilter } }
|
||||||
: null;
|
: null;
|
||||||
return Vue.http.get(endpoint, config);
|
return axios.get(endpoint, config);
|
||||||
},
|
},
|
||||||
replyToDiscussion(endpoint, data) {
|
replyToDiscussion(endpoint, data) {
|
||||||
return Vue.http.post(endpoint, data, { emulateJSON: true });
|
return axios.post(endpoint, data);
|
||||||
},
|
},
|
||||||
updateNote(endpoint, data) {
|
updateNote(endpoint, data) {
|
||||||
return Vue.http.put(endpoint, data, { emulateJSON: true });
|
return axios.put(endpoint, data);
|
||||||
},
|
},
|
||||||
createNewNote(endpoint, data) {
|
createNewNote(endpoint, data) {
|
||||||
return Vue.http.post(endpoint, data, { emulateJSON: true });
|
return axios.post(endpoint, data);
|
||||||
},
|
},
|
||||||
toggleResolveNote(endpoint, isResolved) {
|
toggleResolveNote(endpoint, isResolved) {
|
||||||
const { RESOLVE_NOTE_METHOD_NAME, UNRESOLVE_NOTE_METHOD_NAME } = constants;
|
const { RESOLVE_NOTE_METHOD_NAME, UNRESOLVE_NOTE_METHOD_NAME } = constants;
|
||||||
const method = isResolved ? UNRESOLVE_NOTE_METHOD_NAME : RESOLVE_NOTE_METHOD_NAME;
|
const method = isResolved ? UNRESOLVE_NOTE_METHOD_NAME : RESOLVE_NOTE_METHOD_NAME;
|
||||||
|
|
||||||
return Vue.http[method](endpoint);
|
return axios[method](endpoint);
|
||||||
},
|
},
|
||||||
poll(data = {}) {
|
poll(data = {}) {
|
||||||
const endpoint = data.notesData.notesPath;
|
const endpoint = data.notesData.notesPath;
|
||||||
@ -36,9 +33,9 @@ export default {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return Vue.http.get(endpoint, options);
|
return axios.get(endpoint, options);
|
||||||
},
|
},
|
||||||
toggleIssueState(endpoint, data) {
|
toggleIssueState(endpoint, data) {
|
||||||
return Vue.http.put(endpoint, data);
|
return axios.put(endpoint, data);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -47,13 +47,10 @@ export const setNotesFetchedState = ({ commit }, state) =>
|
|||||||
export const toggleDiscussion = ({ commit }, data) => commit(types.TOGGLE_DISCUSSION, data);
|
export const toggleDiscussion = ({ commit }, data) => commit(types.TOGGLE_DISCUSSION, data);
|
||||||
|
|
||||||
export const fetchDiscussions = ({ commit, dispatch }, { path, filter, persistFilter }) =>
|
export const fetchDiscussions = ({ commit, dispatch }, { path, filter, persistFilter }) =>
|
||||||
service
|
service.fetchDiscussions(path, filter, persistFilter).then(({ data }) => {
|
||||||
.fetchDiscussions(path, filter, persistFilter)
|
commit(types.SET_INITIAL_DISCUSSIONS, data);
|
||||||
.then(res => res.json())
|
dispatch('updateResolvableDiscussionsCounts');
|
||||||
.then(discussions => {
|
});
|
||||||
commit(types.SET_INITIAL_DISCUSSIONS, discussions);
|
|
||||||
dispatch('updateResolvableDiscussionsCounts');
|
|
||||||
});
|
|
||||||
|
|
||||||
export const updateDiscussion = ({ commit, state }, discussion) => {
|
export const updateDiscussion = ({ commit, state }, discussion) => {
|
||||||
commit(types.UPDATE_DISCUSSION, discussion);
|
commit(types.UPDATE_DISCUSSION, discussion);
|
||||||
@ -80,13 +77,10 @@ export const deleteNote = ({ dispatch }, note) =>
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const updateNote = ({ commit, dispatch }, { endpoint, note }) =>
|
export const updateNote = ({ commit, dispatch }, { endpoint, note }) =>
|
||||||
service
|
service.updateNote(endpoint, note).then(({ data }) => {
|
||||||
.updateNote(endpoint, note)
|
commit(types.UPDATE_NOTE, data);
|
||||||
.then(res => res.json())
|
dispatch('startTaskList');
|
||||||
.then(res => {
|
});
|
||||||
commit(types.UPDATE_NOTE, res);
|
|
||||||
dispatch('startTaskList');
|
|
||||||
});
|
|
||||||
|
|
||||||
export const updateOrCreateNotes = ({ commit, state, getters, dispatch }, notes) => {
|
export const updateOrCreateNotes = ({ commit, state, getters, dispatch }, notes) => {
|
||||||
const { notesById } = getters;
|
const { notesById } = getters;
|
||||||
@ -110,40 +104,37 @@ export const updateOrCreateNotes = ({ commit, state, getters, dispatch }, notes)
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const replyToDiscussion = ({ commit, state, getters, dispatch }, { endpoint, data }) =>
|
export const replyToDiscussion = (
|
||||||
service
|
{ commit, state, getters, dispatch },
|
||||||
.replyToDiscussion(endpoint, data)
|
{ endpoint, data: reply },
|
||||||
.then(res => res.json())
|
) =>
|
||||||
.then(res => {
|
service.replyToDiscussion(endpoint, reply).then(({ data }) => {
|
||||||
if (res.discussion) {
|
if (data.discussion) {
|
||||||
commit(types.UPDATE_DISCUSSION, res.discussion);
|
commit(types.UPDATE_DISCUSSION, data.discussion);
|
||||||
|
|
||||||
updateOrCreateNotes({ commit, state, getters, dispatch }, res.discussion.notes);
|
updateOrCreateNotes({ commit, state, getters, dispatch }, data.discussion.notes);
|
||||||
|
|
||||||
dispatch('updateMergeRequestWidget');
|
dispatch('updateMergeRequestWidget');
|
||||||
dispatch('startTaskList');
|
dispatch('startTaskList');
|
||||||
dispatch('updateResolvableDiscussionsCounts');
|
dispatch('updateResolvableDiscussionsCounts');
|
||||||
} else {
|
} else {
|
||||||
commit(types.ADD_NEW_REPLY_TO_DISCUSSION, res);
|
commit(types.ADD_NEW_REPLY_TO_DISCUSSION, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
return res;
|
return data;
|
||||||
});
|
});
|
||||||
|
|
||||||
export const createNewNote = ({ commit, dispatch }, { endpoint, data }) =>
|
export const createNewNote = ({ commit, dispatch }, { endpoint, data: reply }) =>
|
||||||
service
|
service.createNewNote(endpoint, reply).then(({ data }) => {
|
||||||
.createNewNote(endpoint, data)
|
if (!data.errors) {
|
||||||
.then(res => res.json())
|
commit(types.ADD_NEW_NOTE, data);
|
||||||
.then(res => {
|
|
||||||
if (!res.errors) {
|
|
||||||
commit(types.ADD_NEW_NOTE, res);
|
|
||||||
|
|
||||||
dispatch('updateMergeRequestWidget');
|
dispatch('updateMergeRequestWidget');
|
||||||
dispatch('startTaskList');
|
dispatch('startTaskList');
|
||||||
dispatch('updateResolvableDiscussionsCounts');
|
dispatch('updateResolvableDiscussionsCounts');
|
||||||
}
|
}
|
||||||
return res;
|
return data;
|
||||||
});
|
});
|
||||||
|
|
||||||
export const removePlaceholderNotes = ({ commit }) => commit(types.REMOVE_PLACEHOLDER_NOTES);
|
export const removePlaceholderNotes = ({ commit }) => commit(types.REMOVE_PLACEHOLDER_NOTES);
|
||||||
|
|
||||||
@ -165,41 +156,32 @@ export const resolveDiscussion = ({ state, dispatch, getters }, { discussionId }
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const toggleResolveNote = ({ commit, dispatch }, { endpoint, isResolved, discussion }) =>
|
export const toggleResolveNote = ({ commit, dispatch }, { endpoint, isResolved, discussion }) =>
|
||||||
service
|
service.toggleResolveNote(endpoint, isResolved).then(({ data }) => {
|
||||||
.toggleResolveNote(endpoint, isResolved)
|
const mutationType = discussion ? types.UPDATE_DISCUSSION : types.UPDATE_NOTE;
|
||||||
.then(res => res.json())
|
|
||||||
.then(res => {
|
|
||||||
const mutationType = discussion ? types.UPDATE_DISCUSSION : types.UPDATE_NOTE;
|
|
||||||
|
|
||||||
commit(mutationType, res);
|
commit(mutationType, data);
|
||||||
|
|
||||||
dispatch('updateResolvableDiscussionsCounts');
|
dispatch('updateResolvableDiscussionsCounts');
|
||||||
|
|
||||||
dispatch('updateMergeRequestWidget');
|
dispatch('updateMergeRequestWidget');
|
||||||
});
|
});
|
||||||
|
|
||||||
export const closeIssue = ({ commit, dispatch, state }) => {
|
export const closeIssue = ({ commit, dispatch, state }) => {
|
||||||
dispatch('toggleStateButtonLoading', true);
|
dispatch('toggleStateButtonLoading', true);
|
||||||
return service
|
return service.toggleIssueState(state.notesData.closePath).then(({ data }) => {
|
||||||
.toggleIssueState(state.notesData.closePath)
|
commit(types.CLOSE_ISSUE);
|
||||||
.then(res => res.json())
|
dispatch('emitStateChangedEvent', data);
|
||||||
.then(data => {
|
dispatch('toggleStateButtonLoading', false);
|
||||||
commit(types.CLOSE_ISSUE);
|
});
|
||||||
dispatch('emitStateChangedEvent', data);
|
|
||||||
dispatch('toggleStateButtonLoading', false);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const reopenIssue = ({ commit, dispatch, state }) => {
|
export const reopenIssue = ({ commit, dispatch, state }) => {
|
||||||
dispatch('toggleStateButtonLoading', true);
|
dispatch('toggleStateButtonLoading', true);
|
||||||
return service
|
return service.toggleIssueState(state.notesData.reopenPath).then(({ data }) => {
|
||||||
.toggleIssueState(state.notesData.reopenPath)
|
commit(types.REOPEN_ISSUE);
|
||||||
.then(res => res.json())
|
dispatch('emitStateChangedEvent', data);
|
||||||
.then(data => {
|
dispatch('toggleStateButtonLoading', false);
|
||||||
commit(types.REOPEN_ISSUE);
|
});
|
||||||
dispatch('emitStateChangedEvent', data);
|
|
||||||
dispatch('toggleStateButtonLoading', false);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const toggleStateButtonLoading = ({ commit }, value) =>
|
export const toggleStateButtonLoading = ({ commit }, value) =>
|
||||||
@ -340,8 +322,7 @@ export const poll = ({ commit, state, getters, dispatch }) => {
|
|||||||
resource: service,
|
resource: service,
|
||||||
method: 'poll',
|
method: 'poll',
|
||||||
data: state,
|
data: state,
|
||||||
successCallback: resp =>
|
successCallback: ({ data }) => pollSuccessCallBack(data, commit, state, getters, dispatch),
|
||||||
resp.json().then(data => pollSuccessCallBack(data, commit, state, getters, dispatch)),
|
|
||||||
errorCallback: () => Flash(__('Something went wrong while fetching latest comments.')),
|
errorCallback: () => Flash(__('Something went wrong while fetching latest comments.')),
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -376,8 +357,7 @@ export const fetchData = ({ commit, state, getters }) => {
|
|||||||
|
|
||||||
service
|
service
|
||||||
.poll(requestData)
|
.poll(requestData)
|
||||||
.then(resp => resp.json)
|
.then(({ data }) => pollSuccessCallBack(data, commit, state, getters))
|
||||||
.then(data => pollSuccessCallBack(data, commit, state, getters))
|
|
||||||
.catch(() => Flash(__('Something went wrong while fetching latest comments.')));
|
.catch(() => Flash(__('Something went wrong while fetching latest comments.')));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -0,0 +1,45 @@
|
|||||||
|
<script>
|
||||||
|
import { GlLink, GlTooltipDirective } from '@gitlab/ui';
|
||||||
|
import Icon from '~/vue_shared/components/icon.vue';
|
||||||
|
import { s__ } from '~/locale';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'MilestoneList',
|
||||||
|
components: {
|
||||||
|
GlLink,
|
||||||
|
Icon,
|
||||||
|
},
|
||||||
|
directives: {
|
||||||
|
GlTooltip: GlTooltipDirective,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
milestones: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
labelText() {
|
||||||
|
return this.milestones.length === 1 ? s__('Milestone') : s__('Milestones');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<icon name="flag" class="align-middle" /> <span class="js-label-text">{{ labelText }}</span>
|
||||||
|
<template v-for="(milestone, index) in milestones">
|
||||||
|
<gl-link
|
||||||
|
:key="milestone.id"
|
||||||
|
v-gl-tooltip
|
||||||
|
:title="milestone.description"
|
||||||
|
:href="milestone.web_url"
|
||||||
|
>
|
||||||
|
{{ milestone.title }}
|
||||||
|
</gl-link>
|
||||||
|
<template v-if="index !== milestones.length - 1">
|
||||||
|
•
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
@ -5,6 +5,7 @@ import { GlTooltipDirective, GlLink, GlBadge } from '@gitlab/ui';
|
|||||||
import Icon from '~/vue_shared/components/icon.vue';
|
import Icon from '~/vue_shared/components/icon.vue';
|
||||||
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
|
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
|
||||||
import timeagoMixin from '~/vue_shared/mixins/timeago';
|
import timeagoMixin from '~/vue_shared/mixins/timeago';
|
||||||
|
import MilestoneList from './milestone_list.vue';
|
||||||
import { __, sprintf } from '../../locale';
|
import { __, sprintf } from '../../locale';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@ -14,6 +15,7 @@ export default {
|
|||||||
GlBadge,
|
GlBadge,
|
||||||
Icon,
|
Icon,
|
||||||
UserAvatarLink,
|
UserAvatarLink,
|
||||||
|
MilestoneList,
|
||||||
},
|
},
|
||||||
directives: {
|
directives: {
|
||||||
GlTooltip: GlTooltipDirective,
|
GlTooltip: GlTooltipDirective,
|
||||||
@ -49,6 +51,20 @@ export default {
|
|||||||
hasAuthor() {
|
hasAuthor() {
|
||||||
return !_.isEmpty(this.author);
|
return !_.isEmpty(this.author);
|
||||||
},
|
},
|
||||||
|
milestones() {
|
||||||
|
// At the moment, a release can only be associated to
|
||||||
|
// one milestone. This will be expanded to be many-to-many
|
||||||
|
// in the near future, so we pass the milestone as an
|
||||||
|
// array here in anticipation of this change.
|
||||||
|
return [this.release.milestone];
|
||||||
|
},
|
||||||
|
shouldRenderMilestones() {
|
||||||
|
// Similar to the `milestones` computed above,
|
||||||
|
// this check will need to be updated once
|
||||||
|
// the API begins sending an array of milestones
|
||||||
|
// instead of just a single object.
|
||||||
|
return Boolean(this.release.milestone);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@ -73,6 +89,12 @@ export default {
|
|||||||
<span v-gl-tooltip.bottom :title="__('Tag')">{{ release.tag_name }}</span>
|
<span v-gl-tooltip.bottom :title="__('Tag')">{{ release.tag_name }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<milestone-list
|
||||||
|
v-if="shouldRenderMilestones"
|
||||||
|
class="append-right-4 js-milestone-list"
|
||||||
|
:milestones="milestones"
|
||||||
|
/>
|
||||||
|
|
||||||
<div class="append-right-4">
|
<div class="append-right-4">
|
||||||
•
|
•
|
||||||
<span v-gl-tooltip.bottom :title="tooltipTitle(release.released_at)">
|
<span v-gl-tooltip.bottom :title="tooltipTitle(release.released_at)">
|
||||||
|
90
app/services/issues/zoom_link_service.rb
Normal file
90
app/services/issues/zoom_link_service.rb
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Issues
|
||||||
|
class ZoomLinkService < Issues::BaseService
|
||||||
|
def initialize(issue, user)
|
||||||
|
super(issue.project, user)
|
||||||
|
|
||||||
|
@issue = issue
|
||||||
|
end
|
||||||
|
|
||||||
|
def add_link(link)
|
||||||
|
if can_add_link? && (link = parse_link(link))
|
||||||
|
success(_('Zoom meeting added'), append_to_description(link))
|
||||||
|
else
|
||||||
|
error(_('Failed to add a Zoom meeting'))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def can_add_link?
|
||||||
|
available? && !link_in_issue_description?
|
||||||
|
end
|
||||||
|
|
||||||
|
def remove_link
|
||||||
|
if can_remove_link?
|
||||||
|
success(_('Zoom meeting removed'), remove_from_description)
|
||||||
|
else
|
||||||
|
error(_('Failed to remove a Zoom meeting'))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def can_remove_link?
|
||||||
|
available? && link_in_issue_description?
|
||||||
|
end
|
||||||
|
|
||||||
|
def parse_link(link)
|
||||||
|
Gitlab::ZoomLinkExtractor.new(link).links.last
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
attr_reader :issue
|
||||||
|
|
||||||
|
def issue_description
|
||||||
|
issue.description || ''
|
||||||
|
end
|
||||||
|
|
||||||
|
def success(message, description)
|
||||||
|
ServiceResponse
|
||||||
|
.success(message: message, payload: { description: description })
|
||||||
|
end
|
||||||
|
|
||||||
|
def error(message)
|
||||||
|
ServiceResponse.error(message: message)
|
||||||
|
end
|
||||||
|
|
||||||
|
def append_to_description(link)
|
||||||
|
"#{issue_description}\n\n#{link}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def remove_from_description
|
||||||
|
link = parse_link(issue_description)
|
||||||
|
return issue_description unless link
|
||||||
|
|
||||||
|
issue_description.delete_suffix(link).rstrip
|
||||||
|
end
|
||||||
|
|
||||||
|
def link_in_issue_description?
|
||||||
|
link = extract_link_from_issue_description
|
||||||
|
return unless link
|
||||||
|
|
||||||
|
Gitlab::ZoomLinkExtractor.new(link).match?
|
||||||
|
end
|
||||||
|
|
||||||
|
def extract_link_from_issue_description
|
||||||
|
issue_description[/(\S+)\z/, 1]
|
||||||
|
end
|
||||||
|
|
||||||
|
def available?
|
||||||
|
feature_enabled? && can?
|
||||||
|
end
|
||||||
|
|
||||||
|
def feature_enabled?
|
||||||
|
Feature.enabled?(:issue_zoom_integration, project)
|
||||||
|
end
|
||||||
|
|
||||||
|
def can?
|
||||||
|
current_user.can?(:update_issue, project)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
title: Remove vue-resource from notes service
|
||||||
|
merge_request: 32934
|
||||||
|
author: Lee Tickett
|
||||||
|
type: other
|
@ -64,6 +64,8 @@ The following quick actions are applicable to descriptions, discussions and thre
|
|||||||
| `/create_merge_request <branch name>` | ✓ | | | Create a new merge request starting from the current issue |
|
| `/create_merge_request <branch name>` | ✓ | | | Create a new merge request starting from the current issue |
|
||||||
| `/relate #issue1 #issue2` | ✓ | | | Mark issues as related **(STARTER)** |
|
| `/relate #issue1 #issue2` | ✓ | | | Mark issues as related **(STARTER)** |
|
||||||
| `/move <path/to/project>` | ✓ | | | Move this issue to another project |
|
| `/move <path/to/project>` | ✓ | | | Move this issue to another project |
|
||||||
|
| `/zoom <Zoom URL>` | ✓ | | | Add Zoom meeting to this issue. ([Introduced in GitLab 12.3](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/16609) enabled by feature flag `issue_zoom_integration`) |
|
||||||
|
| `/remove_zoom` | ✓ | | | Remove Zoom meeting from this issue. ([Introduced in GitLab 12.3](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/16609) enabled by feature flag `issue_zoom_integration`) |
|
||||||
| `/target_branch <local branch name>` | | ✓ | | Set target branch |
|
| `/target_branch <local branch name>` | | ✓ | | Set target branch |
|
||||||
| `/wip` | | ✓ | | Toggle the Work In Progress status |
|
| `/wip` | | ✓ | | Toggle the Work In Progress status |
|
||||||
| `/approve` | | ✓ | | Approve the merge request |
|
| `/approve` | | ✓ | | Approve the merge request |
|
||||||
|
@ -31,6 +31,9 @@ module.exports = {
|
|||||||
moduleNameMapper: {
|
moduleNameMapper: {
|
||||||
'^~(/.*)$': '<rootDir>/app/assets/javascripts$1',
|
'^~(/.*)$': '<rootDir>/app/assets/javascripts$1',
|
||||||
'^ee(/.*)$': '<rootDir>/ee/app/assets/javascripts$1',
|
'^ee(/.*)$': '<rootDir>/ee/app/assets/javascripts$1',
|
||||||
|
'^ee_component(/.*)$': IS_EE
|
||||||
|
? '<rootDir>/ee/app/assets/javascripts$1'
|
||||||
|
: '<rootDir>/app/assets/javascripts/vue_shared/components/empty_component.js',
|
||||||
'^ee_else_ce(/.*)$': IS_EE
|
'^ee_else_ce(/.*)$': IS_EE
|
||||||
? '<rootDir>/ee/app/assets/javascripts$1'
|
? '<rootDir>/ee/app/assets/javascripts$1'
|
||||||
: '<rootDir>/app/assets/javascripts$1',
|
: '<rootDir>/app/assets/javascripts$1',
|
||||||
|
@ -167,6 +167,49 @@ module Gitlab
|
|||||||
issue_iid: quick_action_target.iid
|
issue_iid: quick_action_target.iid
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
desc _('Add Zoom meeting')
|
||||||
|
explanation _('Adds a Zoom meeting')
|
||||||
|
params '<Zoom URL>'
|
||||||
|
types Issue
|
||||||
|
condition do
|
||||||
|
zoom_link_service.can_add_link?
|
||||||
|
end
|
||||||
|
parse_params do |link|
|
||||||
|
zoom_link_service.parse_link(link)
|
||||||
|
end
|
||||||
|
command :zoom do |link|
|
||||||
|
result = zoom_link_service.add_link(link)
|
||||||
|
|
||||||
|
if result.success?
|
||||||
|
@updates[:description] = result.payload[:description]
|
||||||
|
end
|
||||||
|
|
||||||
|
@execution_message[:zoom] = result.message
|
||||||
|
end
|
||||||
|
|
||||||
|
desc _('Remove Zoom meeting')
|
||||||
|
explanation _('Remove Zoom meeting')
|
||||||
|
execution_message _('Zoom meeting removed')
|
||||||
|
types Issue
|
||||||
|
condition do
|
||||||
|
zoom_link_service.can_remove_link?
|
||||||
|
end
|
||||||
|
command :remove_zoom do
|
||||||
|
result = zoom_link_service.remove_link
|
||||||
|
|
||||||
|
if result.success?
|
||||||
|
@updates[:description] = result.payload[:description]
|
||||||
|
end
|
||||||
|
|
||||||
|
@execution_message[:remove_zoom] = result.message
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def zoom_link_service
|
||||||
|
Issues::ZoomLinkService.new(quick_action_target, current_user)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -836,6 +836,9 @@ msgstr ""
|
|||||||
msgid "Add README"
|
msgid "Add README"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Add Zoom meeting"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Add a %{type} token"
|
msgid "Add a %{type} token"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -1007,6 +1010,9 @@ msgstr ""
|
|||||||
msgid "Adds a To Do."
|
msgid "Adds a To Do."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Adds a Zoom meeting"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Adds an issue to an epic."
|
msgid "Adds an issue to an epic."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -6268,6 +6274,9 @@ msgstr ""
|
|||||||
msgid "Failed create wiki"
|
msgid "Failed create wiki"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Failed to add a Zoom meeting"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Failed to apply commands."
|
msgid "Failed to apply commands."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -6340,6 +6349,9 @@ msgstr ""
|
|||||||
msgid "Failed to protect the environment"
|
msgid "Failed to protect the environment"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Failed to remove a Zoom meeting"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Failed to remove issue from board, please try again."
|
msgid "Failed to remove issue from board, please try again."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -12672,6 +12684,9 @@ msgstr ""
|
|||||||
msgid "Remove Runner"
|
msgid "Remove Runner"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Remove Zoom meeting"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Remove all approvals in a merge request when new commits are pushed to its source branch"
|
msgid "Remove all approvals in a merge request when new commits are pushed to its source branch"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -18118,6 +18133,12 @@ msgstr ""
|
|||||||
msgid "Your request for access has been queued for review."
|
msgid "Your request for access has been queued for review."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Zoom meeting added"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Zoom meeting removed"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "a deleted user"
|
msgid "a deleted user"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -39,6 +39,7 @@ module QA
|
|||||||
end
|
end
|
||||||
|
|
||||||
Page::Project::Issue::Show.perform do |show|
|
Page::Project::Issue::Show.perform do |show|
|
||||||
|
show.select_all_activities_filter
|
||||||
expect(show).to have_element(:reopen_issue_button)
|
expect(show).to have_element(:reopen_issue_button)
|
||||||
expect(show).to have_content("closed via commit #{commit_sha}")
|
expect(show).to have_content("closed via commit #{commit_sha}")
|
||||||
end
|
end
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module QA
|
module QA
|
||||||
context 'Plan' do
|
context 'Plan', :smoke do
|
||||||
describe 'mention' do
|
describe 'mention' do
|
||||||
let(:user) do
|
let(:user) do
|
||||||
Resource::User.fabricate_via_api! do |user|
|
Resource::User.fabricate_via_api! do |user|
|
||||||
|
@ -42,5 +42,6 @@ describe 'Issues > User uses quick actions', :js do
|
|||||||
|
|
||||||
it_behaves_like 'create_merge_request quick action'
|
it_behaves_like 'create_merge_request quick action'
|
||||||
it_behaves_like 'move quick action'
|
it_behaves_like 'move quick action'
|
||||||
|
it_behaves_like 'zoom quick actions'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
import $ from 'helpers/jquery';
|
import $ from 'helpers/jquery';
|
||||||
|
import AxiosMockAdapter from 'axios-mock-adapter';
|
||||||
|
import axios from '~/lib/utils/axios_utils';
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import { mount, createLocalVue } from '@vue/test-utils';
|
import { mount, createLocalVue } from '@vue/test-utils';
|
||||||
import NotesApp from '~/notes/components/notes_app.vue';
|
import NotesApp from '~/notes/components/notes_app.vue';
|
||||||
@ -9,19 +11,10 @@ import { setTestTimeout } from 'helpers/timeout';
|
|||||||
// TODO: use generated fixture (https://gitlab.com/gitlab-org/gitlab-ce/issues/62491)
|
// TODO: use generated fixture (https://gitlab.com/gitlab-org/gitlab-ce/issues/62491)
|
||||||
import * as mockData from '../../../javascripts/notes/mock_data';
|
import * as mockData from '../../../javascripts/notes/mock_data';
|
||||||
|
|
||||||
const originalInterceptors = [...Vue.http.interceptors];
|
|
||||||
|
|
||||||
const emptyResponseInterceptor = (request, next) => {
|
|
||||||
next(
|
|
||||||
request.respondWith(JSON.stringify([]), {
|
|
||||||
status: 200,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
setTestTimeout(1000);
|
setTestTimeout(1000);
|
||||||
|
|
||||||
describe('note_app', () => {
|
describe('note_app', () => {
|
||||||
|
let axiosMock;
|
||||||
let mountComponent;
|
let mountComponent;
|
||||||
let wrapper;
|
let wrapper;
|
||||||
let store;
|
let store;
|
||||||
@ -45,6 +38,8 @@ describe('note_app', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
$('body').attr('data-page', 'projects:merge_requests:show');
|
$('body').attr('data-page', 'projects:merge_requests:show');
|
||||||
|
|
||||||
|
axiosMock = new AxiosMockAdapter(axios);
|
||||||
|
|
||||||
store = createStore();
|
store = createStore();
|
||||||
mountComponent = data => {
|
mountComponent = data => {
|
||||||
const propsData = data || {
|
const propsData = data || {
|
||||||
@ -74,12 +69,12 @@ describe('note_app', () => {
|
|||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
wrapper.destroy();
|
wrapper.destroy();
|
||||||
Vue.http.interceptors = [...originalInterceptors];
|
axiosMock.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('set data', () => {
|
describe('set data', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
Vue.http.interceptors.push(emptyResponseInterceptor);
|
axiosMock.onAny().reply(200, []);
|
||||||
wrapper = mountComponent();
|
wrapper = mountComponent();
|
||||||
return waitForDiscussionsRequest();
|
return waitForDiscussionsRequest();
|
||||||
});
|
});
|
||||||
@ -105,7 +100,7 @@ describe('note_app', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
setFixtures('<div class="js-discussions-count"></div>');
|
setFixtures('<div class="js-discussions-count"></div>');
|
||||||
|
|
||||||
Vue.http.interceptors.push(mockData.individualNoteInterceptor);
|
axiosMock.onAny().reply(mockData.getIndividualNoteResponse);
|
||||||
wrapper = mountComponent();
|
wrapper = mountComponent();
|
||||||
return waitForDiscussionsRequest();
|
return waitForDiscussionsRequest();
|
||||||
});
|
});
|
||||||
@ -146,7 +141,7 @@ describe('note_app', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
setFixtures('<div class="js-discussions-count"></div>');
|
setFixtures('<div class="js-discussions-count"></div>');
|
||||||
|
|
||||||
Vue.http.interceptors.push(mockData.individualNoteInterceptor);
|
axiosMock.onAny().reply(mockData.getIndividualNoteResponse);
|
||||||
store.state.commentsDisabled = true;
|
store.state.commentsDisabled = true;
|
||||||
wrapper = mountComponent();
|
wrapper = mountComponent();
|
||||||
return waitForDiscussionsRequest();
|
return waitForDiscussionsRequest();
|
||||||
@ -163,7 +158,7 @@ describe('note_app', () => {
|
|||||||
|
|
||||||
describe('while fetching data', () => {
|
describe('while fetching data', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
Vue.http.interceptors.push(emptyResponseInterceptor);
|
axiosMock.onAny().reply(200, []);
|
||||||
wrapper = mountComponent();
|
wrapper = mountComponent();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -184,7 +179,7 @@ describe('note_app', () => {
|
|||||||
describe('update note', () => {
|
describe('update note', () => {
|
||||||
describe('individual note', () => {
|
describe('individual note', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
Vue.http.interceptors.push(mockData.individualNoteInterceptor);
|
axiosMock.onAny().reply(mockData.getIndividualNoteResponse);
|
||||||
jest.spyOn(service, 'updateNote');
|
jest.spyOn(service, 'updateNote');
|
||||||
wrapper = mountComponent();
|
wrapper = mountComponent();
|
||||||
return waitForDiscussionsRequest().then(() => {
|
return waitForDiscussionsRequest().then(() => {
|
||||||
@ -206,7 +201,7 @@ describe('note_app', () => {
|
|||||||
|
|
||||||
describe('discussion note', () => {
|
describe('discussion note', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
Vue.http.interceptors.push(mockData.discussionNoteInterceptor);
|
axiosMock.onAny().reply(mockData.getDiscussionNoteResponse);
|
||||||
jest.spyOn(service, 'updateNote');
|
jest.spyOn(service, 'updateNote');
|
||||||
wrapper = mountComponent();
|
wrapper = mountComponent();
|
||||||
return waitForDiscussionsRequest().then(() => {
|
return waitForDiscussionsRequest().then(() => {
|
||||||
@ -229,7 +224,7 @@ describe('note_app', () => {
|
|||||||
|
|
||||||
describe('new note form', () => {
|
describe('new note form', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
Vue.http.interceptors.push(mockData.individualNoteInterceptor);
|
axiosMock.onAny().reply(mockData.getIndividualNoteResponse);
|
||||||
wrapper = mountComponent();
|
wrapper = mountComponent();
|
||||||
return waitForDiscussionsRequest();
|
return waitForDiscussionsRequest();
|
||||||
});
|
});
|
||||||
@ -259,7 +254,7 @@ describe('note_app', () => {
|
|||||||
|
|
||||||
describe('edit form', () => {
|
describe('edit form', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
Vue.http.interceptors.push(mockData.individualNoteInterceptor);
|
axiosMock.onAny().reply(mockData.getIndividualNoteResponse);
|
||||||
wrapper = mountComponent();
|
wrapper = mountComponent();
|
||||||
return waitForDiscussionsRequest();
|
return waitForDiscussionsRequest();
|
||||||
});
|
});
|
||||||
@ -287,7 +282,7 @@ describe('note_app', () => {
|
|||||||
|
|
||||||
describe('emoji awards', () => {
|
describe('emoji awards', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
Vue.http.interceptors.push(emptyResponseInterceptor);
|
axiosMock.onAny().reply(200, []);
|
||||||
wrapper = mountComponent();
|
wrapper = mountComponent();
|
||||||
return waitForDiscussionsRequest();
|
return waitForDiscussionsRequest();
|
||||||
});
|
});
|
||||||
|
56
spec/frontend/releases/components/milestone_list_spec.js
Normal file
56
spec/frontend/releases/components/milestone_list_spec.js
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import { shallowMount } from '@vue/test-utils';
|
||||||
|
import { GlLink } from '@gitlab/ui';
|
||||||
|
import MilestoneList from '~/releases/components/milestone_list.vue';
|
||||||
|
import Icon from '~/vue_shared/components/icon.vue';
|
||||||
|
import _ from 'underscore';
|
||||||
|
import { milestones } from '../mock_data';
|
||||||
|
|
||||||
|
describe('Milestone list', () => {
|
||||||
|
let wrapper;
|
||||||
|
|
||||||
|
const factory = milestonesProp => {
|
||||||
|
wrapper = shallowMount(MilestoneList, {
|
||||||
|
propsData: {
|
||||||
|
milestones: milestonesProp,
|
||||||
|
},
|
||||||
|
sync: false,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
wrapper.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders the milestone icon', () => {
|
||||||
|
factory(milestones);
|
||||||
|
|
||||||
|
expect(wrapper.find(Icon).exists()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders the label as "Milestone" if only a single milestone is passed in', () => {
|
||||||
|
factory(milestones.slice(0, 1));
|
||||||
|
|
||||||
|
expect(wrapper.find('.js-label-text').text()).toEqual('Milestone');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders the label as "Milestones" if more than one milestone is passed in', () => {
|
||||||
|
factory(milestones);
|
||||||
|
|
||||||
|
expect(wrapper.find('.js-label-text').text()).toEqual('Milestones');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders a link to the milestone with a tooltip', () => {
|
||||||
|
const milestone = _.first(milestones);
|
||||||
|
factory([milestone]);
|
||||||
|
|
||||||
|
const milestoneLink = wrapper.find(GlLink);
|
||||||
|
|
||||||
|
expect(milestoneLink.exists()).toBe(true);
|
||||||
|
|
||||||
|
expect(milestoneLink.text()).toBe(milestone.title);
|
||||||
|
|
||||||
|
expect(milestoneLink.attributes('href')).toBe(milestone.web_url);
|
||||||
|
|
||||||
|
expect(milestoneLink.attributes('data-original-title')).toBe(milestone.description);
|
||||||
|
});
|
||||||
|
});
|
120
spec/frontend/releases/components/release_block_spec.js
Normal file
120
spec/frontend/releases/components/release_block_spec.js
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
import { mount } from '@vue/test-utils';
|
||||||
|
import ReleaseBlock from '~/releases/components/release_block.vue';
|
||||||
|
import timeagoMixin from '~/vue_shared/mixins/timeago';
|
||||||
|
import { first } from 'underscore';
|
||||||
|
import { release } from '../mock_data';
|
||||||
|
|
||||||
|
describe('Release block', () => {
|
||||||
|
let wrapper;
|
||||||
|
|
||||||
|
const factory = releaseProp => {
|
||||||
|
wrapper = mount(ReleaseBlock, {
|
||||||
|
propsData: {
|
||||||
|
release: releaseProp,
|
||||||
|
},
|
||||||
|
sync: false,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const milestoneListExists = () => wrapper.find('.js-milestone-list').exists();
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
wrapper.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('with default props', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
factory(release);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders the block with an id equal to the release's tag name", () => {
|
||||||
|
expect(wrapper.attributes().id).toBe('v0.3');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders release name', () => {
|
||||||
|
expect(wrapper.text()).toContain(release.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders commit sha', () => {
|
||||||
|
expect(wrapper.text()).toContain(release.commit.short_id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders tag name', () => {
|
||||||
|
expect(wrapper.text()).toContain(release.tag_name);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders release date', () => {
|
||||||
|
expect(wrapper.text()).toContain(timeagoMixin.methods.timeFormated(release.released_at));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders number of assets provided', () => {
|
||||||
|
expect(wrapper.find('.js-assets-count').text()).toContain(release.assets.count);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders dropdown with the sources', () => {
|
||||||
|
expect(wrapper.findAll('.js-sources-dropdown li').length).toEqual(
|
||||||
|
release.assets.sources.length,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(wrapper.find('.js-sources-dropdown li a').attributes().href).toEqual(
|
||||||
|
first(release.assets.sources).url,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(wrapper.find('.js-sources-dropdown li a').text()).toContain(
|
||||||
|
first(release.assets.sources).format,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders list with the links provided', () => {
|
||||||
|
expect(wrapper.findAll('.js-assets-list li').length).toEqual(release.assets.links.length);
|
||||||
|
|
||||||
|
expect(wrapper.find('.js-assets-list li a').attributes().href).toEqual(
|
||||||
|
first(release.assets.links).url,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(wrapper.find('.js-assets-list li a').text()).toContain(
|
||||||
|
first(release.assets.links).name,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders author avatar', () => {
|
||||||
|
expect(wrapper.find('.user-avatar-link').exists()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('external label', () => {
|
||||||
|
it('renders external label when link is external', () => {
|
||||||
|
expect(wrapper.find('.js-assets-list li a').text()).toContain('external source');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not render external label when link is not external', () => {
|
||||||
|
expect(wrapper.find('.js-assets-list li:nth-child(2) a').text()).not.toContain(
|
||||||
|
'external source',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders the milestone list if at least one milestone is associated to the release', () => {
|
||||||
|
factory(release);
|
||||||
|
|
||||||
|
expect(milestoneListExists()).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not render the milestone list if no milestones are associated to the release', () => {
|
||||||
|
const releaseClone = JSON.parse(JSON.stringify(release));
|
||||||
|
delete releaseClone.milestone;
|
||||||
|
|
||||||
|
factory(releaseClone);
|
||||||
|
|
||||||
|
expect(milestoneListExists()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders upcoming release badge', () => {
|
||||||
|
const releaseClone = JSON.parse(JSON.stringify(release));
|
||||||
|
releaseClone.upcoming_release = true;
|
||||||
|
|
||||||
|
factory(releaseClone);
|
||||||
|
|
||||||
|
expect(wrapper.text()).toContain('Upcoming Release');
|
||||||
|
});
|
||||||
|
});
|
97
spec/frontend/releases/mock_data.js
Normal file
97
spec/frontend/releases/mock_data.js
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
export const milestones = [
|
||||||
|
{
|
||||||
|
id: 50,
|
||||||
|
iid: 2,
|
||||||
|
project_id: 18,
|
||||||
|
title: '13.6',
|
||||||
|
description: 'The 13.6 milestone!',
|
||||||
|
state: 'active',
|
||||||
|
created_at: '2019-08-27T17:22:38.280Z',
|
||||||
|
updated_at: '2019-08-27T17:22:38.280Z',
|
||||||
|
due_date: '2019-09-19',
|
||||||
|
start_date: '2019-08-31',
|
||||||
|
web_url: 'http://0.0.0.0:3001/root/release-test/-/milestones/2',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 49,
|
||||||
|
iid: 1,
|
||||||
|
project_id: 18,
|
||||||
|
title: '13.5',
|
||||||
|
description: 'The 13.5 milestone!',
|
||||||
|
state: 'active',
|
||||||
|
created_at: '2019-08-26T17:55:48.643Z',
|
||||||
|
updated_at: '2019-08-26T17:55:48.643Z',
|
||||||
|
due_date: '2019-10-11',
|
||||||
|
start_date: '2019-08-19',
|
||||||
|
web_url: 'http://0.0.0.0:3001/root/release-test/-/milestones/1',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const release = {
|
||||||
|
name: 'New release',
|
||||||
|
tag_name: 'v0.3',
|
||||||
|
description: 'A super nice release!',
|
||||||
|
description_html: '<p data-sourcepos="1:1-1:21" dir="auto">A super nice release!</p>',
|
||||||
|
created_at: '2019-08-26T17:54:04.952Z',
|
||||||
|
released_at: '2019-08-26T17:54:04.807Z',
|
||||||
|
author: {
|
||||||
|
id: 1,
|
||||||
|
name: 'Administrator',
|
||||||
|
username: 'root',
|
||||||
|
state: 'active',
|
||||||
|
avatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
|
||||||
|
web_url: 'http://0.0.0.0:3001/root',
|
||||||
|
},
|
||||||
|
commit: {
|
||||||
|
id: 'c22b0728d1b465f82898c884d32b01aa642f96c1',
|
||||||
|
short_id: 'c22b0728',
|
||||||
|
created_at: '2019-08-26T17:47:07.000Z',
|
||||||
|
parent_ids: [],
|
||||||
|
title: 'Initial commit',
|
||||||
|
message: 'Initial commit',
|
||||||
|
author_name: 'Administrator',
|
||||||
|
author_email: 'admin@example.com',
|
||||||
|
authored_date: '2019-08-26T17:47:07.000Z',
|
||||||
|
committer_name: 'Administrator',
|
||||||
|
committer_email: 'admin@example.com',
|
||||||
|
committed_date: '2019-08-26T17:47:07.000Z',
|
||||||
|
},
|
||||||
|
upcoming_release: false,
|
||||||
|
milestone: milestones[0],
|
||||||
|
assets: {
|
||||||
|
count: 5,
|
||||||
|
sources: [
|
||||||
|
{
|
||||||
|
format: 'zip',
|
||||||
|
url: 'http://0.0.0.0:3001/root/release-test/-/archive/v0.3/release-test-v0.3.zip',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
format: 'tar.gz',
|
||||||
|
url: 'http://0.0.0.0:3001/root/release-test/-/archive/v0.3/release-test-v0.3.tar.gz',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
format: 'tar.bz2',
|
||||||
|
url: 'http://0.0.0.0:3001/root/release-test/-/archive/v0.3/release-test-v0.3.tar.bz2',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
format: 'tar',
|
||||||
|
url: 'http://0.0.0.0:3001/root/release-test/-/archive/v0.3/release-test-v0.3.tar',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: 'my link',
|
||||||
|
url: 'https://google.com',
|
||||||
|
external: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: 'my second link',
|
||||||
|
url:
|
||||||
|
'https://gitlab.com/gitlab-org/gitlab-ce/-/jobs/artifacts/v11.6.0-rc4/download?job=rspec-mysql+41%2F50',
|
||||||
|
external: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
@ -647,24 +647,12 @@ export const DISCUSSION_NOTE_RESPONSE_MAP = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export function individualNoteInterceptor(request, next) {
|
export function getIndividualNoteResponse(config) {
|
||||||
const body = INDIVIDUAL_NOTE_RESPONSE_MAP[request.method.toUpperCase()][request.url];
|
return [200, INDIVIDUAL_NOTE_RESPONSE_MAP[config.method.toUpperCase()][config.url]];
|
||||||
|
|
||||||
next(
|
|
||||||
request.respondWith(JSON.stringify(body), {
|
|
||||||
status: 200,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function discussionNoteInterceptor(request, next) {
|
export function getDiscussionNoteResponse(config) {
|
||||||
const body = DISCUSSION_NOTE_RESPONSE_MAP[request.method.toUpperCase()][request.url];
|
return [200, DISCUSSION_NOTE_RESPONSE_MAP[config.method.toUpperCase()][config.url]];
|
||||||
|
|
||||||
next(
|
|
||||||
request.respondWith(JSON.stringify(body), {
|
|
||||||
status: 200,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const notesWithDescriptionChanges = [
|
export const notesWithDescriptionChanges = [
|
||||||
|
@ -1,9 +1,6 @@
|
|||||||
import Vue from 'vue';
|
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
import _ from 'underscore';
|
|
||||||
import Api from '~/api';
|
import Api from '~/api';
|
||||||
import { TEST_HOST } from 'spec/test_constants';
|
import { TEST_HOST } from 'spec/test_constants';
|
||||||
import { headersInterceptor } from 'spec/helpers/vue_resource_helper';
|
|
||||||
import actionsModule, * as actions from '~/notes/stores/actions';
|
import actionsModule, * as actions from '~/notes/stores/actions';
|
||||||
import * as mutationTypes from '~/notes/stores/mutation_types';
|
import * as mutationTypes from '~/notes/stores/mutation_types';
|
||||||
import * as notesConstants from '~/notes/constants';
|
import * as notesConstants from '~/notes/constants';
|
||||||
@ -29,6 +26,7 @@ describe('Actions Notes Store', () => {
|
|||||||
let state;
|
let state;
|
||||||
let store;
|
let store;
|
||||||
let flashSpy;
|
let flashSpy;
|
||||||
|
let axiosMock;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
store = createStore();
|
store = createStore();
|
||||||
@ -36,10 +34,12 @@ describe('Actions Notes Store', () => {
|
|||||||
dispatch = jasmine.createSpy('dispatch');
|
dispatch = jasmine.createSpy('dispatch');
|
||||||
state = {};
|
state = {};
|
||||||
flashSpy = spyOnDependency(actionsModule, 'Flash');
|
flashSpy = spyOnDependency(actionsModule, 'Flash');
|
||||||
|
axiosMock = new AxiosMockAdapter(axios);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
resetStore(store);
|
resetStore(store);
|
||||||
|
axiosMock.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('setNotesData', () => {
|
describe('setNotesData', () => {
|
||||||
@ -160,20 +160,8 @@ describe('Actions Notes Store', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('async methods', () => {
|
describe('async methods', () => {
|
||||||
const interceptor = (request, next) => {
|
|
||||||
next(
|
|
||||||
request.respondWith(JSON.stringify({}), {
|
|
||||||
status: 200,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
Vue.http.interceptors.push(interceptor);
|
axiosMock.onAny().reply(200, {});
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('closeIssue', () => {
|
describe('closeIssue', () => {
|
||||||
@ -259,7 +247,7 @@ describe('Actions Notes Store', () => {
|
|||||||
beforeEach(done => {
|
beforeEach(done => {
|
||||||
jasmine.clock().install();
|
jasmine.clock().install();
|
||||||
|
|
||||||
spyOn(Vue.http, 'get').and.callThrough();
|
spyOn(axios, 'get').and.callThrough();
|
||||||
|
|
||||||
store
|
store
|
||||||
.dispatch('setNotesData', notesDataMock)
|
.dispatch('setNotesData', notesDataMock)
|
||||||
@ -272,31 +260,15 @@ describe('Actions Notes Store', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('calls service with last fetched state', done => {
|
it('calls service with last fetched state', done => {
|
||||||
const interceptor = (request, next) => {
|
axiosMock
|
||||||
next(
|
.onAny()
|
||||||
request.respondWith(
|
.reply(200, { notes: [], last_fetched_at: '123456' }, { 'poll-interval': '1000' });
|
||||||
JSON.stringify({
|
|
||||||
notes: [],
|
|
||||||
last_fetched_at: '123456',
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
status: 200,
|
|
||||||
headers: {
|
|
||||||
'poll-interval': '1000',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
Vue.http.interceptors.push(interceptor);
|
|
||||||
Vue.http.interceptors.push(headersInterceptor);
|
|
||||||
|
|
||||||
store
|
store
|
||||||
.dispatch('poll')
|
.dispatch('poll')
|
||||||
.then(() => new Promise(resolve => requestAnimationFrame(resolve)))
|
.then(() => new Promise(resolve => requestAnimationFrame(resolve)))
|
||||||
.then(() => {
|
.then(() => {
|
||||||
expect(Vue.http.get).toHaveBeenCalled();
|
expect(axios.get).toHaveBeenCalled();
|
||||||
expect(store.state.lastFetchedAt).toBe('123456');
|
expect(store.state.lastFetchedAt).toBe('123456');
|
||||||
|
|
||||||
jasmine.clock().tick(1500);
|
jasmine.clock().tick(1500);
|
||||||
@ -308,16 +280,12 @@ describe('Actions Notes Store', () => {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
expect(Vue.http.get.calls.count()).toBe(2);
|
expect(axios.get.calls.count()).toBe(2);
|
||||||
expect(Vue.http.get.calls.mostRecent().args[1].headers).toEqual({
|
expect(axios.get.calls.mostRecent().args[1].headers).toEqual({
|
||||||
'X-Last-Fetched-At': '123456',
|
'X-Last-Fetched-At': '123456',
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.then(() => store.dispatch('stopPolling'))
|
.then(() => store.dispatch('stopPolling'))
|
||||||
.then(() => {
|
|
||||||
Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor);
|
|
||||||
Vue.http.interceptors = _.without(Vue.http.interceptors, headersInterceptor);
|
|
||||||
})
|
|
||||||
.then(done)
|
.then(done)
|
||||||
.catch(done.fail);
|
.catch(done.fail);
|
||||||
});
|
});
|
||||||
@ -338,10 +306,8 @@ describe('Actions Notes Store', () => {
|
|||||||
|
|
||||||
describe('removeNote', () => {
|
describe('removeNote', () => {
|
||||||
const endpoint = `${TEST_HOST}/note`;
|
const endpoint = `${TEST_HOST}/note`;
|
||||||
let axiosMock;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
axiosMock = new AxiosMockAdapter(axios);
|
|
||||||
axiosMock.onDelete(endpoint).replyOnce(200, {});
|
axiosMock.onDelete(endpoint).replyOnce(200, {});
|
||||||
|
|
||||||
$('body').attr('data-page', '');
|
$('body').attr('data-page', '');
|
||||||
@ -411,10 +377,8 @@ describe('Actions Notes Store', () => {
|
|||||||
|
|
||||||
describe('deleteNote', () => {
|
describe('deleteNote', () => {
|
||||||
const endpoint = `${TEST_HOST}/note`;
|
const endpoint = `${TEST_HOST}/note`;
|
||||||
let axiosMock;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
axiosMock = new AxiosMockAdapter(axios);
|
|
||||||
axiosMock.onDelete(endpoint).replyOnce(200, {});
|
axiosMock.onDelete(endpoint).replyOnce(200, {});
|
||||||
|
|
||||||
$('body').attr('data-page', '');
|
$('body').attr('data-page', '');
|
||||||
@ -454,20 +418,9 @@ describe('Actions Notes Store', () => {
|
|||||||
id: 1,
|
id: 1,
|
||||||
valid: true,
|
valid: true,
|
||||||
};
|
};
|
||||||
const interceptor = (request, next) => {
|
|
||||||
next(
|
|
||||||
request.respondWith(JSON.stringify(res), {
|
|
||||||
status: 200,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
Vue.http.interceptors.push(interceptor);
|
axiosMock.onAny().reply(200, res);
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('commits ADD_NEW_NOTE and dispatches updateMergeRequestWidget', done => {
|
it('commits ADD_NEW_NOTE and dispatches updateMergeRequestWidget', done => {
|
||||||
@ -501,20 +454,9 @@ describe('Actions Notes Store', () => {
|
|||||||
const res = {
|
const res = {
|
||||||
errors: ['error'],
|
errors: ['error'],
|
||||||
};
|
};
|
||||||
const interceptor = (request, next) => {
|
|
||||||
next(
|
|
||||||
request.respondWith(JSON.stringify(res), {
|
|
||||||
status: 200,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
Vue.http.interceptors.push(interceptor);
|
axiosMock.onAny().replyOnce(200, res);
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not commit ADD_NEW_NOTE or dispatch updateMergeRequestWidget', done => {
|
it('does not commit ADD_NEW_NOTE or dispatch updateMergeRequestWidget', done => {
|
||||||
@ -534,20 +476,9 @@ describe('Actions Notes Store', () => {
|
|||||||
const res = {
|
const res = {
|
||||||
resolved: true,
|
resolved: true,
|
||||||
};
|
};
|
||||||
const interceptor = (request, next) => {
|
|
||||||
next(
|
|
||||||
request.respondWith(JSON.stringify(res), {
|
|
||||||
status: 200,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
Vue.http.interceptors.push(interceptor);
|
axiosMock.onAny().reply(200, res);
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('as note', () => {
|
describe('as note', () => {
|
||||||
@ -720,32 +651,19 @@ describe('Actions Notes Store', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('replyToDiscussion', () => {
|
describe('replyToDiscussion', () => {
|
||||||
let res = { discussion: { notes: [] } };
|
|
||||||
const payload = { endpoint: TEST_HOST, data: {} };
|
const payload = { endpoint: TEST_HOST, data: {} };
|
||||||
const interceptor = (request, next) => {
|
|
||||||
next(
|
|
||||||
request.respondWith(JSON.stringify(res), {
|
|
||||||
status: 200,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
Vue.http.interceptors.push(interceptor);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('updates discussion if response contains disussion', done => {
|
it('updates discussion if response contains disussion', done => {
|
||||||
|
const discussion = { notes: [] };
|
||||||
|
axiosMock.onAny().reply(200, { discussion });
|
||||||
|
|
||||||
testAction(
|
testAction(
|
||||||
actions.replyToDiscussion,
|
actions.replyToDiscussion,
|
||||||
payload,
|
payload,
|
||||||
{
|
{
|
||||||
notesById: {},
|
notesById: {},
|
||||||
},
|
},
|
||||||
[{ type: mutationTypes.UPDATE_DISCUSSION, payload: res.discussion }],
|
[{ type: mutationTypes.UPDATE_DISCUSSION, payload: discussion }],
|
||||||
[
|
[
|
||||||
{ type: 'updateMergeRequestWidget' },
|
{ type: 'updateMergeRequestWidget' },
|
||||||
{ type: 'startTaskList' },
|
{ type: 'startTaskList' },
|
||||||
@ -756,7 +674,8 @@ describe('Actions Notes Store', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('adds a reply to a discussion', done => {
|
it('adds a reply to a discussion', done => {
|
||||||
res = {};
|
const res = {};
|
||||||
|
axiosMock.onAny().reply(200, res);
|
||||||
|
|
||||||
testAction(
|
testAction(
|
||||||
actions.replyToDiscussion,
|
actions.replyToDiscussion,
|
||||||
|
@ -1,168 +0,0 @@
|
|||||||
import Vue from 'vue';
|
|
||||||
import component from '~/releases/components/release_block.vue';
|
|
||||||
import timeagoMixin from '~/vue_shared/mixins/timeago';
|
|
||||||
|
|
||||||
import mountComponent from '../../helpers/vue_mount_component_helper';
|
|
||||||
|
|
||||||
describe('Release block', () => {
|
|
||||||
const Component = Vue.extend(component);
|
|
||||||
|
|
||||||
const release = {
|
|
||||||
name: 'Bionic Beaver',
|
|
||||||
tag_name: '18.04',
|
|
||||||
description: '## changelog\n\n* line 1\n* line2',
|
|
||||||
description_html: '<div><h2>changelog</h2><ul><li>line1</li<li>line 2</li></ul></div>',
|
|
||||||
author_name: 'Release bot',
|
|
||||||
author_email: 'release-bot@example.com',
|
|
||||||
released_at: '2012-05-28T05:00:00-07:00',
|
|
||||||
author: {
|
|
||||||
avatar_url: 'uploads/-/system/user/avatar/johndoe/avatar.png',
|
|
||||||
id: 482476,
|
|
||||||
name: 'John Doe',
|
|
||||||
path: '/johndoe',
|
|
||||||
state: 'active',
|
|
||||||
status_tooltip_html: null,
|
|
||||||
username: 'johndoe',
|
|
||||||
web_url: 'https://gitlab.com/johndoe',
|
|
||||||
},
|
|
||||||
commit: {
|
|
||||||
id: '2695effb5807a22ff3d138d593fd856244e155e7',
|
|
||||||
short_id: '2695effb',
|
|
||||||
title: 'Initial commit',
|
|
||||||
created_at: '2017-07-26T11:08:53.000+02:00',
|
|
||||||
parent_ids: ['2a4b78934375d7f53875269ffd4f45fd83a84ebe'],
|
|
||||||
message: 'Initial commit',
|
|
||||||
author_name: 'John Smith',
|
|
||||||
author_email: 'john@example.com',
|
|
||||||
authored_date: '2012-05-28T04:42:42-07:00',
|
|
||||||
committer_name: 'Jack Smith',
|
|
||||||
committer_email: 'jack@example.com',
|
|
||||||
committed_date: '2012-05-28T04:42:42-07:00',
|
|
||||||
},
|
|
||||||
assets: {
|
|
||||||
count: 6,
|
|
||||||
sources: [
|
|
||||||
{
|
|
||||||
format: 'zip',
|
|
||||||
url: 'https://gitlab.com/gitlab-org/gitlab-ce/-/archive/v11.3.12/gitlab-ce-v11.3.12.zip',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
format: 'tar.gz',
|
|
||||||
url:
|
|
||||||
'https://gitlab.com/gitlab-org/gitlab-ce/-/archive/v11.3.12/gitlab-ce-v11.3.12.tar.gz',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
format: 'tar.bz2',
|
|
||||||
url:
|
|
||||||
'https://gitlab.com/gitlab-org/gitlab-ce/-/archive/v11.3.12/gitlab-ce-v11.3.12.tar.bz2',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
format: 'tar',
|
|
||||||
url: 'https://gitlab.com/gitlab-org/gitlab-ce/-/archive/v11.3.12/gitlab-ce-v11.3.12.tar',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
links: [
|
|
||||||
{
|
|
||||||
name: 'release-18.04.dmg',
|
|
||||||
url: 'https://my-external-hosting.example.com/scrambled-url/',
|
|
||||||
external: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'binary-linux-amd64',
|
|
||||||
url:
|
|
||||||
'https://gitlab.com/gitlab-org/gitlab-ce/-/jobs/artifacts/v11.6.0-rc4/download?job=rspec-mysql+41%2F50',
|
|
||||||
external: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
let vm;
|
|
||||||
|
|
||||||
const factory = props => mountComponent(Component, { release: props });
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
vm = factory(release);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
vm.$destroy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders the block with an id equal to the release's tag name", () => {
|
|
||||||
expect(vm.$el.id).toBe('18.04');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders release name', () => {
|
|
||||||
expect(vm.$el.textContent).toContain(release.name);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders commit sha', () => {
|
|
||||||
expect(vm.$el.textContent).toContain(release.commit.short_id);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders tag name', () => {
|
|
||||||
expect(vm.$el.textContent).toContain(release.tag_name);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders release date', () => {
|
|
||||||
expect(vm.$el.textContent).toContain(timeagoMixin.methods.timeFormated(release.released_at));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders number of assets provided', () => {
|
|
||||||
expect(vm.$el.querySelector('.js-assets-count').textContent).toContain(release.assets.count);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders dropdown with the sources', () => {
|
|
||||||
expect(vm.$el.querySelectorAll('.js-sources-dropdown li').length).toEqual(
|
|
||||||
release.assets.sources.length,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(vm.$el.querySelector('.js-sources-dropdown li a').getAttribute('href')).toEqual(
|
|
||||||
release.assets.sources[0].url,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(vm.$el.querySelector('.js-sources-dropdown li a').textContent).toContain(
|
|
||||||
release.assets.sources[0].format,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders list with the links provided', () => {
|
|
||||||
expect(vm.$el.querySelectorAll('.js-assets-list li').length).toEqual(
|
|
||||||
release.assets.links.length,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(vm.$el.querySelector('.js-assets-list li a').getAttribute('href')).toEqual(
|
|
||||||
release.assets.links[0].url,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(vm.$el.querySelector('.js-assets-list li a').textContent).toContain(
|
|
||||||
release.assets.links[0].name,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders author avatar', () => {
|
|
||||||
expect(vm.$el.querySelector('.user-avatar-link')).not.toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('external label', () => {
|
|
||||||
it('renders external label when link is external', () => {
|
|
||||||
expect(vm.$el.querySelector('.js-assets-list li a').textContent).toContain('external source');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does not render external label when link is not external', () => {
|
|
||||||
expect(vm.$el.querySelector('.js-assets-list li:nth-child(2) a').textContent).not.toContain(
|
|
||||||
'external source',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('with upcoming_release flag', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
vm = factory(Object.assign({}, release, { upcoming_release: true }));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('renders upcoming release badge', () => {
|
|
||||||
expect(vm.$el.textContent).toContain('Upcoming Release');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
243
spec/services/issues/zoom_link_service_spec.rb
Normal file
243
spec/services/issues/zoom_link_service_spec.rb
Normal file
@ -0,0 +1,243 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
describe Issues::ZoomLinkService do
|
||||||
|
set(:user) { create(:user) }
|
||||||
|
set(:issue) { create(:issue) }
|
||||||
|
|
||||||
|
let(:project) { issue.project }
|
||||||
|
let(:service) { described_class.new(issue, user) }
|
||||||
|
let(:zoom_link) { 'https://zoom.us/j/123456789' }
|
||||||
|
|
||||||
|
before do
|
||||||
|
project.add_reporter(user)
|
||||||
|
end
|
||||||
|
|
||||||
|
shared_context 'with Zoom link' do
|
||||||
|
before do
|
||||||
|
issue.update!(description: "Description\n\n#{zoom_link}")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
shared_context 'with Zoom link not at the end' do
|
||||||
|
before do
|
||||||
|
issue.update!(description: "Description with #{zoom_link} some where")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
shared_context 'without Zoom link' do
|
||||||
|
before do
|
||||||
|
issue.update!(description: "Description\n\nhttp://example.com")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
shared_context 'without issue description' do
|
||||||
|
before do
|
||||||
|
issue.update!(description: nil)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
shared_context 'feature flag disabled' do
|
||||||
|
before do
|
||||||
|
stub_feature_flags(issue_zoom_integration: false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
shared_context 'insufficient permissions' do
|
||||||
|
before do
|
||||||
|
project.add_guest(user)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#add_link' do
|
||||||
|
shared_examples 'can add link' do
|
||||||
|
it 'appends the link to issue description' do
|
||||||
|
expect(result).to be_success
|
||||||
|
expect(result.payload[:description])
|
||||||
|
.to eq("#{issue.description}\n\n#{zoom_link}")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
shared_examples 'cannot add link' do
|
||||||
|
it 'cannot add the link' do
|
||||||
|
expect(result).to be_error
|
||||||
|
expect(result.message).to eq('Failed to add a Zoom meeting')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
subject(:result) { service.add_link(zoom_link) }
|
||||||
|
|
||||||
|
context 'without Zoom link in the issue description' do
|
||||||
|
include_context 'without Zoom link'
|
||||||
|
include_examples 'can add link'
|
||||||
|
|
||||||
|
context 'with invalid Zoom link' do
|
||||||
|
let(:zoom_link) { 'https://not-zoom.link' }
|
||||||
|
|
||||||
|
include_examples 'cannot add link'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when feature flag is disabled' do
|
||||||
|
include_context 'feature flag disabled'
|
||||||
|
include_examples 'cannot add link'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with insufficient permissions' do
|
||||||
|
include_context 'insufficient permissions'
|
||||||
|
include_examples 'cannot add link'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with Zoom link in the issue description' do
|
||||||
|
include_context 'with Zoom link'
|
||||||
|
include_examples 'cannot add link'
|
||||||
|
|
||||||
|
context 'but not at the end' do
|
||||||
|
include_context 'with Zoom link not at the end'
|
||||||
|
include_examples 'can add link'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'without issue description' do
|
||||||
|
include_context 'without issue description'
|
||||||
|
include_examples 'can add link'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#can_add_link?' do
|
||||||
|
subject { service.can_add_link? }
|
||||||
|
|
||||||
|
context 'without Zoom link in the issue description' do
|
||||||
|
include_context 'without Zoom link'
|
||||||
|
|
||||||
|
it { is_expected.to eq(true) }
|
||||||
|
|
||||||
|
context 'when feature flag is disabled' do
|
||||||
|
include_context 'feature flag disabled'
|
||||||
|
|
||||||
|
it { is_expected.to eq(false) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with insufficient permissions' do
|
||||||
|
include_context 'insufficient permissions'
|
||||||
|
|
||||||
|
it { is_expected.to eq(false) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with Zoom link in the issue description' do
|
||||||
|
include_context 'with Zoom link'
|
||||||
|
|
||||||
|
it { is_expected.to eq(false) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#remove_link' do
|
||||||
|
shared_examples 'cannot remove link' do
|
||||||
|
it 'cannot remove the link' do
|
||||||
|
expect(result).to be_error
|
||||||
|
expect(result.message).to eq('Failed to remove a Zoom meeting')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
subject(:result) { service.remove_link }
|
||||||
|
|
||||||
|
context 'with Zoom link in the issue description' do
|
||||||
|
include_context 'with Zoom link'
|
||||||
|
|
||||||
|
it 'removes the link from the issue description' do
|
||||||
|
expect(result).to be_success
|
||||||
|
expect(result.payload[:description])
|
||||||
|
.to eq(issue.description.delete_suffix("\n\n#{zoom_link}"))
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when feature flag is disabled' do
|
||||||
|
include_context 'feature flag disabled'
|
||||||
|
include_examples 'cannot remove link'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with insufficient permissions' do
|
||||||
|
include_context 'insufficient permissions'
|
||||||
|
include_examples 'cannot remove link'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'but not at the end' do
|
||||||
|
include_context 'with Zoom link not at the end'
|
||||||
|
include_examples 'cannot remove link'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'without Zoom link in the issue description' do
|
||||||
|
include_context 'without Zoom link'
|
||||||
|
include_examples 'cannot remove link'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'without issue description' do
|
||||||
|
include_context 'without issue description'
|
||||||
|
include_examples 'cannot remove link'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#can_remove_link?' do
|
||||||
|
subject { service.can_remove_link? }
|
||||||
|
|
||||||
|
context 'with Zoom link in the issue description' do
|
||||||
|
include_context 'with Zoom link'
|
||||||
|
|
||||||
|
it { is_expected.to eq(true) }
|
||||||
|
|
||||||
|
context 'when feature flag is disabled' do
|
||||||
|
include_context 'feature flag disabled'
|
||||||
|
|
||||||
|
it { is_expected.to eq(false) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with insufficient permissions' do
|
||||||
|
include_context 'insufficient permissions'
|
||||||
|
|
||||||
|
it { is_expected.to eq(false) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'without Zoom link in the issue description' do
|
||||||
|
include_context 'without Zoom link'
|
||||||
|
|
||||||
|
it { is_expected.to eq(false) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#parse_link' do
|
||||||
|
subject { service.parse_link(description) }
|
||||||
|
|
||||||
|
context 'with valid Zoom links' do
|
||||||
|
where(:description) do
|
||||||
|
[
|
||||||
|
'Some text https://zoom.us/j/123456789 more text',
|
||||||
|
'Mixed https://zoom.us/j/123456789 http://example.com',
|
||||||
|
'Multiple link https://zoom.us/my/name https://zoom.us/j/123456789'
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
with_them do
|
||||||
|
it { is_expected.to eq('https://zoom.us/j/123456789') }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with invalid Zoom links' do
|
||||||
|
where(:description) do
|
||||||
|
[
|
||||||
|
nil,
|
||||||
|
'',
|
||||||
|
'Text only',
|
||||||
|
'Non-Zoom http://example.com',
|
||||||
|
'Almost Zoom http://zoom.us'
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
with_them do
|
||||||
|
it { is_expected.to eq(nil) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,111 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
shared_examples 'zoom quick actions' do
|
||||||
|
let(:zoom_link) { 'https://zoom.us/j/123456789' }
|
||||||
|
let(:invalid_zoom_link) { 'https://invalid-zoom' }
|
||||||
|
|
||||||
|
before do
|
||||||
|
issue.update!(description: description)
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '/zoom' do
|
||||||
|
shared_examples 'skip silently' do
|
||||||
|
it 'skip addition silently' do
|
||||||
|
add_note("/zoom #{zoom_link}")
|
||||||
|
|
||||||
|
wait_for_requests
|
||||||
|
|
||||||
|
expect(page).not_to have_content('Zoom meeting added')
|
||||||
|
expect(page).not_to have_content('Failed to add a Zoom meeting')
|
||||||
|
expect(issue.reload.description).to eq(description)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
shared_examples 'success' do
|
||||||
|
it 'adds a Zoom link' do
|
||||||
|
add_note("/zoom #{zoom_link}")
|
||||||
|
|
||||||
|
wait_for_requests
|
||||||
|
|
||||||
|
expect(page).to have_content('Zoom meeting added')
|
||||||
|
expect(issue.reload.description).to end_with(zoom_link)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'without issue description' do
|
||||||
|
let(:description) { nil }
|
||||||
|
|
||||||
|
include_examples 'success'
|
||||||
|
|
||||||
|
it 'cannot add invalid zoom link' do
|
||||||
|
add_note("/zoom #{invalid_zoom_link}")
|
||||||
|
|
||||||
|
wait_for_requests
|
||||||
|
|
||||||
|
expect(page).to have_content('Failed to add a Zoom meeting')
|
||||||
|
expect(page).not_to have_content(zoom_link)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when feature flag disabled' do
|
||||||
|
before do
|
||||||
|
stub_feature_flags(issue_zoom_integration: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
include_examples 'skip silently'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with Zoom link not at the end of the issue description' do
|
||||||
|
let(:description) { "A link #{zoom_link} not at the end" }
|
||||||
|
|
||||||
|
include_examples 'success'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with Zoom link at end of the issue description' do
|
||||||
|
let(:description) { "Text\n#{zoom_link}" }
|
||||||
|
|
||||||
|
include_examples 'skip silently'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '/remove_zoom' do
|
||||||
|
shared_examples 'skip silently' do
|
||||||
|
it 'skip removal silently' do
|
||||||
|
add_note('/remove_zoom')
|
||||||
|
|
||||||
|
wait_for_requests
|
||||||
|
|
||||||
|
expect(page).not_to have_content('Zoom meeting removed')
|
||||||
|
expect(page).not_to have_content('Failed to remove a Zoom meeting')
|
||||||
|
expect(issue.reload.description).to eq(description)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with Zoom link in the description' do
|
||||||
|
let(:description) { "Text with #{zoom_link}\n\n\n#{zoom_link}" }
|
||||||
|
|
||||||
|
it 'removes last Zoom link' do
|
||||||
|
add_note('/remove_zoom')
|
||||||
|
|
||||||
|
wait_for_requests
|
||||||
|
|
||||||
|
expect(page).to have_content('Zoom meeting removed')
|
||||||
|
expect(issue.reload.description).to eq("Text with #{zoom_link}")
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when feature flag disabled' do
|
||||||
|
before do
|
||||||
|
stub_feature_flags(issue_zoom_integration: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
include_examples 'skip silently'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with a Zoom link not at the end of the description' do
|
||||||
|
let(:description) { "A link #{zoom_link} not at the end" }
|
||||||
|
|
||||||
|
include_examples 'skip silently'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Reference in New Issue
Block a user