diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-03-16 18:18:33 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-03-16 18:18:33 +0000 |
commit | f64a639bcfa1fc2bc89ca7db268f594306edfd7c (patch) | |
tree | a2c3c2ebcc3b45e596949db485d6ed18ffaacfa1 /app/assets/javascripts/sidebar | |
parent | bfbc3e0d6583ea1a91f627528bedc3d65ba4b10f (diff) | |
download | gitlab-ce-f64a639bcfa1fc2bc89ca7db268f594306edfd7c.tar.gz |
Add latest changes from gitlab-org/gitlab@13-10-stable-eev13.10.0-rc40
Diffstat (limited to 'app/assets/javascripts/sidebar')
26 files changed, 695 insertions, 334 deletions
diff --git a/app/assets/javascripts/sidebar/components/assignees/issuable_assignees.vue b/app/assets/javascripts/sidebar/components/assignees/issuable_assignees.vue index e2dc37a0ac2..b53b7039018 100644 --- a/app/assets/javascripts/sidebar/components/assignees/issuable_assignees.vue +++ b/app/assets/javascripts/sidebar/components/assignees/issuable_assignees.vue @@ -31,13 +31,18 @@ export default { </script> <template> - <div class="gl-display-flex gl-flex-direction-column"> - <div v-if="emptyUsers" data-testid="none"> + <div class="gl-display-flex gl-flex-direction-column issuable-assignees"> + <div + v-if="emptyUsers" + class="gl-display-flex gl-align-items-center gl-text-gray-500" + data-testid="none" + > <span> {{ __('None') }} -</span> <gl-button data-testid="assign-yourself" category="tertiary" variant="link" + class="gl-ml-2" @click="$emit('assign-self')" > <span class="gl-text-gray-500 gl-hover-text-blue-800">{{ __('assign yourself') }}</span> diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue index 8f3f77cb5f0..cc2201ad359 100644 --- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue +++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue @@ -15,13 +15,12 @@ import { IssuableType } from '~/issue_show/constants'; import { __, n__ } from '~/locale'; import IssuableAssignees from '~/sidebar/components/assignees/issuable_assignees.vue'; import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue'; -import { assigneesQueries } from '~/sidebar/constants'; +import { assigneesQueries, ASSIGNEES_DEBOUNCE_DELAY } from '~/sidebar/constants'; import MultiSelectDropdown from '~/vue_shared/components/sidebar/multiselect_dropdown.vue'; export const assigneesWidget = Vue.observable({ updateAssignees: null, }); - export default { i18n: { unassigned: __('Unassigned'), @@ -88,10 +87,10 @@ export default { return this.queryVariables; }, update(data) { - return data.issuable || data.project?.issuable; + return data.workspace?.issuable; }, result({ data }) { - const issuable = data.issuable || data.project?.issuable; + const issuable = data.workspace?.issuable; if (issuable) { this.selected = this.moveCurrentUserToStart(cloneDeep(issuable.assignees.nodes)); } @@ -104,13 +103,24 @@ export default { query: searchUsers, variables() { return { + fullPath: this.fullPath, search: this.search, }; }, update(data) { - return data.users?.nodes || []; + const searchResults = data.workspace?.users?.nodes.map(({ user }) => user) || []; + const mergedSearchResults = this.participants.reduce((acc, current) => { + if ( + !acc.some((user) => current.username === user.username) && + (current.name.includes(this.search) || current.username.includes(this.search)) + ) { + acc.push(current); + } + return acc; + }, searchResults); + return mergedSearchResults; }, - debounce: 250, + debounce: ASSIGNEES_DEBOUNCE_DELAY, skip() { return this.isSearchEmpty; }, @@ -185,7 +195,7 @@ export default { return this.selected.some(isCurrentUser) || this.participants.some(isCurrentUser); }, noUsersFound() { - return !this.isSearchEmpty && this.unselectedFiltered.length === 0; + return !this.isSearchEmpty && this.searchUsers.length === 0; }, showCurrentUser() { return !this.isCurrentUserInParticipants && (this.isSearchEmpty || this.isSearching); @@ -218,7 +228,7 @@ export default { }, }) .then(({ data }) => { - this.$emit('assignees-updated', data); + this.$emit('assignees-updated', data.issuableSetAssignees.issuable.assignees.nodes); return data; }) .catch(() => { @@ -281,6 +291,9 @@ export default { collapseWidget() { this.$refs.toggle.collapse(); }, + showDivider(list) { + return list.length > 0 && this.isSearchEmpty; + }, }, }; </script> @@ -306,6 +319,7 @@ export default { <issuable-assignees :users="assignees" :issuable-type="issuableType" + class="gl-mt-2" @assign-self="assignSelf" /> </template> @@ -334,12 +348,14 @@ export default { data-testid="unassign" @click="selectAssignee()" > - <span :class="selectedIsEmpty ? 'gl-pl-0' : 'gl-pl-6'">{{ - $options.i18n.unassigned - }}</span></gl-dropdown-item + <span + :class="selectedIsEmpty ? 'gl-pl-0' : 'gl-pl-6'" + class="gl-font-weight-bold" + >{{ $options.i18n.unassigned }}</span + ></gl-dropdown-item > - <gl-dropdown-divider data-testid="unassign-divider" /> </template> + <gl-dropdown-divider v-if="showDivider(selectedFiltered)" /> <gl-dropdown-item v-for="item in selectedFiltered" :key="item.id" @@ -358,10 +374,10 @@ export default { /> </gl-avatar-link> </gl-dropdown-item> - <gl-dropdown-divider v-if="!selectedIsEmpty" data-testid="selected-user-divider" /> <template v-if="showCurrentUser"> + <gl-dropdown-divider /> <gl-dropdown-item - data-testid="unselected-participant" + data-testid="current-user" @click.stop="selectAssignee(currentUser)" > <gl-avatar-link> @@ -370,12 +386,12 @@ export default { :label="currentUser.name" :sub-label="currentUser.username" :src="currentUser.avatarUrl" - class="gl-align-items-center" + class="gl-align-items-center gl-pl-6!" /> </gl-avatar-link> </gl-dropdown-item> - <gl-dropdown-divider /> </template> + <gl-dropdown-divider v-if="showDivider(unselectedFiltered)" /> <gl-dropdown-item v-for="unselectedUser in unselectedFiltered" :key="unselectedUser.id" @@ -392,7 +408,7 @@ export default { /> </gl-avatar-link> </gl-dropdown-item> - <gl-dropdown-item v-if="noUsersFound && !isSearching"> + <gl-dropdown-item v-if="noUsersFound && !isSearching" data-testid="empty-results"> {{ __('No matching results') }} </gl-dropdown-item> </template> 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 36775648809..d0da4a9c75a 100644 --- a/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue +++ b/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue @@ -83,7 +83,7 @@ export default { <assignee-avatar-link :user="user" :issuable-type="issuableType" /> </div> </div> - <div v-if="renderShowMoreSection" class="user-list-more"> + <div v-if="renderShowMoreSection" class="user-list-more gl-hover-text-blue-800"> <button type="button" class="btn-link" diff --git a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue deleted file mode 100644 index 57b3705e803..00000000000 --- a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue +++ /dev/null @@ -1,113 +0,0 @@ -<script> -import { GlIcon, GlTooltipDirective } from '@gitlab/ui'; -import { mapState } from 'vuex'; -import { __, sprintf } from '~/locale'; -import eventHub from '~/sidebar/event_hub'; -import EditForm from './edit_form.vue'; - -export default { - components: { - EditForm, - GlIcon, - }, - directives: { - GlTooltip: GlTooltipDirective, - }, - props: { - fullPath: { - required: true, - type: String, - }, - isEditable: { - required: true, - type: Boolean, - }, - issuableType: { - required: false, - type: String, - default: 'issue', - }, - }, - data() { - return { - edit: false, - }; - }, - computed: { - ...mapState({ - confidential: ({ noteableData, confidential }) => { - if (noteableData) { - return noteableData.confidential; - } - return Boolean(confidential); - }, - }), - confidentialityIcon() { - return this.confidential ? 'eye-slash' : 'eye'; - }, - tooltipLabel() { - return this.confidential ? __('Confidential') : __('Not confidential'); - }, - confidentialText() { - return sprintf(__('This %{issuableType} is confidential'), { - issuableType: this.issuableType, - }); - }, - }, - created() { - eventHub.$on('closeConfidentialityForm', this.toggleForm); - }, - beforeDestroy() { - eventHub.$off('closeConfidentialityForm', this.toggleForm); - }, - methods: { - toggleForm() { - this.edit = !this.edit; - }, - }, -}; -</script> - -<template> - <div class="block issuable-sidebar-item confidentiality"> - <div - ref="collapseIcon" - v-gl-tooltip.viewport.left - :title="tooltipLabel" - class="sidebar-collapsed-icon" - @click="toggleForm" - > - <gl-icon :name="confidentialityIcon" /> - </div> - <div class="title hide-collapsed"> - {{ __('Confidentiality') }} - <a - v-if="isEditable" - ref="editLink" - class="float-right confidential-edit" - href="#" - data-track-event="click_edit_button" - data-track-label="right_sidebar" - data-track-property="confidentiality" - @click.prevent="toggleForm" - >{{ __('Edit') }}</a - > - </div> - <div class="value sidebar-item-value hide-collapsed"> - <edit-form - v-if="edit" - :confidential="confidential" - :full-path="fullPath" - :issuable-type="issuableType" - /> - <div v-if="!confidential" class="no-value sidebar-item-value" data-testid="not-confidential"> - <gl-icon :size="16" name="eye" class="sidebar-item-icon inline" /> - {{ __('Not confidential') }} - </div> - <div v-else class="value sidebar-item-value hide-collapsed"> - <gl-icon :size="16" name="eye-slash" class="sidebar-item-icon inline is-active" /> - {{ confidentialText }} - </div> - </div> - </div> -</template> diff --git a/app/assets/javascripts/sidebar/components/confidential/edit_form.vue b/app/assets/javascripts/sidebar/components/confidential/edit_form.vue deleted file mode 100644 index 057224d5918..00000000000 --- a/app/assets/javascripts/sidebar/components/confidential/edit_form.vue +++ /dev/null @@ -1,64 +0,0 @@ -<script> -import { GlSprintf } from '@gitlab/ui'; -import { __ } from '../../../locale'; -import editFormButtons from './edit_form_buttons.vue'; - -export default { - components: { - editFormButtons, - GlSprintf, - }, - props: { - confidential: { - required: true, - type: Boolean, - }, - fullPath: { - required: true, - type: String, - }, - issuableType: { - required: true, - type: String, - }, - }, - computed: { - confidentialityOnWarning() { - return __( - 'You are going to turn on the confidentiality. This means that only team members with %{strongStart}at least Reporter access%{strongEnd} are able to see and leave comments on the %{issuableType}.', - ); - }, - confidentialityOffWarning() { - return __( - '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}.', - ); - }, - }, -}; -</script> - -<template> - <div class="dropdown show"> - <div class="dropdown-menu sidebar-item-warning-message"> - <div> - <p v-if="!confidential"> - <gl-sprintf :message="confidentialityOnWarning"> - <template #strong="{ content }"> - <strong>{{ content }}</strong> - </template> - <template #issuableType>{{ issuableType }}</template> - </gl-sprintf> - </p> - <p v-else> - <gl-sprintf :message="confidentialityOffWarning"> - <template #strong="{ content }"> - <strong>{{ content }}</strong> - </template> - <template #issuableType>{{ issuableType }}</template> - </gl-sprintf> - </p> - <edit-form-buttons :full-path="fullPath" :confidential="confidential" /> - </div> - </div> - </div> -</template> diff --git a/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue b/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue deleted file mode 100644 index 154a228c978..00000000000 --- a/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue +++ /dev/null @@ -1,81 +0,0 @@ -<script> -import { GlButton } from '@gitlab/ui'; -import $ from 'jquery'; -import { mapActions } from 'vuex'; -import { deprecatedCreateFlash as Flash } from '~/flash'; -import { __ } from '~/locale'; -import eventHub from '../../event_hub'; - -export default { - components: { - GlButton, - }, - props: { - fullPath: { - required: true, - type: String, - }, - confidential: { - required: true, - type: Boolean, - }, - }, - data() { - return { - isLoading: false, - }; - }, - computed: { - toggleButtonText() { - if (this.isLoading) { - return __('Applying'); - } - - return this.confidential ? __('Turn Off') : __('Turn On'); - }, - }, - methods: { - ...mapActions(['updateConfidentialityOnIssuable']), - closeForm() { - eventHub.$emit('closeConfidentialityForm'); - $(this.$el).trigger('hidden.gl.dropdown'); - }, - submitForm() { - this.isLoading = true; - const confidential = !this.confidential; - - this.updateConfidentialityOnIssuable({ confidential, fullPath: this.fullPath }) - .then(() => { - eventHub.$emit('updateIssuableConfidentiality', confidential); - }) - .catch((err) => { - Flash( - err || __('Something went wrong trying to change the confidentiality of this issue'), - ); - }) - .finally(() => { - this.closeForm(); - this.isLoading = false; - }); - }, - }, -}; -</script> - -<template> - <div class="sidebar-item-warning-message-actions"> - <gl-button class="gl-mr-3" @click="closeForm"> - {{ __('Cancel') }} - </gl-button> - <gl-button - category="secondary" - variant="warning" - :disabled="isLoading" - :loading="isLoading" - data-testid="confidential-toggle" - @click.prevent="submitForm" - > - {{ toggleButtonText }} - </gl-button> - </div> -</template> diff --git a/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_content.vue b/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_content.vue new file mode 100644 index 00000000000..37a44eb8f01 --- /dev/null +++ b/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_content.vue @@ -0,0 +1,64 @@ +<script> +import { GlIcon, GlTooltipDirective } from '@gitlab/ui'; +import { __, sprintf } from '~/locale'; + +export default { + components: { + GlIcon, + }, + directives: { + GlTooltip: GlTooltipDirective, + }, + props: { + confidential: { + type: Boolean, + required: true, + }, + issuableType: { + type: String, + required: true, + }, + }, + computed: { + confidentialText() { + return this.confidential + ? sprintf(__('This %{issuableType} is confidential'), { + issuableType: this.issuableType, + }) + : __('Not confidential'); + }, + confidentialIcon() { + return this.confidential ? 'eye-slash' : 'eye'; + }, + tooltipLabel() { + return this.confidential ? __('Confidential') : __('Not confidential'); + }, + }, +}; +</script> + +<template> + <div> + <div + v-gl-tooltip.viewport.left + :title="tooltipLabel" + class="sidebar-collapsed-icon" + data-testid="sidebar-collapsed-icon" + @click="$emit('expandSidebar')" + > + <gl-icon + :size="16" + :name="confidentialIcon" + class="sidebar-item-icon inline" + :class="{ 'is-active': confidential }" + /> + </div> + <gl-icon + :size="16" + :name="confidentialIcon" + class="sidebar-item-icon inline hide-collapsed" + :class="{ 'is-active': confidential }" + /> + <span class="hide-collapsed" data-testid="confidential-text">{{ confidentialText }}</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 new file mode 100644 index 00000000000..a21ac73f131 --- /dev/null +++ b/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_form.vue @@ -0,0 +1,136 @@ +<script> +import { GlSprintf, GlButton } from '@gitlab/ui'; +import createFlash from '~/flash'; +import { IssuableType } from '~/issue_show/constants'; +import { __, sprintf } from '~/locale'; +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}.', + ), + 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}.', + ), + }, + components: { + GlSprintf, + GlButton, + }, + inject: ['fullPath', 'iid'], + props: { + confidential: { + required: true, + type: Boolean, + }, + issuableType: { + required: true, + type: String, + }, + }, + data() { + return { + loading: false, + }; + }, + computed: { + toggleButtonText() { + if (this.loading) { + return __('Applying'); + } + return this.confidential ? __('Turn off') : __('Turn on'); + }, + warningMessage() { + return this.confidential + ? this.$options.i18n.confidentialityOffWarning + : this.$options.i18n.confidentialityOnWarning; + }, + workspacePath() { + return this.issuableType === IssuableType.Issue + ? { + projectPath: this.fullPath, + } + : { + groupPath: this.fullPath, + }; + }, + }, + methods: { + submitForm() { + this.loading = true; + this.$apollo + .mutate({ + mutation: confidentialityQueries[this.issuableType].mutation, + variables: { + input: { + ...this.workspacePath, + iid: this.iid, + confidential: !this.confidential, + }, + }, + }) + .then( + ({ + data: { + issuableSetConfidential: { errors }, + }, + }) => { + if (errors.length) { + createFlash({ + message: errors[0], + }); + } else { + this.$emit('closeForm'); + } + }, + ) + .catch(() => { + createFlash({ + message: sprintf( + __('Something went wrong while setting %{issuableType} confidentiality.'), + { + issuableType: this.issuableType, + }, + ), + }); + }) + .finally(() => { + this.loading = false; + }); + }, + }, +}; +</script> + +<template> + <div class="dropdown show"> + <div class="dropdown-menu sidebar-item-warning-message"> + <div> + <p data-testid="warning-message"> + <gl-sprintf :message="warningMessage"> + <template #strong="{ content }"> + <strong>{{ content }}</strong> + </template> + <template #issuableType>{{ issuableType }}</template> + </gl-sprintf> + </p> + <div class="sidebar-item-warning-message-actions"> + <gl-button class="gl-mr-3" data-testid="confidential-cancel" @click="$emit('closeForm')"> + {{ __('Cancel') }} + </gl-button> + <gl-button + category="secondary" + variant="warning" + :disabled="loading" + :loading="loading" + data-testid="confidential-toggle" + @click.prevent="submitForm" + > + {{ toggleButtonText }} + </gl-button> + </div> + </div> + </div> + </div> +</template> diff --git a/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_widget.vue b/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_widget.vue new file mode 100644 index 00000000000..ec5f07f9785 --- /dev/null +++ b/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_widget.vue @@ -0,0 +1,142 @@ +<script> +import produce from 'immer'; +import Vue from 'vue'; +import createFlash from '~/flash'; +import { __, sprintf } from '~/locale'; +import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue'; +import { confidentialityQueries } from '~/sidebar/constants'; +import SidebarConfidentialityContent from './sidebar_confidentiality_content.vue'; +import SidebarConfidentialityForm from './sidebar_confidentiality_form.vue'; + +export const confidentialWidget = Vue.observable({ + setConfidentiality: null, +}); + +const hideDropdownEvent = new CustomEvent('hiddenGlDropdown', { + bubbles: true, +}); + +export default { + tracking: { + event: 'click_edit_button', + label: 'right_sidebar', + property: 'confidentiality', + }, + components: { + SidebarEditableItem, + SidebarConfidentialityContent, + SidebarConfidentialityForm, + }, + inject: ['fullPath', 'iid'], + props: { + issuableType: { + required: true, + type: String, + }, + }, + data() { + return { + confidential: false, + }; + }, + apollo: { + confidential: { + query() { + return confidentialityQueries[this.issuableType].query; + }, + variables() { + return { + fullPath: this.fullPath, + iid: String(this.iid), + }; + }, + update(data) { + return data.workspace?.issuable?.confidential || false; + }, + result({ data }) { + this.$emit('confidentialityUpdated', data.workspace?.issuable?.confidential); + }, + error() { + createFlash({ + message: sprintf( + __('Something went wrong while setting %{issuableType} confidentiality.'), + { + issuableType: this.issuableType, + }, + ), + }); + }, + }, + }, + computed: { + isLoading() { + return this.$apollo.queries.confidential.loading; + }, + }, + mounted() { + confidentialWidget.setConfidentiality = this.setConfidentiality; + }, + destroyed() { + confidentialWidget.setConfidentiality = null; + }, + methods: { + closeForm() { + this.$refs.editable.collapse(); + this.$el.dispatchEvent(hideDropdownEvent); + this.$emit('closeForm'); + }, + // synchronizing the quick action with the sidebar widget + // this is a temporary solution until we have confidentiality real-time updates + setConfidentiality() { + const { defaultClient: client } = this.$apollo.provider.clients; + const sourceData = client.readQuery({ + query: confidentialityQueries[this.issuableType].query, + variables: { fullPath: this.fullPath, iid: this.iid }, + }); + + const data = produce(sourceData, (draftData) => { + draftData.workspace.issuable.confidential = !this.confidential; + }); + + client.writeQuery({ + query: confidentialityQueries[this.issuableType].query, + variables: { fullPath: this.fullPath, iid: this.iid }, + data, + }); + }, + expandSidebar() { + this.$refs.editable.expand(); + this.$emit('expandSidebar'); + }, + }, +}; +</script> + +<template> + <sidebar-editable-item + ref="editable" + :title="__('Confidentiality')" + :tracking="$options.tracking" + :loading="isLoading" + class="block confidentiality" + > + <template #collapsed> + <div> + <sidebar-confidentiality-content + v-if="!isLoading" + :confidential="confidential" + :issuable-type="issuableType" + @expandSidebar="expandSidebar" + /> + </div> + </template> + <template #default> + <sidebar-confidentiality-content :confidential="confidential" :issuable-type="issuableType" /> + <sidebar-confidentiality-form + :confidential="confidential" + :issuable-type="issuableType" + @closeForm="closeForm" + /> + </template> + </sidebar-editable-item> +</template> diff --git a/app/assets/javascripts/sidebar/components/reference/sidebar_reference_widget.vue b/app/assets/javascripts/sidebar/components/reference/sidebar_reference_widget.vue new file mode 100644 index 00000000000..567c921b74e --- /dev/null +++ b/app/assets/javascripts/sidebar/components/reference/sidebar_reference_widget.vue @@ -0,0 +1,84 @@ +<script> +import { GlLoadingIcon } from '@gitlab/ui'; +import { __ } from '~/locale'; +import { referenceQueries } from '~/sidebar/constants'; +import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; + +export default { + i18n: { + copyReference: __('Copy reference'), + text: __('Reference'), + }, + components: { + ClipboardButton, + GlLoadingIcon, + }, + inject: ['fullPath', 'iid'], + props: { + issuableType: { + required: true, + type: String, + }, + }, + data() { + return { + reference: '', + }; + }, + apollo: { + reference: { + query() { + return referenceQueries[this.issuableType].query; + }, + variables() { + return { + fullPath: this.fullPath, + iid: this.iid, + }; + }, + update(data) { + return data.workspace?.issuable?.reference || ''; + }, + error(error) { + this.$emit('fetch-error', { + message: __('An error occurred while fetching reference'), + error, + }); + }, + }, + }, + computed: { + isLoading() { + return this.$apollo.queries.reference.loading; + }, + }, +}; +</script> + +<template> + <div class="sub-block"> + <clipboard-button + v-if="!isLoading" + :title="$options.i18n.copyReference" + :text="reference" + category="tertiary" + css-class="sidebar-collapsed-icon dont-change-state" + tooltip-placement="left" + /> + <div class="gl-display-flex gl-align-items-center gl-justify-between gl-mb-2 hide-collapsed"> + <span class="gl-overflow-hidden gl-text-overflow-ellipsis gl-white-space-nowrap"> + {{ $options.i18n.text }}: {{ reference }} + <gl-loading-icon v-if="isLoading" inline :label="$options.i18n.text" /> + </span> + <clipboard-button + v-if="!isLoading" + :title="$options.i18n.copyReference" + :text="reference" + size="small" + category="tertiary" + css-class="gl-mr-1" + tooltip-placement="left" + /> + </div> + </div> +</template> 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 cbd68f2513a..dd1d54d67f2 100644 --- a/app/assets/javascripts/sidebar/components/reviewers/uncollapsed_reviewer_list.vue +++ b/app/assets/javascripts/sidebar/components/reviewers/uncollapsed_reviewer_list.vue @@ -1,5 +1,6 @@ <script> import { GlButton, GlTooltipDirective, GlIcon } from '@gitlab/ui'; +import { sprintf, s__ } from '~/locale'; import ReviewerAvatarLink from './reviewer_avatar_link.vue'; const LOADING_STATE = 'loading'; @@ -50,6 +51,9 @@ export default { }, }, methods: { + approvedByTooltipTitle(user) { + return sprintf(s__('MergeRequest|Approved by @%{username}'), user); + }, toggleShowLess() { this.showLess = !this.showLess; }, @@ -57,6 +61,7 @@ export default { this.loadingStates[userId] = LOADING_STATE; this.$emit('request-review', { userId, callback: this.requestReviewComplete }); }, + requestReviewComplete(userId, success) { if (success) { this.loadingStates[userId] = SUCCESS_STATE; @@ -86,10 +91,19 @@ export default { <div class="gl-ml-3">@{{ user.username }}</div> </reviewer-avatar-link> <gl-icon + v-if="user.approved" + v-gl-tooltip.left + :size="16" + :title="approvedByTooltipTitle(user)" + name="status-success" + class="float-right gl-my-2 gl-ml-2 gl-text-green-500" + data-testid="re-approved" + /> + <gl-icon v-if="loadingStates[user.id] === $options.SUCCESS_STATE" :size="24" name="check" - class="float-right gl-text-green-500" + class="float-right gl-py-2 gl-mr-2 gl-text-green-500" data-testid="re-request-success" /> <gl-button diff --git a/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue b/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue index 9da839cd133..4ab4606ac1c 100644 --- a/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue +++ b/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue @@ -3,7 +3,12 @@ import { GlButton, GlLoadingIcon } from '@gitlab/ui'; export default { components: { GlButton, GlLoadingIcon }, - inject: ['canUpdate'], + inject: { + canUpdate: {}, + isClassicSidebar: { + default: false, + }, + }, props: { title: { type: String, @@ -15,6 +20,15 @@ export default { required: false, default: false, }, + tracking: { + type: Object, + required: false, + default: () => ({ + event: null, + label: null, + property: null, + }), + }, }, data() { return { @@ -71,24 +85,33 @@ export default { <template> <div> - <div class="gl-display-flex gl-align-items-center gl-mb-3" @click.self="collapse"> - <span data-testid="title">{{ title }}</span> - <gl-loading-icon v-if="loading" inline class="gl-ml-2" /> + <div class="gl-display-flex gl-align-items-center" @click.self="collapse"> + <span class="hide-collapsed" data-testid="title">{{ title }}</span> + <gl-loading-icon v-if="loading" inline class="gl-ml-2 hide-collapsed" /> + <gl-loading-icon + v-if="loading && isClassicSidebar" + inline + class="gl-mx-auto gl-my-0 hide-expanded" + /> <gl-button v-if="canUpdate" variant="link" - class="gl-text-gray-900! gl-hover-text-blue-800! gl-ml-auto js-sidebar-dropdown-toggle" + class="gl-text-gray-900! gl-hover-text-blue-800! gl-ml-auto hide-collapsed" data-testid="edit-button" + :data-track-event="tracking.event" + :data-track-label="tracking.label" + :data-track-property="tracking.property" + data-qa-selector="edit_link" @keyup.esc="toggle" @click="toggle" > {{ __('Edit') }} </gl-button> </div> - <div v-show="!edit" class="gl-text-gray-500" data-testid="collapsed-content"> + <div v-show="!edit" data-testid="collapsed-content"> <slot name="collapsed">{{ __('None') }}</slot> </div> - <div v-show="edit" data-testid="expanded-content"> + <div v-show="edit" data-testid="expanded-content" :class="{ 'gl-mt-3': !isClassicSidebar }"> <slot :edit="edit"></slot> </div> </div> diff --git a/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue index 9b06c20a6f3..c0424dc2873 100644 --- a/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue +++ b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue @@ -122,6 +122,8 @@ export default { :value="subscribed" class="hide-collapsed" data-testid="subscription-toggle" + :label="__('Notifications')" + label-position="hidden" @change="toggleSubscription" /> </div> diff --git a/app/assets/javascripts/sidebar/components/time_tracking/help_state.vue b/app/assets/javascripts/sidebar/components/time_tracking/help_state.vue index e0f60b9af08..d1a5685fdd3 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/help_state.vue +++ b/app/assets/javascripts/sidebar/components/time_tracking/help_state.vue @@ -1,10 +1,14 @@ <script> /* eslint-disable vue/no-v-html */ +import { GlButton } from '@gitlab/ui'; import { joinPaths } from '~/lib/utils/url_utility'; import { sprintf, s__ } from '../../../locale'; export default { name: 'TimeTrackingHelpState', + components: { + GlButton, + }, computed: { href() { return joinPaths(gon.relative_url_root || '', '/help/user/project/time_tracking.md'); @@ -40,7 +44,7 @@ export default { <p>{{ __('Quick actions can be used in the issues description and comment boxes.') }}</p> <p v-html="estimateText"></p> <p v-html="spendText"></p> - <a :href="href" class="btn btn-default learn-more-button"> {{ __('Learn more') }} </a> + <gl-button :href="href">{{ __('Learn more') }}</gl-button> </div> </div> </template> diff --git a/app/assets/javascripts/sidebar/constants.js b/app/assets/javascripts/sidebar/constants.js index 274aa237aea..a0e636488f4 100644 --- a/app/assets/javascripts/sidebar/constants.js +++ b/app/assets/javascripts/sidebar/constants.js @@ -1,9 +1,17 @@ import { IssuableType } from '~/issue_show/constants'; +import epicConfidentialQuery from '~/sidebar/queries/epic_confidential.query.graphql'; +import issueConfidentialQuery from '~/sidebar/queries/issue_confidential.query.graphql'; +import issueReferenceQuery from '~/sidebar/queries/issue_reference.query.graphql'; +import mergeRequestReferenceQuery from '~/sidebar/queries/merge_request_reference.query.graphql'; +import updateEpicMutation from '~/sidebar/queries/update_epic_confidential.mutation.graphql'; +import updateIssueConfidentialMutation from '~/sidebar/queries/update_issue_confidential.mutation.graphql'; import getIssueParticipants from '~/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql'; import getMergeRequestParticipants from '~/vue_shared/components/sidebar/queries/get_mr_participants.query.graphql'; import updateAssigneesMutation from '~/vue_shared/components/sidebar/queries/update_issue_assignees.mutation.graphql'; import updateMergeRequestParticipantsMutation from '~/vue_shared/components/sidebar/queries/update_mr_assignees.mutation.graphql'; +export const ASSIGNEES_DEBOUNCE_DELAY = 250; + export const assigneesQueries = { [IssuableType.Issue]: { query: getIssueParticipants, @@ -14,3 +22,23 @@ export const assigneesQueries = { mutation: updateMergeRequestParticipantsMutation, }, }; + +export const confidentialityQueries = { + [IssuableType.Issue]: { + query: issueConfidentialQuery, + mutation: updateIssueConfidentialMutation, + }, + [IssuableType.Epic]: { + query: epicConfidentialQuery, + mutation: updateEpicMutation, + }, +}; + +export const referenceQueries = { + [IssuableType.Issue]: { + query: issueReferenceQuery, + }, + [IssuableType.MergeRequest]: { + query: mergeRequestReferenceQuery, + }, +}; diff --git a/app/assets/javascripts/sidebar/graphql.js b/app/assets/javascripts/sidebar/graphql.js new file mode 100644 index 00000000000..aa139540a51 --- /dev/null +++ b/app/assets/javascripts/sidebar/graphql.js @@ -0,0 +1,8 @@ +import VueApollo from 'vue-apollo'; +import createDefaultClient from '~/lib/graphql'; + +export const defaultClient = createDefaultClient(); + +export const apolloProvider = new VueApollo({ + defaultClient, +}); diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js index 662edbc4f8d..312c0c89f29 100644 --- a/app/assets/javascripts/sidebar/mount_sidebar.js +++ b/app/assets/javascripts/sidebar/mount_sidebar.js @@ -2,7 +2,7 @@ import $ from 'jquery'; import Vue from 'vue'; import VueApollo from 'vue-apollo'; import createFlash from '~/flash'; -import createDefaultClient from '~/lib/graphql'; +import { IssuableType } from '~/issue_show/constants'; import { isInIssuePage, isInDesignPage, @@ -10,9 +10,11 @@ import { parseBoolean, } from '~/lib/utils/common_utils'; import { __ } from '~/locale'; +import SidebarConfidentialityWidget from '~/sidebar/components/confidential/sidebar_confidentiality_widget.vue'; +import SidebarReferenceWidget from '~/sidebar/components/reference/sidebar_reference_widget.vue'; +import { apolloProvider } from '~/sidebar/graphql'; import Translate from '../vue_shared/translate'; import SidebarAssignees from './components/assignees/sidebar_assignees.vue'; -import ConfidentialIssueSidebar from './components/confidential/confidential_issue_sidebar.vue'; import CopyEmailToClipboard from './components/copy_email_to_clipboard.vue'; import SidebarLabels from './components/labels/sidebar_labels.vue'; import IssuableLockForm from './components/lock/issuable_lock_form.vue'; @@ -54,9 +56,6 @@ function getSidebarAssigneeAvailabilityData() { function mountAssigneesComponent(mediator) { const el = document.getElementById('js-vue-sidebar-assignees'); - const apolloProvider = new VueApollo({ - defaultClient: createDefaultClient(), - }); if (!el) return; @@ -78,7 +77,9 @@ function mountAssigneesComponent(mediator) { field: el.dataset.field, signedIn: el.hasAttribute('data-signed-in'), issuableType: - isInIssuePage() || isInIncidentPage() || isInDesignPage() ? 'issue' : 'merge_request', + isInIssuePage() || isInIncidentPage() || isInDesignPage() + ? IssuableType.Issue + : IssuableType.MergeRequest, assigneeAvailabilityStatus, }, }), @@ -87,9 +88,6 @@ function mountAssigneesComponent(mediator) { function mountReviewersComponent(mediator) { const el = document.getElementById('js-vue-sidebar-reviewers'); - const apolloProvider = new VueApollo({ - defaultClient: createDefaultClient(), - }); if (!el) return; @@ -121,10 +119,6 @@ export function mountSidebarLabels() { return false; } - const apolloProvider = new VueApollo({ - defaultClient: createDefaultClient(), - }); - return new Vue({ el, apolloProvider, @@ -139,39 +133,71 @@ export function mountSidebarLabels() { }); } -function mountConfidentialComponent(mediator) { +function mountConfidentialComponent() { const el = document.getElementById('js-confidential-entry-point'); + if (!el) { + return; + } const { fullPath, iid } = getSidebarOptions(); - - if (!el) return; - const dataNode = document.getElementById('js-confidential-issue-data'); const initialData = JSON.parse(dataNode.innerHTML); - import(/* webpackChunkName: 'notesStore' */ '~/notes/stores') - .then( - ({ store }) => - new Vue({ - el, - store, - components: { - ConfidentialIssueSidebar, - }, - render: (createElement) => - createElement('confidential-issue-sidebar', { - props: { - iid: String(iid), - fullPath, - isEditable: initialData.is_editable, - service: mediator.service, - }, - }), - }), - ) - .catch(() => { - createFlash({ message: __('Failed to load sidebar confidential toggle') }); - }); + // eslint-disable-next-line no-new + new Vue({ + el, + apolloProvider, + components: { + SidebarConfidentialityWidget, + }, + provide: { + iid: String(iid), + fullPath, + canUpdate: initialData.is_editable, + }, + + render: (createElement) => + createElement('sidebar-confidentiality-widget', { + props: { + issuableType: + isInIssuePage() || isInIncidentPage() || isInDesignPage() + ? IssuableType.Issue + : IssuableType.MergeRequest, + }, + }), + }); +} + +function mountReferenceComponent() { + const el = document.getElementById('js-reference-entry-point'); + if (!el) { + return; + } + + const { fullPath, iid } = getSidebarOptions(); + + // eslint-disable-next-line no-new + new Vue({ + el, + apolloProvider, + components: { + SidebarReferenceWidget, + }, + provide: { + iid: String(iid), + fullPath, + }, + + render: (createElement) => + createElement('sidebar-reference-widget', { + props: { + issuableType: + isInIssuePage() || isInIncidentPage() || isInDesignPage() + ? IssuableType.Issue + : IssuableType.MergeRequest, + }, + }), + }); } function mountLockComponent() { @@ -280,9 +306,6 @@ function mountSeverityComponent() { if (!severityContainerEl) { return false; } - const apolloProvider = new VueApollo({ - defaultClient: createDefaultClient(), - }); const { fullPath, iid, severity } = getSidebarOptions(); @@ -322,6 +345,7 @@ export function mountSidebar(mediator) { mountAssigneesComponent(mediator); mountReviewersComponent(mediator); mountConfidentialComponent(mediator); + mountReferenceComponent(mediator); mountLockComponent(); mountParticipantsComponent(mediator); mountSubscriptionsComponent(mediator); diff --git a/app/assets/javascripts/sidebar/queries/epic_confidential.query.graphql b/app/assets/javascripts/sidebar/queries/epic_confidential.query.graphql new file mode 100644 index 00000000000..7a1fdb40e93 --- /dev/null +++ b/app/assets/javascripts/sidebar/queries/epic_confidential.query.graphql @@ -0,0 +1,10 @@ +query epicConfidential($fullPath: ID!, $iid: ID) { + workspace: group(fullPath: $fullPath) { + __typename + issuable: epic(iid: $iid) { + __typename + id + confidential + } + } +} diff --git a/app/assets/javascripts/sidebar/queries/issue_confidential.query.graphql b/app/assets/javascripts/sidebar/queries/issue_confidential.query.graphql new file mode 100644 index 00000000000..92cabf46af7 --- /dev/null +++ b/app/assets/javascripts/sidebar/queries/issue_confidential.query.graphql @@ -0,0 +1,10 @@ +query issueConfidential($fullPath: ID!, $iid: String) { + workspace: project(fullPath: $fullPath) { + __typename + issuable: issue(iid: $iid) { + __typename + id + confidential + } + } +} diff --git a/app/assets/javascripts/sidebar/queries/issue_reference.query.graphql b/app/assets/javascripts/sidebar/queries/issue_reference.query.graphql new file mode 100644 index 00000000000..db4f58a4f69 --- /dev/null +++ b/app/assets/javascripts/sidebar/queries/issue_reference.query.graphql @@ -0,0 +1,10 @@ +query issueReference($fullPath: ID!, $iid: String) { + workspace: project(fullPath: $fullPath) { + __typename + issuable: issue(iid: $iid) { + __typename + id + reference(full: true) + } + } +} diff --git a/app/assets/javascripts/sidebar/queries/merge_request_reference.query.graphql b/app/assets/javascripts/sidebar/queries/merge_request_reference.query.graphql new file mode 100644 index 00000000000..7979a1ccb3e --- /dev/null +++ b/app/assets/javascripts/sidebar/queries/merge_request_reference.query.graphql @@ -0,0 +1,10 @@ +query mergeRequestReference($fullPath: ID!, $iid: String!) { + workspace: project(fullPath: $fullPath) { + __typename + issuable: mergeRequest(iid: $iid) { + __typename + id + reference(full: true) + } + } +} diff --git a/app/assets/javascripts/sidebar/queries/sidebarDetailsMR.query.graphql b/app/assets/javascripts/sidebar/queries/sidebarDetailsMR.query.graphql new file mode 100644 index 00000000000..02498b18832 --- /dev/null +++ b/app/assets/javascripts/sidebar/queries/sidebarDetailsMR.query.graphql @@ -0,0 +1,7 @@ +query mergeRequestSidebarDetails($fullPath: ID!, $iid: String!) { + project(fullPath: $fullPath) { + mergeRequest(iid: $iid) { + iid # currently unused. + } + } +} diff --git a/app/assets/javascripts/sidebar/queries/update_epic_confidential.mutation.graphql b/app/assets/javascripts/sidebar/queries/update_epic_confidential.mutation.graphql new file mode 100644 index 00000000000..69927ddd205 --- /dev/null +++ b/app/assets/javascripts/sidebar/queries/update_epic_confidential.mutation.graphql @@ -0,0 +1,9 @@ +mutation updateEpic($input: UpdateEpicInput!) { + issuableSetConfidential: updateEpic(input: $input) { + issuable: epic { + id + confidential + } + errors + } +} diff --git a/app/assets/javascripts/sidebar/components/confidential/mutations/update_issue_confidential.mutation.graphql b/app/assets/javascripts/sidebar/queries/update_issue_confidential.mutation.graphql index 5caf5f6b555..8f716c882d6 100644 --- a/app/assets/javascripts/sidebar/components/confidential/mutations/update_issue_confidential.mutation.graphql +++ b/app/assets/javascripts/sidebar/queries/update_issue_confidential.mutation.graphql @@ -1,6 +1,7 @@ mutation updateIssueConfidential($input: IssueSetConfidentialInput!) { - issueSetConfidential(input: $input) { - issue { + issuableSetConfidential: issueSetConfidential(input: $input) { + issuable: issue { + id confidential } errors diff --git a/app/assets/javascripts/sidebar/services/sidebar_service.js b/app/assets/javascripts/sidebar/services/sidebar_service.js index f31e4a3e0dd..88501f2c305 100644 --- a/app/assets/javascripts/sidebar/services/sidebar_service.js +++ b/app/assets/javascripts/sidebar/services/sidebar_service.js @@ -1,8 +1,14 @@ -import sidebarDetailsQuery from 'ee_else_ce/sidebar/queries/sidebarDetails.query.graphql'; +import sidebarDetailsIssueQuery from 'ee_else_ce/sidebar/queries/sidebarDetails.query.graphql'; import { convertToGraphQLId } from '~/graphql_shared/utils'; 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/sidebarDetailsMR.query.graphql'; + +const queries = { + merge_request: sidebarDetailsMRQuery, + issue: sidebarDetailsIssueQuery, +}; export const gqClient = createGqClient( {}, @@ -20,6 +26,7 @@ export default class SidebarService { this.projectsAutocompleteEndpoint = endpointMap.projectsAutocompleteEndpoint; this.fullPath = endpointMap.fullPath; this.iid = endpointMap.iid; + this.issuableType = endpointMap.issuableType; SidebarService.singleton = this; } @@ -31,7 +38,7 @@ export default class SidebarService { return Promise.all([ axios.get(this.endpoint), gqClient.query({ - query: sidebarDetailsQuery, + query: this.sidebarDetailsQuery(), variables: { fullPath: this.fullPath, iid: this.iid.toString(), @@ -40,6 +47,10 @@ export default class SidebarService { ]); } + sidebarDetailsQuery() { + return queries[this.issuableType]; + } + update(key, data) { return axios.put(this.endpoint, { [key]: data }); } diff --git a/app/assets/javascripts/sidebar/sidebar_mediator.js b/app/assets/javascripts/sidebar/sidebar_mediator.js index bd382ed0fdb..3595354da80 100644 --- a/app/assets/javascripts/sidebar/sidebar_mediator.js +++ b/app/assets/javascripts/sidebar/sidebar_mediator.js @@ -22,6 +22,7 @@ export default class SidebarMediator { projectsAutocompleteEndpoint: options.projectsAutocompleteEndpoint, fullPath: options.fullPath, iid: options.iid, + issuableType: options.issuableType, }); SidebarMediator.singleton = this; } |