diff options
Diffstat (limited to 'app/assets/javascripts/invite_members')
5 files changed, 87 insertions, 78 deletions
diff --git a/app/assets/javascripts/invite_members/components/invite_members_modal.vue b/app/assets/javascripts/invite_members/components/invite_members_modal.vue index fa1aa6b0d88..607c888b85a 100644 --- a/app/assets/javascripts/invite_members/components/invite_members_modal.vue +++ b/app/assets/javascripts/invite_members/components/invite_members_modal.vue @@ -1,8 +1,7 @@ <script> import { GlAlert, - GlDropdown, - GlDropdownItem, + GlCollapsibleListbox, GlLink, GlSprintf, GlFormCheckboxGroup, @@ -13,6 +12,7 @@ import { import { partition, isString, uniqueId, isEmpty } from 'lodash'; import InviteModalBase from 'ee_else_ce/invite_members/components/invite_modal_base.vue'; import Api from '~/api'; +import Tracking from '~/tracking'; import ExperimentTracking from '~/experimentation/experiment_tracking'; import { BV_SHOW_MODAL, BV_HIDE_MODAL } from '~/lib/utils/constants'; import { getParameterValues } from '~/lib/utils/url_utility'; @@ -22,6 +22,7 @@ import { INVITE_MEMBERS_FOR_TASK, MEMBER_MODAL_LABELS, LEARN_GITLAB, + INVITE_MEMBER_MODAL_TRACKING_CATEGORY, } from '../constants'; import eventHub from '../event_hub'; import { responseFromSuccess } from '../utils/response_message_parser'; @@ -40,8 +41,7 @@ export default { components: { GlAlert, GlLink, - GlDropdown, - GlDropdownItem, + GlCollapsibleListbox, GlSprintf, GlFormCheckboxGroup, GlButton, @@ -52,6 +52,7 @@ export default { ModalConfetti, UserLimitNotification, }, + mixins: [Tracking.mixin({ category: INVITE_MEMBER_MODAL_TRACKING_CATEGORY })], inject: ['newProjectPath'], props: { id: { @@ -109,6 +110,11 @@ export default { required: false, default: () => ({}), }, + activeTrialDataset: { + type: Object, + required: false, + default: () => ({}), + }, reloadPageOnSubmit: { type: Boolean, required: false, @@ -124,6 +130,7 @@ export default { invalidMembers: {}, selectedTasksToBeDone: [], selectedTaskProject: this.projects[0], + selectedTaskProjectId: this.projects[0]?.id, source: 'unknown', mode: 'default', // Kept in sync with "base" @@ -131,6 +138,7 @@ export default { errorsLimit: 2, isErrorsSectionExpanded: false, shouldShowEmptyInvitesAlert: false, + projectsForDropdown: this.projects.map((p) => ({ value: p.id, text: p.title, ...p })), }; }, computed: { @@ -263,11 +271,12 @@ export default { usersToAddById.map((user) => user.id).join(','), ]; }, - openModal({ mode = 'default', source }) { + openModal({ mode = 'default', source = 'unknown' }) { this.mode = mode; this.source = source; this.$root.$emit(BV_SHOW_MODAL, this.modalId); + this.track('render', { label: this.source }); }, closeModal() { this.$root.$emit(BV_HIDE_MODAL, this.modalId); @@ -339,6 +348,12 @@ export default { const tracking = new ExperimentTracking(INVITE_MEMBERS_FOR_TASK.name, { label, property }); tracking.event(INVITE_MEMBERS_FOR_TASK.submit); }, + onCancel() { + this.track('click_cancel', { label: this.source }); + }, + onClose() { + this.track('click_x', { label: this.source }); + }, resetFields() { this.clearValidation(); this.isLoading = false; @@ -347,10 +362,12 @@ export default { this.selectedTasksToBeDone = []; [this.selectedTaskProject] = this.projects; }, - changeSelectedTaskProject(project) { - this.selectedTaskProject = project; + changeSelectedTaskProject(projectId) { + this.selectedTaskProject = this.projects.find((project) => project.id === projectId); }, onInviteSuccess() { + this.track('invite_successful', { label: this.source }); + if (this.reloadPageOnSubmit) { reloadOnInvitationSuccess(); } else { @@ -404,7 +421,10 @@ export default { :new-users-to-invite="newUsersToInvite" :root-group-id="rootId" :users-limit-dataset="usersLimitDataset" + :active-trial-dataset="activeTrialDataset" :full-path="fullPath" + @close="onClose" + @cancel="onCancel" @reset="resetFields" @submit="sendInvite" @access-level="onAccessLevelUpdate" @@ -513,23 +533,14 @@ export default { <label class="gl-mt-5 gl-display-block"> {{ $options.labels.tasksProject.title }} </label> - <gl-dropdown + <gl-collapsible-listbox + v-model="selectedTaskProjectId" + :items="projectsForDropdown" + :block="true" class="gl-w-half gl-xs-w-full" - :text="selectedTaskProject.title" data-testid="invite-members-modal-project-select" - > - <template v-for="project in projects"> - <gl-dropdown-item - :key="project.id" - active-class="is-active" - is-check-item - :is-checked="project.id === selectedTaskProject.id" - @click="changeSelectedTaskProject(project)" - > - {{ project.title }} - </gl-dropdown-item> - </template> - </gl-dropdown> + @select="changeSelectedTaskProject" + /> </template> </template> <gl-alert diff --git a/app/assets/javascripts/invite_members/components/invite_modal_base.vue b/app/assets/javascripts/invite_members/components/invite_modal_base.vue index 2cbd681c67d..1e3b6093f0b 100644 --- a/app/assets/javascripts/invite_members/components/invite_modal_base.vue +++ b/app/assets/javascripts/invite_members/components/invite_modal_base.vue @@ -206,7 +206,7 @@ export default { this.track('render', { category: 'default', label: ON_SHOW_TRACK_LABEL }); } }, - onCloseModal(e) { + onCancel(e) { if (this.preventCancelDefault) { e.preventDefault(); } else { @@ -225,6 +225,9 @@ export default { expiresAt: this.selectedDate, }); }, + onClose() { + this.$emit('close'); + }, }, HEADER_CLOSE_LABEL, ACCESS_EXPIRE_DATE, @@ -249,7 +252,8 @@ export default { :action-cancel="actionCancel" @shown="onShowModal" @primary="onSubmit" - @cancel="onCloseModal" + @cancel="onCancel" + @close="onClose" @hidden="onReset" > <content-transition @@ -267,11 +271,12 @@ export default { <strong>{{ content }}</strong> </template> </gl-sprintf> + <slot name="intro-text-after"></slot> </p> - <slot name="intro-text-after"></slot> </div> <slot name="alert"></slot> + <slot name="active-trial-alert"></slot> <gl-form-group :label="labelSearchField" diff --git a/app/assets/javascripts/invite_members/components/project_select.vue b/app/assets/javascripts/invite_members/components/project_select.vue index b7a3918813b..c1114c240b9 100644 --- a/app/assets/javascripts/invite_members/components/project_select.vue +++ b/app/assets/javascripts/invite_members/components/project_select.vue @@ -1,28 +1,23 @@ <script> -import { - GlAvatarLabeled, - GlDropdown, - GlDropdownItem, - GlDropdownText, - GlSearchBoxByType, -} from '@gitlab/ui'; +import { GlAvatarLabeled, GlCollapsibleListbox } from '@gitlab/ui'; import { debounce } from 'lodash'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { s__ } from '~/locale'; import { getProjects } from '~/rest_api'; import { SEARCH_DELAY, GROUP_FILTERS } from '../constants'; +// We can have GlCollapsibleListbox dropdown panel with full +// width once we implement +// https://gitlab.com/gitlab-org/gitlab-ui/-/issues/2133 +// https://gitlab.com/gitlab-org/gitlab/-/issues/390411 export default { name: 'ProjectSelect', components: { GlAvatarLabeled, - GlDropdown, - GlDropdownItem, - GlDropdownText, - GlSearchBoxByType, + GlCollapsibleListbox, }, model: { - prop: 'selectedProject', + prop: 'selectedProjectId', }, props: { groupsFilter: { @@ -41,18 +36,21 @@ export default { return { isFetching: false, projects: [], - selectedProject: {}, + selectedProjectId: '', searchTerm: '', errorMessage: '', }; }, computed: { selectedProjectName() { - return this.selectedProject.name || this.$options.i18n.dropdownText; + return this.selectedProject.nameWithNamespace || this.$options.i18n.dropdownText; }, isFetchResultEmpty() { return this.projects.length === 0 && !this.isFetching; }, + selectedProject() { + return this.projects.find((prj) => prj.id === this.selectedProjectId) || {}; + }, }, watch: { searchTerm() { @@ -70,10 +68,14 @@ export default { .then((response) => { this.projects = response.data.map((project) => ({ ...convertObjectPropsToCamelCase(project), - name: project.name_with_namespace, + text: project.name_with_namespace, + value: project.id, })); }) .catch(() => { + // To be displayed in GlCollapsibleListbox once we implement + // https://gitlab.com/gitlab-org/gitlab-ui/-/issues/2132 + // https://gitlab.com/gitlab-org/gitlab/-/issues/389974 this.errorMessage = this.$options.i18n.errorFetchingProjects; }) .finally(() => { @@ -83,9 +85,7 @@ export default { fetchProjects() { return getProjects(this.searchTerm, this.$options.defaultFetchOptions); }, - selectProject(project) { - this.selectedProject = project; - + selectProject() { this.$emit('input', this.selectedProject); }, }, @@ -104,40 +104,28 @@ export default { }; </script> <template> - <div> - <gl-dropdown - data-testid="project-select-dropdown" - :text="selectedProjectName" - toggle-class="gl-mb-2" - block - menu-class="gl-w-full!" - > - <gl-search-box-by-type - v-model="searchTerm" - :is-loading="isFetching" - :placeholder="$options.i18n.searchPlaceholder" - data-qa-selector="project_select_dropdown_search_field" + <gl-collapsible-listbox + v-model="selectedProjectId" + searchable + :items="projects" + :searching="isFetching" + :toggle-text="selectedProjectName" + :search-placeholder="$options.i18n.searchPlaceholder" + :no-results-text="$options.i18n.emptySearchResult" + data-testid="project-select-dropdown" + data-qa-selector="project_select_dropdown" + class="gl-collapsible-listbox-w-full" + @search="searchTerm = $event" + @select="selectProject" + > + <template #list-item="{ item }"> + <gl-avatar-labeled + :label="item.text" + :src="item.avatarUrl" + :entity-id="item.id" + :entity-name="item.name" + :size="32" /> - <gl-dropdown-item - v-for="project in projects" - :key="project.id" - :name="project.name" - @click="selectProject(project)" - > - <gl-avatar-labeled - :label="project.name" - :src="project.avatarUrl" - :entity-id="project.id" - :entity-name="project.name" - :size="32" - /> - </gl-dropdown-item> - <gl-dropdown-text v-if="errorMessage" data-testid="error-message"> - <span class="gl-text-gray-500">{{ errorMessage }}</span> - </gl-dropdown-text> - <gl-dropdown-text v-else-if="isFetchResultEmpty" data-testid="empty-result-message"> - <span class="gl-text-gray-500">{{ $options.i18n.emptySearchResult }}</span> - </gl-dropdown-text> - </gl-dropdown> - </div> + </template> + </gl-collapsible-listbox> </template> diff --git a/app/assets/javascripts/invite_members/constants.js b/app/assets/javascripts/invite_members/constants.js index edc0ebff083..ac0b708c55e 100644 --- a/app/assets/javascripts/invite_members/constants.js +++ b/app/assets/javascripts/invite_members/constants.js @@ -20,6 +20,7 @@ export const USERS_FILTER_ALL = 'all'; export const USERS_FILTER_SAML_PROVIDER_ID = 'saml_provider_id'; export const TRIGGER_ELEMENT_BUTTON = 'button'; export const TRIGGER_ELEMENT_SIDE_NAV = 'side-nav'; +export const INVITE_MEMBER_MODAL_TRACKING_CATEGORY = 'invite_members_modal'; export const TRIGGER_DEFAULT_QA_SELECTOR = 'invite_members_button'; export const MEMBERS_MODAL_DEFAULT_TITLE = s__('InviteMembersModal|Invite members'); export const MEMBERS_MODAL_CELEBRATE_TITLE = s__( @@ -138,6 +139,7 @@ export const GROUP_MODAL_LABELS = { export const LEARN_GITLAB = 'learn_gitlab'; export const ON_SHOW_TRACK_LABEL = 'over_limit_modal_viewed'; +export const ON_CELEBRATION_TRACK_LABEL = 'invite_celebration_modal'; export const INFO_ALERT_TITLE = s__( 'InviteMembersModal|Your top-level group %{namespaceName} is over the %{dashboardLimit} user limit.', diff --git a/app/assets/javascripts/invite_members/init_invite_members_modal.js b/app/assets/javascripts/invite_members/init_invite_members_modal.js index 842ab07f368..4f539cd8756 100644 --- a/app/assets/javascripts/invite_members/init_invite_members_modal.js +++ b/app/assets/javascripts/invite_members/init_invite_members_modal.js @@ -41,6 +41,9 @@ export default (function initInviteMembersModal() { usersLimitDataset: convertObjectPropsToCamelCase( JSON.parse(el.dataset.usersLimitDataset || '{}'), ), + activeTrialDataset: convertObjectPropsToCamelCase( + JSON.parse(el.dataset.activeTrialDataset || '{}'), + ), reloadPageOnSubmit: parseBoolean(el.dataset.reloadPageOnSubmit), }, }), |