diff options
Diffstat (limited to 'app/assets')
-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 |
6 files changed, 374 insertions, 37 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')" |