diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2019-09-14 03:06:25 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2019-09-14 03:06:25 +0300 |
commit | a93dfc1b7e55b118b1cf4a67afeb46556292914c (patch) | |
tree | 65b874b7940d0d05c4ebedaef43b8a1009362651 /app | |
parent | 188a57f93bba5953800de490fcc6246966a073fd (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
5 files changed, 218 insertions, 84 deletions
diff --git a/app/assets/javascripts/notes/services/notes_service.js b/app/assets/javascripts/notes/services/notes_service.js index 3d239d8cb6b..4d3dbec435f 100644 --- a/app/assets/javascripts/notes/services/notes_service.js +++ b/app/assets/javascripts/notes/services/notes_service.js @@ -1,31 +1,28 @@ -import Vue from 'vue'; -import VueResource from 'vue-resource'; +import axios from '~/lib/utils/axios_utils'; import * as constants from '../constants'; -Vue.use(VueResource); - export default { fetchDiscussions(endpoint, filter, persistFilter = true) { const config = filter !== undefined ? { params: { notes_filter: filter, persist_filter: persistFilter } } : null; - return Vue.http.get(endpoint, config); + return axios.get(endpoint, config); }, replyToDiscussion(endpoint, data) { - return Vue.http.post(endpoint, data, { emulateJSON: true }); + return axios.post(endpoint, data); }, updateNote(endpoint, data) { - return Vue.http.put(endpoint, data, { emulateJSON: true }); + return axios.put(endpoint, data); }, createNewNote(endpoint, data) { - return Vue.http.post(endpoint, data, { emulateJSON: true }); + return axios.post(endpoint, data); }, toggleResolveNote(endpoint, isResolved) { const { RESOLVE_NOTE_METHOD_NAME, UNRESOLVE_NOTE_METHOD_NAME } = constants; const method = isResolved ? UNRESOLVE_NOTE_METHOD_NAME : RESOLVE_NOTE_METHOD_NAME; - return Vue.http[method](endpoint); + return axios[method](endpoint); }, poll(data = {}) { 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) { - return Vue.http.put(endpoint, data); + return axios.put(endpoint, data); }, }; diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js index 411bd585672..6c236981a24 100644 --- a/app/assets/javascripts/notes/stores/actions.js +++ b/app/assets/javascripts/notes/stores/actions.js @@ -47,13 +47,10 @@ export const setNotesFetchedState = ({ commit }, state) => export const toggleDiscussion = ({ commit }, data) => commit(types.TOGGLE_DISCUSSION, data); export const fetchDiscussions = ({ commit, dispatch }, { path, filter, persistFilter }) => - service - .fetchDiscussions(path, filter, persistFilter) - .then(res => res.json()) - .then(discussions => { - commit(types.SET_INITIAL_DISCUSSIONS, discussions); - dispatch('updateResolvableDiscussionsCounts'); - }); + service.fetchDiscussions(path, filter, persistFilter).then(({ data }) => { + commit(types.SET_INITIAL_DISCUSSIONS, data); + dispatch('updateResolvableDiscussionsCounts'); + }); export const updateDiscussion = ({ commit, state }, discussion) => { commit(types.UPDATE_DISCUSSION, discussion); @@ -80,13 +77,10 @@ export const deleteNote = ({ dispatch }, note) => }); export const updateNote = ({ commit, dispatch }, { endpoint, note }) => - service - .updateNote(endpoint, note) - .then(res => res.json()) - .then(res => { - commit(types.UPDATE_NOTE, res); - dispatch('startTaskList'); - }); + service.updateNote(endpoint, note).then(({ data }) => { + commit(types.UPDATE_NOTE, data); + dispatch('startTaskList'); + }); export const updateOrCreateNotes = ({ commit, state, getters, dispatch }, notes) => { const { notesById } = getters; @@ -110,40 +104,37 @@ export const updateOrCreateNotes = ({ commit, state, getters, dispatch }, notes) }); }; -export const replyToDiscussion = ({ commit, state, getters, dispatch }, { endpoint, data }) => - service - .replyToDiscussion(endpoint, data) - .then(res => res.json()) - .then(res => { - if (res.discussion) { - commit(types.UPDATE_DISCUSSION, res.discussion); +export const replyToDiscussion = ( + { commit, state, getters, dispatch }, + { endpoint, data: reply }, +) => + service.replyToDiscussion(endpoint, reply).then(({ data }) => { + if (data.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('startTaskList'); - dispatch('updateResolvableDiscussionsCounts'); - } else { - commit(types.ADD_NEW_REPLY_TO_DISCUSSION, res); - } + dispatch('updateMergeRequestWidget'); + dispatch('startTaskList'); + dispatch('updateResolvableDiscussionsCounts'); + } else { + commit(types.ADD_NEW_REPLY_TO_DISCUSSION, data); + } - return res; - }); + return data; + }); -export const createNewNote = ({ commit, dispatch }, { endpoint, data }) => - service - .createNewNote(endpoint, data) - .then(res => res.json()) - .then(res => { - if (!res.errors) { - commit(types.ADD_NEW_NOTE, res); - - dispatch('updateMergeRequestWidget'); - dispatch('startTaskList'); - dispatch('updateResolvableDiscussionsCounts'); - } - return res; - }); +export const createNewNote = ({ commit, dispatch }, { endpoint, data: reply }) => + service.createNewNote(endpoint, reply).then(({ data }) => { + if (!data.errors) { + commit(types.ADD_NEW_NOTE, data); + + dispatch('updateMergeRequestWidget'); + dispatch('startTaskList'); + dispatch('updateResolvableDiscussionsCounts'); + } + return data; + }); 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 }) => - service - .toggleResolveNote(endpoint, isResolved) - .then(res => res.json()) - .then(res => { - const mutationType = discussion ? types.UPDATE_DISCUSSION : types.UPDATE_NOTE; + service.toggleResolveNote(endpoint, isResolved).then(({ data }) => { + 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 }) => { dispatch('toggleStateButtonLoading', true); - return service - .toggleIssueState(state.notesData.closePath) - .then(res => res.json()) - .then(data => { - commit(types.CLOSE_ISSUE); - dispatch('emitStateChangedEvent', data); - dispatch('toggleStateButtonLoading', false); - }); + return service.toggleIssueState(state.notesData.closePath).then(({ data }) => { + commit(types.CLOSE_ISSUE); + dispatch('emitStateChangedEvent', data); + dispatch('toggleStateButtonLoading', false); + }); }; export const reopenIssue = ({ commit, dispatch, state }) => { dispatch('toggleStateButtonLoading', true); - return service - .toggleIssueState(state.notesData.reopenPath) - .then(res => res.json()) - .then(data => { - commit(types.REOPEN_ISSUE); - dispatch('emitStateChangedEvent', data); - dispatch('toggleStateButtonLoading', false); - }); + return service.toggleIssueState(state.notesData.reopenPath).then(({ data }) => { + commit(types.REOPEN_ISSUE); + dispatch('emitStateChangedEvent', data); + dispatch('toggleStateButtonLoading', false); + }); }; export const toggleStateButtonLoading = ({ commit }, value) => @@ -340,8 +322,7 @@ export const poll = ({ commit, state, getters, dispatch }) => { resource: service, method: 'poll', data: state, - successCallback: resp => - resp.json().then(data => pollSuccessCallBack(data, commit, state, getters, dispatch)), + successCallback: ({ data }) => pollSuccessCallBack(data, commit, state, getters, dispatch), errorCallback: () => Flash(__('Something went wrong while fetching latest comments.')), }); @@ -376,8 +357,7 @@ export const fetchData = ({ commit, state, getters }) => { service .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.'))); }; diff --git a/app/assets/javascripts/releases/components/milestone_list.vue b/app/assets/javascripts/releases/components/milestone_list.vue new file mode 100644 index 00000000000..53416f0ab4d --- /dev/null +++ b/app/assets/javascripts/releases/components/milestone_list.vue @@ -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> diff --git a/app/assets/javascripts/releases/components/release_block.vue b/app/assets/javascripts/releases/components/release_block.vue index 88b6b4732b1..2dacd8549ad 100644 --- a/app/assets/javascripts/releases/components/release_block.vue +++ b/app/assets/javascripts/releases/components/release_block.vue @@ -5,6 +5,7 @@ import { GlTooltipDirective, GlLink, GlBadge } from '@gitlab/ui'; import Icon from '~/vue_shared/components/icon.vue'; import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; import timeagoMixin from '~/vue_shared/mixins/timeago'; +import MilestoneList from './milestone_list.vue'; import { __, sprintf } from '../../locale'; export default { @@ -14,6 +15,7 @@ export default { GlBadge, Icon, UserAvatarLink, + MilestoneList, }, directives: { GlTooltip: GlTooltipDirective, @@ -49,6 +51,20 @@ export default { hasAuthor() { 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> @@ -73,6 +89,12 @@ export default { <span v-gl-tooltip.bottom :title="__('Tag')">{{ release.tag_name }}</span> </div> + <milestone-list + v-if="shouldRenderMilestones" + class="append-right-4 js-milestone-list" + :milestones="milestones" + /> + <div class="append-right-4"> • <span v-gl-tooltip.bottom :title="tooltipTitle(release.released_at)"> diff --git a/app/services/issues/zoom_link_service.rb b/app/services/issues/zoom_link_service.rb new file mode 100644 index 00000000000..a061ab22875 --- /dev/null +++ b/app/services/issues/zoom_link_service.rb @@ -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 |