diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2024-01-22 18:10:29 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2024-01-22 18:10:29 +0300 |
commit | 917d93d86da4dffd96abcfcf3aa83b0d6fa45286 (patch) | |
tree | 481ea258782443f5eaf2bd5e7dd5c1a7f900e3dd /app | |
parent | e5c31c104e19a08546b17b34b7f1563cce3f89e6 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r-- | app/assets/javascripts/persistent_user_callouts.js | 1 | ||||
-rw-r--r-- | app/assets/javascripts/work_items/components/shared/work_item_sidebar_dropdown_widget_with_edit.vue | 71 | ||||
-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.vue | 292 | ||||
-rw-r--r-- | app/assets/javascripts/work_items/components/work_item_attributes_wrapper.vue | 42 | ||||
-rw-r--r-- | app/assets/javascripts/work_items/components/work_item_milestone_with_edit.vue | 5 | ||||
-rw-r--r-- | app/helpers/application_helper.rb | 2 | ||||
-rw-r--r-- | app/models/users/callout.rb | 3 | ||||
-rw-r--r-- | app/views/dashboard/projects/_zero_authorized_projects.html.haml | 1 | ||||
-rw-r--r-- | app/views/layouts/devise_empty.html.haml | 5 | ||||
-rw-r--r-- | app/views/layouts/minimal.html.haml | 1 |
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 |