summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/issues
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/issues')
-rw-r--r--app/assets/javascripts/issues/constants.js5
-rw-r--r--app/assets/javascripts/issues/create_merge_request_dropdown.js6
-rw-r--r--app/assets/javascripts/issues/list/graphql.js5
-rw-r--r--app/assets/javascripts/issues/list/index.js4
-rw-r--r--app/assets/javascripts/issues/show/components/header_actions.vue165
-rw-r--r--app/assets/javascripts/issues/show/components/new_header_actions_popover.vue82
-rw-r--r--app/assets/javascripts/issues/show/constants.js2
-rw-r--r--app/assets/javascripts/issues/show/index.js2
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),
});