diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2024-01-22 18:10:29 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2024-01-22 18:10:29 +0300 |
commit | 917d93d86da4dffd96abcfcf3aa83b0d6fa45286 (patch) | |
tree | 481ea258782443f5eaf2bd5e7dd5c1a7f900e3dd | |
parent | e5c31c104e19a08546b17b34b7f1563cce3f89e6 (diff) |
Add latest changes from gitlab-org/gitlab@master
29 files changed, 1029 insertions, 137 deletions
diff --git a/.gitlab/ci/review-apps/main.gitlab-ci.yml b/.gitlab/ci/review-apps/main.gitlab-ci.yml index a4d8d8672bb..42897cfc0a1 100644 --- a/.gitlab/ci/review-apps/main.gitlab-ci.yml +++ b/.gitlab/ci/review-apps/main.gitlab-ci.yml @@ -67,7 +67,7 @@ review-build-cng: GITLAB_IMAGE_REPOSITORY: "registry.gitlab.com/gitlab-org/build/cng-mirror" GITLAB_IMAGE_SUFFIX: "ee" 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: 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} diff --git a/app/assets/javascripts/persistent_user_callouts.js b/app/assets/javascripts/persistent_user_callouts.js index 95ef04ceb30..90f2c11dddc 100644 --- a/app/assets/javascripts/persistent_user_callouts.js +++ b/app/assets/javascripts/persistent_user_callouts.js @@ -25,6 +25,7 @@ const PERSISTENT_USER_CALLOUTS = [ '.js-new-nav-for-everyone-callout', '.js-namespace-over-storage-users-combined-alert', '.js-code-suggestions-ga-alert', + '.js-joining-a-project-alert', ]; const initCallouts = () => { diff --git a/app/assets/javascripts/work_items/components/shared/work_item_sidebar_dropdown_widget_with_edit.vue b/app/assets/javascripts/work_items/components/shared/work_item_sidebar_dropdown_widget_with_edit.vue index d94d0494ad9..21512ba6066 100644 --- a/app/assets/javascripts/work_items/components/shared/work_item_sidebar_dropdown_widget_with_edit.vue +++ b/app/assets/javascripts/work_items/components/shared/work_item_sidebar_dropdown_widget_with_edit.vue @@ -39,7 +39,7 @@ export default { default: () => [], }, itemValue: { - type: Object, + type: [Array, String], required: false, default: null, }, @@ -68,30 +68,40 @@ export default { required: false, 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() { return { isEditing: false, - localSelectedItem: this.itemValue?.id, + localSelectedItem: this.itemValue, }; }, computed: { hasValue() { - return this.itemValue != null || !isEmpty(this.item); - }, - listboxText() { - return ( - this.listItems.find(({ value }) => this.localSelectedItem === value)?.text || - this.itemValue?.title || - this.$options.i18n.none - ); + return this.multiSelect ? !isEmpty(this.itemValue) : this.itemValue !== null; }, inputId() { return `work-item-dropdown-listbox-value-${this.dropdownName}`; }, - toggleText() { - return this.toggleDropdownText || this.listboxText; - }, resetButton() { return this.resetButtonLabel || this.$options.i18n.resetButtonText; }, @@ -100,7 +110,7 @@ export default { itemValue: { handler(newVal) { if (!this.isEditing) { - this.localSelectedItem = newVal?.id; + this.localSelectedItem = newVal; } }, }, @@ -114,18 +124,25 @@ export default { }, handleItemClick(item) { this.localSelectedItem = item; - this.$emit('updateValue', item); + if (!this.multiSelect) { + this.$emit('updateValue', item); + } else { + this.$emit('updateSelected', this.localSelectedItem); + } }, onListboxShown() { this.$emit('dropdownShown'); }, onListboxHide() { this.isEditing = false; + if (this.multiSelect) { + this.$emit('updateValue', this.localSelectedItem); + } }, unassignValue() { - this.localSelectedItem = null; + this.localSelectedItem = this.multiSelect ? [] : null; this.isEditing = false; - this.$emit('updateValue', null); + this.$emit('updateValue', this.localSelectedItem); }, }, }; @@ -165,34 +182,42 @@ export default { </div> <gl-collapsible-listbox :id="inputId" + :multiple="multiSelect" block searchable start-opened is-check-centered fluid-width + :infinite-scroll="infiniteScroll" :searching="loading" :header-text="headerText" - :toggle-text="toggleText" + :toggle-text="toggleDropdownText" :no-results-text="$options.i18n.noMatchingResults" :items="listItems" :selected="localSelectedItem" :reset-button-label="resetButton" + :infinite-scroll-loading="infiniteScrollLoading" @reset="unassignValue" @search="debouncedSearchKeyUpdate" @select="handleItemClick" @shown="onListboxShown" @hidden="onListboxHide" + @bottom-reached="$emit('bottomReached')" > <template #list-item="{ item }"> <slot name="list-item" :item="item">{{ item.text }}</slot> </template> + <template v-if="showFooter" #footer> + <div class="gl-border-t-solid gl-border-t-1 gl-border-t-gray-200 gl-p-2!"> + <slot name="footer"></slot> + </div> + </template> </gl-collapsible-listbox> + {{ hasValue }} </gl-form> - <slot v-else-if="hasValue" name="readonly"> - {{ listboxText }} - </slot> - <div v-else class="gl-text-secondary"> + <slot v-else-if="hasValue" name="readonly"></slot> + <slot v-else class="gl-text-secondary" name="none"> {{ $options.i18n.none }} - </div> + </slot> </div> </template> diff --git a/app/assets/javascripts/work_items/components/work_item_assignees.vue b/app/assets/javascripts/work_items/components/work_item_assignees_inline.vue index a9aafbb3d84..a9aafbb3d84 100644 --- a/app/assets/javascripts/work_items/components/work_item_assignees.vue +++ b/app/assets/javascripts/work_items/components/work_item_assignees_inline.vue diff --git a/app/assets/javascripts/work_items/components/work_item_assignees_with_edit.vue b/app/assets/javascripts/work_items/components/work_item_assignees_with_edit.vue new file mode 100644 index 00000000000..bb7baed29f6 --- /dev/null +++ b/app/assets/javascripts/work_items/components/work_item_assignees_with_edit.vue @@ -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> diff --git a/app/assets/javascripts/work_items/components/work_item_attributes_wrapper.vue b/app/assets/javascripts/work_items/components/work_item_attributes_wrapper.vue index 79f0fdca061..76f1938c3ed 100644 --- a/app/assets/javascripts/work_items/components/work_item_attributes_wrapper.vue +++ b/app/assets/javascripts/work_items/components/work_item_attributes_wrapper.vue @@ -15,7 +15,8 @@ import { WORK_ITEM_TYPE_VALUE_TASK, } from '../constants'; 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 WorkItemMilestoneInline from './work_item_milestone_inline.vue'; import WorkItemMilestoneWithEdit from './work_item_milestone_with_edit.vue'; @@ -27,7 +28,8 @@ export default { WorkItemLabels, WorkItemMilestoneInline, WorkItemMilestoneWithEdit, - WorkItemAssignees, + WorkItemAssigneesInline, + WorkItemAssigneesWithEdit, WorkItemDueDate, WorkItemParent, WorkItemParentInline, @@ -114,17 +116,31 @@ export default { <template> <div class="work-item-attributes-wrapper"> - <work-item-assignees - v-if="workItemAssignees" - :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 v-if="workItemAssignees"> + <work-item-assignees-with-edit + v-if="glFeatures.workItemsMvc2" + class="gl-mb-5" + :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)" + /> + <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 v-if="workItemLabels" :can-update="canUpdate" diff --git a/app/assets/javascripts/work_items/components/work_item_milestone_with_edit.vue b/app/assets/javascripts/work_items/components/work_item_milestone_with_edit.vue index 87b41c9d9ea..26d98f87464 100644 --- a/app/assets/javascripts/work_items/components/work_item_milestone_with_edit.vue +++ b/app/assets/javascripts/work_items/components/work_item_milestone_with_edit.vue @@ -91,6 +91,9 @@ export default { expired, })); }, + localMilestoneId() { + return this.localMilestone?.id; + }, }, watch: { workItemMilestone(newVal) { @@ -184,7 +187,7 @@ export default { dropdown-name="milestone" :loading="isLoadingMilestones" :list-items="milestonesList" - :item-value="localMilestone" + :item-value="localMilestoneId" :update-in-progress="updateInProgress" :toggle-dropdown-text="dropdownText" :header-text="__('Select milestone')" diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 892b046e410..62eb83a77b6 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -311,7 +311,7 @@ module ApplicationHelper 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 << '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 << system_message_class diff --git a/app/models/users/callout.rb b/app/models/users/callout.rb index 8d330e4eb6e..d377a370988 100644 --- a/app/models/users/callout.rb +++ b/app/models/users/callout.rb @@ -81,7 +81,8 @@ module Users code_suggestions_ga_non_owner_alert: 79, # EE-only duo_chat_callout: 80, # 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, diff --git a/app/views/dashboard/projects/_zero_authorized_projects.html.haml b/app/views/dashboard/projects/_zero_authorized_projects.html.haml index da25dee1e88..ba4e8961f28 100644 --- a/app/views/dashboard/projects/_zero_authorized_projects.html.haml +++ b/app/views/dashboard/projects/_zero_authorized_projects.html.haml @@ -1,4 +1,5 @@ .container + = render_if_exists 'dashboard/projects/joining_a_project_alert' .gl-text-center.gl-pt-6.gl-pb-7 %h2.gl-font-size-h1{ data: { testid: 'welcome-title-content' } } = _('Welcome to GitLab') diff --git a/app/views/layouts/devise_empty.html.haml b/app/views/layouts/devise_empty.html.haml index faf45ae78ef..d7449ea5be7 100644 --- a/app/views/layouts/devise_empty.html.haml +++ b/app/views/layouts/devise_empty.html.haml @@ -1,6 +1,9 @@ - add_page_specific_style 'page_bundles/login' +- @with_header = true +- page_classes = [user_application_theme, page_class.flatten.compact] + !!! 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" %body.gl-h-full.login-page.navless{ class: "#{system_message_class} #{client_class_list}" } = header_message diff --git a/app/views/layouts/minimal.html.haml b/app/views/layouts/minimal.html.haml index e499b9ae240..fa39b6d45ba 100644 --- a/app/views/layouts/minimal.html.haml +++ b/app/views/layouts/minimal.html.haml @@ -1,3 +1,4 @@ +- @with_header = true - page_classes = page_class.push(@html_class).flatten.compact !!! 5 diff --git a/db/docs/analytics_dashboards_pointers.yml b/db/docs/analytics_dashboards_pointers.yml index b554911d3ad..37cb7e82805 100644 --- a/db/docs/analytics_dashboards_pointers.yml +++ b/db/docs/analytics_dashboards_pointers.yml @@ -3,8 +3,17 @@ table_name: analytics_dashboards_pointers classes: - Analytics::DashboardsPointer feature_categories: - - devops_reports -description: Stores project link with configuration files for Analytics Dashboards group feature. +- devops_reports +description: Stores project link with configuration files for Analytics Dashboards + group feature. introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/107673 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 diff --git a/db/post_migrate/20240118190758_remove_ignored_columns_from_geo_node_statuses.rb b/db/post_migrate/20240118190758_remove_ignored_columns_from_geo_node_statuses.rb new file mode 100644 index 00000000000..0a5f2f5d274 --- /dev/null +++ b/db/post_migrate/20240118190758_remove_ignored_columns_from_geo_node_statuses.rb @@ -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 diff --git a/db/schema_migrations/20240118190758 b/db/schema_migrations/20240118190758 new file mode 100644 index 00000000000..076999958ff --- /dev/null +++ b/db/schema_migrations/20240118190758 @@ -0,0 +1 @@ +78738644f53046494ba1f4a8e49ed9effc8147c8563e311bf3744a31a33449c6
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index a4efb7a734c..d9ceb1e6a9a 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -17301,9 +17301,6 @@ CREATE TABLE geo_node_statuses ( id integer NOT NULL, geo_node_id integer NOT NULL, 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_date timestamp without time zone, cursor_last_event_id bigint, @@ -17315,19 +17312,10 @@ CREATE TABLE geo_node_statuses ( replication_slots_count integer, replication_slots_used_count integer, replication_slots_max_retained_wal_bytes bigint, - job_artifacts_count integer, - job_artifacts_synced_count integer, - job_artifacts_failed_count integer, version 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, 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 ); diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index c63b1cf0352..0799c002a73 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -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="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="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="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. | diff --git a/doc/ci/components/index.md b/doc/ci/components/index.md index 830c91616f7..727baa9be18 100644 --- a/doc/ci/components/index.md +++ b/doc/ci/components/index.md @@ -337,6 +337,15 @@ create-release: After committing and pushing changes, the pipeline tests the component, then creates 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](../yaml/index.md#global-keywords) in a component. diff --git a/doc/subscriptions/self_managed/index.md b/doc/subscriptions/self_managed/index.md index 797a59dded6..6cc50103c75 100644 --- a/doc/subscriptions/self_managed/index.md +++ b/doc/subscriptions/self_managed/index.md @@ -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. 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). ## Renew your subscription diff --git a/locale/gitlab.pot b/locale/gitlab.pot index b025513cce9..697a2af7ba7 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -33944,6 +33944,12 @@ msgstr "" msgid "OnDemandScans|at" 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}." 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." msgstr "" +msgid "WorkItem|%{usersLength} assignees" +msgstr "" + msgid "WorkItem|%{workItemType} deleted" msgstr "" @@ -55797,6 +55806,9 @@ msgstr "" msgid "WorkItem|New task" 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." msgstr "" diff --git a/spec/features/projects/work_items/work_item_spec.rb b/spec/features/projects/work_items/work_item_spec.rb index 33153d21575..9ce068c2ea1 100644 --- a/spec/features/projects/work_items/work_item_spec.rb +++ b/spec/features/projects/work_items/work_item_spec.rb @@ -41,23 +41,62 @@ RSpec.describe 'Work item', :js, feature_category: :team_planning do expect(page).to have_button _('More actions') end - it 'reassigns to another user', - quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/413074' do - find('[data-testid="work-item-assignees-input"]').fill_in(with: user.username) - wait_for_requests + context 'when work_items_mvc_2 is disabled' do + before do + stub_feature_flags(work_items_mvc_2: false) - send_keys(:enter) - find("body").click - wait_for_requests + page.refresh + wait_for_all_requests + end + + it 'reassigns to another user', + quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/413074' do + find('[data-testid="work-item-assignees-input"]').fill_in(with: user.username) + wait_for_requests + + send_keys(:enter) + find("body").click + wait_for_requests + + find('[data-testid="work-item-assignees-input"]').fill_in(with: user2.username) + wait_for_requests + + send_keys(:enter) + find("body").click + wait_for_requests + + expect(work_item.reload.assignees).to include(user2) + 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) - find('[data-testid="work-item-assignees-input"]').fill_in(with: user2.username) - wait_for_requests + wait_for_requests - send_keys(:enter) - find("body").click - wait_for_requests + within('[data-testid="work-item-assignees-with-edit"]') do + click_button 'Edit' + end - expect(work_item.reload.assignees).to include(user2) + select_listbox_item(user2.username) + + wait_for_requests + + expect(work_item.reload.assignees).to include(user2) + end end it_behaves_like 'work items title' @@ -118,9 +157,33 @@ RSpec.describe 'Work item', :js, feature_category: :team_planning do expect(page).to have_selector('[data-testid="award-button"].disabled') end - it 'assignees input field is disabled' do - within('[data-testid="work-item-assignees-input"]') do - expect(page).to have_field(type: 'text', disabled: true) + 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 + within('[data-testid="work-item-assignees-input"]') do + expect(page).to have_field(type: 'text', disabled: true) + 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 diff --git a/spec/frontend/work_items/components/shared/work_item_sidebar_dropdown_widget_with_edit_spec.js b/spec/frontend/work_items/components/shared/work_item_sidebar_dropdown_widget_with_edit_spec.js index 171493e87f8..702c0ab1a6d 100644 --- a/spec/frontend/work_items/components/shared/work_item_sidebar_dropdown_widget_with_edit_spec.js +++ b/spec/frontend/work_items/components/shared/work_item_sidebar_dropdown_widget_with_edit_spec.js @@ -21,6 +21,11 @@ describe('WorkItemSidebarDropdownWidgetWithEdit component', () => { canUpdate = true, isEditing = false, updateInProgress = false, + showFooter = false, + slots = {}, + multiSelect = false, + infiniteScroll = false, + infiniteScrollLoading = false, } = {}) => { wrapper = mountExtended(WorkItemSidebarDropdownWidgetWithEdit, { propsData: { @@ -31,7 +36,12 @@ describe('WorkItemSidebarDropdownWidgetWithEdit component', () => { canUpdate, updateInProgress, headerText: __('Select iteration'), + showFooter, + multiSelect, + infiniteScroll, + infiniteScrollLoading, }, + slots, }); if (isEditing) { @@ -152,10 +162,41 @@ describe('WorkItemSidebarDropdownWidgetWithEdit component', () => { searching: false, infiniteScroll: false, noResultsText: 'No matching results', - toggleText: 'None', searchPlaceholder: 'Search', 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); + }); }); }); diff --git a/spec/frontend/work_items/components/work_item_assignees_spec.js b/spec/frontend/work_items/components/work_item_assignees_inline_spec.js index b34eed21c60..ae7828b6c48 100644 --- a/spec/frontend/work_items/components/work_item_assignees_spec.js +++ b/spec/frontend/work_items/components/work_item_assignees_inline_spec.js @@ -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 InviteMembersTrigger from '~/invite_members/components/invite_members_trigger.vue'; 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 { i18n, DEFAULT_PAGE_SIZE_ASSIGNEES, @@ -35,7 +35,7 @@ Vue.use(VueApollo); const workItemId = 'gid://gitlab/WorkItem/1'; const dropdownItems = projectMembersResponseWithCurrentUser.data.workspace.users.nodes; -describe('WorkItemAssignees component', () => { +describe('WorkItemAssigneesInline component', () => { let wrapper; const findAssigneeLinks = () => wrapper.findAllComponents(GlLink); @@ -88,7 +88,7 @@ describe('WorkItemAssignees component', () => { [updateWorkItemMutation, updateWorkItemMutationHandler], ]); - wrapper = mountExtended(WorkItemAssignees, { + wrapper = mountExtended(WorkItemAssigneesInline, { provide: { isGroup, }, diff --git a/spec/frontend/work_items/components/work_item_assignees_with_edit_spec.js b/spec/frontend/work_items/components/work_item_assignees_with_edit_spec.js new file mode 100644 index 00000000000..f9c875fa124 --- /dev/null +++ b/spec/frontend/work_items/components/work_item_assignees_with_edit_spec.js @@ -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', + }); + }); + }); +}); diff --git a/spec/frontend/work_items/components/work_item_attributes_wrapper_spec.js b/spec/frontend/work_items/components/work_item_attributes_wrapper_spec.js index 43f7027406f..438b975b120 100644 --- a/spec/frontend/work_items/components/work_item_attributes_wrapper_spec.js +++ b/spec/frontend/work_items/components/work_item_attributes_wrapper_spec.js @@ -1,6 +1,6 @@ import { nextTick } from 'vue'; 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 WorkItemLabels from '~/work_items/components/work_item_labels.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 findWorkItemDueDate = () => wrapper.findComponent(WorkItemDueDate); - const findWorkItemAssignees = () => wrapper.findComponent(WorkItemAssignees); + const findWorkItemAssignees = () => wrapper.findComponent(WorkItemAssigneesWithEdit); const findWorkItemLabels = () => wrapper.findComponent(WorkItemLabels); const findWorkItemMilestone = () => wrapper.findComponent(WorkItemMilestoneWithEdit); const findWorkItemMilestoneInline = () => wrapper.findComponent(WorkItemMilestoneInline); diff --git a/spec/frontend/work_items/components/work_item_milestone_with_edit_spec.js b/spec/frontend/work_items/components/work_item_milestone_with_edit_spec.js index d6ad7b6887b..3efc04719e0 100644 --- a/spec/frontend/work_items/components/work_item_milestone_with_edit_spec.js +++ b/spec/frontend/work_items/components/work_item_milestone_with_edit_spec.js @@ -150,7 +150,7 @@ describe('WorkItemMilestoneWithEdit component', () => { await nextTick(); - expect(findSidebarDropdownWidget().props('itemValue').title).toBe(milestoneAtIndex.title); + expect(findSidebarDropdownWidget().props('itemValue')).toBe(milestoneAtIndex.id); }); }); diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index 2445689bf9f..3d9996616d8 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -701,17 +701,31 @@ RSpec.describe ApplicationHelper do end describe 'with-header' do - context 'when current_user' do + context 'when @with_header is falsey' do before do - allow(helper).to receive(:current_user).and_return(user) + helper.instance_variable_set(:@with_header, nil) + end + + context 'when current_user' do + before do + allow(helper).to receive(:current_user).and_return(user) + end + + it { is_expected.not_to include('with-header') } end - it { is_expected.not_to include('with-header') } + context 'when no current_user' do + before do + allow(helper).to receive(:current_user).and_return(nil) + end + + it { is_expected.to include('with-header') } + end end - context 'when no current_user' do + context 'when @with_header is true' do before do - allow(helper).to receive(:current_user).and_return(nil) + helper.instance_variable_set(:@with_header, true) end it { is_expected.to include('with-header') } diff --git a/spec/lib/gitlab/auth/current_user_mode_spec.rb b/spec/lib/gitlab/auth/current_user_mode_spec.rb index 0a68a4a0ae2..650af6af229 100644 --- a/spec/lib/gitlab/auth/current_user_mode_spec.rb +++ b/spec/lib/gitlab/auth/current_user_mode_spec.rb @@ -2,7 +2,7 @@ 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) } subject { described_class.new(user) } diff --git a/spec/support/shared_examples/features/work_items_shared_examples.rb b/spec/support/shared_examples/features/work_items_shared_examples.rb index 0f35681ca7d..4f36d8a046c 100644 --- a/spec/support/shared_examples/features/work_items_shared_examples.rb +++ b/spec/support/shared_examples/features/work_items_shared_examples.rb @@ -167,69 +167,125 @@ RSpec.shared_examples 'work items comments' do |type| end RSpec.shared_examples 'work items assignees' do - 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('[data-testid="work-item-assignees-input"]').fill_in(with: user.username) - wait_for_requests - # submit and simulate blur to save - send_keys(:enter) - find("body").click - wait_for_requests + context 'when the work_items_mvc_2 FF is disabled' do + include_context 'with work_items_mvc_2', false - expect(work_item.reload.assignees).to include(user) - end + 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('[data-testid="work-item-assignees-input"]').fill_in(with: user.username) + wait_for_requests + # submit and simulate blur to save + send_keys(:enter) + find("body").click + wait_for_requests - it 'successfully assigns the current user by clicking `Assign myself` button' do - find('[data-testid="work-item-assignees-input"]').hover - click_button _('Assign yourself') + expect(work_item.reload.assignees).to include(user) + end - expect(work_item.reload.assignees).to include(user) - end + it 'successfully assigns the current user by clicking `Assign myself` button' do + find('[data-testid="work-item-assignees-input"]').hover + click_button _('Assign yourself') - it 'successfully removes all users on clear all button click' do - find('[data-testid="work-item-assignees-input"]').hover - click_button _('Assign yourself') + expect(work_item.reload.assignees).to include(user) + end - expect(work_item.reload.assignees).to include(user) + it 'successfully removes all users on clear all button click' do + find('[data-testid="work-item-assignees-input"]').hover + click_button _('Assign yourself') - find('[data-testid="work-item-assignees-input"]').click - click_button 'Clear all' - find("body").click - wait_for_requests + expect(work_item.reload.assignees).to include(user) - expect(work_item.reload.assignees).not_to include(user) - end + find('[data-testid="work-item-assignees-input"]').click + click_button 'Clear all' + find("body").click + wait_for_requests - it 'successfully removes user on clicking badge cross button' do - find('[data-testid="work-item-assignees-input"]').hover - click_button _('Assign yourself') + expect(work_item.reload.assignees).not_to include(user) + end + + it 'successfully removes user on clicking badge cross button' do + find('[data-testid="work-item-assignees-input"]').hover + click_button _('Assign yourself') + + expect(work_item.reload.assignees).to include(user) - expect(work_item.reload.assignees).to include(user) + within('[data-testid="work-item-assignees-input"]') do + click_button 'Close' + end + find("body").click + wait_for_requests - within('[data-testid="work-item-assignees-input"]') do - click_button 'Close' + expect(work_item.reload.assignees).not_to include(user) end - find("body").click - wait_for_requests - expect(work_item.reload.assignees).not_to include(user) + 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 + + find('[data-testid="work-item-assignees-input"]').hover + click_button _('Assign yourself') + + expect(work_item.reload.assignees).to include(user) + using_session :other_session do + expect(work_item.reload.assignees).to include(user) + end + end end - it 'updates the assignee in real-time' do - Capybara::Session.new(:other_session) + context 'when the work_items_mvc_2 FF is enabled' do + let(:work_item_assignees_selector) { '[data-testid="work-item-assignees-with-edit"]' } - using_session :other_session do - visit work_items_path - expect(work_item.reload.assignees).not_to include(user) + 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 - find('[data-testid="work-item-assignees-input"]').hover - click_button _('Assign yourself') + 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) + using_session :other_session do + expect(work_item.reload.assignees).to include(user) + end end end end @@ -391,15 +447,37 @@ end RSpec.shared_examples 'work items invite members' do include Features::InviteMembersModalHelpers - it 'successfully assigns the current user by searching' do - # The button is only when the mouse is over the input - find('[data-testid="work-item-assignees-input"]').fill_in(with: 'Invite members') - wait_for_requests + 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 + # The button is only when the mouse is over the input + find('[data-testid="work-item-assignees-input"]').fill_in(with: 'Invite members') + wait_for_requests + + click_button('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 - click_button('Invite members') + 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 - page.within invite_modal_selector do - expect(page).to have_text("You're inviting members to the #{work_item.project.name} project") + 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 |