Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2024-01-22 15:10:29 +00:00
parent e5c31c104e
commit 917d93d86d
29 changed files with 1037 additions and 145 deletions

View File

@ -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}

View File

@ -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 = () => {

View File

@ -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>

View File

@ -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>

View File

@ -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"

View File

@ -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')"

View File

@ -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

View File

@ -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,

View File

@ -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')

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1 @@
78738644f53046494ba1f4a8e49ed9effc8147c8563e311bf3744a31a33449c6

View File

@ -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
); );

View File

@ -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. |

View File

@ -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.

View File

@ -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

View File

@ -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 ""

View File

@ -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

View File

@ -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);
});
}); });
}); });

View File

@ -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,
}, },

View File

@ -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',
});
});
});
});

View File

@ -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);

View File

@ -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);
}); });
}); });

View File

@ -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

View File

@ -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) }

View File

@ -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