diff options
Diffstat (limited to 'app/assets/javascripts/issues')
8 files changed, 255 insertions, 16 deletions
diff --git a/app/assets/javascripts/issues/constants.js b/app/assets/javascripts/issues/constants.js index d35355a8f26..371db4eacc3 100644 --- a/app/assets/javascripts/issues/constants.js +++ b/app/assets/javascripts/issues/constants.js @@ -26,3 +26,8 @@ export const IssuableStatusText = { [STATUS_MERGED]: __('Merged'), [STATUS_LOCKED]: __('Open'), }; + +export const IssuableTypeText = { + [TYPE_ISSUE]: __('issue'), + [TYPE_MERGE_REQUEST]: __('merge request'), +}; diff --git a/app/assets/javascripts/issues/create_merge_request_dropdown.js b/app/assets/javascripts/issues/create_merge_request_dropdown.js index c821c18bcb9..de0334b4ffe 100644 --- a/app/assets/javascripts/issues/create_merge_request_dropdown.js +++ b/app/assets/javascripts/issues/create_merge_request_dropdown.js @@ -432,7 +432,7 @@ export default class CreateMergeRequestDropdown { let xhr = null; event.preventDefault(); - if (isConfidentialIssue() && !event.target.classList.contains('js-create-target')) { + if (isConfidentialIssue() && !event.currentTarget.classList.contains('js-create-target')) { this.droplab.hooks.forEach((hook) => hook.list.toggle()); return; @@ -442,9 +442,9 @@ export default class CreateMergeRequestDropdown { return; } - if (event.target.dataset.action === CREATE_MERGE_REQUEST) { + if (event.currentTarget.dataset.action === CREATE_MERGE_REQUEST) { xhr = this.createMergeRequest(); - } else if (event.target.dataset.action === CREATE_BRANCH) { + } else if (event.currentTarget.dataset.action === CREATE_BRANCH) { xhr = this.createBranch(); } diff --git a/app/assets/javascripts/issues/list/graphql.js b/app/assets/javascripts/issues/list/graphql.js index 96330f69965..e64870152bd 100644 --- a/app/assets/javascripts/issues/list/graphql.js +++ b/app/assets/javascripts/issues/list/graphql.js @@ -2,6 +2,8 @@ import produce from 'immer'; import createDefaultClient, { createApolloClientWithCaching } from '~/lib/graphql'; import getIssuesQuery from 'ee_else_ce/issues/list/queries/get_issues.query.graphql'; +let client; + const resolvers = { Mutation: { reorderIssues: (_, { oldIndex, newIndex, namespace, serializedVariables }, { cache }) => { @@ -23,7 +25,8 @@ const resolvers = { }; export async function gqlClient() { - const client = gon.features?.frontendCaching + if (client) return client; + client = gon.features?.frontendCaching ? await createApolloClientWithCaching(resolvers, { localCacheKey: 'issues_list' }) : createDefaultClient(resolvers); return client; diff --git a/app/assets/javascripts/issues/list/index.js b/app/assets/javascripts/issues/list/index.js index 720946ea330..a97b59c1e4f 100644 --- a/app/assets/javascripts/issues/list/index.js +++ b/app/assets/javascripts/issues/list/index.js @@ -6,7 +6,7 @@ import { parseBoolean } from '~/lib/utils/common_utils'; import JiraIssuesImportStatusApp from './components/jira_issues_import_status_app.vue'; import { gqlClient } from './graphql'; -export function mountJiraIssuesListApp() { +export async function mountJiraIssuesListApp() { const el = document.querySelector('.js-jira-issues-import-status-root'); if (!el) { @@ -27,7 +27,7 @@ export function mountJiraIssuesListApp() { el, name: 'JiraIssuesImportStatusRoot', apolloProvider: new VueApollo({ - defaultClient: gqlClient, + defaultClient: await gqlClient(), }), render(createComponent) { return createComponent(JiraIssuesImportStatusApp, { diff --git a/app/assets/javascripts/issues/show/components/header_actions.vue b/app/assets/javascripts/issues/show/components/header_actions.vue index 84def374d13..b929c4dbae0 100644 --- a/app/assets/javascripts/issues/show/components/header_actions.vue +++ b/app/assets/javascripts/issues/show/components/header_actions.vue @@ -2,23 +2,36 @@ import { GlButton, GlDropdown, + GlDropdownDivider, GlDropdownItem, GlLink, GlModal, GlModalDirective, GlTooltipDirective, } from '@gitlab/ui'; +import * as Sentry from '@sentry/browser'; import { mapActions, mapGetters, mapState } from 'vuex'; import { createAlert, VARIANT_SUCCESS } from '~/alert'; import { EVENT_ISSUABLE_VUE_APP_CHANGE } from '~/issuable/constants'; -import { STATUS_CLOSED, TYPE_INCIDENT, TYPE_ISSUE } from '~/issues/constants'; -import { ISSUE_STATE_EVENT_CLOSE, ISSUE_STATE_EVENT_REOPEN } from '~/issues/show/constants'; +import { STATUS_CLOSED, TYPE_INCIDENT, TYPE_ISSUE, IssuableTypeText } from '~/issues/constants'; +import { + ISSUE_STATE_EVENT_CLOSE, + ISSUE_STATE_EVENT_REOPEN, + NEW_ACTIONS_POPOVER_KEY, +} from '~/issues/show/constants'; import { capitalizeFirstCharacter } from '~/lib/utils/text_utility'; +import { getCookie, parseBoolean, setCookie } from '~/lib/utils/common_utils'; import { visitUrl } from '~/lib/utils/url_utility'; import { s__, __, sprintf } from '~/locale'; import eventHub from '~/notes/event_hub'; import Tracking from '~/tracking'; +import toast from '~/vue_shared/plugins/global_toast'; import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue'; +import NewHeaderActionsPopover from '~/issues/show/components/new_header_actions_popover.vue'; +import SidebarSubscriptionsWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue'; +import IssuableLockForm from '~/sidebar/components/lock/issuable_lock_form.vue'; +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; +import issueReferenceQuery from '~/sidebar/queries/issue_reference.query.graphql'; import issuesEventHub from '../event_hub'; import promoteToEpicMutation from '../queries/promote_to_epic.mutation.graphql'; import updateIssueMutation from '../queries/update_issue.mutation.graphql'; @@ -44,21 +57,27 @@ export default { 'The issue was successfully promoted to an epic. Redirecting to epic...', ), reportAbuse: __('Report abuse to administrator'), + referenceFetchError: __('An error occurred while fetching reference'), + copyReferenceText: __('Copy reference'), }, components: { DeleteIssueModal, GlButton, GlDropdown, + GlDropdownDivider, GlDropdownItem, GlLink, GlModal, AbuseCategorySelector, + NewHeaderActionsPopover, + SidebarSubscriptionsWidget, + IssuableLockForm, }, directives: { GlModal: GlModalDirective, GlTooltip: GlTooltipDirective, }, - mixins: [trackingMixin], + mixins: [trackingMixin, glFeatureFlagMixin()], inject: { canCreateIssue: { default: false, @@ -105,15 +124,46 @@ export default { reportedFromUrl: { default: '', }, + issuableEmailAddress: { + default: '', + }, + fullPath: { + default: '', + }, }, data() { return { isReportAbuseDrawerOpen: false, }; }, + apollo: { + issuableReference: { + query: issueReferenceQuery, + variables() { + return { + fullPath: this.fullPath, + iid: this.iid, + }; + }, + update(data) { + return data.workspace?.issuable?.reference || ''; + }, + skip() { + return !this.isMrSidebarMoved; + }, + error(error) { + createAlert({ message: this.$options.i18n.referenceFetchError }); + Sentry.captureException(error); + }, + }, + }, computed: { ...mapState(['isToggleStateButtonLoading']), ...mapGetters(['openState', 'getBlockedByIssues']), + ...mapGetters(['getNoteableData']), + isLocked() { + return this.getNoteableData.discussion_locked; + }, isClosed() { return this.openState === STATUS_CLOSED; }, @@ -157,6 +207,17 @@ export default { hasMobileDropdown() { return this.hasDesktopDropdown || this.showToggleIssueStateButton; }, + copyMailAddressText() { + return sprintf(__('Copy %{issueType} email address'), { + issueType: IssuableTypeText[this.issueType], + }); + }, + isMrSidebarMoved() { + return this.glFeatures.movedMrSidebar; + }, + showLockIssueOption() { + return this.isMrSidebarMoved && this.issueType === TYPE_ISSUE; + }, }, created() { eventHub.$on('toggle.issuable.state', this.toggleIssueState); @@ -166,6 +227,7 @@ export default { }, methods: { ...mapActions(['toggleStateButtonLoading']), + ...mapActions(['updateLockedAttribute']), toggleIssueState() { if (!this.isClosed && this.getBlockedByIssues?.length) { this.$refs.blockedByIssuesModal.show(); @@ -244,7 +306,19 @@ export default { edit() { issuesEventHub.$emit('open.form'); }, + dismissPopover() { + if (this.isMrSidebarMoved && !parseBoolean(getCookie(`${NEW_ACTIONS_POPOVER_KEY}`))) { + setCookie(NEW_ACTIONS_POPOVER_KEY, true); + } + }, + copyReference() { + toast(__('Reference copied')); + }, + copyEmailAddress() { + toast(__('Email address copied')); + }, }, + TYPE_ISSUE, }; </script> @@ -259,6 +333,21 @@ export default { data-testid="mobile-dropdown" :loading="isToggleStateButtonLoading" > + <template v-if="isMrSidebarMoved"> + <sidebar-subscriptions-widget + :iid="String(iid)" + :full-path="fullPath" + :issuable-type="$options.TYPE_ISSUE" + data-testid="notification-toggle" + /> + + <gl-dropdown-divider /> + </template> + + <template v-if="showLockIssueOption"> + <issuable-lock-form :is-editable="false" data-testid="lock-issue-toggle" /> + </template> + <gl-dropdown-item v-if="canUpdateIssue" @click="edit"> {{ $options.i18n.edit }} </gl-dropdown-item> @@ -275,9 +364,21 @@ export default { <gl-dropdown-item v-if="canPromoteToEpic" @click="promoteToEpic"> {{ __('Promote to epic') }} </gl-dropdown-item> - <gl-dropdown-item v-if="!isIssueAuthor" @click="toggleReportAbuseDrawer(true)"> - {{ $options.i18n.reportAbuse }} - </gl-dropdown-item> + <template v-if="isMrSidebarMoved"> + <gl-dropdown-item + :data-clipboard-text="issuableReference" + data-testid="copy-reference" + @click="copyReference" + >{{ $options.i18n.copyReferenceText }}</gl-dropdown-item + > + <gl-dropdown-item + v-if="issuableEmailAddress" + :data-clipboard-text="issuableEmailAddress" + data-testid="copy-email" + @click="copyEmailAddress" + >{{ copyMailAddressText }}</gl-dropdown-item + > + </template> <gl-dropdown-item v-if="canReportSpam" :href="submitAsSpamPath" @@ -287,6 +388,7 @@ export default { {{ __('Submit as spam') }} </gl-dropdown-item> <template v-if="canDestroyIssue"> + <gl-dropdown-divider /> <gl-dropdown-item v-gl-modal="$options.deleteModalId" variant="danger" @@ -295,6 +397,13 @@ export default { {{ deleteButtonText }} </gl-dropdown-item> </template> + <gl-dropdown-item + v-if="!isIssueAuthor" + data-testid="report-abuse-item" + @click="toggleReportAbuseDrawer(true)" + > + {{ $options.i18n.reportAbuse }} + </gl-dropdown-item> </gl-dropdown> <gl-button @@ -322,6 +431,7 @@ export default { <gl-dropdown v-if="hasDesktopDropdown" + id="new-actions-header-dropdown" v-gl-tooltip.hover class="gl-display-none gl-sm-display-inline-flex! gl-sm-ml-3" icon="ellipsis_v" @@ -334,7 +444,19 @@ export default { data-testid="desktop-dropdown" no-caret right + @shown="dismissPopover" > + <template v-if="isMrSidebarMoved"> + <sidebar-subscriptions-widget + :iid="String(iid)" + :full-path="fullPath" + :issuable-type="$options.TYPE_ISSUE" + data-testid="notification-toggle" + /> + + <gl-dropdown-divider /> + </template> + <gl-dropdown-item v-if="canCreateIssue" :href="newIssuePath"> {{ newIssueTypeText }} </gl-dropdown-item> @@ -346,9 +468,24 @@ export default { > {{ __('Promote to epic') }} </gl-dropdown-item> - <gl-dropdown-item v-if="!isIssueAuthor" @click="toggleReportAbuseDrawer(true)"> - {{ $options.i18n.reportAbuse }} - </gl-dropdown-item> + <template v-if="showLockIssueOption"> + <issuable-lock-form :is-editable="false" data-testid="lock-issue-toggle" /> + </template> + <template v-if="isMrSidebarMoved"> + <gl-dropdown-item + :data-clipboard-text="issuableReference" + data-testid="copy-reference" + @click="copyReference" + >{{ $options.i18n.copyReferenceText }}</gl-dropdown-item + > + <gl-dropdown-item + v-if="issuableEmailAddress" + :data-clipboard-text="issuableEmailAddress" + data-testid="copy-email" + @click="copyEmailAddress" + >{{ copyMailAddressText }}</gl-dropdown-item + > + </template> <gl-dropdown-item v-if="canReportSpam" :href="submitAsSpamPath" @@ -357,8 +494,8 @@ export default { > {{ __('Submit as spam') }} </gl-dropdown-item> - <template v-if="canDestroyIssue"> + <gl-dropdown-divider /> <gl-dropdown-item v-gl-modal="$options.deleteModalId" variant="danger" @@ -368,8 +505,16 @@ export default { {{ deleteButtonText }} </gl-dropdown-item> </template> + <gl-dropdown-item + v-if="!isIssueAuthor" + data-testid="report-abuse-item" + @click="toggleReportAbuseDrawer(true)" + > + {{ $options.i18n.reportAbuse }} + </gl-dropdown-item> </gl-dropdown> + <new-header-actions-popover v-if="isMrSidebarMoved" :issue-type="issueType" /> <gl-modal ref="blockedByIssuesModal" modal-id="blocked-by-issues-modal" diff --git a/app/assets/javascripts/issues/show/components/new_header_actions_popover.vue b/app/assets/javascripts/issues/show/components/new_header_actions_popover.vue new file mode 100644 index 00000000000..8262b3ac0ff --- /dev/null +++ b/app/assets/javascripts/issues/show/components/new_header_actions_popover.vue @@ -0,0 +1,82 @@ +<script> +import { GlPopover, GlButton } from '@gitlab/ui'; +import { s__, sprintf } from '~/locale'; +import { getCookie, parseBoolean, setCookie } from '~/lib/utils/common_utils'; +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; +import { NEW_ACTIONS_POPOVER_KEY } from '~/issues/show/constants'; +import { IssuableTypeText } from '~/issues/constants'; + +export default { + name: 'NewHeaderActionsPopover', + i18n: { + popoverText: s__( + 'HeaderAction|Notifications and other %{issueType} actions have moved to this menu.', + ), + confirmButtonText: s__('HeaderAction|Okay!'), + }, + components: { + GlPopover, + GlButton, + }, + mixins: [glFeatureFlagMixin()], + props: { + issueType: { + type: String, + required: true, + }, + }, + data() { + return { + dismissKey: NEW_ACTIONS_POPOVER_KEY, + popoverDismissed: parseBoolean(getCookie(`${NEW_ACTIONS_POPOVER_KEY}`)), + }; + }, + computed: { + popoverText() { + return sprintf(this.$options.i18n.popoverText, { + issueType: IssuableTypeText[this.issueType], + }); + }, + showPopover() { + return !this.popoverDismissed && this.isMrSidebarMoved; + }, + isMrSidebarMoved() { + return this.glFeatures.movedMrSidebar; + }, + }, + methods: { + dismissPopover() { + this.popoverDismissed = true; + setCookie(this.dismissKey, this.popoverDismissed); + }, + }, +}; +</script> + +<template> + <div> + <gl-popover + v-if="showPopover" + target="new-actions-header-dropdown" + container="viewport" + placement="left" + :show="showPopover" + triggers="manual" + content="text" + :css-classes="['gl-p-2 new-header-popover']" + > + <template #title> + <div class="gl-font-base gl-font-weight-normal"> + {{ popoverText }} + </div> + </template> + <gl-button + data-testid="confirm-button" + variant="confirm" + type="submit" + @click="dismissPopover" + >{{ $options.i18n.confirmButtonText }}</gl-button + > + </gl-popover> + </div> +</template> diff --git a/app/assets/javascripts/issues/show/constants.js b/app/assets/javascripts/issues/show/constants.js index 4d8c11f9669..6320e4ef266 100644 --- a/app/assets/javascripts/issues/show/constants.js +++ b/app/assets/javascripts/issues/show/constants.js @@ -17,3 +17,5 @@ export const issueState = { issueType: undefined, isDirty: false, }; + +export const NEW_ACTIONS_POPOVER_KEY = 'new-actions-popover-viewed'; diff --git a/app/assets/javascripts/issues/show/index.js b/app/assets/javascripts/issues/show/index.js index e677328cd2e..100abcbe1e5 100644 --- a/app/assets/javascripts/issues/show/index.js +++ b/app/assets/javascripts/issues/show/index.js @@ -174,6 +174,8 @@ export function initHeaderActions(store, type = '') { reportedUserId: parseInt(el.dataset.reportedUserId, 10), reportedFromUrl: el.dataset.reportedFromUrl, submitAsSpamPath: el.dataset.submitAsSpamPath, + issuableEmailAddress: el.dataset.issuableEmailAddress, + fullPath: el.dataset.projectPath, }, render: (createElement) => createElement(HeaderActions), }); |