Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
		
							parent
							
								
									e5c31c104e
								
							
						
					
					
						commit
						917d93d86d
					
				|  | @ -67,7 +67,7 @@ review-build-cng: | ||||||
|     GITLAB_IMAGE_REPOSITORY: "registry.gitlab.com/gitlab-org/build/cng-mirror" |     GITLAB_IMAGE_REPOSITORY: "registry.gitlab.com/gitlab-org/build/cng-mirror" | ||||||
|     GITLAB_IMAGE_SUFFIX: "ee" |     GITLAB_IMAGE_SUFFIX: "ee" | ||||||
|     GITLAB_REVIEW_APP_BASE_CONFIG_FILE: "scripts/review_apps/base-config.yaml" |     GITLAB_REVIEW_APP_BASE_CONFIG_FILE: "scripts/review_apps/base-config.yaml" | ||||||
|     GITLAB_HELM_CHART_REF: "eace227d3465e17e37b1a2e3764dd244c8e2d716"  # 7.6.1: https://gitlab.com/gitlab-org/charts/gitlab/-/commit/eace227d3465e17e37b1a2e3764dd244c8e2d716 |     GITLAB_HELM_CHART_REF: "c91feed6983b24a1b0dbacaf5050ca5c59af3d46"  # 7.8.0: https://gitlab.com/gitlab-org/charts/gitlab/-/commit/c91feed6983b24a1b0dbacaf5050ca5c59af3d46 | ||||||
|   environment: |   environment: | ||||||
|     name: review/${CI_COMMIT_REF_SLUG}${SCHEDULE_TYPE}  # No separator for SCHEDULE_TYPE so it's compatible as before and looks nice without it |     name: review/${CI_COMMIT_REF_SLUG}${SCHEDULE_TYPE}  # No separator for SCHEDULE_TYPE so it's compatible as before and looks nice without it | ||||||
|     url: https://gitlab-${CI_ENVIRONMENT_SLUG}.${REVIEW_APPS_DOMAIN} |     url: https://gitlab-${CI_ENVIRONMENT_SLUG}.${REVIEW_APPS_DOMAIN} | ||||||
|  |  | ||||||
|  | @ -25,6 +25,7 @@ const PERSISTENT_USER_CALLOUTS = [ | ||||||
|   '.js-new-nav-for-everyone-callout', |   '.js-new-nav-for-everyone-callout', | ||||||
|   '.js-namespace-over-storage-users-combined-alert', |   '.js-namespace-over-storage-users-combined-alert', | ||||||
|   '.js-code-suggestions-ga-alert', |   '.js-code-suggestions-ga-alert', | ||||||
|  |   '.js-joining-a-project-alert', | ||||||
| ]; | ]; | ||||||
| 
 | 
 | ||||||
