diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-11-03 18:09:05 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-11-03 18:09:05 +0300 |
commit | ff8eb438401fc82b883fc4ae69626f0035b69236 (patch) | |
tree | 044a7195c0338c2b31d55dd21a5638068a5722ea /app/assets/javascripts/issue_show | |
parent | 2ac811ce685f906d3e54e78b23f61495c19ad595 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts/issue_show')
4 files changed, 235 insertions, 1 deletions
diff --git a/app/assets/javascripts/issue_show/components/header_actions.vue b/app/assets/javascripts/issue_show/components/header_actions.vue new file mode 100644 index 00000000000..76fc7d0cb47 --- /dev/null +++ b/app/assets/javascripts/issue_show/components/header_actions.vue @@ -0,0 +1,187 @@ +<script> +import { GlButton, GlDropdown, GlDropdownItem, GlIcon, GlLink, GlModal } from '@gitlab/ui'; +import { mapGetters } from 'vuex'; +import createFlash from '~/flash'; +import { IssuableStatus, IssueStateEvent } from '~/issue_show/constants'; +import { __ } from '~/locale'; +import updateIssueMutation from '../queries/update_issue.mutation.graphql'; + +export default { + components: { + GlButton, + GlDropdown, + GlDropdownItem, + GlIcon, + GlLink, + GlModal, + }, + actionCancel: { + text: __('Cancel'), + }, + actionPrimary: { + text: __('Yes, close issue'), + attributes: [{ variant: 'warning' }], + }, + inject: [ + 'canCreateIssue', + 'canReopenIssue', + 'canReportSpam', + 'canUpdateIssue', + 'iid', + 'isIssueAuthor', + 'newIssuePath', + 'projectPath', + 'reportAbusePath', + 'submitAsSpamPath', + ], + data() { + return { + isUpdatingState: false, + }; + }, + computed: { + ...mapGetters(['getNoteableData']), + isClosed() { + return this.getNoteableData.state === IssuableStatus.Closed; + }, + buttonText() { + return this.isClosed ? __('Reopen issue') : __('Close issue'); + }, + buttonVariant() { + return this.isClosed ? 'default' : 'warning'; + }, + showToggleIssueButton() { + const canClose = !this.isClosed && this.canUpdateIssue; + const canReopen = this.isClosed && this.canReopenIssue; + return canClose || canReopen; + }, + }, + methods: { + toggleIssueState() { + if (!this.isClosed && this.getNoteableData?.blocked_by_issues?.length) { + this.$refs.blockedByIssuesModal.show(); + return; + } + + this.invokeUpdateIssueMutation(); + }, + invokeUpdateIssueMutation() { + this.isUpdatingState = true; + + this.$apollo + .mutate({ + mutation: updateIssueMutation, + variables: { + input: { + iid: this.iid.toString(), + projectPath: this.projectPath, + stateEvent: this.isClosed ? IssueStateEvent.Reopen : IssueStateEvent.Close, + }, + }, + }) + .then(({ data }) => { + if (data.updateIssue.errors.length) { + createFlash(data.updateIssue.errors.join('. ')); + return; + } + + const payload = { + detail: { + data: { id: this.iid }, + isClosed: !this.isClosed, + }, + }; + + // Dispatch event which updates open/close state, shared among the issue show page + document.dispatchEvent(new CustomEvent('issuable_vue_app:change', payload)); + }) + .catch(() => createFlash(__('Update failed. Please try again.'))) + .finally(() => { + this.isUpdatingState = false; + }); + }, + }, +}; +</script> + +<template> + <div class="detail-page-header-actions"> + <gl-dropdown class="gl-display-block gl-display-sm-none!" block :text="__('Issue actions')"> + <gl-dropdown-item + v-if="showToggleIssueButton" + :disabled="isUpdatingState" + @click="toggleIssueState" + > + {{ buttonText }} + </gl-dropdown-item> + <gl-dropdown-item v-if="canCreateIssue" :href="newIssuePath"> + {{ __('New issue') }} + </gl-dropdown-item> + <gl-dropdown-item v-if="!isIssueAuthor" :href="reportAbusePath"> + {{ __('Report abuse') }} + </gl-dropdown-item> + <gl-dropdown-item + v-if="canReportSpam" + :href="submitAsSpamPath" + data-method="post" + rel="nofollow" + > + {{ __('Submit as spam') }} + </gl-dropdown-item> + </gl-dropdown> + + <gl-button + v-if="showToggleIssueButton" + class="gl-display-none gl-display-sm-inline-flex!" + category="secondary" + :loading="isUpdatingState" + :variant="buttonVariant" + @click="toggleIssueState" + > + {{ buttonText }} + </gl-button> + + <gl-dropdown + class="gl-display-none gl-display-sm-inline-flex!" + toggle-class="gl-border-0! gl-shadow-none!" + no-caret + right + > + <template #button-content> + <gl-icon name="ellipsis_v" aria-hidden="true" /> + <span class="gl-sr-only">{{ __('Actions') }}</span> + </template> + + <gl-dropdown-item v-if="canCreateIssue" :href="newIssuePath"> + {{ __('New issue') }} + </gl-dropdown-item> + <gl-dropdown-item v-if="!isIssueAuthor" :href="reportAbusePath"> + {{ __('Report abuse') }} + </gl-dropdown-item> + <gl-dropdown-item + v-if="canReportSpam" + :href="submitAsSpamPath" + data-method="post" + rel="nofollow" + > + {{ __('Submit as spam') }} + </gl-dropdown-item> + </gl-dropdown> + + <gl-modal + ref="blockedByIssuesModal" + modal-id="blocked-by-issues-modal" + :action-cancel="$options.actionCancel" + :action-primary="$options.actionPrimary" + :title="__('Are you sure you want to close this blocked issue?')" + @primary="invokeUpdateIssueMutation" + > + <p>{{ __('This issue is currently blocked by the following issues:') }}</p> + <ul> + <li v-for="issue in getNoteableData.blocked_by_issues" :key="issue.iid"> + <gl-link :href="issue.web_url">#{{ issue.iid }}</gl-link> + </li> + </ul> + </gl-modal> + </div> +</template> diff --git a/app/assets/javascripts/issue_show/constants.js b/app/assets/javascripts/issue_show/constants.js index 576cf793717..a5ca91dffd4 100644 --- a/app/assets/javascripts/issue_show/constants.js +++ b/app/assets/javascripts/issue_show/constants.js @@ -18,5 +18,10 @@ export const IssuableType = { MergeRequest: 'merge_request', }; +export const IssueStateEvent = { + Close: 'CLOSE', + Reopen: 'REOPEN', +}; + export const STATUS_PAGE_PUBLISHED = __('Published on status page'); export const JOIN_ZOOM_MEETING = __('Join Zoom meeting'); diff --git a/app/assets/javascripts/issue_show/issue.js b/app/assets/javascripts/issue_show/issue.js index 4af2577e33b..fc9e8e051bb 100644 --- a/app/assets/javascripts/issue_show/issue.js +++ b/app/assets/javascripts/issue_show/issue.js @@ -1,8 +1,12 @@ import Vue from 'vue'; +import VueApollo from 'vue-apollo'; import { mapGetters } from 'vuex'; +import createDefaultClient from '~/lib/graphql'; +import { parseBoolean } from '~/lib/utils/common_utils'; import IssuableApp from './components/app.vue'; +import HeaderActions from './components/header_actions.vue'; -export default function initIssuableApp(issuableData, store) { +export function initIssuableApp(issuableData, store) { return new Vue({ el: document.getElementById('js-issuable-app'), store, @@ -19,3 +23,36 @@ export default function initIssuableApp(issuableData, store) { }, }); } + +export function initIssueHeaderActions(store) { + const el = document.querySelector('.js-issue-header-actions'); + + if (!el) { + return undefined; + } + + Vue.use(VueApollo); + + const apolloProvider = new VueApollo({ + defaultClient: createDefaultClient(), + }); + + return new Vue({ + el, + apolloProvider, + store, + provide: { + canCreateIssue: parseBoolean(el.dataset.canCreateIssue), + canReopenIssue: parseBoolean(el.dataset.canReopenIssue), + canReportSpam: parseBoolean(el.dataset.canReportSpam), + canUpdateIssue: parseBoolean(el.dataset.canUpdateIssue), + iid: el.dataset.iid, + isIssueAuthor: parseBoolean(el.dataset.isIssueAuthor), + newIssuePath: el.dataset.newIssuePath, + projectPath: el.dataset.projectPath, + reportAbusePath: el.dataset.reportAbusePath, + submitAsSpamPath: el.dataset.submitAsSpamPath, + }, + render: createElement => createElement(HeaderActions), + }); +} diff --git a/app/assets/javascripts/issue_show/queries/update_issue.mutation.graphql b/app/assets/javascripts/issue_show/queries/update_issue.mutation.graphql new file mode 100644 index 00000000000..9c28fdded21 --- /dev/null +++ b/app/assets/javascripts/issue_show/queries/update_issue.mutation.graphql @@ -0,0 +1,5 @@ +mutation updateIssue($input: UpdateIssueInput!) { + updateIssue(input: $input) { + errors + } +} |