diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-05-19 10:33:21 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-05-19 10:33:21 +0300 |
commit | 36a59d088eca61b834191dacea009677a96c052f (patch) | |
tree | e4f33972dab5d8ef79e3944a9f403035fceea43f /app/assets/javascripts/sidebar | |
parent | a1761f15ec2cae7c7f7bbda39a75494add0dfd6f (diff) |
Add latest changes from gitlab-org/gitlab@15-0-stable-eev15.0.0-rc42
Diffstat (limited to 'app/assets/javascripts/sidebar')
31 files changed, 291 insertions, 122 deletions
diff --git a/app/assets/javascripts/sidebar/components/assignees/assignee_avatar_link.vue b/app/assets/javascripts/sidebar/components/assignees/assignee_avatar_link.vue index 578c344da02..ef40de82d01 100644 --- a/app/assets/javascripts/sidebar/components/assignees/assignee_avatar_link.vue +++ b/app/assets/javascripts/sidebar/components/assignees/assignee_avatar_link.vue @@ -101,14 +101,15 @@ export default { <template> <!-- must be `d-inline-block` or parent flex-basis causes width issues --> <gl-link - v-gl-tooltip="tooltipOption" :href="assigneeUrl" :title="tooltipTitle" - class="gl-display-inline-block" + :data-user-id="user.id" + data-placement="left" + class="gl-display-inline-block js-user-link" > <!-- use d-flex so that slot can be appropriately styled --> <span class="gl-display-flex"> - <assignee-avatar :user="user" :img-size="32" :issuable-type="issuableType" /> + <assignee-avatar :user="user" :img-size="24" :issuable-type="issuableType" /> <slot></slot> </span> </gl-link> diff --git a/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue b/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue index f98aa0dc77d..6e18cf36690 100644 --- a/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue +++ b/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue @@ -1,6 +1,7 @@ <script> import { GlLoadingIcon, GlIcon } from '@gitlab/ui'; import { n__, __ } from '~/locale'; +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; export default { name: 'AssigneeTitle', @@ -8,6 +9,7 @@ export default { GlLoadingIcon, GlIcon, }, + mixins: [glFeatureFlagMixin()], props: { loading: { type: Boolean, @@ -45,7 +47,7 @@ export default { }; </script> <template> - <div class="hide-collapsed gl-line-height-20 gl-mb-2 gl-text-gray-900"> + <div class="hide-collapsed gl-line-height-20 gl-mb-2 gl-text-gray-900 gl-font-weight-bold"> {{ assigneeTitle }} <gl-loading-icon v-if="loading" size="sm" inline class="align-bottom" /> <a @@ -63,6 +65,7 @@ export default { v-if="showToggle" :aria-label="__('Toggle sidebar')" class="gutter-toggle float-right js-sidebar-toggle" + :class="{ 'gl-display-block gl-md-display-none!': glFeatures.movedMrSidebar }" href="#" role="button" > diff --git a/app/assets/javascripts/sidebar/components/assignees/collapsed_assignee_list.vue b/app/assets/javascripts/sidebar/components/assignees/collapsed_assignee_list.vue index 856687c00ae..50b1955abcc 100644 --- a/app/assets/javascripts/sidebar/components/assignees/collapsed_assignee_list.vue +++ b/app/assets/javascripts/sidebar/components/assignees/collapsed_assignee_list.vue @@ -124,7 +124,11 @@ export default { :issuable-type="issuableType" /> <button v-if="hasMoreThanTwoAssignees" class="btn-link" type="button"> - <span class="avatar-counter sidebar-avatar-counter"> {{ sidebarAvatarCounter }} </span> + <span + class="avatar-counter sidebar-avatar-counter gl-display-flex gl-align-items-center gl-pl-3" + > + {{ sidebarAvatarCounter }} + </span> <gl-icon v-if="isMergeRequest && !allAssigneesCanMerge" name="warning-solid" diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_participant.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_participant.vue index 19f588b28be..e9c68008143 100644 --- a/app/assets/javascripts/sidebar/components/assignees/sidebar_participant.vue +++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_participant.vue @@ -3,6 +3,11 @@ import { GlAvatarLabeled, GlAvatarLink, GlIcon } from '@gitlab/ui'; import { IssuableType } from '~/issues/constants'; import { s__, sprintf } from '~/locale'; +const AVAILABILITY_STATUS = { + NOT_SET: 'NOT_SET', + BUSY: 'BUSY', +}; + export default { components: { GlAvatarLabeled, @@ -22,12 +27,17 @@ export default { }, computed: { userLabel() { - if (!this.user.status) { - return this.user.name; + const { name, status } = this.user; + if (!status || status?.availability !== AVAILABILITY_STATUS.BUSY) { + return name; } - return sprintf(s__('UserAvailability|%{author} (Busy)'), { - author: this.user.name, - }); + return sprintf( + s__('UserAvailability|%{author} (Busy)'), + { + author: name, + }, + false, + ); }, hasCannotMergeIcon() { return this.issuableType === IssuableType.MergeRequest && !this.user.canMerge; diff --git a/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue b/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue index 8717d205dcb..01d29da5486 100644 --- a/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue +++ b/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue @@ -35,13 +35,6 @@ export default { firstUser() { return this.users[0]; }, - hasOneUser() { - if (this.showVerticalList) { - return false; - } - - return this.users.length === 1; - }, hiddenAssigneesLabel() { const { numberOfHiddenAssignees } = this; return sprintf(__('+ %{numberOfHiddenAssignees} more'), { numberOfHiddenAssignees }); @@ -90,30 +83,15 @@ export default { </script> <template> - <assignee-avatar-link - v-if="hasOneUser" - tooltip-placement="left" - :tooltip-has-name="false" - :user="firstUser" - :issuable-type="issuableType" - > - <div class="ml-2 gl-line-height-normal"> - <user-name-with-status :name="firstUser.name" :availability="userAvailability(firstUser)" /> - <div>{{ username }}</div> - </div> - </assignee-avatar-link> - <div v-else> + <div> <div class="gl-display-flex gl-flex-wrap"> <div v-for="(user, index) in uncollapsedUsers" :key="user.id" :class="{ - 'user-item': !showVerticalList, - 'gl-display-inline-block': !showVerticalList, - 'gl-display-grid gl-align-items-center': showVerticalList, - 'gl-mb-3': index !== users.length - 1 && showVerticalList, + 'gl-mb-3': index !== users.length - 1, }" - class="assignee-grid" + class="assignee-grid gl-display-grid gl-align-items-center gl-w-full" > <assignee-avatar-link :user="user" @@ -123,12 +101,10 @@ export default { data-css-area="user" > <div - v-if="showVerticalList" - class="gl-ml-3 gl-line-height-normal gl-display-grid" + class="gl-ml-3 gl-line-height-normal gl-display-grid gl-align-items-center" data-testid="username" > <user-name-with-status :name="user.name" :availability="userAvailability(user)" /> - <span>@{{ user.username }}</span> </div> </assignee-avatar-link> <attention-requested-toggle diff --git a/app/assets/javascripts/sidebar/components/attention_requested_toggle.vue b/app/assets/javascripts/sidebar/components/attention_requested_toggle.vue index cdc1c65a516..031de669489 100644 --- a/app/assets/javascripts/sidebar/components/attention_requested_toggle.vue +++ b/app/assets/javascripts/sidebar/components/attention_requested_toggle.vue @@ -5,9 +5,8 @@ import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants'; export default { i18n: { - attentionRequestedReviewer: __('Request attention to review'), - attentionRequestedAssignee: __('Request attention'), - removeAttentionRequested: __('Remove attention request'), + addAttentionRequest: __('Add attention request'), + removeAttentionRequest: __('Remove attention request'), attentionRequestedNoPermission: __('Attention requested'), noAttentionRequestedNoPermission: __('No attention request'), }, @@ -36,20 +35,35 @@ export default { tooltipTitle() { if (this.user.attention_requested) { if (this.user.can_update_merge_request) { - return this.$options.i18n.removeAttentionRequested; + return this.$options.i18n.removeAttentionRequest; } return this.$options.i18n.attentionRequestedNoPermission; } if (this.user.can_update_merge_request) { - return this.type === 'reviewer' - ? this.$options.i18n.attentionRequestedReviewer - : this.$options.i18n.attentionRequestedAssignee; + return this.$options.i18n.addAttentionRequest; } return this.$options.i18n.noAttentionRequestedNoPermission; }, + request() { + const state = { + variant: 'default', + icon: 'attention', + direction: 'add', + }; + + if (this.user.attention_requested) { + Object.assign(state, { + variant: 'warning', + icon: 'attention-solid', + direction: 'remove', + }); + } + + return state; + }, }, methods: { toggleAttentionRequired() { @@ -60,6 +74,7 @@ export default { this.$emit('toggle-attention-requested', { user: this.user, callback: this.toggleAttentionRequiredComplete, + direction: this.request.direction, }); }, toggleAttentionRequiredComplete() { @@ -77,8 +92,8 @@ export default { > <gl-button :loading="loading" - :variant="user.attention_requested ? 'warning' : 'default'" - :icon="user.attention_requested ? 'attention-solid' : 'attention'" + :variant="request.variant" + :icon="request.icon" :aria-label="tooltipTitle" :class="{ 'gl-pointer-events-none': !user.can_update_merge_request }" size="small" diff --git a/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_content.vue b/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_content.vue index 37a44eb8f01..6afaee91d7a 100644 --- a/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_content.vue +++ b/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_content.vue @@ -1,10 +1,13 @@ <script> -import { GlIcon, GlTooltipDirective } from '@gitlab/ui'; -import { __, sprintf } from '~/locale'; +import { GlIcon, GlAlert, GlTooltipDirective } from '@gitlab/ui'; +import { __ } from '~/locale'; +import { IssuableType, WorkspaceType } from '~/issues/constants'; +import { confidentialityInfoText } from '~/vue_shared/constants'; export default { components: { GlIcon, + GlAlert, }, directives: { GlTooltip: GlTooltipDirective, @@ -20,12 +23,11 @@ export default { }, }, computed: { - confidentialText() { - return this.confidential - ? sprintf(__('This %{issuableType} is confidential'), { - issuableType: this.issuableType, - }) - : __('Not confidential'); + confidentialBodyText() { + return confidentialityInfoText( + this.issuableType === IssuableType.Epic ? WorkspaceType.group : WorkspaceType.project, + this.issuableType, + ); }, confidentialIcon() { return this.confidential ? 'eye-slash' : 'eye'; @@ -59,6 +61,17 @@ export default { class="sidebar-item-icon inline hide-collapsed" :class="{ 'is-active': confidential }" /> - <span class="hide-collapsed" data-testid="confidential-text">{{ confidentialText }}</span> + <span class="hide-collapsed" data-testid="confidential-text"> + {{ tooltipLabel }} + <gl-alert + v-if="confidential" + :show-icon="false" + :dismissible="false" + variant="warning" + class="gl-mt-3" + > + {{ confidentialBodyText }} + </gl-alert> + </span> </div> </template> diff --git a/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_form.vue b/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_form.vue index 209d1cca360..71e40fde77d 100644 --- a/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_form.vue +++ b/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_form.vue @@ -8,7 +8,7 @@ import { confidentialityQueries } from '~/sidebar/constants'; export default { i18n: { confidentialityOnWarning: __( - 'You are going to turn on confidentiality. Only team members with %{strongStart}at least Reporter access%{strongEnd} will be able to see and leave comments on the %{issuableType}.', + 'You are going to turn on confidentiality. Only %{context} members with %{strongStart}at least Reporter role%{strongEnd} can view or be notified about this %{issuableType}.', ), confidentialityOffWarning: __( 'You are going to turn off the confidentiality. This means %{strongStart}everyone%{strongEnd} will be able to see and leave a comment on this %{issuableType}.', @@ -53,6 +53,9 @@ export default { ? this.$options.i18n.confidentialityOffWarning : this.$options.i18n.confidentialityOnWarning; }, + context() { + return this.issuableType === IssuableType.Issue ? __('project') : __('group'); + }, workspacePath() { return this.issuableType === IssuableType.Issue ? { @@ -119,6 +122,7 @@ export default { <template #strong="{ content }"> <strong>{{ content }}</strong> </template> + <template #context>{{ context }}</template> <template #issuableType>{{ issuableType }}</template> </gl-sprintf> </p> diff --git a/app/assets/javascripts/sidebar/components/crm_contacts/crm_contacts.vue b/app/assets/javascripts/sidebar/components/crm_contacts/crm_contacts.vue index 950647f1cb2..67f36f65b5d 100644 --- a/app/assets/javascripts/sidebar/components/crm_contacts/crm_contacts.vue +++ b/app/assets/javascripts/sidebar/components/crm_contacts/crm_contacts.vue @@ -2,7 +2,7 @@ import { GlIcon, GlLink, GlPopover, GlTooltipDirective } from '@gitlab/ui'; import { __, n__, sprintf } from '~/locale'; import createFlash from '~/flash'; -import { convertToGraphQLId } from '~/graphql_shared/utils'; +import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils'; import { TYPE_ISSUE } from '~/graphql_shared/constants'; import getIssueCrmContactsQuery from './queries/get_issue_crm_contacts.query.graphql'; import issueCrmContactsSubscription from './queries/issue_crm_contacts.subscription.graphql'; @@ -21,6 +21,10 @@ export default { type: String, required: true, }, + groupIssuesPath: { + type: String, + required: true, + }, }, data() { return { @@ -85,6 +89,10 @@ export default { Boolean, ); }, + getIssuesPath(contactId) { + const id = getIdFromGraphQLId(contactId); + return `${this.groupIssuesPath}?crm_contact_id=${id}`; + }, }, }; </script> @@ -100,7 +108,7 @@ export default { ><gl-icon name="question-o" /></gl-link> </div> - <div class="title hide-collapsed gl-mb-2 gl-line-height-20"> + <div class="title hide-collapsed gl-mb-2 gl-line-height-20 gl-font-weight-bold"> {{ contactsLabel }} </div> <div class="hide-collapsed gl-display-flex gl-flex-wrap"> @@ -110,8 +118,8 @@ export default { :key="index" class="gl-pr-2" > - <span :id="`contact_${index}`" class="gl-font-weight-bold" - >{{ contact.firstName }} {{ contact.lastName }}{{ divider(index) }}</span + <gl-link :id="`contact_${index}`" :href="getIssuesPath(contact.id)" + >{{ contact.firstName }} {{ contact.lastName }}{{ divider(index) }}</gl-link > <gl-popover v-if="shouldShowPopover(contact)" diff --git a/app/assets/javascripts/sidebar/components/date/sidebar_formatted_date.vue b/app/assets/javascripts/sidebar/components/date/sidebar_formatted_date.vue index 87cf1c29fb0..627b8452508 100644 --- a/app/assets/javascripts/sidebar/components/date/sidebar_formatted_date.vue +++ b/app/assets/javascripts/sidebar/components/date/sidebar_formatted_date.vue @@ -35,7 +35,7 @@ export default { <template> <div class="gl-display-flex gl-align-items-center hide-collapsed"> <span - :class="hasDate ? 'gl-text-gray-900 gl-font-weight-bold' : 'gl-text-gray-500'" + :class="hasDate ? 'gl-text-gray-900' : 'gl-text-gray-500'" data-testid="sidebar-date-value" > {{ formattedDate }} 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 cb49f329f7e..699d1bebea1 100644 --- a/app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue +++ b/app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue @@ -1,7 +1,9 @@ <script> import { GlIcon, GlTooltipDirective } from '@gitlab/ui'; -import { mapGetters } from 'vuex'; -import { __ } from '~/locale'; +import { mapGetters, mapActions } from 'vuex'; +import { __, sprintf } from '~/locale'; +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; +import createFlash from '~/flash'; import eventHub from '~/sidebar/event_hub'; import editForm from './edit_form.vue'; @@ -23,11 +25,11 @@ export default { editForm, GlIcon, }, - directives: { GlTooltip: GlTooltipDirective, }, - + mixins: [glFeatureFlagMixin()], + inject: ['fullPath'], props: { isEditable: { required: true, @@ -41,6 +43,9 @@ export default { }, computed: { ...mapGetters(['getNoteableData']), + isMergeRequest() { + return this.getNoteableData.targetType === 'merge_request' && this.glFeatures.movedMrSidebar; + }, issuableDisplayName() { const isInIssuePage = this.getNoteableData.targetType === this.$options.issue; return isInIssuePage ? __('issue') : __('merge request'); @@ -66,17 +71,49 @@ export default { }, methods: { + ...mapActions(['updateLockedAttribute']), toggleForm() { if (this.isEditable) { this.isLockDialogOpen = !this.isLockDialogOpen; } }, + toggleLocked() { + this.isLoading = true; + + this.updateLockedAttribute({ + locked: !this.isLocked, + fullPath: this.fullPath, + }) + .catch(() => { + const flashMessage = __( + 'Something went wrong trying to change the locked state of this %{issuableDisplayName}', + ); + createFlash({ + message: sprintf(flashMessage, { issuableDisplayName: this.issuableDisplayName }), + }); + }) + .finally(() => { + this.isLoading = false; + }); + }, }, }; </script> <template> - <div class="block issuable-sidebar-item lock"> + <li v-if="isMergeRequest" class="gl-new-dropdown-item"> + <button type="button" class="dropdown-item" @click="toggleLocked"> + <span class="gl-new-dropdown-item-text-wrapper"> + <template v-if="isLocked"> + {{ __('Unlock merge request') }} + </template> + <template v-else> + {{ __('Lock merge request') }} + </template> + </span> + </button> + </li> + <div v-else class="block issuable-sidebar-item lock"> <div v-gl-tooltip.left.viewport="{ title: tooltipLabel }" class="sidebar-collapsed-icon" @@ -86,7 +123,7 @@ export default { <gl-icon :name="lockStatus.icon" class="sidebar-item-icon is-active" /> </div> - <div class="hide-collapsed gl-line-height-20 gl-mb-2 gl-text-gray-900"> + <div class="hide-collapsed gl-line-height-20 gl-mb-2 gl-text-gray-900 gl-font-weight-bold"> {{ sprintf(__('Lock %{issuableDisplayName}'), { issuableDisplayName: issuableDisplayName }) }} <a v-if="isEditable" @@ -111,12 +148,6 @@ export default { /> <div data-testid="lock-status" class="sidebar-item-value" :class="lockStatus.class"> - <gl-icon - :size="16" - :name="lockStatus.icon" - class="sidebar-item-icon" - :class="lockStatus.iconClass" - /> {{ lockStatus.displayText }} </div> </div> diff --git a/app/assets/javascripts/sidebar/components/participants/participants.vue b/app/assets/javascripts/sidebar/components/participants/participants.vue index 3fd35de2132..77e41648e9b 100644 --- a/app/assets/javascripts/sidebar/components/participants/participants.vue +++ b/app/assets/javascripts/sidebar/components/participants/participants.vue @@ -101,7 +101,10 @@ export default { <gl-loading-icon v-if="loading" size="sm" /> <span v-else data-testid="collapsed-count"> {{ participantCount }} </span> </div> - <div v-if="showParticipantLabel" class="title hide-collapsed gl-mb-2 gl-line-height-20"> + <div + v-if="showParticipantLabel" + class="title hide-collapsed gl-mb-2! gl-line-height-20 gl-font-weight-bold" + > <gl-loading-icon v-if="loading" size="sm" :inline="true" /> {{ participantLabel }} </div> diff --git a/app/assets/javascripts/sidebar/components/reviewers/collapsed_reviewer_list.vue b/app/assets/javascripts/sidebar/components/reviewers/collapsed_reviewer_list.vue index 60d8fb4d408..e09b5d913f7 100644 --- a/app/assets/javascripts/sidebar/components/reviewers/collapsed_reviewer_list.vue +++ b/app/assets/javascripts/sidebar/components/reviewers/collapsed_reviewer_list.vue @@ -96,7 +96,11 @@ export default { <gl-icon v-if="hasNoUsers" name="user" :aria-label="__('None')" /> <collapsed-reviewer v-for="user in collapsedUsers" :key="user.id" :user="user" /> <button v-if="hasMoreThanTwoReviewers" class="btn-link" type="button"> - <span class="avatar-counter sidebar-avatar-counter"> {{ sidebarAvatarCounter }} </span> + <span + class="avatar-counter sidebar-avatar-counter gl-display-flex gl-align-items-center gl-pl-3" + > + {{ sidebarAvatarCounter }} + </span> <gl-icon v-if="!allReviewersCanMerge" name="warning-solid" diff --git a/app/assets/javascripts/sidebar/components/reviewers/reviewer_avatar_link.vue b/app/assets/javascripts/sidebar/components/reviewers/reviewer_avatar_link.vue index a11468c8761..36a08482e69 100644 --- a/app/assets/javascripts/sidebar/components/reviewers/reviewer_avatar_link.vue +++ b/app/assets/javascripts/sidebar/components/reviewers/reviewer_avatar_link.vue @@ -47,8 +47,6 @@ export default { return sprintf(__('%{userName} (cannot merge)'), { userName: this.user.name }); } else if (this.cannotMerge) { return __('Cannot merge'); - } else if (this.tooltipHasName) { - return this.user.name; } return ''; @@ -70,14 +68,15 @@ export default { <template> <!-- must be `d-inline-block` or parent flex-basis causes width issues --> <gl-link - v-gl-tooltip="tooltipOption" :href="reviewerUrl" :title="tooltipTitle" - class="gl-display-inline-block" + :data-user-id="user.id" + data-placement="left" + class="gl-display-inline-block js-user-link" > <!-- use d-flex so that slot can be appropriately styled --> <span class="gl-display-flex"> - <reviewer-avatar :user="user" :img-size="32" :issuable-type="issuableType" /> + <reviewer-avatar :user="user" :img-size="24" :issuable-type="issuableType" /> <slot :user="user"></slot> </span> </gl-link> diff --git a/app/assets/javascripts/sidebar/components/reviewers/reviewer_title.vue b/app/assets/javascripts/sidebar/components/reviewers/reviewer_title.vue index 367dcdb961b..933b9b11b40 100644 --- a/app/assets/javascripts/sidebar/components/reviewers/reviewer_title.vue +++ b/app/assets/javascripts/sidebar/components/reviewers/reviewer_title.vue @@ -33,7 +33,7 @@ export default { }; </script> <template> - <div class="hide-collapsed gl-line-height-20 gl-mb-2 gl-text-gray-900"> + <div class="hide-collapsed gl-line-height-20 gl-mb-2 gl-text-gray-900 gl-font-weight-bold"> {{ reviewerTitle }} <gl-loading-icon v-if="loading" size="sm" inline class="align-bottom" /> <a diff --git a/app/assets/javascripts/sidebar/components/reviewers/uncollapsed_reviewer_list.vue b/app/assets/javascripts/sidebar/components/reviewers/uncollapsed_reviewer_list.vue index 3e6be3487b1..2f58e11c00f 100644 --- a/app/assets/javascripts/sidebar/components/reviewers/uncollapsed_reviewer_list.vue +++ b/app/assets/javascripts/sidebar/components/reviewers/uncollapsed_reviewer_list.vue @@ -98,7 +98,7 @@ export default { 'gl-mb-3': index !== users.length - 1, 'attention-requests': glFeatures.mrAttentionRequests, }" - class="gl-display-grid gl-align-items-center reviewer-grid" + class="gl-display-grid gl-align-items-center reviewer-grid gl-mr-2" data-testid="reviewer" > <reviewer-avatar-link @@ -108,9 +108,8 @@ export default { class="gl-word-break-word gl-mr-2" data-css-area="user" > - <div class="gl-ml-3 gl-line-height-normal gl-display-grid"> - <span>{{ user.name }}</span> - <span>@{{ user.username }}</span> + <div class="gl-ml-3 gl-line-height-normal gl-display-grid gl-align-items-center"> + {{ user.name }} </div> </reviewer-avatar-link> <attention-requested-toggle diff --git a/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue b/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue index ec23e817127..897cab45fe4 100644 --- a/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue +++ b/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue @@ -331,20 +331,19 @@ export default { :data-testid="`select-${formatIssuableAttribute.kebab}`" :class="isClassicSidebar ? 'hide-collapsed' : 'gl-mt-3'" > - <span v-if="updating" class="gl-font-weight-bold">{{ selectedTitle }}</span> + <span v-if="updating">{{ selectedTitle }}</span> <span v-else-if="!currentAttribute" class="gl-text-gray-500"> {{ $options.i18n.none }} </span> <slot v-else name="value" - :attributeTitle="attributeTitle" - :attributeUrl="attributeUrl" - :currentAttribute="currentAttribute" + :attribute-title="attributeTitle" + :attribute-url="attributeUrl" + :current-attribute="currentAttribute" > <gl-link v-gl-tooltip="tooltipText" - class="gl-text-gray-900! gl-font-weight-bold" :href="attributeUrl" :data-qa-selector="`${formatIssuableAttribute.snake}_link`" > @@ -389,9 +388,9 @@ export default { <slot v-else name="list" - :attributesList="attributesList" - :isAttributeChecked="isAttributeChecked" - :updateAttribute="updateAttribute" + :attributes-list="attributesList" + :is-attribute-checked="isAttributeChecked" + :update-attribute="updateAttribute" > <gl-dropdown-item v-for="attrItem in attributesList" diff --git a/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue b/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue index 056b3e98a1c..7551b181a58 100644 --- a/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue +++ b/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue @@ -110,7 +110,7 @@ export default { <template> <div> <div - class="gl-display-flex gl-align-items-center gl-line-height-20 gl-mb-2 gl-text-gray-900" + class="gl-display-flex gl-align-items-center gl-line-height-20 gl-mb-2 gl-text-gray-900 gl-font-weight-bold" @click.self="collapse" > <span class="hide-collapsed" data-testid="title" @click="collapse"> 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 7a10a9f3a4c..1bafa845665 100644 --- a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue +++ b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue @@ -5,6 +5,7 @@ import { IssuableType } from '~/issues/constants'; import { isLoggedIn } from '~/lib/utils/common_utils'; import { __, sprintf } from '~/locale'; import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue'; +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { subscribedQueries, Tracking } from '~/sidebar/constants'; const ICON_ON = 'notifications'; @@ -25,6 +26,7 @@ export default { GlToggle, SidebarEditableItem, }, + mixins: [glFeatureFlagMixin()], props: { iid: { type: String, @@ -82,6 +84,9 @@ export default { }, }, computed: { + isMergeRequest() { + return this.issuableType === IssuableType.MergeRequest && this.glFeatures.movedMrSidebar; + }, isLoading() { return this.$apollo.queries?.subscribed?.loading || this.loading; }, @@ -171,7 +176,20 @@ export default { </script> <template> + <li v-if="isMergeRequest" class="gl-new-dropdown-item"> + <button type="button" class="dropdown-item" @click="toggleSubscribed"> + <span class="gl-new-dropdown-item-text-wrapper"> + <template v-if="subscribed"> + {{ __('Turn off notifications') }} + </template> + <template v-else> + {{ __('Turn on notifications') }} + </template> + </span> + </button> + </li> <sidebar-editable-item + v-else ref="editable" :title="$options.i18n.notifications" :tracking="$options.tracking" diff --git a/app/assets/javascripts/sidebar/components/time_tracking/report.vue b/app/assets/javascripts/sidebar/components/time_tracking/report.vue index 5d4031ac68b..d9797961d40 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/report.vue +++ b/app/assets/javascripts/sidebar/components/time_tracking/report.vue @@ -78,9 +78,9 @@ export default { }, }, fields: [ - { key: 'spentAt', label: __('Spent At'), sortable: true }, + { key: 'spentAt', label: __('Spent At'), sortable: true, tdClass: 'gl-w-quarter' }, { key: 'user', label: __('User'), sortable: true }, - { key: 'timeSpent', label: __('Time Spent'), sortable: true }, + { key: 'timeSpent', label: __('Time Spent'), sortable: true, tdClass: 'gl-w-15' }, { key: 'summary', label: __('Summary / Note'), sortable: true }, ], }; diff --git a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue index fdbcef22bba..057bb9f0100 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue +++ b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue @@ -204,7 +204,7 @@ export default { :time-estimate-human-readable="humanTimeEstimate" /> <div - class="hide-collapsed gl-line-height-20 gl-text-gray-900 gl-display-flex gl-align-items-center" + class="hide-collapsed gl-line-height-20 gl-text-gray-900 gl-display-flex gl-align-items-center gl-font-weight-bold gl-mr-3" > {{ __('Time tracking') }} <gl-loading-icon v-if="isTimeTrackingInfoLoading" size="sm" class="gl-ml-2" inline /> @@ -221,8 +221,7 @@ export default { </div> <div v-if="!isTimeTrackingInfoLoading" class="hide-collapsed"> <div v-if="showEstimateOnlyState" data-testid="estimateOnlyPane"> - <span class="gl-font-weight-bold">{{ $options.i18n.estimatedOnlyText }} </span - >{{ humanTimeEstimate }} + <span>{{ $options.i18n.estimatedOnlyText }} </span>{{ humanTimeEstimate }} </div> <time-tracking-spent-only-pane v-if="showSpentOnlyState" @@ -250,6 +249,7 @@ export default { </gl-link> <gl-modal modal-id="time-tracking-report" + size="lg" :title="__('Time tracking report')" :hide-footer="true" > diff --git a/app/assets/javascripts/sidebar/components/todo_toggle/sidebar_todo_widget.vue b/app/assets/javascripts/sidebar/components/todo_toggle/sidebar_todo_widget.vue index eabba619af5..482b9343e70 100644 --- a/app/assets/javascripts/sidebar/components/todo_toggle/sidebar_todo_widget.vue +++ b/app/assets/javascripts/sidebar/components/todo_toggle/sidebar_todo_widget.vue @@ -6,6 +6,9 @@ import { __, sprintf } from '~/locale'; import { todoQueries, TodoMutationTypes, todoMutations } from '~/sidebar/constants'; import { todoLabel } from '~/vue_shared/components/sidebar/todo_toggle//utils'; import TodoButton from '~/vue_shared/components/sidebar/todo_toggle/todo_button.vue'; +import Tracking from '~/tracking'; + +const trackingMixin = Tracking.mixin(); export default { components: { @@ -16,6 +19,7 @@ export default { directives: { GlTooltip: GlTooltipDirective, }, + mixins: [trackingMixin], inject: { isClassicSidebar: { default: false, @@ -151,6 +155,10 @@ export default { message: errors[0], }); } + this.track('click_todo', { + label: 'right_sidebar', + property: this.hasTodo, + }); }, ) .catch(() => { diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js index 2a7d967cb61..351bb50d941 100644 --- a/app/assets/javascripts/sidebar/mount_sidebar.js +++ b/app/assets/javascripts/sidebar/mount_sidebar.js @@ -218,7 +218,7 @@ function mountCrmContactsComponent() { if (!el) return; - const { issueId } = el.dataset; + const { issueId, groupIssuesPath } = el.dataset; // eslint-disable-next-line no-new new Vue({ el, @@ -231,6 +231,7 @@ function mountCrmContactsComponent() { createElement('crm-contacts', { props: { issueId, + groupIssuesPath, }, }), }); @@ -430,10 +431,7 @@ function mountLockComponent(store) { return; } - const { fullPath } = getSidebarOptions(); - - const dataNode = document.getElementById('js-lock-issue-data'); - const initialData = JSON.parse(dataNode.innerHTML); + const { fullPath, editable } = getSidebarOptions(); // eslint-disable-next-line no-new new Vue({ @@ -446,7 +444,7 @@ function mountLockComponent(store) { render: (createElement) => createElement(IssuableLockForm, { props: { - isEditable: initialData.is_editable, + isEditable: editable, }, }), }); diff --git a/app/assets/javascripts/sidebar/queries/remove_attention_request.mutation.graphql b/app/assets/javascripts/sidebar/queries/remove_attention_request.mutation.graphql new file mode 100644 index 00000000000..d9b9c04fd63 --- /dev/null +++ b/app/assets/javascripts/sidebar/queries/remove_attention_request.mutation.graphql @@ -0,0 +1,7 @@ +mutation mergeRequestRemoveAttentionRequest($projectPath: ID!, $iid: String!, $userId: UserID!) { + mergeRequestRemoveAttentionRequest( + input: { projectPath: $projectPath, iid: $iid, userId: $userId } + ) { + errors + } +} diff --git a/app/assets/javascripts/sidebar/queries/request_attention.mutation.graphql b/app/assets/javascripts/sidebar/queries/request_attention.mutation.graphql new file mode 100644 index 00000000000..99a86e4fe5c --- /dev/null +++ b/app/assets/javascripts/sidebar/queries/request_attention.mutation.graphql @@ -0,0 +1,5 @@ +mutation mergeRequestRequestAttention($projectPath: ID!, $iid: String!, $userId: UserID!) { + mergeRequestRequestAttention(input: { projectPath: $projectPath, iid: $iid, userId: $userId }) { + errors + } +} diff --git a/app/assets/javascripts/sidebar/queries/reviewer_rereview.mutation.graphql b/app/assets/javascripts/sidebar/queries/reviewer_rereview.mutation.graphql index 73765e7d77b..0d66ee0d6e5 100644 --- a/app/assets/javascripts/sidebar/queries/reviewer_rereview.mutation.graphql +++ b/app/assets/javascripts/sidebar/queries/reviewer_rereview.mutation.graphql @@ -1,4 +1,4 @@ -mutation mergeRequestRequestRereview($projectPath: ID!, $iid: String!, $userId: ID!) { +mutation mergeRequestRequestRereview($projectPath: ID!, $iid: String!, $userId: UserID!) { mergeRequestReviewerRereview(input: { projectPath: $projectPath, iid: $iid, userId: $userId }) { errors } diff --git a/app/assets/javascripts/sidebar/queries/toggle_attention_requested.mutation.graphql b/app/assets/javascripts/sidebar/queries/toggle_attention_requested.mutation.graphql deleted file mode 100644 index a9f4af6e1b9..00000000000 --- a/app/assets/javascripts/sidebar/queries/toggle_attention_requested.mutation.graphql +++ /dev/null @@ -1,7 +0,0 @@ -mutation mergeRequestToggleAttentionRequested($projectPath: ID!, $iid: String!, $userId: ID!) { - mergeRequestToggleAttentionRequested( - input: { projectPath: $projectPath, iid: $iid, userId: $userId } - ) { - errors - } -} diff --git a/app/assets/javascripts/sidebar/queries/update_merge_request_milestone.mutation.graphql b/app/assets/javascripts/sidebar/queries/update_merge_request_milestone.mutation.graphql index 368f06fac7f..938953ccfb2 100644 --- a/app/assets/javascripts/sidebar/queries/update_merge_request_milestone.mutation.graphql +++ b/app/assets/javascripts/sidebar/queries/update_merge_request_milestone.mutation.graphql @@ -1,4 +1,4 @@ -mutation mergeRequestSetMilestone($fullPath: ID!, $iid: String!, $attributeId: ID) { +mutation mergeRequestSetMilestone($fullPath: ID!, $iid: String!, $attributeId: MilestoneID) { issuableSetAttribute: mergeRequestSetMilestone( input: { projectPath: $fullPath, iid: $iid, milestoneId: $attributeId } ) { diff --git a/app/assets/javascripts/sidebar/services/sidebar_service.js b/app/assets/javascripts/sidebar/services/sidebar_service.js index 90d8f2098bb..ea170203576 100644 --- a/app/assets/javascripts/sidebar/services/sidebar_service.js +++ b/app/assets/javascripts/sidebar/services/sidebar_service.js @@ -5,7 +5,8 @@ import createGqClient, { fetchPolicies } from '~/lib/graphql'; import axios from '~/lib/utils/axios_utils'; import reviewerRereviewMutation from '../queries/reviewer_rereview.mutation.graphql'; import sidebarDetailsMRQuery from '../queries/sidebar_details_mr.query.graphql'; -import toggleAttentionRequestedMutation from '../queries/toggle_attention_requested.mutation.graphql'; +import requestAttentionMutation from '../queries/request_attention.mutation.graphql'; +import removeAttentionRequestMutation from '../queries/remove_attention_request.mutation.graphql'; const queries = { merge_request: sidebarDetailsMRQuery, @@ -92,9 +93,19 @@ export default class SidebarService { }); } - toggleAttentionRequested(userId) { + requestAttention(userId) { return gqClient.mutate({ - mutation: toggleAttentionRequestedMutation, + mutation: requestAttentionMutation, + variables: { + userId: convertToGraphQLId(TYPE_USER, `${userId}`), + projectPath: this.fullPath, + iid: this.iid.toString(), + }, + }); + } + removeAttentionRequest(userId) { + return gqClient.mutate({ + mutation: removeAttentionRequestMutation, variables: { userId: convertToGraphQLId(TYPE_USER, `${userId}`), projectPath: this.fullPath, diff --git a/app/assets/javascripts/sidebar/sidebar_mediator.js b/app/assets/javascripts/sidebar/sidebar_mediator.js index 83fb8f31dfb..7df901577b8 100644 --- a/app/assets/javascripts/sidebar/sidebar_mediator.js +++ b/app/assets/javascripts/sidebar/sidebar_mediator.js @@ -40,6 +40,7 @@ export default class SidebarMediator { const data = { assignee_ids: assignees }; try { + const { currentUserHasAttention } = this.store; const res = await this.service.update(field, data); this.store.overwrite('assignees', res.data.assignees); @@ -48,6 +49,10 @@ export default class SidebarMediator { this.store.overwrite('reviewers', res.data.reviewers); } + if (currentUserHasAttention && this.store.isAddingAssignee) { + toast(__('Assigned user(s). Your attention request was removed.')); + } + return Promise.resolve(res); } catch (e) { return Promise.reject(e); @@ -63,11 +68,16 @@ export default class SidebarMediator { const data = { reviewer_ids: reviewers }; try { + const { currentUserHasAttention } = this.store; const res = await this.service.update(field, data); this.store.overwrite('reviewers', res.data.reviewers); this.store.overwrite('assignees', res.data.assignees); + if (currentUserHasAttention && this.store.isAddingAssignee) { + toast(__('Requested review. Your attention request was removed.')); + } + return Promise.resolve(res); } catch (e) { return Promise.reject(); @@ -98,14 +108,19 @@ export default class SidebarMediator { } } - async toggleAttentionRequested(type, { user, callback }) { + async toggleAttentionRequested(type, { user, callback, direction }) { + const mutations = { + add: (id) => this.service.requestAttention(id), + remove: (id) => this.service.removeAttentionRequest(id), + }; + try { const isReviewer = type === 'reviewer'; const reviewerOrAssignee = isReviewer ? this.store.findReviewer(user) : this.store.findAssignee(user); - await this.service.toggleAttentionRequested(user.id); + await mutations[direction]?.(user.id); if (reviewerOrAssignee.attention_requested) { toast( @@ -115,12 +130,22 @@ export default class SidebarMediator { ); } else { const currentUserId = gon.current_user_id; + const { currentUserHasAttention } = this.store; if (currentUserId !== user.id) { this.removeCurrentUserAttentionRequested(); } - toast(sprintf(__('Requested attention from @%{username}'), { username: user.username })); + toast( + currentUserHasAttention && currentUserId !== user.id + ? sprintf( + __( + 'Requested attention from @%{username}. Your own attention request was removed.', + ), + { username: user.username }, + ) + : sprintf(__('Requested attention from @%{username}'), { username: user.username }), + ); } this.store.updateReviewer(user.id, 'attention_requested'); @@ -138,7 +163,7 @@ export default class SidebarMediator { captureError: true, actionConfig: { title: __('Try again'), - clickHandler: () => this.toggleAttentionRequired(type, { user, callback }), + clickHandler: () => this.toggleAttentionRequired(type, { user, callback, direction }), }, }); } diff --git a/app/assets/javascripts/sidebar/stores/sidebar_store.js b/app/assets/javascripts/sidebar/stores/sidebar_store.js index 2caa6f4f0a0..ca85ee7fd94 100644 --- a/app/assets/javascripts/sidebar/stores/sidebar_store.js +++ b/app/assets/javascripts/sidebar/stores/sidebar_store.js @@ -18,7 +18,9 @@ export default class SidebarStore { this.humanTimeSpent = ''; this.timeTrackingLimitToHours = timeTrackingLimitToHours; this.assignees = []; + this.addingAssignees = []; this.reviewers = []; + this.addingReviewers = []; this.isFetching = { assignees: true, reviewers: true, @@ -32,6 +34,7 @@ export default class SidebarStore { this.subscribeDisabledDescription = ''; this.subscribed = null; this.changing = false; + this.issuableType = options.issuableType; SidebarStore.singleton = this; } @@ -73,12 +76,20 @@ export default class SidebarStore { if (!this.findAssignee(assignee)) { this.changing = true; this.assignees.push(assignee); + + if (assignee.id !== this.currentUser.id) { + this.addingAssignees.push(assignee.id); + } } } addReviewer(reviewer) { if (!this.findReviewer(reviewer)) { this.reviewers.push(reviewer); + + if (reviewer.id !== this.currentUser.id) { + this.addingReviewers.push(reviewer.id); + } } } @@ -114,12 +125,14 @@ export default class SidebarStore { if (assignee) { this.changing = true; this.assignees = this.assignees.filter(({ id }) => id !== assignee.id); + this.addingAssignees = this.addingAssignees.filter(({ id }) => id !== assignee.id); } } removeReviewer(reviewer) { if (reviewer) { this.reviewers = this.reviewers.filter(({ id }) => id !== reviewer.id); + this.addingReviewers = this.addingReviewers.filter(({ id }) => id !== reviewer.id); } } @@ -147,4 +160,26 @@ export default class SidebarStore { setMoveToProjectId(moveToProjectId) { this.moveToProjectId = moveToProjectId; } + + get currentUserHasAttention() { + if (!window.gon?.features?.mrAttentionRequests || !this.isMergeRequest) return false; + + const currentUserId = this.currentUser.id; + const currentUserReviewer = this.findReviewer({ id: currentUserId }); + const currentUserAssignee = this.findAssignee({ id: currentUserId }); + + return currentUserReviewer?.attention_requested || currentUserAssignee?.attention_requested; + } + + get isAddingAssignee() { + return this.addingAssignees.length > 0; + } + + get isAddingReviewer() { + return this.addingReviewers.length > 0; + } + + get isMergeRequest() { + return this.issuableType === 'merge_request'; + } } |