diff options
Diffstat (limited to 'app/assets/javascripts/sidebar/components')
9 files changed, 463 insertions, 163 deletions
diff --git a/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue b/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue index 6e18cf36690..2a9100f0cb5 100644 --- a/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue +++ b/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue @@ -55,6 +55,7 @@ export default { class="js-sidebar-dropdown-toggle edit-link btn gl-text-gray-900! gl-ml-auto hide-collapsed btn-default btn-sm gl-button btn-default-tertiary float-right" href="#" data-test-id="edit-link" + data-qa-selector="edit_link" data-track-action="click_edit_button" data-track-label="right_sidebar" data-track-property="assignee" diff --git a/app/assets/javascripts/sidebar/components/assignees/assignees.vue b/app/assets/javascripts/sidebar/components/assignees/assignees.vue index 29ea390a81d..cf07752a0b8 100644 --- a/app/assets/javascripts/sidebar/components/assignees/assignees.vue +++ b/app/assets/javascripts/sidebar/components/assignees/assignees.vue @@ -56,6 +56,7 @@ export default { type="button" class="gl-button btn-link gl-reset-color!" data-testid="assign-yourself" + data-qa-selector="assign_yourself_button" @click="assignSelf" > {{ __('assign yourself') }} 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 0e4d4c74160..d83ae782e26 100644 --- a/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue +++ b/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue @@ -91,6 +91,7 @@ export default { <div class="gl-ml-3 gl-line-height-normal gl-display-grid gl-align-items-center" data-testid="username" + data-qa-selector="username" > <user-name-with-status :name="user.name" :availability="userAvailability(user)" /> </div> diff --git a/app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue b/app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue index 98468583992..c262d65f6ce 100644 --- a/app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue +++ b/app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue @@ -170,7 +170,7 @@ export default { this.$emit('closeForm'); }, openDatePicker() { - this.$refs.datePicker.calendar.show(); + this.$refs.datePicker.show(); }, setFixedDate(isFixed) { const date = this.issuable[dateFields[this.dateType].dateFixed]; diff --git a/app/assets/javascripts/sidebar/components/milestone/milestone_dropdown.vue b/app/assets/javascripts/sidebar/components/milestone/milestone_dropdown.vue new file mode 100644 index 00000000000..1fff089eab4 --- /dev/null +++ b/app/assets/javascripts/sidebar/components/milestone/milestone_dropdown.vue @@ -0,0 +1,115 @@ +<script> +import { GlDropdownItem } from '@gitlab/ui'; +import { TYPE_MILESTONE } from '~/graphql_shared/constants'; +import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils'; +import { IssuableType, WorkspaceType } from '~/issues/constants'; +import { __ } from '~/locale'; +import { IssuableAttributeType } from '../../constants'; +import SidebarDropdown from '../sidebar_dropdown.vue'; + +const noMilestone = { + id: 0, + title: __('No milestone'), +}; + +const placeholderMilestone = { + id: -1, + title: __('Select milestone'), +}; + +export default { + issuableAttribute: IssuableAttributeType.Milestone, + components: { + GlDropdownItem, + SidebarDropdown, + }, + props: { + attrWorkspacePath: { + required: true, + type: String, + }, + canAdminMilestone: { + type: Boolean, + required: false, + default: false, + }, + issuableType: { + type: String, + required: true, + validator(value) { + return [IssuableType.Issue, IssuableType.MergeRequest].includes(value); + }, + }, + inputName: { + type: String, + required: false, + default: 'update[milestone_id]', + }, + milestoneId: { + type: String, + required: false, + default: '', + }, + milestoneTitle: { + type: String, + required: false, + default: '', + }, + projectMilestonesPath: { + type: String, + required: false, + default: '', + }, + workspaceType: { + type: String, + required: true, + validator(value) { + return [WorkspaceType.group, WorkspaceType.project].includes(value); + }, + }, + }, + data() { + return { + milestone: this.milestoneId + ? { id: convertToGraphQLId(TYPE_MILESTONE, this.milestoneId), title: this.milestoneTitle } + : placeholderMilestone, + }; + }, + computed: { + footerItemText() { + return this.canAdminMilestone ? __('Manage milestones') : __('View milestones'); + }, + value() { + return this.milestone.id === placeholderMilestone.id + ? undefined + : getIdFromGraphQLId(this.milestone.id); + }, + }, + methods: { + handleChange(milestone) { + this.milestone = milestone.id === null ? noMilestone : milestone; + }, + }, +}; +</script> + +<template> + <div> + <input type="hidden" :name="inputName" :value="value" /> + <sidebar-dropdown + :attr-workspace-path="attrWorkspacePath" + :current-attribute="milestone" + :issuable-attribute="$options.issuableAttribute" + :issuable-type="issuableType" + :workspace-type="workspaceType" + data-qa-selector="issuable_milestone_dropdown" + @change="handleChange" + > + <template #footer> + <gl-dropdown-item v-if="projectMilestonesPath" :href="projectMilestonesPath"> + {{ footerItemText }} + </gl-dropdown-item> + </template> + </sidebar-dropdown> + </div> +</template> diff --git a/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue b/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue index ad061dd2e6b..5f1350690eb 100644 --- a/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue +++ b/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue @@ -9,6 +9,8 @@ import eventHub from '~/sidebar/event_hub'; import Store from '~/sidebar/stores/sidebar_store'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import getMergeRequestReviewersQuery from '~/vue_shared/components/sidebar/queries/get_merge_request_reviewers.query.graphql'; +import mergeRequestReviewersUpdatedSubscription from '~/vue_shared/components/sidebar/queries/merge_request_reviewers.subscription.graphql'; +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import ReviewerTitle from './reviewer_title.vue'; import Reviewers from './reviewers.vue'; @@ -66,6 +68,36 @@ export default { error() { createAlert({ message: __('An error occurred while fetching reviewers.') }); }, + subscribeToMore: { + document() { + return mergeRequestReviewersUpdatedSubscription; + }, + variables() { + return { + issuableId: this.issuable?.id, + }; + }, + skip() { + return !this.issuable?.id || !this.isRealtimeEnabled; + }, + updateQuery( + _, + { + subscriptionData: { + data: { mergeRequestReviewersUpdated }, + }, + }, + ) { + if (mergeRequestReviewersUpdated) { + this.store.setReviewersFromRealtime( + mergeRequestReviewersUpdated.reviewers.nodes.map((r) => ({ + ...r, + id: getIdFromGraphQLId(r.id), + })), + ); + } + }, + }, }, }, data() { @@ -87,6 +119,9 @@ export default { canUpdate() { return this.issuable.userPermissions?.adminMergeRequest || false; }, + isRealtimeEnabled() { + return this.glFeatures.realtimeReviewers; + }, }, created() { this.store = new Store(); diff --git a/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers_inputs.vue b/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers_inputs.vue new file mode 100644 index 00000000000..a135dfdca72 --- /dev/null +++ b/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers_inputs.vue @@ -0,0 +1,34 @@ +<script> +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; +import { state } from './sidebar_reviewers.vue'; + +export default { + data() { + return state; + }, + computed: { + reviewers() { + return this.issuable?.reviewers?.nodes || []; + }, + }, + methods: { + getIdFromGraphQLId, + }, +}; +</script> + +<template> + <div> + <input + v-for="reviewer in reviewers" + :key="reviewer.id" + type="hidden" + name="merge_request[reviewer_ids][]" + :value="getIdFromGraphQLId(reviewer.id)" + :data-avatar-url="reviewer.avatarUrl" + :data-name="reviewer.name" + :data-username="reviewer.username" + :data-can-merge="reviewer.mergeRequestInteraction.canMerge" + /> + </div> +</template> diff --git a/app/assets/javascripts/sidebar/components/sidebar_dropdown.vue b/app/assets/javascripts/sidebar/components/sidebar_dropdown.vue new file mode 100644 index 00000000000..26e2bc96f54 --- /dev/null +++ b/app/assets/javascripts/sidebar/components/sidebar_dropdown.vue @@ -0,0 +1,252 @@ +<script> +import { + GlDropdown, + GlDropdownDivider, + GlDropdownItem, + GlDropdownText, + GlLoadingIcon, + GlSearchBoxByType, +} from '@gitlab/ui'; +import { kebabCase, snakeCase } from 'lodash'; +import { IssuableType, WorkspaceType } from '~/issues/constants'; +import { __ } from '~/locale'; +import { + defaultEpicSort, + dropdowni18nText, + epicIidPattern, + issuableAttributesQueries, + IssuableAttributeState, + IssuableAttributeType, + IssuableAttributeTypeKeyMap, + LocalizedIssuableAttributeType, + noAttributeId, +} from 'ee_else_ce/sidebar/constants'; +import { createAlert } from '~/flash'; +import { PathIdSeparator } from '~/related_issues/constants'; + +export default { + noAttributeId, + i18n: { + expired: __('(expired)'), + }, + components: { + GlDropdown, + GlDropdownItem, + GlDropdownText, + GlDropdownDivider, + GlSearchBoxByType, + GlLoadingIcon, + }, + inject: { + issuableAttributesQueries: { + default: issuableAttributesQueries, + }, + issuableAttributesState: { + default: IssuableAttributeState, + }, + widgetTitleText: { + default: { + [IssuableAttributeType.Milestone]: __('Milestone'), + expired: __('(expired)'), + none: __('None'), + }, + }, + }, + props: { + attrWorkspacePath: { + required: true, + type: String, + }, + currentAttribute: { + type: Object, + required: false, + default: () => ({}), + }, + issuableAttribute: { + type: String, + required: true, + }, + issuableType: { + type: String, + required: true, + validator(value) { + return [IssuableType.Issue, IssuableType.MergeRequest].includes(value); + }, + }, + workspaceType: { + type: String, + required: false, + default: WorkspaceType.project, + validator(value) { + return [WorkspaceType.group, WorkspaceType.project].includes(value); + }, + }, + }, + data() { + return { + attributesList: [], + searchTerm: '', + skipQuery: true, + }; + }, + apollo: { + attributesList: { + query() { + const { list } = this.issuableAttributeQuery; + const { query } = list[this.issuableType]; + return query[this.workspaceType] || query; + }, + variables() { + if (!this.isEpic) { + return { + fullPath: this.attrWorkspacePath, + title: this.searchTerm, + state: this.issuableAttributesState[this.issuableAttribute], + }; + } + + const variables = { + fullPath: this.attrWorkspacePath, + state: this.issuableAttributesState[this.issuableAttribute], + sort: defaultEpicSort, + }; + + if (epicIidPattern.test(this.searchTerm)) { + const matches = this.searchTerm.match(epicIidPattern); + variables.iidStartsWith = matches.groups.iid; + } else if (this.searchTerm !== '') { + variables.in = 'TITLE'; + variables.title = this.searchTerm; + } + + return variables; + }, + update: (data) => data?.workspace?.attributes?.nodes ?? [], + error(error) { + createAlert({ message: this.i18n.listFetchError, captureError: true, error }); + }, + skip() { + if ( + this.isEpic && + this.searchTerm.startsWith(PathIdSeparator.Epic) && + this.searchTerm.length < 2 + ) { + return true; + } + return this.skipQuery; + }, + debounce: 250, + }, + }, + computed: { + attributeTypeTitle() { + return this.widgetTitleText[this.issuableAttribute]; + }, + dropdownText() { + return this.currentAttribute ? this.currentAttribute?.title : this.attributeTypeTitle; + }, + emptyPropsList() { + return this.attributesList.length === 0; + }, + i18n() { + const localizedAttribute = + LocalizedIssuableAttributeType[IssuableAttributeTypeKeyMap[this.issuableAttribute]]; + return dropdowni18nText(localizedAttribute, this.issuableType); + }, + isEpic() { + // MV to EE https://gitlab.com/gitlab-org/gitlab/-/issues/345311 + return this.issuableAttribute === IssuableType.Epic; + }, + issuableAttributeQuery() { + return this.issuableAttributesQueries[this.issuableAttribute]; + }, + formatIssuableAttribute() { + return { + kebab: kebabCase(this.issuableAttribute), + snake: snakeCase(this.issuableAttribute), + }; + }, + }, + methods: { + isAttributeChecked(attributeId) { + return ( + attributeId === this.currentAttribute?.id || (!this.currentAttribute?.id && !attributeId) + ); + }, + isAttributeOverdue(attribute) { + return this.issuableAttribute === IssuableAttributeType.Milestone + ? attribute?.expired + : false; + }, + handleShow() { + this.skipQuery = false; + }, + setFocus() { + this.$refs.search.focusInput(); + }, + show() { + this.$refs.dropdown.show(); + }, + updateAttribute(attribute) { + this.$emit('change', attribute); + }, + }, +}; +</script> + +<template> + <gl-dropdown + ref="dropdown" + block + :header-text="i18n.assignAttribute" + lazy + :text="dropdownText" + toggle-class="gl-m-0" + @show="handleShow" + @shown="setFocus" + > + <gl-search-box-by-type ref="search" v-model="searchTerm" :placeholder="__('Search')" /> + <gl-dropdown-item + :data-testid="`no-${formatIssuableAttribute.kebab}-item`" + is-check-item + :is-checked="isAttributeChecked($options.noAttributeId)" + @click="$emit('change', { id: $options.noAttributeId })" + > + {{ i18n.noAttribute }} + </gl-dropdown-item> + <gl-dropdown-divider /> + <gl-loading-icon + v-if="$apollo.queries.attributesList.loading" + size="sm" + class="gl-py-4" + data-testid="loading-icon-dropdown" + /> + <template v-else> + <gl-dropdown-text v-if="emptyPropsList"> + {{ i18n.noAttributesFound }} + </gl-dropdown-text> + <slot + v-else + name="list" + :attributes-list="attributesList" + :is-attribute-checked="isAttributeChecked" + :update-attribute="updateAttribute" + > + <gl-dropdown-item + v-for="attrItem in attributesList" + :key="attrItem.id" + is-check-item + :is-checked="isAttributeChecked(attrItem.id)" + :data-testid="`${formatIssuableAttribute.kebab}-items`" + @click="updateAttribute(attrItem)" + > + {{ attrItem.title }} + <template v-if="isAttributeOverdue(attrItem)">{{ $options.i18n.expired }}</template> + </gl-dropdown-item> + </slot> + </template> + <template #footer> + <slot name="footer"></slot> + </template> + </gl-dropdown> +</template> diff --git a/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue b/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue index c33b1468ca4..a685929cdea 100644 --- a/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue +++ b/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue @@ -1,17 +1,5 @@ <script> -import { - GlLink, - GlDropdown, - GlDropdownItem, - GlDropdownText, - GlSearchBoxByType, - GlDropdownDivider, - GlLoadingIcon, - GlIcon, - GlTooltipDirective, - GlPopover, - GlButton, -} from '@gitlab/ui'; +import { GlButton, GlIcon, GlLink, GlPopover, GlTooltipDirective } from '@gitlab/ui'; import { kebabCase, snakeCase } from 'lodash'; import { createAlert } from '~/flash'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; @@ -22,19 +10,15 @@ import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue' import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { dropdowni18nText, - Tracking, - IssuableAttributeState, - IssuableAttributeType, LocalizedIssuableAttributeType, IssuableAttributeTypeKeyMap, issuableAttributesQueries, - noAttributeId, - defaultEpicSort, - epicIidPattern, + IssuableAttributeType, + Tracking, } from 'ee_else_ce/sidebar/constants'; +import SidebarDropdown from './sidebar_dropdown.vue'; export default { - noAttributeId, i18n: { expired: __('(expired)'), none: __('None'), @@ -43,17 +27,12 @@ export default { GlTooltip: GlTooltipDirective, }, components: { - SidebarEditableItem, GlLink, - GlDropdown, - GlDropdownItem, - GlDropdownText, - GlDropdownDivider, - GlSearchBoxByType, GlIcon, - GlLoadingIcon, GlPopover, GlButton, + SidebarDropdown, + SidebarEditableItem, }, mixins: [glFeatureFlagMixin()], inject: { @@ -63,9 +42,6 @@ export default { issuableAttributesQueries: { default: issuableAttributesQueries, }, - issuableAttributesState: { - default: IssuableAttributeState, - }, widgetTitleText: { default: { [IssuableAttributeType.Milestone]: __('Milestone'), @@ -74,7 +50,6 @@ export default { }, }, }, - props: { issuableAttribute: { type: String, @@ -134,67 +109,14 @@ export default { }); }, }, - attributesList: { - query() { - const { list } = this.issuableAttributeQuery; - const { query } = list[this.issuableType]; - - return query; - }, - skip() { - if (this.isEpic && this.searchTerm.startsWith('&') && this.searchTerm.length < 2) { - return true; - } - - return !this.editing; - }, - debounce: 250, - variables() { - if (!this.isEpic) { - return { - fullPath: this.attrWorkspacePath, - title: this.searchTerm, - state: this.issuableAttributesState[this.issuableAttribute], - }; - } - - const variables = { - fullPath: this.attrWorkspacePath, - state: this.issuableAttributesState[this.issuableAttribute], - sort: defaultEpicSort, - }; - - if (epicIidPattern.test(this.searchTerm)) { - const matches = this.searchTerm.match(epicIidPattern); - variables.iidStartsWith = matches.groups.iid; - } else if (this.searchTerm !== '') { - variables.in = 'TITLE'; - variables.title = this.searchTerm; - } - - return variables; - }, - update(data) { - if (data?.workspace) { - return data?.workspace?.attributes.nodes; - } - return []; - }, - error(error) { - createAlert({ message: this.i18n.listFetchError, captureError: true, error }); - }, - }, }, data() { return { - searchTerm: '', - editing: false, updating: false, selectedTitle: null, currentAttribute: null, hasCurrentAttribute: false, editConfirmation: false, - attributesList: [], tracking: { event: Tracking.editEvent, label: Tracking.rightSidebarLabel, @@ -212,15 +134,9 @@ export default { attributeUrl() { return this.currentAttribute?.webUrl; }, - dropdownText() { - return this.currentAttribute ? this.currentAttribute?.title : this.attributeTypeTitle; - }, loading() { return this.$apollo.queries.currentAttribute.loading; }, - emptyPropsList() { - return this.attributesList.length === 0; - }, attributeTypeTitle() { return this.widgetTitleText[this.issuableAttribute]; }, @@ -256,16 +172,12 @@ export default { }, }, methods: { - updateAttribute(attributeId) { - if (this.currentAttribute === null && attributeId === null) return; - if (attributeId === this.currentAttribute?.id) return; + updateAttribute({ id }) { + if (this.currentAttribute === null && id === null) return; + if (id === this.currentAttribute?.id) return; this.updating = true; - const selectedAttribute = - Boolean(attributeId) && this.attributesList.find((p) => p.id === attributeId); - this.selectedTitle = selectedAttribute ? selectedAttribute.title : this.widgetTitleText.none; - const { current } = this.issuableAttributeQuery; const { mutation } = current[this.issuableType]; @@ -277,8 +189,8 @@ export default { attributeId: this.issuableAttribute === IssuableAttributeType.Milestone && this.issuableType === IssuableType.Issue - ? getIdFromGraphQLId(attributeId) - : attributeId, + ? getIdFromGraphQLId(id) + : id, iid: this.iid, }, }) @@ -298,32 +210,16 @@ export default { }) .finally(() => { this.updating = false; - this.searchTerm = ''; this.selectedTitle = null; }); }, - isAttributeChecked(attributeId = undefined) { - return ( - attributeId === this.currentAttribute?.id || (!this.currentAttribute?.id && !attributeId) - ); - }, isAttributeOverdue(attribute) { return this.issuableAttribute === IssuableAttributeType.Milestone ? attribute?.expired : false; }, showDropdown() { - this.$refs.newDropdown.show(); - }, - handleOpen() { - this.editing = true; - this.showDropdown(); - }, - handleClose() { - this.editing = false; - }, - setFocus() { - this.$refs.search.focusInput(); + this.$refs.dropdown.show(); }, handlePopoverClose() { this.$refs.popover.$emit('close'); @@ -349,8 +245,7 @@ export default { :tracking="tracking" :should-show-confirmation-popover="shouldShowConfirmationPopover" :loading="updating || loading" - @open="handleOpen" - @close="handleClose" + @open="showDropdown" @edit-confirm="handleEditConfirmation" > <template #collapsed> @@ -432,58 +327,24 @@ export default { </gl-popover> </template> <template v-else #default> - <gl-dropdown - ref="newDropdown" - lazy - :header-text="i18n.assignAttribute" - :text="dropdownText" - :loading="loading" - class="gl-w-full" - toggle-class="gl-max-w-100" - block - @shown="setFocus" + <sidebar-dropdown + ref="dropdown" + :attr-workspace-path="attrWorkspacePath" + :current-attribute="currentAttribute" + :issuable-attribute="issuableAttribute" + :issuable-type="issuableType" + @change="updateAttribute" > - <gl-search-box-by-type ref="search" v-model="searchTerm" /> - <gl-dropdown-item - :data-testid="`no-${formatIssuableAttribute.kebab}-item`" - is-check-item - :is-checked="isAttributeChecked($options.noAttributeId)" - @click="updateAttribute($options.noAttributeId)" - > - {{ i18n.noAttribute }} - </gl-dropdown-item> - <gl-dropdown-divider /> - <gl-loading-icon - v-if="$apollo.queries.attributesList.loading" - size="sm" - class="gl-py-4" - data-testid="loading-icon-dropdown" - /> - <template v-else> - <gl-dropdown-text v-if="emptyPropsList"> - {{ i18n.noAttributesFound }} - </gl-dropdown-text> + <template #list="{ attributesList, isAttributeChecked, updateAttribute: update }"> <slot - v-else name="list" :attributes-list="attributesList" :is-attribute-checked="isAttributeChecked" - :update-attribute="updateAttribute" + :update-attribute="update" > - <gl-dropdown-item - v-for="attrItem in attributesList" - :key="attrItem.id" - is-check-item - :is-checked="isAttributeChecked(attrItem.id)" - :data-testid="`${formatIssuableAttribute.kebab}-items`" - @click="updateAttribute(attrItem.id)" - > - {{ attrItem.title }} - <span v-if="isAttributeOverdue(attrItem)">{{ $options.i18n.expired }}</span> - </gl-dropdown-item> </slot> </template> - </gl-dropdown> + </sidebar-dropdown> </template> </sidebar-editable-item> </template> |