diff options
Diffstat (limited to 'app/assets')
10 files changed, 321 insertions, 389 deletions
diff --git a/app/assets/javascripts/design_management/components/design_notes/design_note.vue b/app/assets/javascripts/design_management/components/design_notes/design_note.vue index 54ffd986588..b247f17fd97 100644 --- a/app/assets/javascripts/design_management/components/design_notes/design_note.vue +++ b/app/assets/javascripts/design_management/components/design_notes/design_note.vue @@ -13,6 +13,7 @@ import SafeHtml from '~/vue_shared/directives/safe_html'; import { getIdFromGraphQLId, convertToGraphQLId } from '~/graphql_shared/utils'; import { TYPENAME_USER } from '~/graphql_shared/constants'; import { __ } from '~/locale'; +import { setUrlFragment } from '~/lib/utils/url_utility'; import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import EmojiPicker from '~/emoji/components/picker.vue'; @@ -29,6 +30,7 @@ export default { editCommentLabel: __('Edit comment'), moreActionsLabel: __('More actions'), deleteCommentText: __('Delete comment'), + copyCommentLink: __('Copy link'), }, components: { DesignNoteAwardsList, @@ -133,6 +135,18 @@ export default { }, }, { + text: this.$options.i18n.copyCommentLink, + action: () => { + this.$toast.show(__('Link copied to clipboard.')); + }, + extraAttrs: { + 'data-clipboard-text': setUrlFragment( + window.location.href, + `note_${this.noteAnchorId}`, + ), + }, + }, + { text: this.$options.i18n.deleteCommentText, action: () => { this.$emit('delete-note', this.note); diff --git a/app/assets/javascripts/issues/show/components/header_actions.vue b/app/assets/javascripts/issues/show/components/header_actions.vue index 1c1b15e2026..c50b8009284 100644 --- a/app/assets/javascripts/issues/show/components/header_actions.vue +++ b/app/assets/javascripts/issues/show/components/header_actions.vue @@ -1,9 +1,9 @@ <script> import { GlButton, - GlDropdown, + GlDisclosureDropdown, GlDropdownDivider, - GlDropdownItem, + GlDisclosureDropdownItem, GlLink, GlModal, GlModalDirective, @@ -59,9 +59,9 @@ export default { components: { DeleteIssueModal, GlButton, - GlDropdown, + GlDisclosureDropdown, GlDropdownDivider, - GlDropdownItem, + GlDisclosureDropdownItem, GlLink, GlModal, AbuseCategorySelector, @@ -184,6 +184,18 @@ export default { showMovedSidebarOptions() { return this.isMrSidebarMoved && this.isUserSignedIn; }, + newIssueItem() { + return { + text: this.newIssueTypeText, + href: this.newIssuePath, + }; + }, + submitSpamItem() { + return { + text: __('Submit as spam'), + href: this.submitAsSpamPath, + }; + }, }, created() { eventHub.$on('toggle.issuable.state', this.toggleIssueState); @@ -197,6 +209,7 @@ export default { toggleIssueState() { if (!this.isClosed && this.getBlockedByIssues?.length) { this.$refs.blockedByIssuesModal.show(); + this.closeActionsDropdown(); return; } @@ -204,6 +217,7 @@ export default { }, toggleReportAbuseDrawer(isOpen) { this.isReportAbuseDrawerOpen = isOpen; + this.closeActionsDropdown(); }, invokeUpdateIssueMutation() { this.toggleStateButtonLoading(true); @@ -237,6 +251,7 @@ export default { .catch(() => createAlert({ message: __('Error occurred while updating the issue status') })) .finally(() => { this.toggleStateButtonLoading(false); + this.closeActionsDropdown(); }); }, promoteToEpic() { @@ -267,16 +282,24 @@ export default { .catch(() => createAlert({ message: this.$options.i18n.promoteErrorMessage })) .finally(() => { this.toggleStateButtonLoading(false); + this.closeActionsDropdown(); }); }, edit() { issuesEventHub.$emit('open.form'); + this.closeActionsDropdown(); }, copyReference() { toast(__('Reference copied')); + this.closeActionsDropdown(); }, copyEmailAddress() { toast(__('Email address copied')); + this.closeActionsDropdown(); + }, + closeActionsDropdown() { + this.$refs.issuableActionsDropdownMobile?.close(); + this.$refs.issuableActionsDropdownDesktop?.close(); }, }, TYPE_ISSUE, @@ -285,87 +308,90 @@ export default { <template> <div class="detail-page-header-actions gl-display-flex gl-align-self-start gl-sm-gap-3"> - <gl-dropdown - v-if="hasMobileDropdown" - class="gl-sm-display-none! w-100" - block - :text="dropdownText" - data-testid="mobile-dropdown" - :loading="isToggleStateButtonLoading" - > - <template v-if="showMovedSidebarOptions"> - <sidebar-subscriptions-widget - :iid="String(iid)" - :full-path="fullPath" - :issuable-type="$options.TYPE_ISSUE" - data-testid="notification-toggle" - /> + <div class="gl-sm-display-none! w-100"> + <gl-disclosure-dropdown + v-if="hasMobileDropdown" + ref="issuableActionsDropdownMobile" + toggle-class="gl-w-full" + block + :toggle-text="dropdownText" + :auto-close="false" + data-testid="mobile-dropdown" + :loading="isToggleStateButtonLoading" + placement="right" + > + <template v-if="showMovedSidebarOptions"> + <sidebar-subscriptions-widget + :iid="String(iid)" + :full-path="fullPath" + :issuable-type="$options.TYPE_ISSUE" + data-testid="notification-toggle" + /> - <gl-dropdown-divider /> - </template> + <gl-dropdown-divider /> + </template> - <template v-if="showLockIssueOption"> - <issuable-lock-form :is-editable="false" data-testid="lock-issue-toggle" /> - </template> + <template v-if="showLockIssueOption"> + <issuable-lock-form :is-editable="false" data-testid="lock-issue-toggle" /> + </template> - <gl-dropdown-item v-if="canUpdateIssue" @click="edit"> - {{ $options.i18n.edit }} - </gl-dropdown-item> - <gl-dropdown-item - v-if="showToggleIssueStateButton" - :data-testid="`mobile_${qaSelector}`" - @click="toggleIssueState" - > - {{ buttonText }} - </gl-dropdown-item> - <gl-dropdown-item v-if="canCreateIssue" :href="newIssuePath"> - {{ newIssueTypeText }} - </gl-dropdown-item> - <gl-dropdown-item v-if="canPromoteToEpic" @click="promoteToEpic"> - {{ __('Promote to epic') }} - </gl-dropdown-item> - <template v-if="isMrSidebarMoved"> - <gl-dropdown-item - :data-clipboard-text="issuableReference" - button-class="js-copy-reference" - data-testid="copy-reference" - @click="copyReference" - >{{ $options.i18n.copyReferenceText }}</gl-dropdown-item - > - <gl-dropdown-item - v-if="issuableEmailAddress && showMovedSidebarOptions" - :data-clipboard-text="issuableEmailAddress" - data-testid="copy-email" - @click="copyEmailAddress" - >{{ copyMailAddressText }}</gl-dropdown-item + <gl-disclosure-dropdown-item v-if="canUpdateIssue" @action="edit"> + <template #list-item>{{ $options.i18n.edit }}</template> + </gl-disclosure-dropdown-item> + <gl-disclosure-dropdown-item + v-if="showToggleIssueStateButton" + :data-testid="`mobile_${qaSelector}`" + @action="toggleIssueState" > - </template> - <gl-dropdown-item - v-if="canReportSpam" - :href="submitAsSpamPath" - data-method="post" - rel="nofollow" - > - {{ __('Submit as spam') }} - </gl-dropdown-item> - <template v-if="canDestroyIssue"> - <gl-dropdown-divider /> - <gl-dropdown-item - v-gl-modal="$options.deleteModalId" - variant="danger" - @click="track('click_dropdown')" + <template #list-item>{{ buttonText }}</template> + </gl-disclosure-dropdown-item> + <gl-disclosure-dropdown-item v-if="canCreateIssue" :item="newIssueItem" /> + <gl-disclosure-dropdown-item v-if="canPromoteToEpic" @action="promoteToEpic"> + <template #list-item>{{ __('Promote to epic') }}</template> + </gl-disclosure-dropdown-item> + <template v-if="isMrSidebarMoved"> + <gl-disclosure-dropdown-item + :data-clipboard-text="issuableReference" + button-class="js-copy-reference" + data-testid="copy-reference" + @action="copyReference" + ><template #list-item>{{ + $options.i18n.copyReferenceText + }}</template></gl-disclosure-dropdown-item + > + <gl-disclosure-dropdown-item + v-if="issuableEmailAddress && showMovedSidebarOptions" + :data-clipboard-text="issuableEmailAddress" + data-testid="copy-email" + @action="copyEmailAddress" + >{{ copyMailAddressText }}</gl-disclosure-dropdown-item + > + </template> + <gl-disclosure-dropdown-item + v-if="canReportSpam" + :item="submitSpamItem" + data-method="post" + rel="nofollow" + /> + <template v-if="canDestroyIssue"> + <gl-dropdown-divider /> + <gl-disclosure-dropdown-item + v-gl-modal="$options.deleteModalId" + variant="danger" + @action="track('click_dropdown')" + > + <template #list-item>{{ deleteButtonText }}</template> + </gl-disclosure-dropdown-item> + </template> + <gl-disclosure-dropdown-item + v-if="!isIssueAuthor && isUserSignedIn" + data-testid="report-abuse-item" + @action="toggleReportAbuseDrawer(true)" > - {{ deleteButtonText }} - </gl-dropdown-item> - </template> - <gl-dropdown-item - v-if="!isIssueAuthor && isUserSignedIn" - data-testid="report-abuse-item" - @click="toggleReportAbuseDrawer(true)" - > - {{ $options.i18n.reportAbuse }} - </gl-dropdown-item> - </gl-dropdown> + <template #list-item>{{ $options.i18n.reportAbuse }}</template> + </gl-disclosure-dropdown-item> + </gl-disclosure-dropdown> + </div> <gl-button v-if="canUpdateIssue" @@ -379,20 +405,22 @@ export default { {{ $options.i18n.edit }} </gl-button> - <gl-dropdown + <gl-disclosure-dropdown v-if="hasDesktopDropdown" id="new-actions-header-dropdown" + ref="issuableActionsDropdownDesktop" v-gl-tooltip.hover class="gl-display-none gl-sm-display-inline-flex!" icon="ellipsis_v" category="tertiary" - :text="dropdownText" - :text-sr-only="true" + placement="left" + :toggle-text="dropdownText" + text-sr-only :title="dropdownText" :aria-label="dropdownText" + :auto-close="false" data-testid="desktop-dropdown" no-caret - right > <template v-if="showMovedSidebarOptions && !glFeatures.notificationsTodosButtons"> <sidebar-subscriptions-widget @@ -401,73 +429,70 @@ export default { :issuable-type="$options.TYPE_ISSUE" data-testid="notification-toggle" /> - <gl-dropdown-divider /> </template> - <gl-dropdown-item + <gl-disclosure-dropdown-item v-if="showToggleIssueStateButton" data-testid="toggle-issue-state-button" - @click="toggleIssueState" + @action="toggleIssueState" > - {{ buttonText }} - </gl-dropdown-item> - <gl-dropdown-item v-if="canCreateIssue && isUserSignedIn" :href="newIssuePath"> - {{ newIssueTypeText }} - </gl-dropdown-item> - <gl-dropdown-item + <template #list-item>{{ buttonText }}</template> + </gl-disclosure-dropdown-item> + <gl-disclosure-dropdown-item v-if="canCreateIssue && isUserSignedIn" :item="newIssueItem" /> + <gl-disclosure-dropdown-item v-if="canPromoteToEpic" :disabled="isToggleStateButtonLoading" data-testid="promote-button" - @click="promoteToEpic" + @action="promoteToEpic" > - {{ __('Promote to epic') }} - </gl-dropdown-item> + <template #list-item>{{ __('Promote to epic') }}</template> + </gl-disclosure-dropdown-item> <template v-if="showLockIssueOption"> <issuable-lock-form :is-editable="false" data-testid="lock-issue-toggle" /> </template> <template v-if="isMrSidebarMoved"> - <gl-dropdown-item + <gl-disclosure-dropdown-item :data-clipboard-text="issuableReference" button-class="js-copy-reference" data-testid="copy-reference" - @click="copyReference" - >{{ $options.i18n.copyReferenceText }}</gl-dropdown-item + @action="copyReference" + ><template #list-item>{{ + $options.i18n.copyReferenceText + }}</template></gl-disclosure-dropdown-item > - <gl-dropdown-item + <gl-disclosure-dropdown-item v-if="issuableEmailAddress && showMovedSidebarOptions" :data-clipboard-text="issuableEmailAddress" data-testid="copy-email" - @click="copyEmailAddress" - >{{ copyMailAddressText }}</gl-dropdown-item + @action="copyEmailAddress" + ><template #list-item>{{ copyMailAddressText }}</template></gl-disclosure-dropdown-item > </template> - <gl-dropdown-divider v-if="canDestroyIssue || canReportSpam || !isIssueAuthor" /> - <gl-dropdown-item + <gl-dropdown-divider v-if="showToggleIssueStateButton || canDestroyIssue || canReportSpam" /> + <gl-disclosure-dropdown-item v-if="canReportSpam" - :href="submitAsSpamPath" + :item="submitSpamItem" data-method="post" rel="nofollow" - > - {{ __('Submit as spam') }} - </gl-dropdown-item> - <gl-dropdown-item + /> + <gl-disclosure-dropdown-item v-if="!isIssueAuthor && isUserSignedIn" data-testid="report-abuse-item" - @click="toggleReportAbuseDrawer(true)" + @action="toggleReportAbuseDrawer(true)" > - {{ $options.i18n.reportAbuse }} - </gl-dropdown-item> + <template #list-item>{{ $options.i18n.reportAbuse }}</template> + </gl-disclosure-dropdown-item> <template v-if="canDestroyIssue"> - <gl-dropdown-item + <gl-disclosure-dropdown-item v-gl-modal="$options.deleteModalId" variant="danger" data-testid="delete-issue-button" - @click="track('click_dropdown')" + @action="track('click_dropdown')" > - {{ deleteButtonText }} - </gl-dropdown-item> + <template #list-item>{{ deleteButtonText }}</template> + </gl-disclosure-dropdown-item> </template> - </gl-dropdown> + </gl-disclosure-dropdown> <gl-modal ref="blockedByIssuesModal" diff --git a/app/assets/javascripts/projects/settings/repository/branch_rules/app.vue b/app/assets/javascripts/projects/settings/repository/branch_rules/app.vue index 7753b850744..7d9ad83a1c6 100644 --- a/app/assets/javascripts/projects/settings/repository/branch_rules/app.vue +++ b/app/assets/javascripts/projects/settings/repository/branch_rules/app.vue @@ -76,7 +76,7 @@ export default { v-gl-modal="$options.modalId" size="small" class="gl-ml-3" - data-qa-selector="add_branch_rule_button" + data-testid="add-branch-rule-button" >{{ $options.i18n.addBranchRule }}</gl-button > </template> diff --git a/app/assets/javascripts/projects/settings/repository/branch_rules/components/branch_rule.vue b/app/assets/javascripts/projects/settings/repository/branch_rules/components/branch_rule.vue index f45a5b12db6..0a5fa288828 100644 --- a/app/assets/javascripts/projects/settings/repository/branch_rules/components/branch_rule.vue +++ b/app/assets/javascripts/projects/settings/repository/branch_rules/components/branch_rule.vue @@ -156,7 +156,7 @@ export default { <li> <div class="gl-display-flex gl-justify-content-space-between" - data-qa-selector="branch_content" + data-testid="branch-content" :data-qa-branch-name="name" > <div> @@ -178,7 +178,7 @@ export default { class="gl-align-self-start" category="tertiary" size="small" - data-qa-selector="details_button" + data-testid="details-button" :href="detailsPath" > {{ $options.i18n.detailsButtonLabel }}</gl-button diff --git a/app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue b/app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue index 165499696de..16235275a54 100644 --- a/app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue +++ b/app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue @@ -170,9 +170,14 @@ export default { </script> <template> - <li v-if="isMovedMrSidebar && isIssuable" class="gl-dropdown-item"> - <button type="button" class="dropdown-item" data-testid="issuable-lock" @click="toggleLocked"> - <span class="gl-dropdown-item-text-wrapper"> + <li v-if="isMovedMrSidebar && isIssuable" class="gl-new-dropdown-item"> + <button + type="button" + class="gl-new-dropdown-item-content" + data-testid="issuable-lock" + @click="toggleLocked" + > + <span class="gl-new-dropdown-item-text-wrapper"> <template v-if="isLoading"> <gl-loading-icon inline size="sm" /> {{ lockToggleInProgressText }} </template> diff --git a/app/assets/javascripts/sidebar/components/move/issuable_move_dropdown.vue b/app/assets/javascripts/sidebar/components/move/issuable_move_dropdown.vue index 34a4da946d6..ea8e0c4b950 100644 --- a/app/assets/javascripts/sidebar/components/move/issuable_move_dropdown.vue +++ b/app/assets/javascripts/sidebar/components/move/issuable_move_dropdown.vue @@ -1,26 +1,20 @@ <script> import { GlIcon, - GlLoadingIcon, - GlDropdown, - GlDropdownForm, - GlDropdownItem, - GlSearchBoxByType, GlButton, + GlCollapsibleListbox, GlTooltipDirective as GlTooltip, } from '@gitlab/ui'; - +import { debounce } from 'lodash'; +import { __ } from '~/locale'; +import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; import axios from '~/lib/utils/axios_utils'; export default { components: { GlIcon, - GlLoadingIcon, - GlDropdown, - GlDropdownForm, - GlDropdownItem, - GlSearchBoxByType, GlButton, + GlCollapsibleListbox, }, directives: { GlTooltip, @@ -51,82 +45,58 @@ export default { }, data() { return { - projectsListLoading: false, - projectsListLoadFailed: false, - searchKey: '', projects: [], - selectedProject: null, - projectItemClick: false, + projectsList: [], + selectedProjects: [], + noResultsText: '', + isSearching: false, }; }, - computed: { - hasNoSearchResults() { - return Boolean( - !this.projectsListLoading && - !this.projectsListLoadFailed && - this.searchKey && - !this.projects.length, - ); - }, - failedToLoadResults() { - return !this.projectsListLoading && this.projectsListLoadFailed; - }, - }, - watch: { - searchKey(value = '') { - this.fetchProjects(value); - }, + mounted() { + this.fetchProjects = debounce(this.fetchProjects, DEFAULT_DEBOUNCE_AND_THROTTLE_MS); }, methods: { - fetchProjects(search = '') { - this.projectsListLoading = true; - this.projectsListLoadFailed = false; - return axios - .get(this.projectsFetchPath, { + triggerSearch() { + this.$refs.dropdown.search(); + }, + async fetchProjects(search = '') { + this.isSearching = true; + + try { + const { data } = await axios.get(this.projectsFetchPath, { params: { search, }, - }) - .then(({ data }) => { - this.projects = data; - this.$refs.searchInput.focusInput(); - }) - .catch(() => { - this.projectsListLoadFailed = true; - }) - .finally(() => { - this.projectsListLoading = false; }); - }, - isSelectedProject(project) { - if (this.selectedProject) { - return this.selectedProject.id === project.id; - } - return false; - }, - /** - * This handler is to prevent dropdown - * from closing when an item is selected - * and emit an event only when dropdown closes. - */ - handleDropdownHide(e) { - if (this.projectItemClick) { - e.preventDefault(); - this.projectItemClick = false; - } else { - this.$emit('dropdown-close'); + this.projects = data; + this.projectsList = data.map((item) => ({ + value: item.id, + text: item.name_with_namespace, + })); + + if (!this.projectsList.length) { + this.noResultsText = __('No matching results'); + } + } catch (e) { + this.noResultsText = __('Failed to load projects'); + } finally { + this.isSearching = false; } }, - handleDropdownCloseClick() { - this.$refs.dropdown.hide(); - }, - handleProjectSelect(project) { - this.selectedProject = project.id === this.selectedProject?.id ? null : project; - this.projectItemClick = true; + handleProjectSelect(items) { + // hack: simulate a single select to prevent the dropdown from closing + // todo: switch back to single select when https://gitlab.com/gitlab-org/gitlab-ui/-/issues/2363 is fixed + this.selectedProjects = [items[items.length - 1]]; }, handleMoveClick() { - this.$refs.dropdown.hide(); - this.$emit('move-issuable', this.selectedProject); + this.$refs.dropdown.close(); + this.$emit( + 'move-issuable', + this.projects.find((item) => item.id === this.selectedProjects[0]), + ); + }, + handleDropdownHide() { + this.$emit('dropdown-close'); }, }, }; @@ -143,79 +113,45 @@ export default { > <gl-icon name="arrow-right" /> </div> - <gl-dropdown + <gl-collapsible-listbox ref="dropdown" + v-model="selectedProjects" + :items="projectsList" :block="true" - :disabled="moveInProgress || disabled" - class="hide-collapsed" - toggle-class="js-sidebar-dropdown-toggle" - @shown="fetchProjects" - @hide="handleDropdownHide" + :multiple="true" + :searchable="true" + :searching="isSearching" + :search-placeholder="__('Search project')" + :no-results-text="noResultsText" + :header-text="dropdownButtonTitle" + @hidden="handleDropdownHide" + @shown="triggerSearch" + @search="fetchProjects" + @select="handleProjectSelect" > - <template #button-content - ><gl-loading-icon v-if="moveInProgress" size="sm" class="gl-mr-3" />{{ - dropdownButtonTitle - }}</template - > - <gl-dropdown-form class="gl-pt-0"> - <div - data-testid="header" - class="gl-display-flex gl-pb-3 gl-border-1 gl-border-b-solid gl-border-gray-100" - > - <span class="gl-flex-grow-1 gl-text-center gl-font-weight-bold gl-py-1">{{ - dropdownHeaderTitle - }}</span> - <gl-button - variant="link" - icon="close" - class="gl-mr-2 gl-w-auto! gl-p-2!" - :aria-label="__('Close')" - @click.prevent="handleDropdownCloseClick" - /> - </div> - <gl-search-box-by-type - ref="searchInput" - v-model.trim="searchKey" - :placeholder="__('Search project')" - :debounce="300" - /> - <div data-testid="content" class="dropdown-content"> - <gl-loading-icon v-if="projectsListLoading" size="lg" class="gl-p-5" /> - <ul v-else> - <gl-dropdown-item - v-for="project in projects" - :key="project.id" - is-check-item - :is-checked="isSelectedProject(project)" - @click.stop.prevent="handleProjectSelect(project)" - >{{ project.name_with_namespace }}</gl-dropdown-item - > - </ul> - <div v-if="hasNoSearchResults" class="gl-text-center gl-p-3"> - {{ __('No matching results') }} - </div> - <div - v-if="failedToLoadResults" - data-testid="failed-load-results" - class="gl-text-center gl-p-3" - > - {{ __('Failed to load projects') }} - </div> - </div> - <div - data-testid="footer" - class="gl-pt-3 gl-px-3 gl-border-1 gl-border-t-solid gl-border-gray-100" + <template #toggle> + <gl-button + :loading="moveInProgress" + size="medium" + class="gl-w-full js-sidebar-dropdown-toggle hide-collapsed" + data-testid="dropdown-button" + :disabled="moveInProgress || disabled" + >{{ dropdownButtonTitle }}</gl-button > + </template> + <template #footer> + <div data-testid="footer" class="gl-p-3"> <gl-button category="primary" variant="confirm" - :disabled="!Boolean(selectedProject)" - class="gl-w-full issuable-move-button" + :disabled="!Boolean(selectedProjects.length)" + class="gl-w-full" + data-testid="dropdown-move-button" @click="handleMoveClick" >{{ __('Move') }}</gl-button > </div> - </gl-dropdown-form> - </gl-dropdown> + </template> + </gl-collapsible-listbox> </div> </template> diff --git a/app/assets/javascripts/work_items/components/work_item_milestone.vue b/app/assets/javascripts/work_items/components/work_item_milestone.vue index fe09105e173..9c6fa158169 100644 --- a/app/assets/javascripts/work_items/components/work_item_milestone.vue +++ b/app/assets/javascripts/work_items/components/work_item_milestone.vue @@ -1,13 +1,5 @@ <script> -import { - GlFormGroup, - GlDropdown, - GlDropdownItem, - GlDropdownDivider, - GlSkeletonLoader, - GlSearchBoxByType, - GlDropdownText, -} from '@gitlab/ui'; +import { GlCollapsibleListbox, GlFormGroup, GlSkeletonLoader } from '@gitlab/ui'; import { debounce } from 'lodash'; import * as Sentry from '~/sentry/sentry_browser_wrapper'; import Tracking from '~/tracking'; @@ -22,7 +14,8 @@ import { TRACKING_CATEGORY_SHOW, } from '../constants'; -const noMilestoneId = 'no-milestone-id'; +export const noMilestoneId = 'no-milestone-id'; +const noMilestoneItem = { text: s__('WorkItem|No milestone'), value: noMilestoneId }; export default { i18n: { @@ -37,13 +30,9 @@ export default { EXPIRED_TEXT: __('(expired)'), }, components: { + GlCollapsibleListbox, GlFormGroup, - GlDropdown, - GlDropdownItem, - GlDropdownDivider, GlSkeletonLoader, - GlSearchBoxByType, - GlDropdownText, }, mixins: [Tracking.mixin()], props: { @@ -74,11 +63,23 @@ export default { data() { return { localMilestone: this.workItemMilestone, + localMilestoneId: this.workItemMilestone?.id, searchTerm: '', shouldFetch: false, updateInProgress: false, - isFocused: false, milestones: [], + dropdownGroups: [ + { + text: this.$options.i18n.NO_MILESTONE, + textSrOnly: true, + options: [noMilestoneItem], + }, + { + text: __('Milestones'), + textSrOnly: true, + options: [], + }, + ], }; }, computed: { @@ -103,23 +104,29 @@ export default { isLoadingMilestones() { return this.$apollo.queries.milestones.loading; }, - isNoMilestone() { - return this.localMilestone?.id === noMilestoneId || !this.localMilestone?.id; + milestonesList() { + return ( + this.milestones.map(({ id, title, expired }) => { + return { + value: id, + text: title, + expired, + }; + }) ?? [] + ); }, - dropdownClasses() { - return { - 'gl-text-gray-500!': this.canUpdate && this.isNoMilestone, - 'is-not-focused': !this.isFocused, - 'gl-min-w-20': true, - }; + toggleClasses() { + const toggleClasses = ['gl-max-w-full']; + + if (this.localMilestoneId === noMilestoneId) { + toggleClasses.push('gl-text-gray-500!'); + } + return toggleClasses; }, }, watch: { - workItemMilestone: { - handler(newVal) { - this.localMilestone = newVal; - }, - deep: true, + milestones() { + this.dropdownGroups[1].options = this.milestonesList; }, }, created() { @@ -152,15 +159,11 @@ export default { this.localMilestone = milestone; }, onDropdownShown() { - this.$refs.search.focusInput(); this.shouldFetch = true; - this.isFocused = true; }, onDropdownHide() { - this.isFocused = false; this.searchTerm = ''; this.shouldFetch = false; - this.updateMilestone(); }, setSearchKey(value) { this.searchTerm = value; @@ -169,6 +172,9 @@ export default { return this.localMilestone?.id === milestone?.id; }, updateMilestone() { + this.localMilestone = + this.milestones.find(({ id }) => id === this.localMilestoneId) ?? noMilestoneItem; + if (this.workItemMilestone?.id === this.localMilestone?.id) { return; } @@ -182,8 +188,7 @@ export default { input: { id: this.workItemId, milestoneWidget: { - milestoneId: - this.localMilestone?.id === 'no-milestone-id' ? null : this.localMilestone?.id, + milestoneId: this.localMilestoneId === noMilestoneId ? null : this.localMilestoneId, }, }, }, @@ -222,49 +227,45 @@ export default { > {{ dropdownText }} </span> - <gl-dropdown + + <gl-collapsible-listbox v-else id="milestone-value" - class="gl-pl-0 gl-max-w-full work-item-field-value" - :toggle-class="dropdownClasses" - :text="dropdownText" + v-model="localMilestoneId" + :items="dropdownGroups" + category="tertiary" + data-testid="work-item-milestone-dropdown" + class="gl-max-w-full" + :toggle-text="dropdownText" :loading="updateInProgress" + :toggle-class="toggleClasses" + searchable + @select="updateMilestone" @shown="onDropdownShown" - @hide="onDropdownHide" + @hidden="onDropdownHide" + @search="debouncedSearchKeyUpdate" > - <template #header> - <gl-search-box-by-type ref="search" :value="searchTerm" @input="debouncedSearchKeyUpdate" /> + <template #list-item="{ item }"> + {{ item.text }} + <span v-if="item.expired">{{ $options.i18n.EXPIRED_TEXT }}</span> </template> - <gl-dropdown-item - data-testid="no-milestone" - is-check-item - :is-checked="isNoMilestone" - @click="handleMilestoneClick({ id: 'no-milestone-id' })" - > - {{ $options.i18n.NO_MILESTONE }} - </gl-dropdown-item> - <gl-dropdown-divider /> - <gl-dropdown-text v-if="isLoadingMilestones"> - <gl-skeleton-loader :height="90"> + <template #footer> + <gl-skeleton-loader v-if="isLoadingMilestones" :height="90"> <rect width="380" height="10" x="10" y="15" rx="4" /> <rect width="280" height="10" x="10" y="30" rx="4" /> <rect width="380" height="10" x="10" y="50" rx="4" /> <rect width="280" height="10" x="10" y="65" rx="4" /> </gl-skeleton-loader> - </gl-dropdown-text> - <template v-else-if="milestones.length"> - <gl-dropdown-item - v-for="milestone in milestones" - :key="milestone.id" - is-check-item - :is-checked="isMilestoneChecked(milestone)" - @click="handleMilestoneClick(milestone)" + + <div + v-else-if="!milestones.length" + aria-live="assertive" + class="gl-pl-7 gl-pr-5 gl-py-3 gl-font-base gl-text-gray-600" + data-testid="no-results-text" > - {{ milestone.title }} - <template v-if="milestone.expired">{{ $options.i18n.EXPIRED_TEXT }}</template> - </gl-dropdown-item> + {{ $options.i18n.NO_MATCHING_RESULTS }} + </div> </template> - <gl-dropdown-text v-else>{{ $options.i18n.NO_MATCHING_RESULTS }}</gl-dropdown-text> - </gl-dropdown> + </gl-collapsible-listbox> </gl-form-group> </template> diff --git a/app/assets/javascripts/work_items/components/work_item_parent.vue b/app/assets/javascripts/work_items/components/work_item_parent.vue index 23660f24783..74f0ec42905 100644 --- a/app/assets/javascripts/work_items/components/work_item_parent.vue +++ b/app/assets/javascripts/work_items/components/work_item_parent.vue @@ -61,7 +61,6 @@ export default { searchStarted: false, availableWorkItems: [], localSelectedItem: this.parent?.id, - isNotFocused: true, oldParent: this.parent, }; }, @@ -82,14 +81,6 @@ export default { workItems() { return this.availableWorkItems.map(({ id, title }) => ({ text: title, value: id })); }, - listboxCategory() { - return this.searchStarted ? 'secondary' : 'tertiary'; - }, - listboxClasses() { - return { - 'is-not-focused': this.isNotFocused && !this.searchStarted, - }; - }, parentType() { return SUPPORTED_PARENT_TYPE_MAP[this.workItemType]; }, @@ -184,19 +175,10 @@ export default { }, onListboxShown() { this.searchStarted = true; - this.isNotFocused = false; }, onListboxHide() { this.searchStarted = false; this.search = ''; - this.isNotFocused = true; - }, - setListboxFocused() { - // This is to match the caret behaviour of parent listbox - // to the other dropdown fields of work items - if (document.activeElement.parentElement.id !== 'work-item-parent-listbox-value') { - this.isNotFocused = true; - } }, }, }; @@ -219,30 +201,20 @@ export default { > {{ listboxText }} </span> - <div - v-else - :class="{ 'gl-max-w-max-content': !workItemsMvc2Enabled }" - @mouseover="isNotFocused = false" - @mouseleave="setListboxFocused" - @focusout="isNotFocused = true" - @focusin="isNotFocused = false" - > + <div v-else :class="{ 'gl-max-w-max-content': !workItemsMvc2Enabled }"> <gl-collapsible-listbox id="work-item-parent-listbox-value" class="gl-max-w-max-content" data-testid="work-item-parent-listbox" - block searchable - :no-caret="isNotFocused && !searchStarted" is-check-centered - :category="listboxCategory" + category="tertiary" :searching="isLoading" :header-text="$options.i18n.assignParentLabel" :no-results-text="$options.i18n.noMatchingResults" :loading="updateInProgress" :items="workItems" :toggle-text="listboxText" - :toggle-class="listboxClasses" :selected="localSelectedItem" :reset-button-label="$options.i18n.unAssign" @reset="unAssignParent" diff --git a/app/assets/stylesheets/page_bundles/issuable.scss b/app/assets/stylesheets/page_bundles/issuable.scss index 07614c5271a..d52cd45e575 100644 --- a/app/assets/stylesheets/page_bundles/issuable.scss +++ b/app/assets/stylesheets/page_bundles/issuable.scss @@ -114,29 +114,6 @@ } } -/* - * Following overrides are done to prevent - * legacy dropdown styles from influencing - * GitLab UI components used within GlDropdown - */ -.issuable-move-dropdown { - .b-dropdown-form { - @include gl-p-0; - } - - .gl-search-box-by-type button.gl-clear-icon-button:hover { - @include gl-bg-transparent; - - &:focus { - @include gl-focus($inset: true); - } - } - - .issuable-move-button:not(.disabled):hover { - @include gl-text-white; - } -} - .suggestion-footer { font-size: 12px; line-height: 15px; diff --git a/app/assets/stylesheets/page_bundles/work_items.scss b/app/assets/stylesheets/page_bundles/work_items.scss index f41a1f540e3..154803c7d88 100644 --- a/app/assets/stylesheets/page_bundles/work_items.scss +++ b/app/assets/stylesheets/page_bundles/work_items.scss @@ -67,6 +67,7 @@ $work-item-sticky-header-height: 52px; } } +//TODO: remove all the styles related to `gl-dropdown` when all `.work-item-dropdown`s are migrated .work-item-dropdown { // duplicate classname because we are fighting with gl-button styles .gl-dropdown-toggle.gl-dropdown-toggle { @@ -95,24 +96,25 @@ $work-item-sticky-header-height: 52px; // need to override the listbox styles to match with dropdown // till the dropdown are converted to listbox - .gl-new-dropdown-toggle { + .gl-new-dropdown-toggle.gl-new-dropdown-toggle { &:hover, &:focus { - background: none !important; box-shadow: $work-item-field-inset-shadow; background-color: $input-bg; - } - .is-not-focused { - &.gl-new-dropdown-button-text { - margin: 0 0.25rem; + .gl-dark & { + // $input-bg is overridden in dark mode but that does not + // work in page bundles currently, manually override here + background-color: var(--gray-50, $input-bg); } } - } - .gl-new-dropdown-toggle.is-not-focused { - .gl-new-dropdown-button-text { - margin: 0 0.25rem; + &:not(:hover, :focus) { + box-shadow: none; + + .gl-new-dropdown-chevron { + visibility: hidden; + } } } |