| const initCallouts = () => { | const initCallouts = () => { | ||||||
|  |  | ||||||
|  | @ -39,7 +39,7 @@ export default { | ||||||
|       default: () => [], |       default: () => [], | ||||||
|     }, |     }, | ||||||
|     itemValue: { |     itemValue: { | ||||||
|       type: Object, |       type: [Array, String], | ||||||
|       required: false, |       required: false, | ||||||
|       default: null, |       default: null, | ||||||
|     }, |     }, | ||||||
|  | @ -68,30 +68,40 @@ export default { | ||||||
|       required: false, |       required: false, | ||||||
|       default: '', |       default: '', | ||||||
|     }, |     }, | ||||||
|  |     multiSelect: { | ||||||
|  |       type: Boolean, | ||||||
|  |       required: false, | ||||||
|  |       default: false, | ||||||
|  |     }, | ||||||
|  |     showFooter: { | ||||||
|  |       type: Boolean, | ||||||
|  |       required: false, | ||||||
|  |       default: false, | ||||||
|  |     }, | ||||||
|  |     infiniteScroll: { | ||||||
|  |       type: Boolean, | ||||||
|  |       required: false, | ||||||
|  |       default: false, | ||||||
|  |     }, | ||||||
|  |     infiniteScrollLoading: { | ||||||
|  |       type: Boolean, | ||||||
|  |       required: false, | ||||||
|  |       default: false, | ||||||
|  |     }, | ||||||
|   }, |   }, | ||||||
|   data() { |   data() { | ||||||
|     return { |     return { | ||||||
|       isEditing: false, |       isEditing: false, | ||||||
|       localSelectedItem: this.itemValue?.id, |       localSelectedItem: this.itemValue, | ||||||
|     }; |     }; | ||||||
|   }, |   }, | ||||||
|   computed: { |   computed: { | ||||||
|     hasValue() { |     hasValue() { | ||||||
|       return this.itemValue != null || !isEmpty(this.item); |       return this.multiSelect ? !isEmpty(this.itemValue) : this.itemValue !== null; | ||||||
|     }, |  | ||||||
|     listboxText() { |  | ||||||
|       return ( |  | ||||||
|         this.listItems.find(({ value }) => this.localSelectedItem === value)?.text || |  | ||||||
|         this.itemValue?.title || |  | ||||||
|         this.$options.i18n.none |  | ||||||
|       ); |  | ||||||
|     }, |     }, | ||||||
|     inputId() { |     inputId() { | ||||||
|       return `work-item-dropdown-listbox-value-${this.dropdownName}`; |       return `work-item-dropdown-listbox-value-${this.dropdownName}`; | ||||||
|     }, |     }, | ||||||
|     toggleText() { |  | ||||||
|       return this.toggleDropdownText || this.listboxText; |  | ||||||
|     }, |  | ||||||
|     resetButton() { |     resetButton() { | ||||||
|       return this.resetButtonLabel || this.$options.i18n.resetButtonText; |       return this.resetButtonLabel || this.$options.i18n.resetButtonText; | ||||||
|     }, |     }, | ||||||
|  | @ -100,7 +110,7 @@ export default { | ||||||
|     itemValue: { |     itemValue: { | ||||||
|       handler(newVal) { |       handler(newVal) { | ||||||
|         if (!this.isEditing) { |         if (!this.isEditing) { | ||||||
|           this.localSelectedItem = newVal?.id; |           this.localSelectedItem = newVal; | ||||||
|         } |         } | ||||||
|       }, |       }, | ||||||
|     }, |     }, | ||||||
|  | @ -114,18 +124,25 @@ export default { | ||||||
|     }, |     }, | ||||||
|     handleItemClick(item) { |     handleItemClick(item) { | ||||||
|       this.localSelectedItem = item; |       this.localSelectedItem = item; | ||||||
|  |       if (!this.multiSelect) { | ||||||
|         this.$emit('updateValue', item); |         this.$emit('updateValue', item); | ||||||
|  |       } else { | ||||||
|  |         this.$emit('updateSelected', this.localSelectedItem); | ||||||
|  |       } | ||||||
|     }, |     }, | ||||||
|     onListboxShown() { |     onListboxShown() { | ||||||
|       this.$emit('dropdownShown'); |       this.$emit('dropdownShown'); | ||||||
|     }, |     }, | ||||||
|     onListboxHide() { |     onListboxHide() { | ||||||
|       this.isEditing = false; |       this.isEditing = false; | ||||||
|  |       if (this.multiSelect) { | ||||||
|  |         this.$emit('updateValue', this.localSelectedItem); | ||||||
|  |       } | ||||||
|     }, |     }, | ||||||
|     unassignValue() { |     unassignValue() { | ||||||
|       this.localSelectedItem = null; |       this.localSelectedItem = this.multiSelect ? [] : null; | ||||||
|       this.isEditing = false; |       this.isEditing = false; | ||||||
|       this.$emit('updateValue', null); |       this.$emit('updateValue', this.localSelectedItem); | ||||||
|     }, |     }, | ||||||
|   }, |   }, | ||||||
| }; | }; | ||||||
|  | @ -165,34 +182,42 @@ export default { | ||||||
|       </div> |       </div> | ||||||
|       <gl-collapsible-listbox |       <gl-collapsible-listbox | ||||||
|         :id="inputId" |         :id="inputId" | ||||||
|  |         :multiple="multiSelect" | ||||||
|         block |         block | ||||||
|         searchable |         searchable | ||||||
|         start-opened |         start-opened | ||||||
|         is-check-centered |         is-check-centered | ||||||
|         fluid-width |         fluid-width | ||||||
|  |         :infinite-scroll="infiniteScroll" | ||||||
|         :searching="loading" |         :searching="loading" | ||||||
|         :header-text="headerText" |         :header-text="headerText" | ||||||
|         :toggle-text="toggleText" |         :toggle-text="toggleDropdownText" | ||||||
|         :no-results-text="$options.i18n.noMatchingResults" |         :no-results-text="$options.i18n.noMatchingResults" | ||||||
|         :items="listItems" |         :items="listItems" | ||||||
|         :selected="localSelectedItem" |         :selected="localSelectedItem" | ||||||
|         :reset-button-label="resetButton" |         :reset-button-label="resetButton" | ||||||
|  |         :infinite-scroll-loading="infiniteScrollLoading" | ||||||
|         @reset="unassignValue" |         @reset="unassignValue" | ||||||
|         @search="debouncedSearchKeyUpdate" |         @search="debouncedSearchKeyUpdate" | ||||||
|         @select="handleItemClick" |         @select="handleItemClick" | ||||||
|         @shown="onListboxShown" |         @shown="onListboxShown" | ||||||
|         @hidden="onListboxHide" |         @hidden="onListboxHide" | ||||||
|  |         @bottom-reached="$emit('bottomReached')" | ||||||
|       > |       > | ||||||
|         <template #list-item="{ item }"> |         <template #list-item="{ item }"> | ||||||
|           <slot name="list-item" :item="item">{{ item.text }}</slot> |           <slot name="list-item" :item="item">{{ item.text }}</slot> | ||||||
|         </template> |         </template> | ||||||
|       </gl-collapsible-listbox> |         <template v-if="showFooter" #footer> | ||||||
|     </gl-form> |           <div class="gl-border-t-solid gl-border-t-1 gl-border-t-gray-200 gl-p-2!"> | ||||||
|     <slot v-else-if="hasValue" name="readonly"> |             <slot name="footer"></slot> | ||||||
|       {{ listboxText }} |           </div> | ||||||
|     </slot> |         </template> | ||||||
|     <div v-else class="gl-text-secondary"> |       </gl-collapsible-listbox> | ||||||
|       {{ $options.i18n.none }} |       {{ hasValue }} | ||||||
|     </div> |     </gl-form> | ||||||
|  |     <slot v-else-if="hasValue" name="readonly"></slot> | ||||||
|  |     <slot v-else class="gl-text-secondary" name="none"> | ||||||
|  |       {{ $options.i18n.none }} | ||||||
|  |     </slot> | ||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
|  | @ -0,0 +1,292 @@ | ||||||
|  | <script> | ||||||
|  | import { GlButton } from '@gitlab/ui'; | ||||||
|  | import { isEmpty } from 'lodash'; | ||||||
|  | import currentUserQuery from '~/graphql_shared/queries/current_user.query.graphql'; | ||||||
|  | import groupUsersSearchQuery from '~/graphql_shared/queries/group_users_search.query.graphql'; | ||||||
|  | import usersSearchQuery from '~/graphql_shared/queries/users_search.query.graphql'; | ||||||
|  | import InviteMembersTrigger from '~/invite_members/components/invite_members_trigger.vue'; | ||||||
|  | import SidebarParticipant from '~/sidebar/components/assignees/sidebar_participant.vue'; | ||||||
|  | import UncollapsedAssigneeList from '~/sidebar/components/assignees/uncollapsed_assignee_list.vue'; | ||||||
|  | import WorkItemSidebarDropdownWidgetWithEdit from '~/work_items/components/shared/work_item_sidebar_dropdown_widget_with_edit.vue'; | ||||||
|  | import { s__, sprintf, __ } from '~/locale'; | ||||||
|  | import Tracking from '~/tracking'; | ||||||
|  | import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql'; | ||||||
|  | import { i18n, TRACKING_CATEGORY_SHOW, DEFAULT_PAGE_SIZE_ASSIGNEES } from '../constants'; | ||||||
|  | 
 | ||||||
|  | export default { | ||||||
|  |   components: { | ||||||
|  |     WorkItemSidebarDropdownWidgetWithEdit, | ||||||
|  |     InviteMembersTrigger, | ||||||
|  |     SidebarParticipant, | ||||||
|  |     GlButton, | ||||||
|  |     UncollapsedAssigneeList, | ||||||
|  |   }, | ||||||
|  |   mixins: [Tracking.mixin()], | ||||||
|  |   inject: ['isGroup'], | ||||||
|  |   props: { | ||||||
|  |     fullPath: { | ||||||
|  |       type: String, | ||||||
|  |       required: true, | ||||||
|  |     }, | ||||||
|  |     workItemId: { | ||||||
|  |       type: String, | ||||||
|  |       required: true, | ||||||
|  |     }, | ||||||
|  |     assignees: { | ||||||
|  |       type: Array, | ||||||
|  |       required: true, | ||||||
|  |     }, | ||||||
|  |     allowsMultipleAssignees: { | ||||||
|  |       type: Boolean, | ||||||
|  |       required: true, | ||||||
|  |     }, | ||||||
|  |     workItemType: { | ||||||
|  |       type: String, | ||||||
|  |       required: true, | ||||||
|  |     }, | ||||||
|  |     canUpdate: { | ||||||
|  |       type: Boolean, | ||||||
|  |       required: false, | ||||||
|  |       default: false, | ||||||
|  |     }, | ||||||
|  |     canInviteMembers: { | ||||||
|  |       type: Boolean, | ||||||
|  |       required: false, | ||||||
|  |       default: false, | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  |   data() { | ||||||
|  |     return { | ||||||
|  |       localAssigneeIds: this.assignees.map(({ id }) => id), | ||||||
|  |       searchStarted: false, | ||||||
|  |       searchKey: '', | ||||||
|  |       users: { | ||||||
|  |         nodes: [], | ||||||
|  |       }, | ||||||
|  |       currentUser: null, | ||||||
|  |       isLoadingMore: false, | ||||||
|  |       updateInProgress: false, | ||||||
|  |     }; | ||||||
|  |   }, | ||||||
|  |   apollo: { | ||||||
|  |     users: { | ||||||
|  |       query() { | ||||||
|  |         return this.isGroup ? groupUsersSearchQuery : usersSearchQuery; | ||||||
|  |       }, | ||||||
|  |       variables() { | ||||||
|  |         return { | ||||||
|  |           fullPath: this.fullPath, | ||||||
|  |           search: this.searchKey, | ||||||
|  |           first: DEFAULT_PAGE_SIZE_ASSIGNEES, | ||||||
|  |         }; | ||||||
|  |       }, | ||||||
|  |       skip() { | ||||||
|  |         return !this.searchStarted; | ||||||
|  |       }, | ||||||
|  |       update(data) { | ||||||
|  |         return data.workspace?.users; | ||||||
|  |       }, | ||||||
|  |       error() { | ||||||
|  |         this.$emit('error', i18n.fetchError); | ||||||
|  |       }, | ||||||
|  |     }, | ||||||
|  |     currentUser: { | ||||||
|  |       query: currentUserQuery, | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  |   computed: { | ||||||
|  |     searchUsers() { | ||||||
|  |       return this.users.nodes.map(({ user }) => ({ | ||||||
|  |         ...user, | ||||||
|  |         value: user.id, | ||||||
|  |         text: user.name, | ||||||
|  |       })); | ||||||
|  |     }, | ||||||
|  |     pageInfo() { | ||||||
|  |       return this.users.pageInfo; | ||||||
|  |     }, | ||||||
|  |     tracking() { | ||||||
|  |       return { | ||||||
|  |         category: TRACKING_CATEGORY_SHOW, | ||||||
|  |         label: 'item_assignees', | ||||||
|  |         property: `type_${this.workItemType}`, | ||||||
|  |       }; | ||||||
|  |     }, | ||||||
|  |     isLoadingUsers() { | ||||||
|  |       return this.$apollo.queries.users.loading && !this.isLoadingMore; | ||||||
|  |     }, | ||||||
|  |     hasNextPage() { | ||||||
|  |       return this.pageInfo?.hasNextPage; | ||||||
|  |     }, | ||||||
|  |     selectedAssigneeIds() { | ||||||
|  |       return this.allowsMultipleAssignees ? this.localAssigneeIds : this.localAssigneeIds[0]; | ||||||
|  |     }, | ||||||
|  |     dropdownText() { | ||||||
|  |       if (this.localAssigneeIds.length === 0) { | ||||||
|  |         return s__('WorkItem|No assignees'); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       return this.localAssigneeIds.length === 1 | ||||||
|  |         ? this.localAssignees.map(({ name }) => name).join(', ') | ||||||
|  |         : sprintf(s__('WorkItem|%{usersLength} assignees'), { | ||||||
|  |             usersLength: this.localAssigneeIds.length, | ||||||
|  |           }); | ||||||
|  |     }, | ||||||
|  |     dropdownLabel() { | ||||||
|  |       return this.allowsMultipleAssignees ? __('Assignees') : __('Assignee'); | ||||||
|  |     }, | ||||||
|  |     headerText() { | ||||||
|  |       return this.allowsMultipleAssignees ? __('Select assignees') : __('Select assignee'); | ||||||
|  |     }, | ||||||
|  |     filteredAssignees() { | ||||||
|  |       return isEmpty(this.searchUsers) | ||||||
|  |         ? this.assignees | ||||||
|  |         : this.searchUsers.filter(({ id }) => this.localAssigneeIds.includes(id)); | ||||||
|  |     }, | ||||||
|  |     localAssignees() { | ||||||
|  |       return this.filteredAssignees || []; | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  |   watch: { | ||||||
|  |     assignees: { | ||||||
|  |       handler(newVal) { | ||||||
|  |         this.localAssigneeIds = newVal.map(({ id }) => id); | ||||||
|  |       }, | ||||||
|  |       deep: true, | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  |   methods: { | ||||||
|  |     handleAssigneesInput(assignees) { | ||||||
|  |       this.setLocalAssigneeIdsOnEvent(assignees); | ||||||
|  |       this.setAssignees(); | ||||||
|  |     }, | ||||||
|  |     handleAssigneeClick(assignees) { | ||||||
|  |       this.setLocalAssigneeIdsOnEvent(assignees); | ||||||
|  |     }, | ||||||
|  |     async setAssignees() { | ||||||
|  |       this.updateInProgress = true; | ||||||
|  |       const { localAssigneeIds } = this; | ||||||
|  | 
 | ||||||
|  |       try { | ||||||
|  |         const { | ||||||
|  |           data: { | ||||||
|  |             workItemUpdate: { errors }, | ||||||
|  |           }, | ||||||
|  |         } = await this.$apollo.mutate({ | ||||||
|  |           mutation: updateWorkItemMutation, | ||||||
|  |           variables: { | ||||||
|  |             input: { | ||||||
|  |               id: this.workItemId, | ||||||
|  |               assigneesWidget: { | ||||||
|  |                 assigneeIds: localAssigneeIds, | ||||||
|  |               }, | ||||||
|  |             }, | ||||||
|  |           }, | ||||||
|  |         }); | ||||||
|  |         if (errors.length > 0) { | ||||||
|  |           this.throwUpdateError(); | ||||||
|  |           return; | ||||||
|  |         } | ||||||
|  |         this.track('updated_assignees'); | ||||||
|  |       } catch { | ||||||
|  |         this.throwUpdateError(); | ||||||
|  |       } finally { | ||||||
|  |         this.updateInProgress = false; | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     setLocalAssigneeIdsOnEvent(assignees) { | ||||||
|  |       const singleSelectAssignee = assignees === null ? [] : [assignees]; | ||||||
|  |       this.localAssigneeIds = this.allowsMultipleAssignees ? assignees : singleSelectAssignee; | ||||||
|  |     }, | ||||||
|  |     async fetchMoreAssignees() { | ||||||
|  |       if (this.isLoadingMore && !this.hasNextPage) return; | ||||||
|  | 
 | ||||||
|  |       this.isLoadingMore = true; | ||||||
|  |       await this.$apollo.queries.users.fetchMore({ | ||||||
|  |         variables: { | ||||||
|  |           after: this.pageInfo.endCursor, | ||||||
|  |           first: DEFAULT_PAGE_SIZE_ASSIGNEES, | ||||||
|  |         }, | ||||||
|  |       }); | ||||||
|  |       this.isLoadingMore = false; | ||||||
|  |     }, | ||||||
|  |     setSearchKey(value) { | ||||||
|  |       this.searchKey = value; | ||||||
|  |       this.searchStarted = true; | ||||||
|  |     }, | ||||||
|  |     assignToCurrentUser() { | ||||||
|  |       const assignees = this.allowsMultipleAssignees ? [this.currentUser.id] : this.currentUser.id; | ||||||
|  |       this.setLocalAssigneeIdsOnEvent(assignees); | ||||||
|  |       this.setAssignees(); | ||||||
|  |     }, | ||||||
|  |     throwUpdateError() { | ||||||
|  |       this.$emit('error', i18n.updateError); | ||||||
|  |       // If mutation is rejected, we're rolling back to initial state | ||||||
|  |       this.localAssigneeIds = this.assignees.map(({ id }) => id); | ||||||
|  |     }, | ||||||
|  |     onDropdownShown() { | ||||||
|  |       this.searchStarted = true; | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  | }; | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <template> | ||||||
|  |   <work-item-sidebar-dropdown-widget-with-edit | ||||||
|  |     :multi-select="allowsMultipleAssignees" | ||||||
|  |     class="issuable-assignees gl-mt-2" | ||||||
|  |     :dropdown-label="dropdownLabel" | ||||||
|  |     :can-update="canUpdate" | ||||||
|  |     dropdown-name="assignees" | ||||||
|  |     show-footer | ||||||
|  |     :infinite-scroll="hasNextPage" | ||||||
|  |     :infinite-scroll-loading="isLoadingMore" | ||||||
|  |     :loading="isLoadingUsers" | ||||||
|  |     :list-items="searchUsers" | ||||||
|  |     :item-value="selectedAssigneeIds" | ||||||
|  |     :toggle-dropdown-text="dropdownText" | ||||||
|  |     :header-text="headerText" | ||||||
|  |     :update-in-progress="updateInProgress" | ||||||
|  |     :reset-button-label="__('Clear')" | ||||||
|  |     data-testid="work-item-assignees-with-edit" | ||||||
|  |     @dropdownShown="onDropdownShown" | ||||||
|  |     @searchStarted="setSearchKey" | ||||||
|  |     @updateValue="handleAssigneesInput" | ||||||
|  |     @updateSelected="handleAssigneeClick" | ||||||
|  |     @bottomReached="fetchMoreAssignees" | ||||||
|  |   > | ||||||
|  |     <template #list-item="{ item }"> | ||||||
|  |       <sidebar-participant :user="item" /> | ||||||
|  |     </template> | ||||||
|  |     <template v-if="canInviteMembers" #footer> | ||||||
|  |       <gl-button category="tertiary" block class="gl-justify-content-start!"> | ||||||
|  |         <invite-members-trigger | ||||||
|  |           :display-text="__('Invite members')" | ||||||
|  |           trigger-element="side-nav" | ||||||
|  |           icon="plus" | ||||||
|  |           trigger-source="work-item-assignees-with-edit" | ||||||
|  |           classes="gl-hover-text-decoration-none! gl-pb-2" | ||||||
|  |         /> | ||||||
|  |       </gl-button> | ||||||
|  |     </template> | ||||||
|  |     <template #none> | ||||||
|  |       <div class="gl-display-flex gl-align-items-center gl-text-gray-500 gl-gap-2"> | ||||||
|  |         <span>{{ __('None') }}</span> | ||||||
|  |         <template v-if="currentUser && canUpdate"> | ||||||
|  |           <span>-</span> | ||||||
|  |           <gl-button variant="link" data-testid="assign-self" @click.stop="assignToCurrentUser" | ||||||
|  |             ><span class="gl-text-gray-500 gl-hover-text-blue-800">{{ | ||||||
|  |               __('assign yourself') | ||||||
|  |             }}</span></gl-button | ||||||
|  |           > | ||||||
|  |         </template> | ||||||
|  |       </div> | ||||||
|  |     </template> | ||||||
|  |     <template #readonly> | ||||||
|  |       <uncollapsed-assignee-list | ||||||
|  |         :users="localAssignees" | ||||||
|  |         show-less-assignees-class="gl-hover-bg-transparent!" | ||||||
|  |       /> | ||||||
|  |     </template> | ||||||
|  |   </work-item-sidebar-dropdown-widget-with-edit> | ||||||
|  | </template> | ||||||
|  | @ -15,7 +15,8 @@ import { | ||||||
|   WORK_ITEM_TYPE_VALUE_TASK, |   WORK_ITEM_TYPE_VALUE_TASK, | ||||||
| } from '../constants'; | } from '../constants'; | ||||||
| import WorkItemDueDate from './work_item_due_date.vue'; | import WorkItemDueDate from './work_item_due_date.vue'; | ||||||
| import WorkItemAssignees from './work_item_assignees.vue'; | import WorkItemAssigneesInline from './work_item_assignees_inline.vue'; | ||||||
|  | import WorkItemAssigneesWithEdit from './work_item_assignees_with_edit.vue'; | ||||||
| import WorkItemLabels from './work_item_labels.vue'; | import WorkItemLabels from './work_item_labels.vue'; | ||||||
| import WorkItemMilestoneInline from './work_item_milestone_inline.vue'; | import WorkItemMilestoneInline from './work_item_milestone_inline.vue'; | ||||||
| import WorkItemMilestoneWithEdit from './work_item_milestone_with_edit.vue'; | import WorkItemMilestoneWithEdit from './work_item_milestone_with_edit.vue'; | ||||||
|  | @ -27,7 +28,8 @@ export default { | ||||||
|     WorkItemLabels, |     WorkItemLabels, | ||||||
|     WorkItemMilestoneInline, |     WorkItemMilestoneInline, | ||||||
|     WorkItemMilestoneWithEdit, |     WorkItemMilestoneWithEdit, | ||||||
|     WorkItemAssignees, |     WorkItemAssigneesInline, | ||||||
|  |     WorkItemAssigneesWithEdit, | ||||||
|     WorkItemDueDate, |     WorkItemDueDate, | ||||||
|     WorkItemParent, |     WorkItemParent, | ||||||
|     WorkItemParentInline, |     WorkItemParentInline, | ||||||
|  | @ -114,8 +116,10 @@ export default { | ||||||
| 
 | 
 | ||||||
| <template> | <template> | ||||||
|   <div class="work-item-attributes-wrapper"> |   <div class="work-item-attributes-wrapper"> | ||||||
|     <work-item-assignees |     <template v-if="workItemAssignees"> | ||||||
|       v-if="workItemAssignees" |       <work-item-assignees-with-edit | ||||||
|  |         v-if="glFeatures.workItemsMvc2" | ||||||
|  |         class="gl-mb-5" | ||||||
|         :can-update="canUpdate" |         :can-update="canUpdate" | ||||||
|         :full-path="fullPath" |         :full-path="fullPath" | ||||||
|         :work-item-id="workItem.id" |         :work-item-id="workItem.id" | ||||||
|  | @ -125,6 +129,18 @@ export default { | ||||||
|         :can-invite-members="workItemAssignees.canInviteMembers" |         :can-invite-members="workItemAssignees.canInviteMembers" | ||||||
|         @error="$emit('error', $event)" |         @error="$emit('error', $event)" | ||||||
|       /> |       /> | ||||||
|  |       <work-item-assignees-inline | ||||||
|  |         v-else | ||||||
|  |         :can-update="canUpdate" | ||||||
|  |         :full-path="fullPath" | ||||||
|  |         :work-item-id="workItem.id" | ||||||
|  |         :assignees="workItemAssignees.assignees.nodes" | ||||||
|  |         :allows-multiple-assignees="workItemAssignees.allowsMultipleAssignees" | ||||||
|  |         :work-item-type="workItemType" | ||||||
|  |         :can-invite-members="workItemAssignees.canInviteMembers" | ||||||
|  |         @error="$emit('error', $event)" | ||||||
|  |       /> | ||||||
|  |     </template> | ||||||
|     <work-item-labels |     <work-item-labels | ||||||
|       v-if="workItemLabels" |       v-if="workItemLabels" | ||||||
|       :can-update="canUpdate" |       :can-update="canUpdate" | ||||||
|  |  | ||||||
|  | @ -91,6 +91,9 @@ export default { | ||||||
|         expired, |         expired, | ||||||
|       })); |       })); | ||||||
|     }, |     }, | ||||||
|  |     localMilestoneId() { | ||||||
|  |       return this.localMilestone?.id; | ||||||
|  |     }, | ||||||
|   }, |   }, | ||||||
|   watch: { |   watch: { | ||||||
|     workItemMilestone(newVal) { |     workItemMilestone(newVal) { | ||||||
|  | @ -184,7 +187,7 @@ export default { | ||||||
|     dropdown-name="milestone" |     dropdown-name="milestone" | ||||||
|     :loading="isLoadingMilestones" |     :loading="isLoadingMilestones" | ||||||
|     :list-items="milestonesList" |     :list-items="milestonesList" | ||||||
|     :item-value="localMilestone" |     :item-value="localMilestoneId" | ||||||
|     :update-in-progress="updateInProgress" |     :update-in-progress="updateInProgress" | ||||||
|     :toggle-dropdown-text="dropdownText" |     :toggle-dropdown-text="dropdownText" | ||||||
|     :header-text="__('Select milestone')" |     :header-text="__('Select milestone')" | ||||||
|  |  | ||||||
|  | @ -311,7 +311,7 @@ module ApplicationHelper | ||||||
|     class_names << 'issue-boards-page gl-overflow-auto' if current_controller?(:boards) |     class_names << 'issue-boards-page gl-overflow-auto' if current_controller?(:boards) | ||||||
|     class_names << 'epic-boards-page gl-overflow-auto' if current_controller?(:epic_boards) |     class_names << 'epic-boards-page gl-overflow-auto' if current_controller?(:epic_boards) | ||||||
|     class_names << 'with-performance-bar' if performance_bar_enabled? |     class_names << 'with-performance-bar' if performance_bar_enabled? | ||||||
|     class_names << 'with-header' unless current_user |     class_names << 'with-header' if @with_header || !current_user | ||||||
|     class_names << 'with-top-bar' unless @hide_top_bar_padding |     class_names << 'with-top-bar' unless @hide_top_bar_padding | ||||||
|     class_names << system_message_class |     class_names << system_message_class | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -81,7 +81,8 @@ module Users | ||||||
|       code_suggestions_ga_non_owner_alert: 79, # EE-only |       code_suggestions_ga_non_owner_alert: 79, # EE-only | ||||||
|       duo_chat_callout: 80, # EE-only |       duo_chat_callout: 80, # EE-only | ||||||
|       code_suggestions_ga_owner_alert: 81, # EE-only |       code_suggestions_ga_owner_alert: 81, # EE-only | ||||||
|       product_analytics_dashboard_feedback: 82 # EE-only |       product_analytics_dashboard_feedback: 82, # EE-only | ||||||
|  |       joining_a_project_alert: 83 # EE-only | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     validates :feature_name, |     validates :feature_name, | ||||||
|  |  | ||||||
|  | @ -1,4 +1,5 @@ | ||||||
| .container | .container | ||||||
|  |   = render_if_exists 'dashboard/projects/joining_a_project_alert' | ||||||
|   .gl-text-center.gl-pt-6.gl-pb-7 |   .gl-text-center.gl-pt-6.gl-pb-7 | ||||||
|     %h2.gl-font-size-h1{ data: { testid: 'welcome-title-content' } } |     %h2.gl-font-size-h1{ data: { testid: 'welcome-title-content' } } | ||||||
|       = _('Welcome to GitLab') |       = _('Welcome to GitLab') | ||||||
|  |  | ||||||
|  | @ -1,6 +1,9 @@ | ||||||
| - add_page_specific_style 'page_bundles/login' | - add_page_specific_style 'page_bundles/login' | ||||||
|  | - @with_header = true | ||||||
|  | - page_classes = [user_application_theme, page_class.flatten.compact] | ||||||
|  | 
 | ||||||
| !!! 5 | !!! 5 | ||||||
| %html.html-devise-layout{ class: user_application_theme, lang: I18n.locale } | %html.html-devise-layout{ class: page_classes, lang: I18n.locale } | ||||||
|   = render "layouts/head" |   = render "layouts/head" | ||||||
|   %body.gl-h-full.login-page.navless{ class: "#{system_message_class} #{client_class_list}" } |   %body.gl-h-full.login-page.navless{ class: "#{system_message_class} #{client_class_list}" } | ||||||
|     = header_message |     = header_message | ||||||
|  |  | ||||||
|  | @ -1,3 +1,4 @@ | ||||||
|  | - @with_header = true | ||||||
| - page_classes = page_class.push(@html_class).flatten.compact | - page_classes = page_class.push(@html_class).flatten.compact | ||||||
| 
 | 
 | ||||||
| !!! 5 | !!! 5 | ||||||
|  |  | ||||||
|  | @ -4,7 +4,16 @@ classes: | ||||||
| - Analytics::DashboardsPointer | - Analytics::DashboardsPointer | ||||||
| feature_categories: | feature_categories: | ||||||
| - devops_reports | - devops_reports | ||||||
| description: Stores project link with configuration files for Analytics Dashboards group feature. | description: Stores project link with configuration files for Analytics Dashboards | ||||||
|  |   group feature. | ||||||
| introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/107673 | introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/107673 | ||||||
| milestone: '15.8' | milestone: '15.8' | ||||||
| gitlab_schema: gitlab_main | gitlab_schema: gitlab_main_cell | ||||||
|  | allow_cross_joins: | ||||||
|  | - gitlab_main_clusterwide | ||||||
|  | allow_cross_transactions: | ||||||
|  | - gitlab_main_clusterwide | ||||||
|  | allow_cross_foreign_keys: | ||||||
|  | - gitlab_main_clusterwide | ||||||
|  | sharding_key: | ||||||
|  |   target_project_id: projects | ||||||
|  |  | ||||||
|  | @ -0,0 +1,34 @@ | ||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | class RemoveIgnoredColumnsFromGeoNodeStatuses < Gitlab::Database::Migration[2.2] | ||||||
|  |   disable_ddl_transaction! | ||||||
|  | 
 | ||||||
|  |   milestone '16.9' | ||||||
|  | 
 | ||||||
|  |   IGNORED_COLLUMNS = [ | ||||||
|  |     :container_repositories_count, | ||||||
|  |     :container_repositories_failed_count, | ||||||
|  |     :container_repositories_registry_count, | ||||||
|  |     :container_repositories_synced_count, | ||||||
|  |     :job_artifacts_count, | ||||||
|  |     :job_artifacts_failed_count, | ||||||
|  |     :job_artifacts_synced_count, | ||||||
|  |     :job_artifacts_synced_missing_on_primary_count, | ||||||
|  |     :lfs_objects_count, | ||||||
|  |     :lfs_objects_failed_count, | ||||||
|  |     :lfs_objects_synced_count, | ||||||
|  |     :lfs_objects_synced_missing_on_primary_count | ||||||
|  |   ] | ||||||
|  | 
 | ||||||
|  |   def up | ||||||
|  |     IGNORED_COLLUMNS.each do |column_name| | ||||||
|  |       remove_column :geo_node_statuses, column_name, if_exists: true | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def down | ||||||
|  |     IGNORED_COLLUMNS.each do |column_name| | ||||||
|  |       add_column :geo_node_statuses, column_name, :integer, if_not_exists: true | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
|  | @ -0,0 +1 @@ | ||||||
|  | 78738644f53046494ba1f4a8e49ed9effc8147c8563e311bf3744a31a33449c6 | ||||||
|  | @ -17301,9 +17301,6 @@ CREATE TABLE geo_node_statuses ( | ||||||
|     id integer NOT NULL, |     id integer NOT NULL, | ||||||
|     geo_node_id integer NOT NULL, |     geo_node_id integer NOT NULL, | ||||||
|     db_replication_lag_seconds integer, |     db_replication_lag_seconds integer, | ||||||
|     lfs_objects_count integer, |  | ||||||
|     lfs_objects_synced_count integer, |  | ||||||
|     lfs_objects_failed_count integer, |  | ||||||
|     last_event_id bigint, |     last_event_id bigint, | ||||||
|     last_event_date timestamp without time zone, |     last_event_date timestamp without time zone, | ||||||
|     cursor_last_event_id bigint, |     cursor_last_event_id bigint, | ||||||
|  | @ -17315,19 +17312,10 @@ CREATE TABLE geo_node_statuses ( | ||||||
|     replication_slots_count integer, |     replication_slots_count integer, | ||||||
|     replication_slots_used_count integer, |     replication_slots_used_count integer, | ||||||
|     replication_slots_max_retained_wal_bytes bigint, |     replication_slots_max_retained_wal_bytes bigint, | ||||||
|     job_artifacts_count integer, |  | ||||||
|     job_artifacts_synced_count integer, |  | ||||||
|     job_artifacts_failed_count integer, |  | ||||||
|     version character varying, |     version character varying, | ||||||
|     revision character varying, |     revision character varying, | ||||||
|     lfs_objects_synced_missing_on_primary_count integer, |  | ||||||
|     job_artifacts_synced_missing_on_primary_count integer, |  | ||||||
|     storage_configuration_digest bytea, |     storage_configuration_digest bytea, | ||||||
|     projects_count integer, |     projects_count integer, | ||||||
|     container_repositories_count integer, |  | ||||||
|     container_repositories_synced_count integer, |  | ||||||
|     container_repositories_failed_count integer, |  | ||||||
|     container_repositories_registry_count integer, |  | ||||||
|     status jsonb DEFAULT '{}'::jsonb NOT NULL |     status jsonb DEFAULT '{}'::jsonb NOT NULL | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -31865,6 +31865,7 @@ Name of the feature that the callout is for. | ||||||
| | <a id="usercalloutfeaturenameenumgeo_migrate_hashed_storage"></a>`GEO_MIGRATE_HASHED_STORAGE` | Callout feature name for geo_migrate_hashed_storage. | | | <a id="usercalloutfeaturenameenumgeo_migrate_hashed_storage"></a>`GEO_MIGRATE_HASHED_STORAGE` | Callout feature name for geo_migrate_hashed_storage. | | ||||||
| | <a id="usercalloutfeaturenameenumgke_cluster_integration"></a>`GKE_CLUSTER_INTEGRATION` | Callout feature name for gke_cluster_integration. | | | <a id="usercalloutfeaturenameenumgke_cluster_integration"></a>`GKE_CLUSTER_INTEGRATION` | Callout feature name for gke_cluster_integration. | | ||||||
| | <a id="usercalloutfeaturenameenumgold_trial_billings"></a>`GOLD_TRIAL_BILLINGS` | Callout feature name for gold_trial_billings. | | | <a id="usercalloutfeaturenameenumgold_trial_billings"></a>`GOLD_TRIAL_BILLINGS` | Callout feature name for gold_trial_billings. | | ||||||
|  | | <a id="usercalloutfeaturenameenumjoining_a_project_alert"></a>`JOINING_A_PROJECT_ALERT` | Callout feature name for joining_a_project_alert. | | ||||||
| | <a id="usercalloutfeaturenameenummerge_request_settings_moved_callout"></a>`MERGE_REQUEST_SETTINGS_MOVED_CALLOUT` | Callout feature name for merge_request_settings_moved_callout. | | | <a id="usercalloutfeaturenameenummerge_request_settings_moved_callout"></a>`MERGE_REQUEST_SETTINGS_MOVED_CALLOUT` | Callout feature name for merge_request_settings_moved_callout. | | ||||||
| | <a id="usercalloutfeaturenameenummr_experience_survey"></a>`MR_EXPERIENCE_SURVEY` | Callout feature name for mr_experience_survey. | | | <a id="usercalloutfeaturenameenummr_experience_survey"></a>`MR_EXPERIENCE_SURVEY` | Callout feature name for mr_experience_survey. | | ||||||
| | <a id="usercalloutfeaturenameenumnamespace_over_storage_users_combined_alert"></a>`NAMESPACE_OVER_STORAGE_USERS_COMBINED_ALERT` | Callout feature name for namespace_over_storage_users_combined_alert. | | | <a id="usercalloutfeaturenameenumnamespace_over_storage_users_combined_alert"></a>`NAMESPACE_OVER_STORAGE_USERS_COMBINED_ALERT` | Callout feature name for namespace_over_storage_users_combined_alert. | | ||||||
|  |  | ||||||
|  | @ -337,6 +337,15 @@ create-release: | ||||||
| After committing and pushing changes, the pipeline tests the component, then creates | After committing and pushing changes, the pipeline tests the component, then creates | ||||||
| a release if the earlier jobs pass. | a release if the earlier jobs pass. | ||||||
| 
 | 
 | ||||||
|  | #### Test a component against sample files | ||||||
|  | 
 | ||||||
|  | In some cases, components require source files to interact with. For example, a component | ||||||
|  | that builds Go source code likely needs some samples of Go to test against. Alternatively, | ||||||
|  | a component that builds Docker images likely needs some sample Dockerfiles to test against. | ||||||
|  | 
 | ||||||
|  | You can include sample files like these directly in the component project, to be used | ||||||
|  | during component testing. For example, you can see the [code-quality CI/CD component's testing samples](https://gitlab.com/components/code-quality/-/tree/main/src). | ||||||
|  | 
 | ||||||
| ### Avoid using global keywords | ### Avoid using global keywords | ||||||
| 
 | 
 | ||||||
| Avoid using [global keywords](../yaml/index.md#global-keywords) in a component. | Avoid using [global keywords](../yaml/index.md#global-keywords) in a component. | ||||||
|  |  | ||||||
|  | @ -275,7 +275,6 @@ NOTES: | ||||||
| - A custom format is used for [dates](https://gitlab.com/gitlab-org/gitlab/blob/3be39f19ac3412c089be28553e6f91b681e5d739/config/initializers/date_time_formats.rb#L7) and [times](https://gitlab.com/gitlab-org/gitlab/blob/3be39f19ac3412c089be28553e6f91b681e5d739/config/initializers/date_time_formats.rb#L13) in CSV files. | - A custom format is used for [dates](https://gitlab.com/gitlab-org/gitlab/blob/3be39f19ac3412c089be28553e6f91b681e5d739/config/initializers/date_time_formats.rb#L7) and [times](https://gitlab.com/gitlab-org/gitlab/blob/3be39f19ac3412c089be28553e6f91b681e5d739/config/initializers/date_time_formats.rb#L13) in CSV files. | ||||||
| 
 | 
 | ||||||
| WARNING: | WARNING: | ||||||
| 
 |  | ||||||
| Do not open the license usage file. If you open the file, failures might occur when [you submit your license usage data](../../administration/license_file.md#submit-license-usage-data). | Do not open the license usage file. If you open the file, failures might occur when [you submit your license usage data](../../administration/license_file.md#submit-license-usage-data). | ||||||
| 
 | 
 | ||||||
| ## Renew your subscription | ## Renew your subscription | ||||||
|  |  | ||||||
|  | @ -33944,6 +33944,12 @@ msgstr "" | ||||||
| msgid "OnDemandScans|at" | msgid "OnDemandScans|at" | ||||||
| msgstr "" | msgstr "" | ||||||
| 
 | 
 | ||||||
|  | msgid "Onboarding|If you can't find your organization, request an invite from your company's GitLab administrator." | ||||||
|  | msgstr "" | ||||||
|  | 
 | ||||||
|  | msgid "Onboarding|Looking for your team?" | ||||||
|  | msgstr "" | ||||||
|  | 
 | ||||||
| msgid "Once imported, repositories can be mirrored over SSH. Read more %{link_start}here%{link_end}." | msgid "Once imported, repositories can be mirrored over SSH. Read more %{link_start}here%{link_end}." | ||||||
| msgstr "" | msgstr "" | ||||||
| 
 | 
 | ||||||
|  | @ -55627,6 +55633,9 @@ msgstr "" | ||||||
| msgid "WorkItem|%{invalidWorkItemsList} cannot be added: Cannot assign a non-confidential %{childWorkItemType} to a confidential parent %{parentWorkItemType}. Make the selected %{childWorkItemType} confidential and try again." | msgid "WorkItem|%{invalidWorkItemsList} cannot be added: Cannot assign a non-confidential %{childWorkItemType} to a confidential parent %{parentWorkItemType}. Make the selected %{childWorkItemType} confidential and try again." | ||||||
| msgstr "" | msgstr "" | ||||||
| 
 | 
 | ||||||
|  | msgid "WorkItem|%{usersLength} assignees" | ||||||
|  | msgstr "" | ||||||
|  | 
 | ||||||
| msgid "WorkItem|%{workItemType} deleted" | msgid "WorkItem|%{workItemType} deleted" | ||||||
| msgstr "" | msgstr "" | ||||||
| 
 | 
 | ||||||
|  | @ -55797,6 +55806,9 @@ msgstr "" | ||||||
| msgid "WorkItem|New task" | msgid "WorkItem|New task" | ||||||
| msgstr "" | msgstr "" | ||||||
| 
 | 
 | ||||||
|  | msgid "WorkItem|No assignees" | ||||||
|  | msgstr "" | ||||||
|  | 
 | ||||||
| msgid "WorkItem|No child items are currently assigned. Use child items to break down this issue into smaller parts." | msgid "WorkItem|No child items are currently assigned. Use child items to break down this issue into smaller parts." | ||||||
| msgstr "" | msgstr "" | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -41,6 +41,14 @@ RSpec.describe 'Work item', :js, feature_category: :team_planning do | ||||||
|       expect(page).to have_button _('More actions') |       expect(page).to have_button _('More actions') | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|  |     context 'when work_items_mvc_2 is disabled' do | ||||||
|  |       before do | ||||||
|  |         stub_feature_flags(work_items_mvc_2: false) | ||||||
|  | 
 | ||||||
|  |         page.refresh | ||||||
|  |         wait_for_all_requests | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|       it 'reassigns to another user', |       it 'reassigns to another user', | ||||||
|         quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/413074' do |         quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/413074' do | ||||||
|         find('[data-testid="work-item-assignees-input"]').fill_in(with: user.username) |         find('[data-testid="work-item-assignees-input"]').fill_in(with: user.username) | ||||||
|  | @ -59,6 +67,37 @@ RSpec.describe 'Work item', :js, feature_category: :team_planning do | ||||||
| 
 | 
 | ||||||
|         expect(work_item.reload.assignees).to include(user2) |         expect(work_item.reload.assignees).to include(user2) | ||||||
|       end |       end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     context 'when work_items_mvc_2 is enabled' do | ||||||
|  |       before do | ||||||
|  |         stub_feature_flags(work_items_mvc_2: true) | ||||||
|  | 
 | ||||||
|  |         page.refresh | ||||||
|  |         wait_for_all_requests | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       it 'reassigns to another user', | ||||||
|  |         quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/413074' do | ||||||
|  |         within('[data-testid="work-item-assignees-with-edit"]') do | ||||||
|  |           click_button 'Edit' | ||||||
|  |         end | ||||||
|  | 
 | ||||||
|  |         select_listbox_item(user.username) | ||||||
|  | 
 | ||||||
|  |         wait_for_requests | ||||||
|  | 
 | ||||||
|  |         within('[data-testid="work-item-assignees-with-edit"]') do | ||||||
|  |           click_button 'Edit' | ||||||
|  |         end | ||||||
|  | 
 | ||||||
|  |         select_listbox_item(user2.username) | ||||||
|  | 
 | ||||||
|  |         wait_for_requests | ||||||
|  | 
 | ||||||
|  |         expect(work_item.reload.assignees).to include(user2) | ||||||
|  |       end | ||||||
|  |     end | ||||||
| 
 | 
 | ||||||
|     it_behaves_like 'work items title' |     it_behaves_like 'work items title' | ||||||
|     it_behaves_like 'work items toggle status button' |     it_behaves_like 'work items toggle status button' | ||||||
|  | @ -118,11 +157,35 @@ RSpec.describe 'Work item', :js, feature_category: :team_planning do | ||||||
|       expect(page).to have_selector('[data-testid="award-button"].disabled') |       expect(page).to have_selector('[data-testid="award-button"].disabled') | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|  |     context 'when work_items_mvc_2 is disabled' do | ||||||
|  |       before do | ||||||
|  |         stub_feature_flags(work_items_mvc_2: false) | ||||||
|  | 
 | ||||||
|  |         page.refresh | ||||||
|  |         wait_for_all_requests | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|       it 'assignees input field is disabled' do |       it 'assignees input field is disabled' do | ||||||
|         within('[data-testid="work-item-assignees-input"]') do |         within('[data-testid="work-item-assignees-input"]') do | ||||||
|           expect(page).to have_field(type: 'text', disabled: true) |           expect(page).to have_field(type: 'text', disabled: true) | ||||||
|         end |         end | ||||||
|       end |       end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     context 'when work_items_mvc_2 is enabled' do | ||||||
|  |       before do | ||||||
|  |         stub_feature_flags(work_items_mvc_2: true) | ||||||
|  | 
 | ||||||
|  |         page.refresh | ||||||
|  |         wait_for_all_requests | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       it 'assignees edit button is not visible' do | ||||||
|  |         within('[data-testid="work-item-assignees-with-edit"]') do | ||||||
|  |           expect(page).not_to have_button('Edit') | ||||||
|  |         end | ||||||
|  |       end | ||||||
|  |     end | ||||||
| 
 | 
 | ||||||
|     it 'labels input field is disabled' do |     it 'labels input field is disabled' do | ||||||
|       within('[data-testid="work-item-labels-input"]') do |       within('[data-testid="work-item-labels-input"]') do | ||||||
|  |  | ||||||
|  | @ -21,6 +21,11 @@ describe('WorkItemSidebarDropdownWidgetWithEdit component', () => { | ||||||
|     canUpdate = true, |     canUpdate = true, | ||||||
|     isEditing = false, |     isEditing = false, | ||||||
|     updateInProgress = false, |     updateInProgress = false, | ||||||
|  |     showFooter = false, | ||||||
|  |     slots = {}, | ||||||
|  |     multiSelect = false, | ||||||
|  |     infiniteScroll = false, | ||||||
|  |     infiniteScrollLoading = false, | ||||||
|   } = {}) => { |   } = {}) => { | ||||||
|     wrapper = mountExtended(WorkItemSidebarDropdownWidgetWithEdit, { |     wrapper = mountExtended(WorkItemSidebarDropdownWidgetWithEdit, { | ||||||
|       propsData: { |       propsData: { | ||||||
|  | @ -31,7 +36,12 @@ describe('WorkItemSidebarDropdownWidgetWithEdit component', () => { | ||||||
|         canUpdate, |         canUpdate, | ||||||
|         updateInProgress, |         updateInProgress, | ||||||
|         headerText: __('Select iteration'), |         headerText: __('Select iteration'), | ||||||
|  |         showFooter, | ||||||
|  |         multiSelect, | ||||||
|  |         infiniteScroll, | ||||||
|  |         infiniteScrollLoading, | ||||||
|       }, |       }, | ||||||
|  |       slots, | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     if (isEditing) { |     if (isEditing) { | ||||||
|  | @ -152,10 +162,41 @@ describe('WorkItemSidebarDropdownWidgetWithEdit component', () => { | ||||||
|         searching: false, |         searching: false, | ||||||
|         infiniteScroll: false, |         infiniteScroll: false, | ||||||
|         noResultsText: 'No matching results', |         noResultsText: 'No matching results', | ||||||
|         toggleText: 'None', |  | ||||||
|         searchPlaceholder: 'Search', |         searchPlaceholder: 'Search', | ||||||
|         resetButtonLabel: 'Clear', |         resetButtonLabel: 'Clear', | ||||||
|       }); |       }); | ||||||
|     }); |     }); | ||||||
|  | 
 | ||||||
|  |     it('renders the footer when enabled', async () => { | ||||||
|  |       const FOOTER_SLOT_HTML = 'Test message'; | ||||||
|  |       createComponent({ isEditing: true, showFooter: true, slots: { footer: FOOTER_SLOT_HTML } }); | ||||||
|  | 
 | ||||||
|  |       await nextTick(); | ||||||
|  |       expect(wrapper.text()).toContain(FOOTER_SLOT_HTML); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('supports multiselect', async () => { | ||||||
|  |       createComponent({ isEditing: true, multiSelect: true }); | ||||||
|  | 
 | ||||||
|  |       await nextTick(); | ||||||
|  | 
 | ||||||
|  |       expect(findCollapsibleListbox().props('multiple')).toBe(true); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('supports infinite scrolling', async () => { | ||||||
|  |       createComponent({ isEditing: true, infiniteScroll: true }); | ||||||
|  | 
 | ||||||
|  |       await nextTick(); | ||||||
|  | 
 | ||||||
|  |       expect(findCollapsibleListbox().props('infiniteScroll')).toBe(true); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('shows loader when bottom reached', async () => { | ||||||
|  |       createComponent({ isEditing: true, infiniteScroll: true, infiniteScrollLoading: true }); | ||||||
|  | 
 | ||||||
|  |       await nextTick(); | ||||||
|  | 
 | ||||||
|  |       expect(findCollapsibleListbox().props('infiniteScrollLoading')).toBe(true); | ||||||
|  |     }); | ||||||
|   }); |   }); | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | @ -11,7 +11,7 @@ import usersSearchQuery from '~/graphql_shared/queries/users_search.query.graphq | ||||||
| import currentUserQuery from '~/graphql_shared/queries/current_user.query.graphql'; | import currentUserQuery from '~/graphql_shared/queries/current_user.query.graphql'; | ||||||
| import InviteMembersTrigger from '~/invite_members/components/invite_members_trigger.vue'; | import InviteMembersTrigger from '~/invite_members/components/invite_members_trigger.vue'; | ||||||
| import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql'; | import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql'; | ||||||
| import WorkItemAssignees from '~/work_items/components/work_item_assignees.vue'; | import WorkItemAssigneesInline from '~/work_items/components/work_item_assignees_inline.vue'; | ||||||
| import { | import { | ||||||
|   i18n, |   i18n, | ||||||
|   DEFAULT_PAGE_SIZE_ASSIGNEES, |   DEFAULT_PAGE_SIZE_ASSIGNEES, | ||||||
|  | @ -35,7 +35,7 @@ Vue.use(VueApollo); | ||||||
| const workItemId = 'gid://gitlab/WorkItem/1'; | const workItemId = 'gid://gitlab/WorkItem/1'; | ||||||
| const dropdownItems = projectMembersResponseWithCurrentUser.data.workspace.users.nodes; | const dropdownItems = projectMembersResponseWithCurrentUser.data.workspace.users.nodes; | ||||||
| 
 | 
 | ||||||
| describe('WorkItemAssignees component', () => { | describe('WorkItemAssigneesInline component', () => { | ||||||
|   let wrapper; |   let wrapper; | ||||||
| 
 | 
 | ||||||
|   const findAssigneeLinks = () => wrapper.findAllComponents(GlLink); |   const findAssigneeLinks = () => wrapper.findAllComponents(GlLink); | ||||||
|  | @ -88,7 +88,7 @@ describe('WorkItemAssignees component', () => { | ||||||
|       [updateWorkItemMutation, updateWorkItemMutationHandler], |       [updateWorkItemMutation, updateWorkItemMutationHandler], | ||||||
|     ]); |     ]); | ||||||
| 
 | 
 | ||||||
|     wrapper = mountExtended(WorkItemAssignees, { |     wrapper = mountExtended(WorkItemAssigneesInline, { | ||||||
|       provide: { |       provide: { | ||||||
|         isGroup, |         isGroup, | ||||||
|       }, |       }, | ||||||
|  | @ -0,0 +1,300 @@ | ||||||
|  | import Vue, { nextTick } from 'vue'; | ||||||
|  | import VueApollo from 'vue-apollo'; | ||||||
|  | import WorkItemAssignees from '~/work_items/components/work_item_assignees_with_edit.vue'; | ||||||
|  | import WorkItemSidebarDropdownWidgetWithEdit from '~/work_items/components/shared/work_item_sidebar_dropdown_widget_with_edit.vue'; | ||||||
|  | import groupUsersSearchQuery from '~/graphql_shared/queries/group_users_search.query.graphql'; | ||||||
|  | import usersSearchQuery from '~/graphql_shared/queries/users_search.query.graphql'; | ||||||
|  | import currentUserQuery from '~/graphql_shared/queries/current_user.query.graphql'; | ||||||
|  | import InviteMembersTrigger from '~/invite_members/components/invite_members_trigger.vue'; | ||||||
|  | import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql'; | ||||||
|  | import createMockApollo from 'helpers/mock_apollo_helper'; | ||||||
|  | import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; | ||||||
|  | import { mockTracking } from 'helpers/tracking_helper'; | ||||||
|  | import waitForPromises from 'helpers/wait_for_promises'; | ||||||
|  | import { | ||||||
|  |   projectMembersResponseWithCurrentUser, | ||||||
|  |   mockAssignees, | ||||||
|  |   currentUserResponse, | ||||||
|  |   currentUserNullResponse, | ||||||
|  |   updateWorkItemMutationResponse, | ||||||
|  |   projectMembersResponseWithCurrentUserWithNextPage, | ||||||
|  |   projectMembersResponseWithNoMatchingUsers, | ||||||
|  | } from 'jest/work_items/mock_data'; | ||||||
|  | import { DEFAULT_PAGE_SIZE_ASSIGNEES, i18n, TRACKING_CATEGORY_SHOW } from '~/work_items/constants'; | ||||||
|  | 
 | ||||||
|  | const workItemId = 'gid://gitlab/WorkItem/1'; | ||||||
|  | 
 | ||||||
|  | describe('WorkItemAssigneesWithEdit component', () => { | ||||||
|  |   Vue.use(VueApollo); | ||||||
|  | 
 | ||||||
|  |   let wrapper; | ||||||
|  | 
 | ||||||
|  |   const findInviteMembersTrigger = () => wrapper.findComponent(InviteMembersTrigger); | ||||||
|  |   const findAssignSelfButton = () => wrapper.findByTestId('assign-self'); | ||||||
|  |   const findSidebarDropdownWidget = () => | ||||||
|  |     wrapper.findComponent(WorkItemSidebarDropdownWidgetWithEdit); | ||||||
|  | 
 | ||||||
|  |   const successSearchQueryHandler = jest | ||||||
|  |     .fn() | ||||||
|  |     .mockResolvedValue(projectMembersResponseWithCurrentUser); | ||||||
|  |   const successGroupSearchQueryHandler = jest | ||||||
|  |     .fn() | ||||||
|  |     .mockResolvedValue(projectMembersResponseWithCurrentUser); | ||||||
|  |   const successSearchQueryHandlerWithMoreAssignees = jest | ||||||
|  |     .fn() | ||||||
|  |     .mockResolvedValue(projectMembersResponseWithCurrentUserWithNextPage); | ||||||
|  |   const successCurrentUserQueryHandler = jest.fn().mockResolvedValue(currentUserResponse); | ||||||
|  |   const noCurrentUserQueryHandler = jest.fn().mockResolvedValue(currentUserNullResponse); | ||||||
|  |   const successUpdateWorkItemMutationHandler = jest | ||||||
|  |     .fn() | ||||||
|  |     .mockResolvedValue(updateWorkItemMutationResponse); | ||||||
|  |   const successSearchWithNoMatchingUsers = jest | ||||||
|  |     .fn() | ||||||
|  |     .mockResolvedValue(projectMembersResponseWithNoMatchingUsers); | ||||||
|  | 
 | ||||||
|  |   const errorHandler = jest.fn().mockRejectedValue('Houston, we have a problem'); | ||||||
|  | 
 | ||||||
|  |   const showDropdown = () => { | ||||||
|  |     findSidebarDropdownWidget().vm.$emit('dropdownShown'); | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   const createComponent = ({ | ||||||
|  |     assignees = mockAssignees, | ||||||
|  |     searchQueryHandler = successSearchQueryHandler, | ||||||
|  |     currentUserQueryHandler = successCurrentUserQueryHandler, | ||||||
|  |     allowsMultipleAssignees = false, | ||||||
|  |     canInviteMembers = false, | ||||||
|  |     canUpdate = true, | ||||||
|  |   } = {}) => { | ||||||
|  |     const apolloProvider = createMockApollo([ | ||||||
|  |       [usersSearchQuery, searchQueryHandler], | ||||||
|  |       [groupUsersSearchQuery, successGroupSearchQueryHandler], | ||||||
|  |       [currentUserQuery, currentUserQueryHandler], | ||||||
|  |       [updateWorkItemMutation, successUpdateWorkItemMutationHandler], | ||||||
|  |     ]); | ||||||
|  | 
 | ||||||
|  |     wrapper = shallowMountExtended(WorkItemAssignees, { | ||||||
|  |       provide: { | ||||||
|  |         isGroup: false, | ||||||
|  |       }, | ||||||
|  |       propsData: { | ||||||
|  |         assignees, | ||||||
|  |         fullPath: 'test-project-path', | ||||||
|  |         workItemId, | ||||||
|  |         allowsMultipleAssignees, | ||||||
|  |         workItemType: 'Task', | ||||||
|  |         canUpdate, | ||||||
|  |         canInviteMembers, | ||||||
|  |       }, | ||||||
|  |       apolloProvider, | ||||||
|  |     }); | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   it('has "Assignee" label for single select', () => { | ||||||
|  |     createComponent(); | ||||||
|  | 
 | ||||||
|  |     expect(findSidebarDropdownWidget().props('dropdownLabel')).toBe('Assignee'); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   describe('Dropdown search', () => { | ||||||
|  |     it('shows no items in the dropdown when no results matching', async () => { | ||||||
|  |       createComponent({ searchQueryHandler: successSearchWithNoMatchingUsers }); | ||||||
|  |       showDropdown(); | ||||||
|  |       await waitForPromises(); | ||||||
|  | 
 | ||||||
|  |       expect(findSidebarDropdownWidget().props('listItems')).toHaveLength(0); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('emits error event if search users query fails', async () => { | ||||||
|  |       createComponent({ searchQueryHandler: errorHandler }); | ||||||
|  |       showDropdown(); | ||||||
|  |       await waitForPromises(); | ||||||
|  | 
 | ||||||
|  |       expect(wrapper.emitted('error')).toEqual([[i18n.fetchError]]); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   describe('when assigning to current user', () => { | ||||||
|  |     it('does not show `Assign yourself` button if current user is loading', () => { | ||||||
|  |       createComponent(); | ||||||
|  | 
 | ||||||
|  |       expect(findAssignSelfButton().exists()).toBe(false); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('does now show `Assign yourself` button if user is not logged in', async () => { | ||||||
|  |       createComponent({ currentUserQueryHandler: noCurrentUserQueryHandler, assignees: [] }); | ||||||
|  |       await waitForPromises(); | ||||||
|  | 
 | ||||||
|  |       expect(findAssignSelfButton().exists()).toBe(false); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   describe('Dropdown options', () => { | ||||||
|  |     beforeEach(() => { | ||||||
|  |       createComponent({ canUpdate: true }); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('calls successSearchQueryHandler with variables when dropdown is opened', async () => { | ||||||
|  |       showDropdown(); | ||||||
|  | 
 | ||||||
|  |       await waitForPromises(); | ||||||
|  | 
 | ||||||
|  |       expect(successSearchQueryHandler).toHaveBeenCalledWith({ | ||||||
|  |         first: DEFAULT_PAGE_SIZE_ASSIGNEES, | ||||||
|  |         fullPath: 'test-project-path', | ||||||
|  |         search: '', | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('shows the skeleton loader when the items are being fetched on click', async () => { | ||||||
|  |       showDropdown(); | ||||||
|  | 
 | ||||||
|  |       await nextTick(); | ||||||
|  | 
 | ||||||
|  |       expect(findSidebarDropdownWidget().props('loading')).toBe(true); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('shows the iterations in dropdown when the items have finished fetching', async () => { | ||||||
|  |       showDropdown(); | ||||||
|  | 
 | ||||||
|  |       await waitForPromises(); | ||||||
|  | 
 | ||||||
|  |       expect(findSidebarDropdownWidget().props('loading')).toBe(false); | ||||||
|  |       expect(findSidebarDropdownWidget().props('listItems')).toHaveLength( | ||||||
|  |         projectMembersResponseWithCurrentUser.data.workspace.users.nodes.length, | ||||||
|  |       ); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   describe('when user is logged in and there are no assignees', () => { | ||||||
|  |     beforeEach(() => { | ||||||
|  |       createComponent({ assignees: [] }); | ||||||
|  |       return waitForPromises(); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('renders `Assign yourself` button', () => { | ||||||
|  |       expect(findAssignSelfButton().exists()).toBe(true); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('calls update work item assignees mutation with current user as a variable on button click', async () => { | ||||||
|  |       const { currentUser } = currentUserResponse.data; | ||||||
|  |       findAssignSelfButton().vm.$emit('click', new MouseEvent('click')); | ||||||
|  |       await nextTick(); | ||||||
|  | 
 | ||||||
|  |       expect(successUpdateWorkItemMutationHandler).toHaveBeenCalledWith({ | ||||||
|  |         input: { | ||||||
|  |           id: workItemId, | ||||||
|  |           assigneesWidget: { | ||||||
|  |             assigneeIds: [currentUser.id], | ||||||
|  |           }, | ||||||
|  |         }, | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   describe('when multiple assignees are allowed', () => { | ||||||
|  |     beforeEach(() => { | ||||||
|  |       createComponent({ allowsMultipleAssignees: true, assignees: [] }); | ||||||
|  |       return waitForPromises(); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('renders `Assignees` as label and `Select assignees` as dropdown button header', () => { | ||||||
|  |       expect(findSidebarDropdownWidget().props()).toMatchObject({ | ||||||
|  |         dropdownLabel: 'Assignees', | ||||||
|  |         headerText: 'Select assignees', | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('adds multiple assignees when collapsible listbox provides multiple values', async () => { | ||||||
|  |       showDropdown(); | ||||||
|  |       await waitForPromises(); | ||||||
|  | 
 | ||||||
|  |       findSidebarDropdownWidget().vm.$emit('updateValue', [ | ||||||
|  |         'gid://gitlab/User/5', | ||||||
|  |         'gid://gitlab/User/6', | ||||||
|  |       ]); | ||||||
|  |       await nextTick(); | ||||||
|  | 
 | ||||||
|  |       expect(findSidebarDropdownWidget().props('itemValue')).toHaveLength(2); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   describe('tracking', () => { | ||||||
|  |     let trackingSpy; | ||||||
|  | 
 | ||||||
|  |     beforeEach(() => { | ||||||
|  |       createComponent(); | ||||||
|  |       trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     afterEach(() => { | ||||||
|  |       trackingSpy = null; | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('tracks editing the assignees on dropdown widget updateValue', async () => { | ||||||
|  |       showDropdown(); | ||||||
|  |       await waitForPromises(); | ||||||
|  | 
 | ||||||
|  |       findSidebarDropdownWidget().vm.$emit('updateValue', mockAssignees[0].id); | ||||||
|  |       await waitForPromises(); | ||||||
|  | 
 | ||||||
|  |       expect(trackingSpy).toHaveBeenCalledWith(TRACKING_CATEGORY_SHOW, 'updated_assignees', { | ||||||
|  |         category: TRACKING_CATEGORY_SHOW, | ||||||
|  |         label: 'item_assignees', | ||||||
|  |         property: 'type_Task', | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   describe('invite members', () => { | ||||||
|  |     it('does not render `Invite members` link if user has no permission to invite members', () => { | ||||||
|  |       createComponent(); | ||||||
|  | 
 | ||||||
|  |       expect(findInviteMembersTrigger().exists()).toBe(false); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('renders `Invite members` link if user has a permission to invite members', () => { | ||||||
|  |       createComponent({ canInviteMembers: true }); | ||||||
|  | 
 | ||||||
|  |       expect(findInviteMembersTrigger().exists()).toBe(true); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   describe('load more assignees', () => { | ||||||
|  |     it('does not have infinite scroll when no matching users', async () => { | ||||||
|  |       createComponent({ searchQueryHandler: successSearchWithNoMatchingUsers }); | ||||||
|  | 
 | ||||||
|  |       showDropdown(); | ||||||
|  |       await waitForPromises(); | ||||||
|  | 
 | ||||||
|  |       expect(findSidebarDropdownWidget().props('infiniteScroll')).toBe(false); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('does not trigger load more when does not have next page', async () => { | ||||||
|  |       createComponent(); | ||||||
|  | 
 | ||||||
|  |       showDropdown(); | ||||||
|  |       await waitForPromises(); | ||||||
|  | 
 | ||||||
|  |       expect(findSidebarDropdownWidget().props('infiniteScroll')).toBe(false); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     it('triggers load more when there are more users', async () => { | ||||||
|  |       createComponent({ searchQueryHandler: successSearchQueryHandlerWithMoreAssignees }); | ||||||
|  | 
 | ||||||
|  |       showDropdown(); | ||||||
|  |       await waitForPromises(); | ||||||
|  | 
 | ||||||
|  |       findSidebarDropdownWidget().vm.$emit('bottomReached'); | ||||||
|  |       await waitForPromises(); | ||||||
|  | 
 | ||||||
|  |       expect(successSearchQueryHandlerWithMoreAssignees).toHaveBeenCalledWith({ | ||||||
|  |         first: DEFAULT_PAGE_SIZE_ASSIGNEES, | ||||||
|  |         after: | ||||||
|  |           projectMembersResponseWithCurrentUserWithNextPage.data.workspace.users.pageInfo.endCursor, | ||||||
|  |         search: '', | ||||||
|  |         fullPath: 'test-project-path', | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
|  | @ -1,6 +1,6 @@ | ||||||
| import { nextTick } from 'vue'; | import { nextTick } from 'vue'; | ||||||
| import { shallowMount } from '@vue/test-utils'; | import { shallowMount } from '@vue/test-utils'; | ||||||
| import WorkItemAssignees from '~/work_items/components/work_item_assignees.vue'; | import WorkItemAssigneesWithEdit from '~/work_items/components/work_item_assignees_with_edit.vue'; | ||||||
| import WorkItemDueDate from '~/work_items/components/work_item_due_date.vue'; | import WorkItemDueDate from '~/work_items/components/work_item_due_date.vue'; | ||||||
| import WorkItemLabels from '~/work_items/components/work_item_labels.vue'; | import WorkItemLabels from '~/work_items/components/work_item_labels.vue'; | ||||||
| import WorkItemMilestoneInline from '~/work_items/components/work_item_milestone_inline.vue'; | import WorkItemMilestoneInline from '~/work_items/components/work_item_milestone_inline.vue'; | ||||||
|  | @ -23,7 +23,7 @@ describe('WorkItemAttributesWrapper component', () => { | ||||||
|   const workItemQueryResponse = workItemResponseFactory({ canUpdate: true, canDelete: true }); |   const workItemQueryResponse = workItemResponseFactory({ canUpdate: true, canDelete: true }); | ||||||
| 
 | 
 | ||||||
|   const findWorkItemDueDate = () => wrapper.findComponent(WorkItemDueDate); |   const findWorkItemDueDate = () => wrapper.findComponent(WorkItemDueDate); | ||||||
|   const findWorkItemAssignees = () => wrapper.findComponent(WorkItemAssignees); |   const findWorkItemAssignees = () => wrapper.findComponent(WorkItemAssigneesWithEdit); | ||||||
|   const findWorkItemLabels = () => wrapper.findComponent(WorkItemLabels); |   const findWorkItemLabels = () => wrapper.findComponent(WorkItemLabels); | ||||||
|   const findWorkItemMilestone = () => wrapper.findComponent(WorkItemMilestoneWithEdit); |   const findWorkItemMilestone = () => wrapper.findComponent(WorkItemMilestoneWithEdit); | ||||||
|   const findWorkItemMilestoneInline = () => wrapper.findComponent(WorkItemMilestoneInline); |   const findWorkItemMilestoneInline = () => wrapper.findComponent(WorkItemMilestoneInline); | ||||||
|  |  | ||||||
|  | @ -150,7 +150,7 @@ describe('WorkItemMilestoneWithEdit component', () => { | ||||||
| 
 | 
 | ||||||
|       await nextTick(); |       await nextTick(); | ||||||
| 
 | 
 | ||||||
|       expect(findSidebarDropdownWidget().props('itemValue').title).toBe(milestoneAtIndex.title); |       expect(findSidebarDropdownWidget().props('itemValue')).toBe(milestoneAtIndex.id); | ||||||
|     }); |     }); | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -701,6 +701,11 @@ RSpec.describe ApplicationHelper do | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     describe 'with-header' do |     describe 'with-header' do | ||||||
|  |       context 'when @with_header is falsey' do | ||||||
|  |         before do | ||||||
|  |           helper.instance_variable_set(:@with_header, nil) | ||||||
|  |         end | ||||||
|  | 
 | ||||||
|         context 'when current_user' do |         context 'when current_user' do | ||||||
|           before do |           before do | ||||||
|             allow(helper).to receive(:current_user).and_return(user) |             allow(helper).to receive(:current_user).and_return(user) | ||||||
|  | @ -718,6 +723,15 @@ RSpec.describe ApplicationHelper do | ||||||
|         end |         end | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|  |       context 'when @with_header is true' do | ||||||
|  |         before do | ||||||
|  |           helper.instance_variable_set(:@with_header, true) | ||||||
|  |         end | ||||||
|  | 
 | ||||||
|  |         it { is_expected.to include('with-header') } | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|     describe 'with-top-bar' do |     describe 'with-top-bar' do | ||||||
|       context 'when @hide_top_bar_padding is false' do |       context 'when @hide_top_bar_padding is false' do | ||||||
|         before do |         before do | ||||||
|  |  | ||||||
|  | @ -2,7 +2,7 @@ | ||||||
| 
 | 
 | ||||||
| require 'spec_helper' | require 'spec_helper' | ||||||
| 
 | 
 | ||||||
| RSpec.describe Gitlab::Auth::CurrentUserMode, :request_store do | RSpec.describe Gitlab::Auth::CurrentUserMode, :request_store, feature_category: :system_access do | ||||||
|   let(:user) { build_stubbed(:user) } |   let(:user) { build_stubbed(:user) } | ||||||
| 
 | 
 | ||||||
|   subject { described_class.new(user) } |   subject { described_class.new(user) } | ||||||
|  |  | ||||||
|  | @ -167,6 +167,9 @@ RSpec.shared_examples 'work items comments' do |type| | ||||||
| end | end | ||||||
| 
 | 
 | ||||||
| RSpec.shared_examples 'work items assignees' do | RSpec.shared_examples 'work items assignees' do | ||||||
|  |   context 'when the work_items_mvc_2 FF is disabled' do | ||||||
|  |     include_context 'with work_items_mvc_2', false | ||||||
|  | 
 | ||||||
|     it 'successfully assigns the current user by searching', |     it 'successfully assigns the current user by searching', | ||||||
|       quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/413074' do |       quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/413074' do | ||||||
|       # The button is only when the mouse is over the input |       # The button is only when the mouse is over the input | ||||||
|  | @ -234,6 +237,59 @@ RSpec.shared_examples 'work items assignees' do | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|  |   context 'when the work_items_mvc_2 FF is enabled' do | ||||||
|  |     let(:work_item_assignees_selector) { '[data-testid="work-item-assignees-with-edit"]' } | ||||||
|  | 
 | ||||||
|  |     include_context 'with work_items_mvc_2', true | ||||||
|  | 
 | ||||||
|  |     it 'successfully assigns the current user by searching', | ||||||
|  |       quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/413074' do | ||||||
|  |       # The button is only when the mouse is over the input | ||||||
|  |       find_and_click_edit(work_item_assignees_selector) | ||||||
|  | 
 | ||||||
|  |       select_listbox_item(user.username) | ||||||
|  | 
 | ||||||
|  |       find("body").click | ||||||
|  |       wait_for_all_requests | ||||||
|  | 
 | ||||||
|  |       expect(work_item.assignees).to include(user) | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     it 'successfully removes all users on clear all button click' do | ||||||
|  |       find_and_click_edit(work_item_assignees_selector) | ||||||
|  | 
 | ||||||
|  |       select_listbox_item(user.username) | ||||||
|  | 
 | ||||||
|  |       find("body").click | ||||||
|  |       wait_for_requests | ||||||
|  | 
 | ||||||
|  |       find_and_click_edit(work_item_assignees_selector) | ||||||
|  | 
 | ||||||
|  |       find_and_click_clear(work_item_assignees_selector) | ||||||
|  |       wait_for_all_requests | ||||||
|  | 
 | ||||||
|  |       expect(work_item.assignees).not_to include(user) | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     it 'updates the assignee in real-time' do | ||||||
|  |       Capybara::Session.new(:other_session) | ||||||
|  | 
 | ||||||
|  |       using_session :other_session do | ||||||
|  |         visit work_items_path | ||||||
|  |         expect(work_item.reload.assignees).not_to include(user) | ||||||
|  |       end | ||||||
|  | 
 | ||||||
|  |       click_button 'assign yourself' | ||||||
|  |       wait_for_all_requests | ||||||
|  | 
 | ||||||
|  |       expect(work_item.reload.assignees).to include(user) | ||||||
|  |       using_session :other_session do | ||||||
|  |         expect(work_item.reload.assignees).to include(user) | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
|  | 
 | ||||||
| RSpec.shared_examples 'work items labels' do | RSpec.shared_examples 'work items labels' do | ||||||
|   let(:label_title_selector) { '[data-testid="labels-title"]' } |   let(:label_title_selector) { '[data-testid="labels-title"]' } | ||||||
|   let(:labels_input_selector) { '[data-testid="work-item-labels-input"]' } |   let(:labels_input_selector) { '[data-testid="work-item-labels-input"]' } | ||||||
|  | @ -391,6 +447,9 @@ end | ||||||
| RSpec.shared_examples 'work items invite members' do | RSpec.shared_examples 'work items invite members' do | ||||||
|   include Features::InviteMembersModalHelpers |   include Features::InviteMembersModalHelpers | ||||||
| 
 | 
 | ||||||
|  |   context 'when the work_items_mvc_2 FF is disabled' do | ||||||
|  |     include_context 'with work_items_mvc_2', false | ||||||
|  | 
 | ||||||
|     it 'successfully assigns the current user by searching' do |     it 'successfully assigns the current user by searching' do | ||||||
|       # The button is only when the mouse is over the input |       # The button is only when the mouse is over the input | ||||||
|       find('[data-testid="work-item-assignees-input"]').fill_in(with: 'Invite members') |       find('[data-testid="work-item-assignees-input"]').fill_in(with: 'Invite members') | ||||||
|  | @ -404,6 +463,25 @@ RSpec.shared_examples 'work items invite members' do | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|  |   context 'when the work_items_mvc_2 FF is enabled' do | ||||||
|  |     let(:work_item_assignees_selector) { '[data-testid="work-item-assignees-with-edit"]' } | ||||||
|  | 
 | ||||||
|  |     include_context 'with work_items_mvc_2', true | ||||||
|  | 
 | ||||||
|  |     it 'successfully assigns the current user by searching' do | ||||||
|  |       # The button is only when the mouse is over the input | ||||||
|  |       find_and_click_edit(work_item_assignees_selector) | ||||||
|  |       wait_for_requests | ||||||
|  | 
 | ||||||
|  |       click_link('Invite members') | ||||||
|  | 
 | ||||||
|  |       page.within invite_modal_selector do | ||||||
|  |         expect(page).to have_text("You're inviting members to the #{work_item.project.name} project") | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
|  | 
 | ||||||
| RSpec.shared_examples 'work items milestone' do | RSpec.shared_examples 'work items milestone' do | ||||||
|   context 'on work_items_mvc_2 FF off' do |   context 'on work_items_mvc_2 FF off' do | ||||||
|     include_context 'with work_items_mvc_2', false |     include_context 'with work_items_mvc_2', false | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue