Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
e5c31c104e
commit
917d93d86d
|
|
@ -67,7 +67,7 @@ review-build-cng:
|
|||
GITLAB_IMAGE_REPOSITORY: "registry.gitlab.com/gitlab-org/build/cng-mirror"
|
||||
GITLAB_IMAGE_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}
|
||||
|
|
|
|||
|
|
@ -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 = () => {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
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>
|
||||
</gl-collapsible-listbox>
|
||||
</gl-form>
|
||||
<slot v-else-if="hasValue" name="readonly">
|
||||
{{ listboxText }}
|
||||
</slot>
|
||||
<div v-else class="gl-text-secondary">
|
||||
{{ $options.i18n.none }}
|
||||
</div>
|
||||
<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"></slot>
|
||||
<slot v-else class="gl-text-secondary" name="none">
|
||||
{{ $options.i18n.none }}
|
||||
</slot>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,292 @@
|
|||
<script>
|
||||
import { GlButton } from '@gitlab/ui';
|
||||
import { isEmpty } from 'lodash';
|
||||
import currentUserQuery from '~/graphql_shared/queries/current_user.query.graphql';
|
||||
import groupUsersSearchQuery from '~/graphql_shared/queries/group_users_search.query.graphql';
|
||||
import usersSearchQuery from '~/graphql_shared/queries/users_search.query.graphql';
|
||||
import InviteMembersTrigger from '~/invite_members/components/invite_members_trigger.vue';
|
||||
import SidebarParticipant from '~/sidebar/components/assignees/sidebar_participant.vue';
|
||||
import UncollapsedAssigneeList from '~/sidebar/components/assignees/uncollapsed_assignee_list.vue';
|
||||
import WorkItemSidebarDropdownWidgetWithEdit from '~/work_items/components/shared/work_item_sidebar_dropdown_widget_with_edit.vue';
|
||||
import { s__, sprintf, __ } from '~/locale';
|
||||
import Tracking from '~/tracking';
|
||||
import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql';
|
||||
import { i18n, TRACKING_CATEGORY_SHOW, DEFAULT_PAGE_SIZE_ASSIGNEES } from '../constants';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
WorkItemSidebarDropdownWidgetWithEdit,
|
||||
InviteMembersTrigger,
|
||||
SidebarParticipant,
|
||||
GlButton,
|
||||
UncollapsedAssigneeList,
|
||||
},
|
||||
mixins: [Tracking.mixin()],
|
||||
inject: ['isGroup'],
|
||||
props: {
|
||||
fullPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
workItemId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
assignees: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
allowsMultipleAssignees: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
workItemType: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
canUpdate: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
canInviteMembers: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
localAssigneeIds: this.assignees.map(({ id }) => id),
|
||||
searchStarted: false,
|
||||
searchKey: '',
|
||||
users: {
|
||||
nodes: [],
|
||||
},
|
||||
currentUser: null,
|
||||
isLoadingMore: false,
|
||||
updateInProgress: false,
|
||||
};
|
||||
},
|
||||
apollo: {
|
||||
users: {
|
||||
query() {
|
||||
return this.isGroup ? groupUsersSearchQuery : usersSearchQuery;
|
||||
},
|
||||
variables() {
|
||||
return {
|
||||
fullPath: this.fullPath,
|
||||
search: this.searchKey,
|
||||
first: DEFAULT_PAGE_SIZE_ASSIGNEES,
|
||||
};
|
||||
},
|
||||
skip() {
|
||||
return !this.searchStarted;
|
||||
},
|
||||
update(data) {
|
||||
return data.workspace?.users;
|
||||
},
|
||||
error() {
|
||||
this.$emit('error', i18n.fetchError);
|
||||
},
|
||||
},
|
||||
currentUser: {
|
||||
query: currentUserQuery,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
searchUsers() {
|
||||
return this.users.nodes.map(({ user }) => ({
|
||||
...user,
|
||||
value: user.id,
|
||||
text: user.name,
|
||||
}));
|
||||
},
|
||||
pageInfo() {
|
||||
return this.users.pageInfo;
|
||||
},
|
||||
tracking() {
|
||||
return {
|
||||
category: TRACKING_CATEGORY_SHOW,
|
||||
label: 'item_assignees',
|
||||
property: `type_${this.workItemType}`,
|
||||
};
|
||||
},
|
||||
isLoadingUsers() {
|
||||
return this.$apollo.queries.users.loading && !this.isLoadingMore;
|
||||
},
|
||||
hasNextPage() {
|
||||
return this.pageInfo?.hasNextPage;
|
||||
},
|
||||
selectedAssigneeIds() {
|
||||
return this.allowsMultipleAssignees ? this.localAssigneeIds : this.localAssigneeIds[0];
|
||||
},
|
||||
dropdownText() {
|
||||
if (this.localAssigneeIds.length === 0) {
|
||||
return s__('WorkItem|No assignees');
|
||||
}
|
||||
|
||||
return this.localAssigneeIds.length === 1
|
||||
? this.localAssignees.map(({ name }) => name).join(', ')
|
||||
: sprintf(s__('WorkItem|%{usersLength} assignees'), {
|
||||
usersLength: this.localAssigneeIds.length,
|
||||
});
|
||||
},
|
||||
dropdownLabel() {
|
||||
return this.allowsMultipleAssignees ? __('Assignees') : __('Assignee');
|
||||
},
|
||||
headerText() {
|
||||
return this.allowsMultipleAssignees ? __('Select assignees') : __('Select assignee');
|
||||
},
|
||||
filteredAssignees() {
|
||||
return isEmpty(this.searchUsers)
|
||||
? this.assignees
|
||||
: this.searchUsers.filter(({ id }) => this.localAssigneeIds.includes(id));
|
||||
},
|
||||
localAssignees() {
|
||||
return this.filteredAssignees || [];
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
assignees: {
|
||||
handler(newVal) {
|
||||
this.localAssigneeIds = newVal.map(({ id }) => id);
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleAssigneesInput(assignees) {
|
||||
this.setLocalAssigneeIdsOnEvent(assignees);
|
||||
this.setAssignees();
|
||||
},
|
||||
handleAssigneeClick(assignees) {
|
||||
this.setLocalAssigneeIdsOnEvent(assignees);
|
||||
},
|
||||
async setAssignees() {
|
||||
this.updateInProgress = true;
|
||||
const { localAssigneeIds } = this;
|
||||
|
||||
try {
|
||||
const {
|
||||
data: {
|
||||
workItemUpdate: { errors },
|
||||
},
|
||||
} = await this.$apollo.mutate({
|
||||
mutation: updateWorkItemMutation,
|
||||
variables: {
|
||||
input: {
|
||||
id: this.workItemId,
|
||||
assigneesWidget: {
|
||||
assigneeIds: localAssigneeIds,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
if (errors.length > 0) {
|
||||
this.throwUpdateError();
|
||||
return;
|
||||
}
|
||||
this.track('updated_assignees');
|
||||
} catch {
|
||||
this.throwUpdateError();
|
||||
} finally {
|
||||
this.updateInProgress = false;
|
||||
}
|
||||
},
|
||||
setLocalAssigneeIdsOnEvent(assignees) {
|
||||
const singleSelectAssignee = assignees === null ? [] : [assignees];
|
||||
this.localAssigneeIds = this.allowsMultipleAssignees ? assignees : singleSelectAssignee;
|
||||
},
|
||||
async fetchMoreAssignees() {
|
||||
if (this.isLoadingMore && !this.hasNextPage) return;
|
||||
|
||||
this.isLoadingMore = true;
|
||||
await this.$apollo.queries.users.fetchMore({
|
||||
variables: {
|
||||
after: this.pageInfo.endCursor,
|
||||
first: DEFAULT_PAGE_SIZE_ASSIGNEES,
|
||||
},
|
||||
});
|
||||
this.isLoadingMore = false;
|
||||
},
|
||||
setSearchKey(value) {
|
||||
this.searchKey = value;
|
||||
this.searchStarted = true;
|
||||
},
|
||||
assignToCurrentUser() {
|
||||
const assignees = this.allowsMultipleAssignees ? [this.currentUser.id] : this.currentUser.id;
|
||||
this.setLocalAssigneeIdsOnEvent(assignees);
|
||||
this.setAssignees();
|
||||
},
|
||||
throwUpdateError() {
|
||||
this.$emit('error', i18n.updateError);
|
||||
// If mutation is rejected, we're rolling back to initial state
|
||||
this.localAssigneeIds = this.assignees.map(({ id }) => id);
|
||||
},
|
||||
onDropdownShown() {
|
||||
this.searchStarted = true;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<work-item-sidebar-dropdown-widget-with-edit
|
||||
:multi-select="allowsMultipleAssignees"
|
||||
class="issuable-assignees gl-mt-2"
|
||||
:dropdown-label="dropdownLabel"
|
||||
:can-update="canUpdate"
|
||||
dropdown-name="assignees"
|
||||
show-footer
|
||||
:infinite-scroll="hasNextPage"
|
||||
:infinite-scroll-loading="isLoadingMore"
|
||||
:loading="isLoadingUsers"
|
||||
:list-items="searchUsers"
|
||||
:item-value="selectedAssigneeIds"
|
||||
:toggle-dropdown-text="dropdownText"
|
||||
:header-text="headerText"
|
||||
:update-in-progress="updateInProgress"
|
||||
:reset-button-label="__('Clear')"
|
||||
data-testid="work-item-assignees-with-edit"
|
||||
@dropdownShown="onDropdownShown"
|
||||
@searchStarted="setSearchKey"
|
||||
@updateValue="handleAssigneesInput"
|
||||
@updateSelected="handleAssigneeClick"
|
||||
@bottomReached="fetchMoreAssignees"
|
||||
>
|
||||
<template #list-item="{ item }">
|
||||
<sidebar-participant :user="item" />
|
||||
</template>
|
||||
<template v-if="canInviteMembers" #footer>
|
||||
<gl-button category="tertiary" block class="gl-justify-content-start!">
|
||||
<invite-members-trigger
|
||||
:display-text="__('Invite members')"
|
||||
trigger-element="side-nav"
|
||||
icon="plus"
|
||||
trigger-source="work-item-assignees-with-edit"
|
||||
classes="gl-hover-text-decoration-none! gl-pb-2"
|
||||
/>
|
||||
</gl-button>
|
||||
</template>
|
||||
<template #none>
|
||||
<div class="gl-display-flex gl-align-items-center gl-text-gray-500 gl-gap-2">
|
||||
<span>{{ __('None') }}</span>
|
||||
<template v-if="currentUser && canUpdate">
|
||||
<span>-</span>
|
||||
<gl-button variant="link" data-testid="assign-self" @click.stop="assignToCurrentUser"
|
||||
><span class="gl-text-gray-500 gl-hover-text-blue-800">{{
|
||||
__('assign yourself')
|
||||
}}</span></gl-button
|
||||
>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<template #readonly>
|
||||
<uncollapsed-assignee-list
|
||||
:users="localAssignees"
|
||||
show-less-assignees-class="gl-hover-bg-transparent!"
|
||||
/>
|
||||
</template>
|
||||
</work-item-sidebar-dropdown-widget-with-edit>
|
||||
</template>
|
||||
|
|
@ -15,7 +15,8 @@ import {
|
|||
WORK_ITEM_TYPE_VALUE_TASK,
|
||||
} 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,8 +116,10 @@ export default {
|
|||
|
||||
<template>
|
||||
<div class="work-item-attributes-wrapper">
|
||||
<work-item-assignees
|
||||
v-if="workItemAssignees"
|
||||
<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"
|
||||
|
|
@ -125,6 +129,18 @@ export default {
|
|||
: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"
|
||||
|
|
|
|||
|
|
@ -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')"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
- @with_header = true
|
||||
- page_classes = page_class.push(@html_class).flatten.compact
|
||||
|
||||
!!! 5
|
||||
|
|
|
|||
|
|
@ -4,7 +4,16 @@ classes:
|
|||
- Analytics::DashboardsPointer
|
||||
feature_categories:
|
||||
- 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
|
||||
milestone: '15.8'
|
||||
gitlab_schema: gitlab_main
|
||||
gitlab_schema: gitlab_main_cell
|
||||
allow_cross_joins:
|
||||
- gitlab_main_clusterwide
|
||||
allow_cross_transactions:
|
||||
- gitlab_main_clusterwide
|
||||
allow_cross_foreign_keys:
|
||||
- gitlab_main_clusterwide
|
||||
sharding_key:
|
||||
target_project_id: projects
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class RemoveIgnoredColumnsFromGeoNodeStatuses < Gitlab::Database::Migration[2.2]
|
||||
disable_ddl_transaction!
|
||||
|
||||
milestone '16.9'
|
||||
|
||||
IGNORED_COLLUMNS = [
|
||||
:container_repositories_count,
|
||||
:container_repositories_failed_count,
|
||||
:container_repositories_registry_count,
|
||||
:container_repositories_synced_count,
|
||||
:job_artifacts_count,
|
||||
:job_artifacts_failed_count,
|
||||
:job_artifacts_synced_count,
|
||||
:job_artifacts_synced_missing_on_primary_count,
|
||||
:lfs_objects_count,
|
||||
:lfs_objects_failed_count,
|
||||
:lfs_objects_synced_count,
|
||||
:lfs_objects_synced_missing_on_primary_count
|
||||
]
|
||||
|
||||
def up
|
||||
IGNORED_COLLUMNS.each do |column_name|
|
||||
remove_column :geo_node_statuses, column_name, if_exists: true
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
IGNORED_COLLUMNS.each do |column_name|
|
||||
add_column :geo_node_statuses, column_name, :integer, if_not_exists: true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1 @@
|
|||
78738644f53046494ba1f4a8e49ed9effc8147c8563e311bf3744a31a33449c6
|
||||
|
|
@ -17301,9 +17301,6 @@ CREATE TABLE geo_node_statuses (
|
|||
id integer NOT NULL,
|
||||
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
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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. |
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 ""
|
||||
|
||||
|
|
|
|||
|
|
@ -41,6 +41,14 @@ RSpec.describe 'Work item', :js, feature_category: :team_planning do
|
|||
expect(page).to have_button _('More actions')
|
||||
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',
|
||||
quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/413074' do
|
||||
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)
|
||||
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 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')
|
||||
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
|
||||
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
|
||||
|
||||
it 'labels input field is disabled' do
|
||||
within('[data-testid="work-item-labels-input"]') do
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
|
|
@ -0,0 +1,300 @@
|
|||
import Vue, { nextTick } from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import WorkItemAssignees from '~/work_items/components/work_item_assignees_with_edit.vue';
|
||||
import WorkItemSidebarDropdownWidgetWithEdit from '~/work_items/components/shared/work_item_sidebar_dropdown_widget_with_edit.vue';
|
||||
import groupUsersSearchQuery from '~/graphql_shared/queries/group_users_search.query.graphql';
|
||||
import usersSearchQuery from '~/graphql_shared/queries/users_search.query.graphql';
|
||||
import currentUserQuery from '~/graphql_shared/queries/current_user.query.graphql';
|
||||
import InviteMembersTrigger from '~/invite_members/components/invite_members_trigger.vue';
|
||||
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import { mockTracking } from 'helpers/tracking_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import {
|
||||
projectMembersResponseWithCurrentUser,
|
||||
mockAssignees,
|
||||
currentUserResponse,
|
||||
currentUserNullResponse,
|
||||
updateWorkItemMutationResponse,
|
||||
projectMembersResponseWithCurrentUserWithNextPage,
|
||||
projectMembersResponseWithNoMatchingUsers,
|
||||
} from 'jest/work_items/mock_data';
|
||||
import { DEFAULT_PAGE_SIZE_ASSIGNEES, i18n, TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
|
||||
|
||||
const workItemId = 'gid://gitlab/WorkItem/1';
|
||||
|
||||
describe('WorkItemAssigneesWithEdit component', () => {
|
||||
Vue.use(VueApollo);
|
||||
|
||||
let wrapper;
|
||||
|
||||
const findInviteMembersTrigger = () => wrapper.findComponent(InviteMembersTrigger);
|
||||
const findAssignSelfButton = () => wrapper.findByTestId('assign-self');
|
||||
const findSidebarDropdownWidget = () =>
|
||||
wrapper.findComponent(WorkItemSidebarDropdownWidgetWithEdit);
|
||||
|
||||
const successSearchQueryHandler = jest
|
||||
.fn()
|
||||
.mockResolvedValue(projectMembersResponseWithCurrentUser);
|
||||
const successGroupSearchQueryHandler = jest
|
||||
.fn()
|
||||
.mockResolvedValue(projectMembersResponseWithCurrentUser);
|
||||
const successSearchQueryHandlerWithMoreAssignees = jest
|
||||
.fn()
|
||||
.mockResolvedValue(projectMembersResponseWithCurrentUserWithNextPage);
|
||||
const successCurrentUserQueryHandler = jest.fn().mockResolvedValue(currentUserResponse);
|
||||
const noCurrentUserQueryHandler = jest.fn().mockResolvedValue(currentUserNullResponse);
|
||||
const successUpdateWorkItemMutationHandler = jest
|
||||
.fn()
|
||||
.mockResolvedValue(updateWorkItemMutationResponse);
|
||||
const successSearchWithNoMatchingUsers = jest
|
||||
.fn()
|
||||
.mockResolvedValue(projectMembersResponseWithNoMatchingUsers);
|
||||
|
||||
const errorHandler = jest.fn().mockRejectedValue('Houston, we have a problem');
|
||||
|
||||
const showDropdown = () => {
|
||||
findSidebarDropdownWidget().vm.$emit('dropdownShown');
|
||||
};
|
||||
|
||||
const createComponent = ({
|
||||
assignees = mockAssignees,
|
||||
searchQueryHandler = successSearchQueryHandler,
|
||||
currentUserQueryHandler = successCurrentUserQueryHandler,
|
||||
allowsMultipleAssignees = false,
|
||||
canInviteMembers = false,
|
||||
canUpdate = true,
|
||||
} = {}) => {
|
||||
const apolloProvider = createMockApollo([
|
||||
[usersSearchQuery, searchQueryHandler],
|
||||
[groupUsersSearchQuery, successGroupSearchQueryHandler],
|
||||
[currentUserQuery, currentUserQueryHandler],
|
||||
[updateWorkItemMutation, successUpdateWorkItemMutationHandler],
|
||||
]);
|
||||
|
||||
wrapper = shallowMountExtended(WorkItemAssignees, {
|
||||
provide: {
|
||||
isGroup: false,
|
||||
},
|
||||
propsData: {
|
||||
assignees,
|
||||
fullPath: 'test-project-path',
|
||||
workItemId,
|
||||
allowsMultipleAssignees,
|
||||
workItemType: 'Task',
|
||||
canUpdate,
|
||||
canInviteMembers,
|
||||
},
|
||||
apolloProvider,
|
||||
});
|
||||
};
|
||||
|
||||
it('has "Assignee" label for single select', () => {
|
||||
createComponent();
|
||||
|
||||
expect(findSidebarDropdownWidget().props('dropdownLabel')).toBe('Assignee');
|
||||
});
|
||||
|
||||
describe('Dropdown search', () => {
|
||||
it('shows no items in the dropdown when no results matching', async () => {
|
||||
createComponent({ searchQueryHandler: successSearchWithNoMatchingUsers });
|
||||
showDropdown();
|
||||
await waitForPromises();
|
||||
|
||||
expect(findSidebarDropdownWidget().props('listItems')).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('emits error event if search users query fails', async () => {
|
||||
createComponent({ searchQueryHandler: errorHandler });
|
||||
showDropdown();
|
||||
await waitForPromises();
|
||||
|
||||
expect(wrapper.emitted('error')).toEqual([[i18n.fetchError]]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when assigning to current user', () => {
|
||||
it('does not show `Assign yourself` button if current user is loading', () => {
|
||||
createComponent();
|
||||
|
||||
expect(findAssignSelfButton().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('does now show `Assign yourself` button if user is not logged in', async () => {
|
||||
createComponent({ currentUserQueryHandler: noCurrentUserQueryHandler, assignees: [] });
|
||||
await waitForPromises();
|
||||
|
||||
expect(findAssignSelfButton().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Dropdown options', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({ canUpdate: true });
|
||||
});
|
||||
|
||||
it('calls successSearchQueryHandler with variables when dropdown is opened', async () => {
|
||||
showDropdown();
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(successSearchQueryHandler).toHaveBeenCalledWith({
|
||||
first: DEFAULT_PAGE_SIZE_ASSIGNEES,
|
||||
fullPath: 'test-project-path',
|
||||
search: '',
|
||||
});
|
||||
});
|
||||
|
||||
it('shows the skeleton loader when the items are being fetched on click', async () => {
|
||||
showDropdown();
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(findSidebarDropdownWidget().props('loading')).toBe(true);
|
||||
});
|
||||
|
||||
it('shows the iterations in dropdown when the items have finished fetching', async () => {
|
||||
showDropdown();
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(findSidebarDropdownWidget().props('loading')).toBe(false);
|
||||
expect(findSidebarDropdownWidget().props('listItems')).toHaveLength(
|
||||
projectMembersResponseWithCurrentUser.data.workspace.users.nodes.length,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when user is logged in and there are no assignees', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({ assignees: [] });
|
||||
return waitForPromises();
|
||||
});
|
||||
|
||||
it('renders `Assign yourself` button', () => {
|
||||
expect(findAssignSelfButton().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('calls update work item assignees mutation with current user as a variable on button click', async () => {
|
||||
const { currentUser } = currentUserResponse.data;
|
||||
findAssignSelfButton().vm.$emit('click', new MouseEvent('click'));
|
||||
await nextTick();
|
||||
|
||||
expect(successUpdateWorkItemMutationHandler).toHaveBeenCalledWith({
|
||||
input: {
|
||||
id: workItemId,
|
||||
assigneesWidget: {
|
||||
assigneeIds: [currentUser.id],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when multiple assignees are allowed', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({ allowsMultipleAssignees: true, assignees: [] });
|
||||
return waitForPromises();
|
||||
});
|
||||
|
||||
it('renders `Assignees` as label and `Select assignees` as dropdown button header', () => {
|
||||
expect(findSidebarDropdownWidget().props()).toMatchObject({
|
||||
dropdownLabel: 'Assignees',
|
||||
headerText: 'Select assignees',
|
||||
});
|
||||
});
|
||||
|
||||
it('adds multiple assignees when collapsible listbox provides multiple values', async () => {
|
||||
showDropdown();
|
||||
await waitForPromises();
|
||||
|
||||
findSidebarDropdownWidget().vm.$emit('updateValue', [
|
||||
'gid://gitlab/User/5',
|
||||
'gid://gitlab/User/6',
|
||||
]);
|
||||
await nextTick();
|
||||
|
||||
expect(findSidebarDropdownWidget().props('itemValue')).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('tracking', () => {
|
||||
let trackingSpy;
|
||||
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
trackingSpy = null;
|
||||
});
|
||||
|
||||
it('tracks editing the assignees on dropdown widget updateValue', async () => {
|
||||
showDropdown();
|
||||
await waitForPromises();
|
||||
|
||||
findSidebarDropdownWidget().vm.$emit('updateValue', mockAssignees[0].id);
|
||||
await waitForPromises();
|
||||
|
||||
expect(trackingSpy).toHaveBeenCalledWith(TRACKING_CATEGORY_SHOW, 'updated_assignees', {
|
||||
category: TRACKING_CATEGORY_SHOW,
|
||||
label: 'item_assignees',
|
||||
property: 'type_Task',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('invite members', () => {
|
||||
it('does not render `Invite members` link if user has no permission to invite members', () => {
|
||||
createComponent();
|
||||
|
||||
expect(findInviteMembersTrigger().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('renders `Invite members` link if user has a permission to invite members', () => {
|
||||
createComponent({ canInviteMembers: true });
|
||||
|
||||
expect(findInviteMembersTrigger().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('load more assignees', () => {
|
||||
it('does not have infinite scroll when no matching users', async () => {
|
||||
createComponent({ searchQueryHandler: successSearchWithNoMatchingUsers });
|
||||
|
||||
showDropdown();
|
||||
await waitForPromises();
|
||||
|
||||
expect(findSidebarDropdownWidget().props('infiniteScroll')).toBe(false);
|
||||
});
|
||||
|
||||
it('does not trigger load more when does not have next page', async () => {
|
||||
createComponent();
|
||||
|
||||
showDropdown();
|
||||
await waitForPromises();
|
||||
|
||||
expect(findSidebarDropdownWidget().props('infiniteScroll')).toBe(false);
|
||||
});
|
||||
|
||||
it('triggers load more when there are more users', async () => {
|
||||
createComponent({ searchQueryHandler: successSearchQueryHandlerWithMoreAssignees });
|
||||
|
||||
showDropdown();
|
||||
await waitForPromises();
|
||||
|
||||
findSidebarDropdownWidget().vm.$emit('bottomReached');
|
||||
await waitForPromises();
|
||||
|
||||
expect(successSearchQueryHandlerWithMoreAssignees).toHaveBeenCalledWith({
|
||||
first: DEFAULT_PAGE_SIZE_ASSIGNEES,
|
||||
after:
|
||||
projectMembersResponseWithCurrentUserWithNextPage.data.workspace.users.pageInfo.endCursor,
|
||||
search: '',
|
||||
fullPath: 'test-project-path',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { nextTick } from 'vue';
|
||||
import { 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);
|
||||
|
|
|
|||
|
|
@ -150,7 +150,7 @@ describe('WorkItemMilestoneWithEdit component', () => {
|
|||
|
||||
await nextTick();
|
||||
|
||||
expect(findSidebarDropdownWidget().props('itemValue').title).toBe(milestoneAtIndex.title);
|
||||
expect(findSidebarDropdownWidget().props('itemValue')).toBe(milestoneAtIndex.id);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -701,6 +701,11 @@ RSpec.describe ApplicationHelper do
|
|||
end
|
||||
|
||||
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
|
||||
before do
|
||||
allow(helper).to receive(:current_user).and_return(user)
|
||||
|
|
@ -718,6 +723,15 @@ RSpec.describe ApplicationHelper do
|
|||
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
|
||||
context 'when @hide_top_bar_padding is false' do
|
||||
before do
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
|
|
|
|||
|
|
@ -167,6 +167,9 @@ RSpec.shared_examples 'work items comments' do |type|
|
|||
end
|
||||
|
||||
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',
|
||||
quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/413074' do
|
||||
# The button is only when the mouse is over the input
|
||||
|
|
@ -234,6 +237,59 @@ RSpec.shared_examples 'work items assignees' do
|
|||
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
|
||||
let(:label_title_selector) { '[data-testid="labels-title"]' }
|
||||
let(:labels_input_selector) { '[data-testid="work-item-labels-input"]' }
|
||||
|
|
@ -391,6 +447,9 @@ end
|
|||
RSpec.shared_examples 'work items invite members' do
|
||||
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
|
||||
# The button is only when the mouse is over the input
|
||||
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
|
||||
|
||||
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
|
||||
context 'on work_items_mvc_2 FF off' do
|
||||
include_context 'with work_items_mvc_2', false
|
||||
|
|
|
|||
Loading…
Reference in New Issue