Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2024-01-22 18:10:29 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2024-01-22 18:10:29 +0300
commit917d93d86da4dffd96abcfcf3aa83b0d6fa45286 (patch)
tree481ea258782443f5eaf2bd5e7dd5c1a7f900e3dd /app
parente5c31c104e19a08546b17b34b7f1563cce3f89e6 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/persistent_user_callouts.js1
-rw-r--r--app/assets/javascripts/work_items/components/shared/work_item_sidebar_dropdown_widget_with_edit.vue71
-rw-r--r--app/assets/javascripts/work_items/components/work_item_assignees_inline.vue (renamed from app/assets/javascripts/work_items/components/work_item_assignees.vue)0
-rw-r--r--app/assets/javascripts/work_items/components/work_item_assignees_with_edit.vue292
-rw-r--r--app/assets/javascripts/work_items/components/work_item_attributes_wrapper.vue42
-rw-r--r--app/assets/javascripts/work_items/components/work_item_milestone_with_edit.vue5
-rw-r--r--app/helpers/application_helper.rb2
-rw-r--r--app/models/users/callout.rb3
-rw-r--r--app/views/dashboard/projects/_zero_authorized_projects.html.haml1
-rw-r--r--app/views/layouts/devise_empty.html.haml5
-rw-r--r--app/views/layouts/minimal.html.haml1
11 files changed, 383 insertions, 40 deletions
diff --git a/app/assets/javascripts/persistent_user_callouts.js b/app/assets/javascripts/persistent_user_callouts.js
index 95ef04ceb30..90f2c11dddc 100644
--- a/app/assets/javascripts/persistent_user_callouts.js
+++ b/app/assets/javascripts/persistent_user_callouts.js
@@ -25,6 +25,7 @@ const PERSISTENT_USER_CALLOUTS = [
'.js-new-nav-for-everyone-callout',
'.js-namespace-over-storage-users-combined-alert',
'.js-code-suggestions-ga-alert',
+ '.js-joining-a-project-alert',
];
const initCallouts = () => {
diff --git a/app/assets/javascripts/work_items/components/shared/work_item_sidebar_dropdown_widget_with_edit.vue b/app/assets/javascripts/work_items/components/shared/work_item_sidebar_dropdown_widget_with_edit.vue
index d94d0494ad9..21512ba6066 100644
--- a/app/assets/javascripts/work_items/components/shared/work_item_sidebar_dropdown_widget_with_edit.vue
+++ b/app/assets/javascripts/work_items/components/shared/work_item_sidebar_dropdown_widget_with_edit.vue
@@ -39,7 +39,7 @@ export default {
default: () => [],
},
itemValue: {
- type: Object,
+ type: [Array, String],
required: false,
default: null,
},
@@ -68,30 +68,40 @@ export default {
required: false,
default: '',
},
+ multiSelect: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ showFooter: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ infiniteScroll: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ infiniteScrollLoading: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
isEditing: false,
- localSelectedItem: this.itemValue?.id,
+ localSelectedItem: this.itemValue,
};
},
computed: {
hasValue() {
- return this.itemValue != null || !isEmpty(this.item);
- },
- listboxText() {
- return (
- this.listItems.find(({ value }) => this.localSelectedItem === value)?.text ||
- this.itemValue?.title ||
- this.$options.i18n.none
- );
+ return this.multiSelect ? !isEmpty(this.itemValue) : this.itemValue !== null;
},
inputId() {
return `work-item-dropdown-listbox-value-${this.dropdownName}`;
},
- toggleText() {
- return this.toggleDropdownText || this.listboxText;
- },
resetButton() {
return this.resetButtonLabel || this.$options.i18n.resetButtonText;
},
@@ -100,7 +110,7 @@ export default {
itemValue: {
handler(newVal) {
if (!this.isEditing) {
- this.localSelectedItem = newVal?.id;
+ this.localSelectedItem = newVal;
}
},
},
@@ -114,18 +124,25 @@ export default {
},
handleItemClick(item) {
this.localSelectedItem = item;
- this.$emit('updateValue', item);
+ if (!this.multiSelect) {
+ this.$emit('updateValue', item);
+ } else {
+ this.$emit('updateSelected', this.localSelectedItem);
+ }
},
onListboxShown() {
this.$emit('dropdownShown');
},
onListboxHide() {
this.isEditing = false;
+ if (this.multiSelect) {
+ this.$emit('updateValue', this.localSelectedItem);
+ }
},
unassignValue() {
- this.localSelectedItem = null;
+ this.localSelectedItem = this.multiSelect ? [] : null;
this.isEditing = false;
- this.$emit('updateValue', null);
+ this.$emit('updateValue', this.localSelectedItem);
},
},
};
@@ -165,34 +182,42 @@ export default {
</div>
<gl-collapsible-listbox
:id="inputId"
+ :multiple="multiSelect"
block
searchable
start-opened
is-check-centered
fluid-width
+ :infinite-scroll="infiniteScroll"
:searching="loading"
:header-text="headerText"
- :toggle-text="toggleText"
+ :toggle-text="toggleDropdownText"
:no-results-text="$options.i18n.noMatchingResults"
:items="listItems"
:selected="localSelectedItem"
:reset-button-label="resetButton"
+ :infinite-scroll-loading="infiniteScrollLoading"
@reset="unassignValue"
@search="debouncedSearchKeyUpdate"
@select="handleItemClick"
@shown="onListboxShown"
@hidden="onListboxHide"
+ @bottom-reached="$emit('bottomReached')"
>
<template #list-item="{ item }">
<slot name="list-item" :item="item">{{ item.text }}</slot>
</template>
+ <template v-if="showFooter" #footer>
+ <div class="gl-border-t-solid gl-border-t-1 gl-border-t-gray-200 gl-p-2!">
+ <slot name="footer"></slot>
+ </div>
+ </template>
</gl-collapsible-listbox>
+ {{ hasValue }}
</gl-form>
- <slot v-else-if="hasValue" name="readonly">
- {{ listboxText }}
- </slot>
- <div v-else class="gl-text-secondary">
+ <slot v-else-if="hasValue" name="readonly"></slot>
+ <slot v-else class="gl-text-secondary" name="none">
{{ $options.i18n.none }}
- </div>
+ </slot>
</div>
</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_assignees.vue b/app/assets/javascripts/work_items/components/work_item_assignees_inline.vue
index a9aafbb3d84..a9aafbb3d84 100644
--- a/app/assets/javascripts/work_items/components/work_item_assignees.vue
+++ b/app/assets/javascripts/work_items/components/work_item_assignees_inline.vue
diff --git a/app/assets/javascripts/work_items/components/work_item_assignees_with_edit.vue b/app/assets/javascripts/work_items/components/work_item_assignees_with_edit.vue
new file mode 100644
index 00000000000..bb7baed29f6
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/work_item_assignees_with_edit.vue
@@ -0,0 +1,292 @@
+<script>
+import { GlButton } from '@gitlab/ui';
+import { isEmpty } from 'lodash';
+import currentUserQuery from '~/graphql_shared/queries/current_user.query.graphql';
+import groupUsersSearchQuery from '~/graphql_shared/queries/group_users_search.query.graphql';
+import usersSearchQuery from '~/graphql_shared/queries/users_search.query.graphql';
+import InviteMembersTrigger from '~/invite_members/components/invite_members_trigger.vue';
+import SidebarParticipant from '~/sidebar/components/assignees/sidebar_participant.vue';
+import UncollapsedAssigneeList from '~/sidebar/components/assignees/uncollapsed_assignee_list.vue';
+import WorkItemSidebarDropdownWidgetWithEdit from '~/work_items/components/shared/work_item_sidebar_dropdown_widget_with_edit.vue';
+import { s__, sprintf, __ } from '~/locale';
+import Tracking from '~/tracking';
+import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql';
+import { i18n, TRACKING_CATEGORY_SHOW, DEFAULT_PAGE_SIZE_ASSIGNEES } from '../constants';
+
+export default {
+ components: {
+ WorkItemSidebarDropdownWidgetWithEdit,
+ InviteMembersTrigger,
+ SidebarParticipant,
+ GlButton,
+ UncollapsedAssigneeList,
+ },
+ mixins: [Tracking.mixin()],
+ inject: ['isGroup'],
+ props: {
+ fullPath: {
+ type: String,
+ required: true,
+ },
+ workItemId: {
+ type: String,
+ required: true,
+ },
+ assignees: {
+ type: Array,
+ required: true,
+ },
+ allowsMultipleAssignees: {
+ type: Boolean,
+ required: true,
+ },
+ workItemType: {
+ type: String,
+ required: true,
+ },
+ canUpdate: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ canInviteMembers: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ localAssigneeIds: this.assignees.map(({ id }) => id),
+ searchStarted: false,
+ searchKey: '',
+ users: {
+ nodes: [],
+ },
+ currentUser: null,
+ isLoadingMore: false,
+ updateInProgress: false,
+ };
+ },
+ apollo: {
+ users: {
+ query() {
+ return this.isGroup ? groupUsersSearchQuery : usersSearchQuery;
+ },
+ variables() {
+ return {
+ fullPath: this.fullPath,
+ search: this.searchKey,
+ first: DEFAULT_PAGE_SIZE_ASSIGNEES,
+ };
+ },
+ skip() {
+ return !this.searchStarted;
+ },
+ update(data) {
+ return data.workspace?.users;
+ },
+ error() {
+ this.$emit('error', i18n.fetchError);
+ },
+ },
+ currentUser: {
+ query: currentUserQuery,
+ },
+ },
+ computed: {
+ searchUsers() {
+ return this.users.nodes.map(({ user }) => ({
+ ...user,
+ value: user.id,
+ text: user.name,
+ }));
+ },
+ pageInfo() {
+ return this.users.pageInfo;
+ },
+ tracking() {
+ return {
+ category: TRACKING_CATEGORY_SHOW,
+ label: 'item_assignees',
+ property: `type_${this.workItemType}`,
+ };
+ },
+ isLoadingUsers() {
+ return this.$apollo.queries.users.loading && !this.isLoadingMore;
+ },
+ hasNextPage() {
+ return this.pageInfo?.hasNextPage;
+ },
+ selectedAssigneeIds() {
+ return this.allowsMultipleAssignees ? this.localAssigneeIds : this.localAssigneeIds[0];
+ },
+ dropdownText() {
+ if (this.localAssigneeIds.length === 0) {
+ return s__('WorkItem|No assignees');
+ }
+
+ return this.localAssigneeIds.length === 1
+ ? this.localAssignees.map(({ name }) => name).join(', ')
+ : sprintf(s__('WorkItem|%{usersLength} assignees'), {
+ usersLength: this.localAssigneeIds.length,
+ });
+ },
+ dropdownLabel() {
+ return this.allowsMultipleAssignees ? __('Assignees') : __('Assignee');
+ },
+ headerText() {
+ return this.allowsMultipleAssignees ? __('Select assignees') : __('Select assignee');
+ },
+ filteredAssignees() {
+ return isEmpty(this.searchUsers)
+ ? this.assignees
+ : this.searchUsers.filter(({ id }) => this.localAssigneeIds.includes(id));
+ },
+ localAssignees() {
+ return this.filteredAssignees || [];
+ },
+ },
+ watch: {
+ assignees: {
+ handler(newVal) {
+ this.localAssigneeIds = newVal.map(({ id }) => id);
+ },
+ deep: true,
+ },
+ },
+ methods: {
+ handleAssigneesInput(assignees) {
+ this.setLocalAssigneeIdsOnEvent(assignees);
+ this.setAssignees();
+ },
+ handleAssigneeClick(assignees) {
+ this.setLocalAssigneeIdsOnEvent(assignees);
+ },
+ async setAssignees() {
+ this.updateInProgress = true;
+ const { localAssigneeIds } = this;
+
+ try {
+ const {
+ data: {
+ workItemUpdate: { errors },
+ },
+ } = await this.$apollo.mutate({
+ mutation: updateWorkItemMutation,
+ variables: {
+ input: {
+ id: this.workItemId,
+ assigneesWidget: {
+ assigneeIds: localAssigneeIds,
+ },
+ },
+ },
+ });
+ if (errors.length > 0) {
+ this.throwUpdateError();
+ return;
+ }
+ this.track('updated_assignees');
+ } catch {
+ this.throwUpdateError();
+ } finally {
+ this.updateInProgress = false;
+ }
+ },
+ setLocalAssigneeIdsOnEvent(assignees) {
+ const singleSelectAssignee = assignees === null ? [] : [assignees];
+ this.localAssigneeIds = this.allowsMultipleAssignees ? assignees : singleSelectAssignee;
+ },
+ async fetchMoreAssignees() {
+ if (this.isLoadingMore && !this.hasNextPage) return;
+
+ this.isLoadingMore = true;
+ await this.$apollo.queries.users.fetchMore({
+ variables: {
+ after: this.pageInfo.endCursor,
+ first: DEFAULT_PAGE_SIZE_ASSIGNEES,
+ },
+ });
+ this.isLoadingMore = false;
+ },
+ setSearchKey(value) {
+ this.searchKey = value;
+ this.searchStarted = true;
+ },
+ assignToCurrentUser() {
+ const assignees = this.allowsMultipleAssignees ? [this.currentUser.id] : this.currentUser.id;
+ this.setLocalAssigneeIdsOnEvent(assignees);
+ this.setAssignees();
+ },
+ throwUpdateError() {
+ this.$emit('error', i18n.updateError);
+ // If mutation is rejected, we're rolling back to initial state
+ this.localAssigneeIds = this.assignees.map(({ id }) => id);
+ },
+ onDropdownShown() {
+ this.searchStarted = true;
+ },
+ },
+};
+</script>
+
+<template>
+ <work-item-sidebar-dropdown-widget-with-edit
+ :multi-select="allowsMultipleAssignees"
+ class="issuable-assignees gl-mt-2"
+ :dropdown-label="dropdownLabel"
+ :can-update="canUpdate"
+ dropdown-name="assignees"
+ show-footer
+ :infinite-scroll="hasNextPage"
+ :infinite-scroll-loading="isLoadingMore"
+ :loading="isLoadingUsers"
+ :list-items="searchUsers"
+ :item-value="selectedAssigneeIds"
+ :toggle-dropdown-text="dropdownText"
+ :header-text="headerText"
+ :update-in-progress="updateInProgress"
+ :reset-button-label="__('Clear')"
+ data-testid="work-item-assignees-with-edit"
+ @dropdownShown="onDropdownShown"
+ @searchStarted="setSearchKey"
+ @updateValue="handleAssigneesInput"
+ @updateSelected="handleAssigneeClick"
+ @bottomReached="fetchMoreAssignees"
+ >
+ <template #list-item="{ item }">
+ <sidebar-participant :user="item" />
+ </template>
+ <template v-if="canInviteMembers" #footer>
+ <gl-button category="tertiary" block class="gl-justify-content-start!">
+ <invite-members-trigger
+ :display-text="__('Invite members')"
+ trigger-element="side-nav"
+ icon="plus"
+ trigger-source="work-item-assignees-with-edit"
+ classes="gl-hover-text-decoration-none! gl-pb-2"
+ />
+ </gl-button>
+ </template>
+ <template #none>
+ <div class="gl-display-flex gl-align-items-center gl-text-gray-500 gl-gap-2">
+ <span>{{ __('None') }}</span>
+ <template v-if="currentUser && canUpdate">
+ <span>-</span>
+ <gl-button variant="link" data-testid="assign-self" @click.stop="assignToCurrentUser"
+ ><span class="gl-text-gray-500 gl-hover-text-blue-800">{{
+ __('assign yourself')
+ }}</span></gl-button
+ >
+ </template>
+ </div>
+ </template>
+ <template #readonly>
+ <uncollapsed-assignee-list
+ :users="localAssignees"
+ show-less-assignees-class="gl-hover-bg-transparent!"
+ />
+ </template>
+ </work-item-sidebar-dropdown-widget-with-edit>
+</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_attributes_wrapper.vue b/app/assets/javascripts/work_items/components/work_item_attributes_wrapper.vue
index 79f0fdca061..76f1938c3ed 100644
--- a/app/assets/javascripts/work_items/components/work_item_attributes_wrapper.vue
+++ b/app/assets/javascripts/work_items/components/work_item_attributes_wrapper.vue
@@ -15,7 +15,8 @@ import {
WORK_ITEM_TYPE_VALUE_TASK,
} from '../constants';
import WorkItemDueDate from './work_item_due_date.vue';
-import WorkItemAssignees from './work_item_assignees.vue';
+import WorkItemAssigneesInline from './work_item_assignees_inline.vue';
+import WorkItemAssigneesWithEdit from './work_item_assignees_with_edit.vue';
import WorkItemLabels from './work_item_labels.vue';
import WorkItemMilestoneInline from './work_item_milestone_inline.vue';
import WorkItemMilestoneWithEdit from './work_item_milestone_with_edit.vue';
@@ -27,7 +28,8 @@ export default {
WorkItemLabels,
WorkItemMilestoneInline,
WorkItemMilestoneWithEdit,
- WorkItemAssignees,
+ WorkItemAssigneesInline,
+ WorkItemAssigneesWithEdit,
WorkItemDueDate,
WorkItemParent,
WorkItemParentInline,
@@ -114,17 +116,31 @@ export default {
<template>
<div class="work-item-attributes-wrapper">
- <work-item-assignees
- v-if="workItemAssignees"
- :can-update="canUpdate"
- :full-path="fullPath"
- :work-item-id="workItem.id"
- :assignees="workItemAssignees.assignees.nodes"
- :allows-multiple-assignees="workItemAssignees.allowsMultipleAssignees"
- :work-item-type="workItemType"
- :can-invite-members="workItemAssignees.canInviteMembers"
- @error="$emit('error', $event)"
- />
+ <template v-if="workItemAssignees">
+ <work-item-assignees-with-edit
+ v-if="glFeatures.workItemsMvc2"
+ class="gl-mb-5"
+ :can-update="canUpdate"
+ :full-path="fullPath"
+ :work-item-id="workItem.id"
+ :assignees="workItemAssignees.assignees.nodes"
+ :allows-multiple-assignees="workItemAssignees.allowsMultipleAssignees"
+ :work-item-type="workItemType"
+ :can-invite-members="workItemAssignees.canInviteMembers"
+ @error="$emit('error', $event)"
+ />
+ <work-item-assignees-inline
+ v-else
+ :can-update="canUpdate"
+ :full-path="fullPath"
+ :work-item-id="workItem.id"
+ :assignees="workItemAssignees.assignees.nodes"
+ :allows-multiple-assignees="workItemAssignees.allowsMultipleAssignees"
+ :work-item-type="workItemType"
+ :can-invite-members="workItemAssignees.canInviteMembers"
+ @error="$emit('error', $event)"
+ />
+ </template>
<work-item-labels
v-if="workItemLabels"
:can-update="canUpdate"
diff --git a/app/assets/javascripts/work_items/components/work_item_milestone_with_edit.vue b/app/assets/javascripts/work_items/components/work_item_milestone_with_edit.vue
index 87b41c9d9ea..26d98f87464 100644
--- a/app/assets/javascripts/work_items/components/work_item_milestone_with_edit.vue
+++ b/app/assets/javascripts/work_items/components/work_item_milestone_with_edit.vue
@@ -91,6 +91,9 @@ export default {
expired,
}));
},
+ localMilestoneId() {
+ return this.localMilestone?.id;
+ },
},
watch: {
workItemMilestone(newVal) {
@@ -184,7 +187,7 @@ export default {
dropdown-name="milestone"
:loading="isLoadingMilestones"
:list-items="milestonesList"
- :item-value="localMilestone"
+ :item-value="localMilestoneId"
:update-in-progress="updateInProgress"
:toggle-dropdown-text="dropdownText"
:header-text="__('Select milestone')"
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 892b046e410..62eb83a77b6 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -311,7 +311,7 @@ module ApplicationHelper
class_names << 'issue-boards-page gl-overflow-auto' if current_controller?(:boards)
class_names << 'epic-boards-page gl-overflow-auto' if current_controller?(:epic_boards)
class_names << 'with-performance-bar' if performance_bar_enabled?
- class_names << 'with-header' unless current_user
+ class_names << 'with-header' if @with_header || !current_user
class_names << 'with-top-bar' unless @hide_top_bar_padding
class_names << system_message_class
diff --git a/app/models/users/callout.rb b/app/models/users/callout.rb
index 8d330e4eb6e..d377a370988 100644
--- a/app/models/users/callout.rb
+++ b/app/models/users/callout.rb
@@ -81,7 +81,8 @@ module Users
code_suggestions_ga_non_owner_alert: 79, # EE-only
duo_chat_callout: 80, # EE-only
code_suggestions_ga_owner_alert: 81, # EE-only
- product_analytics_dashboard_feedback: 82 # EE-only
+ product_analytics_dashboard_feedback: 82, # EE-only
+ joining_a_project_alert: 83 # EE-only
}
validates :feature_name,
diff --git a/app/views/dashboard/projects/_zero_authorized_projects.html.haml b/app/views/dashboard/projects/_zero_authorized_projects.html.haml
index da25dee1e88..ba4e8961f28 100644
--- a/app/views/dashboard/projects/_zero_authorized_projects.html.haml
+++ b/app/views/dashboard/projects/_zero_authorized_projects.html.haml
@@ -1,4 +1,5 @@
.container
+ = render_if_exists 'dashboard/projects/joining_a_project_alert'
.gl-text-center.gl-pt-6.gl-pb-7
%h2.gl-font-size-h1{ data: { testid: 'welcome-title-content' } }
= _('Welcome to GitLab')
diff --git a/app/views/layouts/devise_empty.html.haml b/app/views/layouts/devise_empty.html.haml
index faf45ae78ef..d7449ea5be7 100644
--- a/app/views/layouts/devise_empty.html.haml
+++ b/app/views/layouts/devise_empty.html.haml
@@ -1,6 +1,9 @@
- add_page_specific_style 'page_bundles/login'
+- @with_header = true
+- page_classes = [user_application_theme, page_class.flatten.compact]
+
!!! 5
-%html.html-devise-layout{ class: user_application_theme, lang: I18n.locale }
+%html.html-devise-layout{ class: page_classes, lang: I18n.locale }
= render "layouts/head"
%body.gl-h-full.login-page.navless{ class: "#{system_message_class} #{client_class_list}" }
= header_message
diff --git a/app/views/layouts/minimal.html.haml b/app/views/layouts/minimal.html.haml
index e499b9ae240..fa39b6d45ba 100644
--- a/app/views/layouts/minimal.html.haml
+++ b/app/views/layouts/minimal.html.haml
@@ -1,3 +1,4 @@
+- @with_header = true
- page_classes = page_class.push(@html_class).flatten.compact
!!! 5