diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-04-19 15:19:34 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-04-19 15:19:34 +0000 |
commit | b6d63c915a91aeb7a4437349c53e68be8c50cf4e (patch) | |
tree | 8617959c1d6b9137e4cefad06aedbf574295cd6c /app/assets/javascripts | |
parent | 2017bc90a671eac669f0114b6ef508e151409c4f (diff) | |
download | gitlab-ce-b6d63c915a91aeb7a4437349c53e68be8c50cf4e.tar.gz |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts')
24 files changed, 550 insertions, 147 deletions
diff --git a/app/assets/javascripts/entrypoints/super_sidebar.js b/app/assets/javascripts/entrypoints/super_sidebar.js index 308077f98b1..6e88a998096 100644 --- a/app/assets/javascripts/entrypoints/super_sidebar.js +++ b/app/assets/javascripts/entrypoints/super_sidebar.js @@ -1,5 +1,6 @@ import '~/webpack'; import '~/commons'; -import { initSuperSidebar } from '~/super_sidebar/super_sidebar_bundle'; +import { initSuperSidebar, initSuperSidebarToggle } from '~/super_sidebar/super_sidebar_bundle'; initSuperSidebar(); +initSuperSidebarToggle(); diff --git a/app/assets/javascripts/invite_members/components/invite_members_modal.vue b/app/assets/javascripts/invite_members/components/invite_members_modal.vue index a2d3b47d8f0..e99a61caf3f 100644 --- a/app/assets/javascripts/invite_members/components/invite_members_modal.vue +++ b/app/assets/javascripts/invite_members/components/invite_members_modal.vue @@ -14,18 +14,20 @@ import InviteModalBase from 'ee_else_ce/invite_members/components/invite_modal_b import Api from '~/api'; import Tracking from '~/tracking'; import { BV_SHOW_MODAL, BV_HIDE_MODAL } from '~/lib/utils/constants'; -import { getParameterValues } from '~/lib/utils/url_utility'; import { n__, sprintf } from '~/locale'; import { + memberName, + triggerExternalAlert, + qualifiesForTasksToBeDone, +} from 'ee_else_ce/invite_members/utils/member_utils'; +import { USERS_FILTER_ALL, INVITE_MEMBERS_FOR_TASK, MEMBER_MODAL_LABELS, - LEARN_GITLAB, INVITE_MEMBER_MODAL_TRACKING_CATEGORY, } from '../constants'; import eventHub from '../event_hub'; import { responseFromSuccess } from '../utils/response_message_parser'; -import { memberName } from '../utils/member_utils'; import { getInvalidFeedbackMessage } from '../utils/get_invalid_feedback_message'; import { displaySuccessfulInvitationAlert, @@ -169,11 +171,7 @@ export default { ); }, tasksToBeDoneEnabled() { - return ( - (getParameterValues('open_modal')[0] === 'invite_members_for_task' || - this.isOnLearnGitlab) && - this.tasksToBeDoneOptions.length - ); + return qualifiesForTasksToBeDone(this.source) && this.tasksToBeDoneOptions.length; }, showTasksToBeDone() { return ( @@ -192,9 +190,6 @@ export default { ? this.selectedTaskProject.id : ''; }, - isOnLearnGitlab() { - return this.source === LEARN_GITLAB; - }, showUserLimitNotification() { return !isEmpty(this.usersLimitDataset.alertVariant); }, @@ -283,7 +278,24 @@ export default { this.shouldShowEmptyInvitesAlert = true; this.$refs.alerts.focus(); }, - sendInvite({ accessLevel, expiresAt }) { + getInvitePayload({ accessLevel, expiresAt }) { + const [usersToInviteByEmail, usersToAddById] = this.partitionNewUsersToInvite(); + + const email = usersToInviteByEmail !== '' ? { email: usersToInviteByEmail } : {}; + const userId = usersToAddById !== '' ? { user_id: usersToAddById } : {}; + + return { + format: 'json', + expires_at: expiresAt, + access_level: accessLevel, + invite_source: this.source, + tasks_to_be_done: this.tasksToBeDoneForPost, + tasks_project_id: this.tasksProjectForPost, + ...email, + ...userId, + }; + }, + async sendInvite({ accessLevel, expiresAt }) { this.isLoading = true; this.clearValidation(); @@ -292,40 +304,28 @@ export default { return; } - const [usersToInviteByEmail, usersToAddById] = this.partitionNewUsersToInvite(); + this.trackInviteMembersForTask(); const apiAddByInvite = this.isProject ? Api.inviteProjectMembers.bind(Api) : Api.inviteGroupMembers.bind(Api); - const email = usersToInviteByEmail !== '' ? { email: usersToInviteByEmail } : {}; - const userId = usersToAddById !== '' ? { user_id: usersToAddById } : {}; + try { + const payload = this.getInvitePayload({ accessLevel, expiresAt }); + const response = await apiAddByInvite(this.id, payload); - this.trackinviteMembersForTask(); + const { error, message } = responseFromSuccess(response); - apiAddByInvite(this.id, { - format: 'json', - expires_at: expiresAt, - access_level: accessLevel, - invite_source: this.source, - tasks_to_be_done: this.tasksToBeDoneForPost, - tasks_project_id: this.tasksProjectForPost, - ...email, - ...userId, - }) - .then((response) => { - const { error, message } = responseFromSuccess(response); - - if (error) { - this.showMemberErrors(message); - } else { - this.onInviteSuccess(); - } - }) - .catch((e) => this.showInvalidFeedbackMessage(e)) - .finally(() => { - this.isLoading = false; - }); + if (error) { + this.showMemberErrors(message); + } else { + this.onInviteSuccess(); + } + } catch (e) { + this.showInvalidFeedbackMessage(e); + } finally { + this.isLoading = false; + } }, showMemberErrors(message) { this.invalidMembers = message; @@ -335,7 +335,7 @@ export default { // initial token creation hits this and nothing is found... so safe navigation return this.newUsersToInvite.find((member) => memberName(member) === username)?.name; }, - trackinviteMembersForTask() { + trackInviteMembersForTask() { const label = 'selected_tasks_to_be_done'; const property = this.selectedTasksToBeDone.join(','); this.track(INVITE_MEMBERS_FOR_TASK.submit, { label, property }); @@ -367,9 +367,7 @@ export default { } }, showSuccessMessage() { - if (this.isOnLearnGitlab) { - eventHub.$emit('showSuccessfulInvitationsAlert'); - } else { + if (!triggerExternalAlert(this.source)) { this.$toast.show(this.$options.labels.toastMessageSuccessful); } @@ -421,7 +419,9 @@ export default { @access-level="onAccessLevelUpdate" > <template #intro-text-before> - <div v-if="isCelebration" class="gl-p-4 gl-font-size-h1"><gl-emoji data-name="tada" /></div> + <div v-if="isCelebration" class="gl-p-4 gl-font-size-h1"> + <gl-emoji data-name="tada" /> + </div> </template> <template #intro-text-after> <br /> diff --git a/app/assets/javascripts/invite_members/constants.js b/app/assets/javascripts/invite_members/constants.js index f373ef81e68..d5e9e498c6b 100644 --- a/app/assets/javascripts/invite_members/constants.js +++ b/app/assets/javascripts/invite_members/constants.js @@ -150,7 +150,6 @@ export const GROUP_MODAL_LABELS = { toastMessageSuccessful: TOAST_MESSAGE_SUCCESSFUL, }; -export const LEARN_GITLAB = 'learn_gitlab'; export const ON_SHOW_TRACK_LABEL = 'over_limit_modal_viewed'; export const ON_CELEBRATION_TRACK_LABEL = 'invite_celebration_modal'; diff --git a/app/assets/javascripts/invite_members/utils/member_utils.js b/app/assets/javascripts/invite_members/utils/member_utils.js index d85162626f1..240a3a89686 100644 --- a/app/assets/javascripts/invite_members/utils/member_utils.js +++ b/app/assets/javascripts/invite_members/utils/member_utils.js @@ -1,4 +1,14 @@ +import { getParameterValues } from '~/lib/utils/url_utility'; + export function memberName(member) { // user defined tokens(invites by email) will have email in `name` and will not contain `username` return member.username || member.name; } + +export function triggerExternalAlert() { + return false; +} + +export function qualifiesForTasksToBeDone() { + return getParameterValues('open_modal')[0] === 'invite_members_for_task'; +} 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), }); diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js index 297b8ae1fc2..58e4553d00d 100644 --- a/app/assets/javascripts/right_sidebar.js +++ b/app/assets/javascripts/right_sidebar.js @@ -56,8 +56,10 @@ Sidebar.prototype.addEventListeners = function () { const layoutPage = document.querySelector('.layout-page'); const rightSidebar = document.querySelector('.js-right-sidebar'); - updateSidebarClasses(layoutPage, rightSidebar); - window.addEventListener('resize', () => updateSidebarClasses(layoutPage, rightSidebar)); + if (rightSidebar.classList.contains('right-sidebar-merge-requests')) { + updateSidebarClasses(layoutPage, rightSidebar); + window.addEventListener('resize', () => updateSidebarClasses(layoutPage, rightSidebar)); + } } }; diff --git a/app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue b/app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue index 1eff4db3970..06876546fa4 100644 --- a/app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue +++ b/app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue @@ -1,8 +1,9 @@ <script> import { GlIcon, GlTooltipDirective, GlOutsideDirective as Outside } from '@gitlab/ui'; import { mapGetters, mapActions } from 'vuex'; -import { TYPE_ISSUE, TYPE_MERGE_REQUEST } from '~/issues/constants'; +import { TYPE_ISSUE } from '~/issues/constants'; import { __, sprintf } from '~/locale'; +import { capitalizeFirstCharacter } from '~/lib/utils/text_utility'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { createAlert } from '~/alert'; import toast from '~/vue_shared/plugins/global_toast'; @@ -45,10 +46,8 @@ export default { }, computed: { ...mapGetters(['getNoteableData']), - isMergeRequest() { - return ( - this.getNoteableData.targetType === TYPE_MERGE_REQUEST && this.glFeatures.movedMrSidebar - ); + isMovedMrSidebar() { + return this.glFeatures.movedMrSidebar; }, issuableDisplayName() { const isInIssuePage = this.getNoteableData.targetType === TYPE_ISSUE; @@ -60,7 +59,6 @@ export default { lockStatus() { return this.isLocked ? this.$options.locked : this.$options.unlocked; }, - tooltipLabel() { return this.isLocked ? __('Locked') : __('Unlocked'); }, @@ -89,8 +87,13 @@ export default { fullPath: this.fullPath, }) .then(() => { - if (this.isMergeRequest) { - toast(this.isLocked ? __('Merge request locked.') : __('Merge request unlocked.')); + if (this.isMovedMrSidebar) { + toast( + sprintf(__('%{issuableDisplayName} %{lockStatus}.'), { + issuableDisplayName: capitalizeFirstCharacter(this.issuableDisplayName), + lockStatus: this.isLocked ? __('locked') : __('unlocked'), + }), + ); } }) .catch(() => { @@ -113,14 +116,14 @@ export default { </script> <template> - <li v-if="isMergeRequest" class="gl-dropdown-item"> - <button type="button" class="dropdown-item" @click="toggleLocked"> + <li v-if="isMovedMrSidebar" class="gl-dropdown-item"> + <button type="button" class="dropdown-item" data-testid="issuable-lock" @click="toggleLocked"> <span class="gl-dropdown-item-text-wrapper"> <template v-if="isLocked"> - {{ __('Unlock merge request') }} + {{ sprintf(__('Unlock %{issuableType}'), { issuableType: issuableDisplayName }) }} </template> <template v-else> - {{ __('Lock merge request') }} + {{ sprintf(__('Lock %{issuableType}'), { issuableType: issuableDisplayName }) }} </template> </span> </button> diff --git a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue index 344fa880131..f2b960ed02c 100644 --- a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue +++ b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue @@ -1,12 +1,7 @@ <script> import { GlDropdownForm, GlIcon, GlLoadingIcon, GlToggle, GlTooltipDirective } from '@gitlab/ui'; import { createAlert } from '~/alert'; -import { - TYPE_EPIC, - TYPE_MERGE_REQUEST, - WORKSPACE_GROUP, - WORKSPACE_PROJECT, -} from '~/issues/constants'; +import { TYPE_EPIC, WORKSPACE_GROUP, WORKSPACE_PROJECT } from '~/issues/constants'; import { isLoggedIn } from '~/lib/utils/common_utils'; import { __, sprintf } from '~/locale'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; @@ -91,8 +86,8 @@ export default { }, }, computed: { - isMergeRequest() { - return this.issuableType === TYPE_MERGE_REQUEST && this.glFeatures.movedMrSidebar; + isMovedMrSidebar() { + return this.glFeatures.movedMrSidebar; }, isLoading() { return this.$apollo.queries?.subscribed?.loading || this.loading; @@ -148,7 +143,7 @@ export default { }); } - if (this.isMergeRequest) { + if (this.isMovedMrSidebar) { toast(subscribed ? __('Notifications turned on.') : __('Notifications turned off.')); } }, @@ -187,7 +182,7 @@ export default { </script> <template> - <gl-dropdown-form v-if="isMergeRequest" class="gl-dropdown-item"> + <gl-dropdown-form v-if="isMovedMrSidebar" class="gl-dropdown-item"> <div class="gl-px-5 gl-pb-2 gl-pt-1"> <gl-toggle :value="subscribed" diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js index 2c56dc34701..0bf4105fdd6 100644 --- a/app/assets/javascripts/sidebar/mount_sidebar.js +++ b/app/assets/javascripts/sidebar/mount_sidebar.js @@ -17,6 +17,7 @@ import { __ } from '~/locale'; import { apolloProvider } from '~/graphql_shared/issuable_client'; import Translate from '~/vue_shared/translate'; import UserSelect from '~/vue_shared/components/user_select/user_select.vue'; +import NewHeaderActionsPopover from '~/issues/show/components/new_header_actions_popover.vue'; import CollapsedAssigneeList from './components/assignees/collapsed_assignee_list.vue'; import SidebarAssignees from './components/assignees/sidebar_assignees.vue'; import SidebarAssigneesWidget from './components/assignees/sidebar_assignees_widget.vue'; @@ -645,7 +646,7 @@ function mountCopyEmailToClipboard() { }); } -export function mountMoveIssuesButton() { +export async function mountMoveIssuesButton() { const el = document.querySelector('.js-move-issues'); if (!el) { @@ -658,7 +659,7 @@ export function mountMoveIssuesButton() { el, name: 'MoveIssuesRoot', apolloProvider: new VueApollo({ - defaultClient: gqlClient, + defaultClient: await gqlClient(), }), render: (createElement) => createElement(MoveIssuesButton, { @@ -787,6 +788,21 @@ export function mountAssigneesDropdown() { }); } +function mountNewIssuePopover() { + const el = document.querySelector('.js-sidebar-header-popover'); + + if (!el) { + return null; + } + + return new Vue({ + el, + name: 'NewHeaderActionsPopover', + render: (createElement) => + createElement(NewHeaderActionsPopover, { props: { issueType: TYPE_MERGE_REQUEST } }), + }); +} + const isAssigneesWidgetShown = (isInIssuePage() || isInDesignPage() || isInMRPage()) && gon.features.issueAssigneesWidget; @@ -814,6 +830,7 @@ export function mountSidebar(mediator, store) { mountSidebarSeverityWidget(); mountSidebarEscalationStatus(); mountMoveIssueButton(); + mountNewIssuePopover(); } export { getSidebarOptions }; diff --git a/app/assets/javascripts/super_sidebar/components/super_sidebar.vue b/app/assets/javascripts/super_sidebar/components/super_sidebar.vue index e9198952382..4b54e317639 100644 --- a/app/assets/javascripts/super_sidebar/components/super_sidebar.vue +++ b/app/assets/javascripts/super_sidebar/components/super_sidebar.vue @@ -1,8 +1,13 @@ <script> import { GlButton, GlCollapse } from '@gitlab/ui'; import { __ } from '~/locale'; -import { isCollapsed, toggleSuperSidebarCollapsed } from '../super_sidebar_collapsed_state_manager'; -import { SIDEBAR_VISIBILITY_CLASS } from '../constants'; +import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; +import { + sidebarState, + SUPER_SIDEBAR_PEEK_OPEN_DELAY, + SUPER_SIDEBAR_PEEK_CLOSE_DELAY, +} from '../constants'; +import { toggleSuperSidebarCollapsed } from '../super_sidebar_collapsed_state_manager'; import UserBar from './user_bar.vue'; import SidebarPortalTarget from './sidebar_portal_target.vue'; import ContextSwitcherToggle from './context_switcher_toggle.vue'; @@ -11,7 +16,6 @@ import HelpCenter from './help_center.vue'; import SidebarMenu from './sidebar_menu.vue'; export default { - SIDEBAR_VISIBILITY_CLASS, components: { GlButton, GlCollapse, @@ -22,6 +26,7 @@ export default { SidebarMenu, SidebarPortalTarget, }, + mixins: [glFeatureFlagsMixin()], i18n: { skipToMainContent: __('Skip to main content'), }, @@ -32,10 +37,7 @@ export default { }, }, data() { - return { - contextSwitcherOpen: false, - isCollapsed: isCollapsed(), - }; + return sidebarState; }, computed: { menuItems() { @@ -49,6 +51,34 @@ export default { onContextSwitcherShown() { this.$refs['context-switcher'].focusInput(); }, + onHoverAreaMouseEnter() { + this.openPeekTimer = setTimeout(this.openPeek, SUPER_SIDEBAR_PEEK_OPEN_DELAY); + }, + onHoverAreaMouseLeave() { + clearTimeout(this.openPeekTimer); + }, + onSidebarMouseEnter() { + clearTimeout(this.closePeekTimer); + }, + onSidebarMouseLeave() { + this.closePeekTimer = setTimeout(this.closePeek, SUPER_SIDEBAR_PEEK_CLOSE_DELAY); + }, + closePeek() { + if (this.isPeek) { + this.isPeek = false; + this.isCollapsed = true; + } + }, + openPeek() { + this.isPeek = true; + this.isCollapsed = false; + + // Cancel and start the timer to close sidebar, in case the user moves + // the cursor fast enough away to not trigger a mouseenter event. + // This is cancelled if the user moves the cursor into the sidebar. + this.onSidebarMouseEnter(); + this.onSidebarMouseLeave(); + }, }, }; </script> @@ -56,14 +86,22 @@ export default { <template> <div> <div class="super-sidebar-overlay" @click="collapseSidebar"></div> + <div + v-if="!isPeek && glFeatures.superSidebarPeek" + class="super-sidebar-hover-area gl-fixed gl-left-0 gl-top-0 gl-bottom-0 gl-w-3" + data-testid="super-sidebar-hover-area" + @mouseenter="onHoverAreaMouseEnter" + @mouseleave="onHoverAreaMouseLeave" + ></div> <aside id="super-sidebar" class="super-sidebar" - :class="{ [$options.SIDEBAR_VISIBILITY_CLASS]: isCollapsed }" + :class="{ 'super-sidebar-peek': isPeek }" data-testid="super-sidebar" data-qa-selector="navbar" :inert="isCollapsed" - tabindex="-1" + @mouseenter="onSidebarMouseEnter" + @mouseleave="onSidebarMouseLeave" > <gl-button class="super-sidebar-skip-to gl-sr-only-focusable gl-absolute gl-left-3 gl-right-3 gl-top-3" @@ -72,7 +110,7 @@ export default { > {{ $options.i18n.skipToMainContent }} </gl-button> - <user-bar :sidebar-data="sidebarData" /> + <user-bar :has-collapse-button="!isPeek" :sidebar-data="sidebarData" /> <div class="gl-display-flex gl-flex-direction-column gl-flex-grow-1 gl-overflow-hidden"> <div class="gl-flex-grow-1 gl-overflow-auto"> <context-switcher-toggle diff --git a/app/assets/javascripts/super_sidebar/components/super_sidebar_toggle.vue b/app/assets/javascripts/super_sidebar/components/super_sidebar_toggle.vue new file mode 100644 index 00000000000..3064b91ca7d --- /dev/null +++ b/app/assets/javascripts/super_sidebar/components/super_sidebar_toggle.vue @@ -0,0 +1,80 @@ +<script> +import { GlButton, GlTooltipDirective } from '@gitlab/ui'; +import { __ } from '~/locale'; +import { JS_TOGGLE_COLLAPSE_CLASS, JS_TOGGLE_EXPAND_CLASS, sidebarState } from '../constants'; +import { toggleSuperSidebarCollapsed } from '../super_sidebar_collapsed_state_manager'; + +export default { + components: { + GlButton, + }, + directives: { + GlTooltip: GlTooltipDirective, + }, + props: { + tooltipContainer: { + type: String, + required: false, + default: null, + }, + tooltipPlacement: { + type: String, + required: false, + default: 'right', + }, + }, + i18n: { + collapseSidebar: __('Collapse sidebar'), + expandSidebar: __('Expand sidebar'), + navigationSidebar: __('Navigation sidebar'), + }, + data() { + return sidebarState; + }, + computed: { + tooltipTitle() { + if (this.isPeek) return ''; + + return this.isCollapsed + ? this.$options.i18n.expandSidebar + : this.$options.i18n.collapseSidebar; + }, + tooltip() { + return { + placement: this.tooltipPlacement, + container: this.tooltipContainer, + title: this.tooltipTitle, + }; + }, + ariaExpanded() { + return String(!this.isCollapsed); + }, + }, + methods: { + toggle() { + toggleSuperSidebarCollapsed(!this.isCollapsed, true); + this.focusOtherToggle(); + }, + focusOtherToggle() { + this.$nextTick(() => { + const classSelector = this.isCollapsed ? JS_TOGGLE_EXPAND_CLASS : JS_TOGGLE_COLLAPSE_CLASS; + const otherToggle = document.querySelector(`.${classSelector}`); + otherToggle?.focus(); + }); + }, + }, +}; +</script> + +<template> + <gl-button + v-gl-tooltip.hover="tooltip" + aria-controls="super-sidebar" + :aria-expanded="ariaExpanded" + :aria-label="$options.i18n.navigationSidebar" + icon="sidebar" + category="tertiary" + :disabled="isPeek" + @click="toggle" + /> +</template> diff --git a/app/assets/javascripts/super_sidebar/components/user_bar.vue b/app/assets/javascripts/super_sidebar/components/user_bar.vue index e96b896825a..f311c5242f5 100644 --- a/app/assets/javascripts/super_sidebar/components/user_bar.vue +++ b/app/assets/javascripts/super_sidebar/components/user_bar.vue @@ -4,11 +4,12 @@ import { __, s__, sprintf } from '~/locale'; import SafeHtml from '~/vue_shared/directives/safe_html'; import { highCountTrim } from '~/lib/utils/text_utility'; import logo from '../../../../views/shared/_logo.svg'; -import { toggleSuperSidebarCollapsed } from '../super_sidebar_collapsed_state_manager'; +import { JS_TOGGLE_COLLAPSE_CLASS } from '../constants'; import CreateMenu from './create_menu.vue'; import Counter from './counter.vue'; import MergeRequestMenu from './merge_request_menu.vue'; import UserMenu from './user_menu.vue'; +import SuperSidebarToggle from './super_sidebar_toggle.vue'; import { SEARCH_MODAL_ID } from './global_search/constants'; export default { @@ -16,6 +17,7 @@ export default { /* eslint-disable-next-line @gitlab/require-i18n-strings */ NEXT_LABEL: 'Next', logo, + JS_TOGGLE_COLLAPSE_CLASS, SEARCH_MODAL_ID, components: { Counter, @@ -28,14 +30,13 @@ export default { import( /* webpackChunkName: 'global_search_modal' */ './global_search/components/global_search.vue' ), + SuperSidebarToggle, }, i18n: { - collapseSidebar: __('Collapse sidebar'), createNew: __('Create new...'), homepage: __('Homepage'), issues: __('Issues'), mergeRequests: __('Merge requests'), - navigationSidebar: __('Navigation sidebar'), search: __('Search'), searchKbdHelp: sprintf( s__('GlobalSearch|Search GitLab %{kbdOpen}/%{kbdClose}'), @@ -52,6 +53,11 @@ export default { }, inject: ['rootPath', 'isImpersonating'], props: { + hasCollapseButton: { + default: true, + type: Boolean, + required: false, + }, sidebarData: { type: Object, required: true, @@ -75,9 +81,6 @@ export default { document.removeEventListener('todo:toggle', this.updateTodos); }, methods: { - collapseSidebar() { - toggleSuperSidebarCollapsed(true, true, true); - }, updateTodos(e) { this.todoCount = e.detail.count || 0; }, @@ -114,14 +117,12 @@ export default { {{ $options.NEXT_LABEL }} </gl-badge> <div class="gl-flex-grow-1"></div> - <gl-button - v-gl-tooltip:super-sidebar.hover.bottom="$options.i18n.collapseSidebar" - aria-controls="super-sidebar" - aria-expanded="true" - :aria-label="$options.i18n.navigationSidebar" - icon="sidebar" - category="tertiary" - @click="collapseSidebar" + <super-sidebar-toggle + v-if="hasCollapseButton" + :class="$options.JS_TOGGLE_COLLAPSE_CLASS" + tooltip-placement="bottom" + tooltip-container="super-sidebar" + data-testid="super-sidebar-collapse-button" /> <create-menu :groups="sidebarData.create_new_menu_groups" /> diff --git a/app/assets/javascripts/super_sidebar/constants.js b/app/assets/javascripts/super_sidebar/constants.js index 5c4a6a9dfc1..4f5b027c138 100644 --- a/app/assets/javascripts/super_sidebar/constants.js +++ b/app/assets/javascripts/super_sidebar/constants.js @@ -5,16 +5,27 @@ import Vue from 'vue'; export const SIDEBAR_PORTAL_ID = 'sidebar-portal-mount'; +export const JS_TOGGLE_COLLAPSE_CLASS = 'js-super-sidebar-toggle-collapse'; +export const JS_TOGGLE_EXPAND_CLASS = 'js-super-sidebar-toggle-expand'; export const portalState = Vue.observable({ ready: false, }); -export const SIDEBAR_VISIBILITY_CLASS = 'gl-visibility-hidden'; +export const sidebarState = Vue.observable({ + contextSwitcherOpen: false, + isCollapsed: false, + isPeek: false, + openPeekTimer: null, + closePeekTimer: null, +}); export const MAX_FREQUENT_PROJECTS_COUNT = 5; export const MAX_FREQUENT_GROUPS_COUNT = 3; +export const SUPER_SIDEBAR_PEEK_OPEN_DELAY = 200; +export const SUPER_SIDEBAR_PEEK_CLOSE_DELAY = 500; + export const TRACKING_UNKNOWN_ID = 'item_without_id'; export const TRACKING_UNKNOWN_PANEL = 'nav_panel_unknown'; export const CLICK_MENU_ITEM_ACTION = 'click_menu_item'; diff --git a/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js b/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js index 58b49f218ad..fdd29a1719c 100644 --- a/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js +++ b/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js @@ -3,12 +3,14 @@ import VueApollo from 'vue-apollo'; import createDefaultClient from '~/lib/graphql'; import { convertObjectPropsToCamelCase, parseBoolean } from '~/lib/utils/common_utils'; import { initStatusTriggers } from '../header'; +import { JS_TOGGLE_EXPAND_CLASS } from './constants'; import createStore from './components/global_search/store'; import { bindSuperSidebarCollapsedEvents, initSuperSidebarCollapsedState, } from './super_sidebar_collapsed_state_manager'; import SuperSidebar from './components/super_sidebar.vue'; +import SuperSidebarToggle from './components/super_sidebar_toggle.vue'; Vue.use(VueApollo); @@ -58,4 +60,28 @@ export const initSuperSidebar = () => { }); }; +/** + * Guard against multiple instantiations, since the js-* class is persisted + * in the Vue component. + */ +let toggleInstantiated = false; + +export const initSuperSidebarToggle = () => { + const el = document.querySelector(`.${JS_TOGGLE_EXPAND_CLASS}`); + + if (!el || toggleInstantiated) return false; + + toggleInstantiated = true; + + return new Vue({ + el, + name: 'SuperSidebarToggleRoot', + render(h) { + // Copy classes from HAML-defined button to ensure same positioning, + // including JS_TOGGLE_EXPAND_CLASS. + return h(SuperSidebarToggle, { class: el.className }); + }, + }); +}; + requestIdleCallback(initStatusTriggers); diff --git a/app/assets/javascripts/super_sidebar/super_sidebar_collapsed_state_manager.js b/app/assets/javascripts/super_sidebar/super_sidebar_collapsed_state_manager.js index ba5495ba014..17e07146678 100644 --- a/app/assets/javascripts/super_sidebar/super_sidebar_collapsed_state_manager.js +++ b/app/assets/javascripts/super_sidebar/super_sidebar_collapsed_state_manager.js @@ -1,7 +1,7 @@ import { GlBreakpointInstance as bp, breakpoints } from '@gitlab/ui/dist/utils'; import { debounce } from 'lodash'; import { setCookie, getCookie } from '~/lib/utils/common_utils'; -import { SIDEBAR_VISIBILITY_CLASS } from './constants'; +import { sidebarState } from './constants'; export const SIDEBAR_COLLAPSED_CLASS = 'page-with-super-sidebar-collapsed'; export const SIDEBAR_COLLAPSED_COOKIE = 'super_sidebar_collapsed'; @@ -10,7 +10,6 @@ export const SIDEBAR_TRANSITION_DURATION = 200; export const findPage = () => document.querySelector('.page-with-super-sidebar'); export const findSidebar = () => document.querySelector('.super-sidebar'); -export const findToggle = () => document.querySelector('.js-super-sidebar-toggle'); export const isCollapsed = () => findPage().classList.contains(SIDEBAR_COLLAPSED_CLASS); @@ -21,35 +20,14 @@ export const isDesktopBreakpoint = () => bp.windowWidth() >= breakpoints.xl; export const getCollapsedCookie = () => getCookie(SIDEBAR_COLLAPSED_COOKIE) === 'true'; -const show = (sidebar, isUserAction) => { - sidebar.classList.remove(SIDEBAR_VISIBILITY_CLASS); - if (isUserAction) { - sidebar.focus(); - } -}; - -const hide = (sidebar, toggle, isUserAction) => { - setTimeout(() => { - sidebar.classList.add(SIDEBAR_VISIBILITY_CLASS); - if (isUserAction) { - toggle?.focus(); - } - }, SIDEBAR_TRANSITION_DURATION); -}; +export const toggleSuperSidebarCollapsed = (collapsed, saveCookie) => { + clearTimeout(sidebarState.openPeekTimer); + clearTimeout(sidebarState.closePeekTimer); -export const toggleSuperSidebarCollapsed = (collapsed, saveCookie, isUserAction) => { - const page = findPage(); - const toggle = findToggle(); - const sidebar = findSidebar(); + findPage().classList.toggle(SIDEBAR_COLLAPSED_CLASS, collapsed); - page.classList.toggle(SIDEBAR_COLLAPSED_CLASS, collapsed); - sidebar.inert = collapsed; - - if (collapsed) { - hide(sidebar, toggle, isUserAction); - } else { - show(sidebar, isUserAction); - } + sidebarState.isPeek = false; + sidebarState.isCollapsed = collapsed; if (saveCookie && isDesktopBreakpoint()) { setCookie(SIDEBAR_COLLAPSED_COOKIE, collapsed, { @@ -64,9 +42,5 @@ export const initSuperSidebarCollapsedState = () => { }; export const bindSuperSidebarCollapsedEvents = () => { - findToggle()?.addEventListener('click', () => { - toggleSuperSidebarCollapsed(!isCollapsed(), true, true); - }); - window.addEventListener('resize', debounce(initSuperSidebarCollapsedState, 100)); }; diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue index 49eb11f8081..6d1cadf15be 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue @@ -176,6 +176,12 @@ export default { <template> <div> <div class="flash-container js-suggestions-flash"></div> - <div v-show="isRendered" ref="container" v-safe-html="noteHtml" class="md suggestions"></div> + <div + v-show="isRendered" + ref="container" + v-safe-html="noteHtml" + data-testid="suggestions-container" + class="md suggestions" + ></div> </div> </template> diff --git a/app/assets/javascripts/vue_shared/security_reports/constants.js b/app/assets/javascripts/vue_shared/security_reports/constants.js index 597268a40d3..8b523645973 100644 --- a/app/assets/javascripts/vue_shared/security_reports/constants.js +++ b/app/assets/javascripts/vue_shared/security_reports/constants.js @@ -29,6 +29,7 @@ export const REPORT_TYPE_COVERAGE_FUZZING = 'coverage_fuzzing'; export const REPORT_TYPE_CORPUS_MANAGEMENT = 'corpus_management'; export const REPORT_TYPE_LICENSE_COMPLIANCE = 'license_scanning'; export const REPORT_TYPE_API_FUZZING = 'api_fuzzing'; +export const REPORT_TYPE_MANUALLY_ADDED = 'generic'; /** * SecurityReportTypeEnum values for use with GraphQL. |