From 2072ce67553ddf12d580de279bc2e17ec8878216 Mon Sep 17 00:00:00 2001 From: Shane Maglangit <smaglangit@gitlab.com> Date: Wed, 18 Dec 2024 12:47:19 +0000 Subject: [PATCH] Move organization role change dropdown to drawer --- .../organizations/shared/constants.js | 9 + .../users/components/user_details_drawer.vue | 160 ++++++++++++ .../users/components/users_view.vue | 96 +++---- locale/gitlab.pot | 3 + .../components/user_details_drawer_spec.js | 239 ++++++++++++++++++ .../users/components/users_view_spec.js | 193 +++----------- 6 files changed, 478 insertions(+), 222 deletions(-) create mode 100644 app/assets/javascripts/organizations/users/components/user_details_drawer.vue create mode 100644 spec/frontend/organizations/users/components/user_details_drawer_spec.js diff --git a/app/assets/javascripts/organizations/shared/constants.js b/app/assets/javascripts/organizations/shared/constants.js index c0661e79de974..b65ebfd06ce8b 100644 --- a/app/assets/javascripts/organizations/shared/constants.js +++ b/app/assets/javascripts/organizations/shared/constants.js @@ -9,6 +9,15 @@ export const ORGANIZATION_ROOT_ROUTE_NAME = 'root'; export const ACCESS_LEVEL_DEFAULT = 'default'; export const ACCESS_LEVEL_OWNER = 'owner'; +// Matches `app/graphql/types/organizations/organization_user_access_level_enum.rb +export const ACCESS_LEVEL_DEFAULT_STRING = 'DEFAULT'; +export const ACCESS_LEVEL_OWNER_STRING = 'OWNER'; + +export const ACCESS_LEVEL_LABEL = { + [ACCESS_LEVEL_DEFAULT_STRING]: __('User'), + [ACCESS_LEVEL_OWNER_STRING]: __('Owner'), +}; + export const FORM_FIELD_NAME = 'name'; export const FORM_FIELD_ID = 'id'; export const FORM_FIELD_PATH = 'path'; diff --git a/app/assets/javascripts/organizations/users/components/user_details_drawer.vue b/app/assets/javascripts/organizations/users/components/user_details_drawer.vue new file mode 100644 index 0000000000000..6aa5218db18fc --- /dev/null +++ b/app/assets/javascripts/organizations/users/components/user_details_drawer.vue @@ -0,0 +1,160 @@ +<script> +import { GlCollapsibleListbox, GlDrawer, GlTooltipDirective } from '@gitlab/ui'; +import UserAvatar from '~/vue_shared/components/users_table/user_avatar.vue'; +import { + ACCESS_LEVEL_DEFAULT_STRING, + ACCESS_LEVEL_LABEL, + ACCESS_LEVEL_OWNER_STRING, +} from '~/organizations/shared/constants'; +import { getContentWrapperHeight } from '~/lib/utils/dom_utils'; +import { DRAWER_Z_INDEX } from '~/lib/utils/constants'; +import { s__ } from '~/locale'; +import organizationUserUpdateMutation from '~/organizations/users/graphql/mutations/organization_user_update.mutation.graphql'; +import { createAlert } from '~/alert'; + +export default { + name: 'UserDetailsDrawer', + components: { + GlCollapsibleListbox, + GlDrawer, + UserAvatar, + }, + directives: { + GlTooltip: GlTooltipDirective, + }, + inject: ['paths'], + i18n: { + title: s__('Organization|Organization user details'), + roleListboxLabel: s__('Organization|Organization role'), + disabledRoleListboxTooltipText: s__('Organization|Organizations must have at least one owner.'), + errorMessage: s__( + 'Organization|An error occurred updating the organization role. Please try again.', + ), + successMessage: s__('Organization|Organization role was updated successfully.'), + }, + roleListboxItems: [ + { + text: ACCESS_LEVEL_LABEL[ACCESS_LEVEL_DEFAULT_STRING], + value: ACCESS_LEVEL_DEFAULT_STRING, + }, + { + text: ACCESS_LEVEL_LABEL[ACCESS_LEVEL_OWNER_STRING], + value: ACCESS_LEVEL_OWNER_STRING, + }, + ], + props: { + user: { + type: Object, + required: false, + default: null, + }, + }, + data() { + return { + initialAccessLevel: this.user?.accessLevel.stringValue, + selectedAccessLevel: this.user?.accessLevel.stringValue, + loading: false, + }; + }, + computed: { + drawerHeaderHeight() { + return getContentWrapperHeight(); + }, + roleListboxDisabled() { + return this.user?.isLastOwner; + }, + roleListboxTooltip() { + return this.roleListboxDisabled ? this.$options.i18n.disabledRoleListboxTooltipText : null; + }, + }, + watch: { + user(value) { + this.initialAccessLevel = value?.accessLevel.stringValue; + this.selectedAccessLevel = value?.accessLevel.stringValue; + }, + }, + methods: { + setLoading(value) { + this.loading = value; + this.$emit('loading', value); + }, + onUpdateSuccess() { + this.initialAccessLevel = this.selectedAccessLevel; + this.$toast.show(this.$options.i18n.successMessage); + this.$emit('role-change'); + }, + async onRoleSelect() { + this.setLoading(true); + + try { + const { + data: { + organizationUserUpdate: { errors }, + }, + } = await this.$apollo.mutate({ + mutation: organizationUserUpdateMutation, + variables: { + input: { + id: this.user.gid, + accessLevel: this.selectedAccessLevel, + }, + }, + }); + + if (errors.length) { + createAlert({ message: errors[0] }); + return; + } + + this.onUpdateSuccess(); + } catch (error) { + createAlert({ message: this.$options.i18n.errorMessage, error, captureError: true }); + } finally { + this.setLoading(false); + } + }, + close() { + this.$emit('close'); + }, + }, + DRAWER_Z_INDEX, +}; +</script> + +<template> + <gl-drawer + v-if="user" + open + header-sticky + :header-height="drawerHeaderHeight" + :z-index="$options.DRAWER_Z_INDEX" + @close="close" + > + <template #title> + <h4 class="gl-m-0">{{ $options.i18n.title }}</h4> + </template> + <template #default> + <div> + <user-avatar :user="user" :admin-user-path="paths.adminUser" /> + </div> + <div> + <h5>{{ $options.i18n.roleListboxLabel }}</h5> + <div + v-gl-tooltip="{ disabled: !roleListboxTooltip, title: roleListboxTooltip }" + class="gl-rounded-base focus:gl-focus" + :tabindex="roleListboxDisabled && 0" + > + <gl-collapsible-listbox + v-model="selectedAccessLevel" + block + toggle-class="gl-form-input-xl" + :disabled="roleListboxDisabled" + :items="$options.roleListboxItems" + :loading="loading" + @select="onRoleSelect" + /> + </div> + </div> + </template> + </gl-drawer> +</template> diff --git a/app/assets/javascripts/organizations/users/components/users_view.vue b/app/assets/javascripts/organizations/users/components/users_view.vue index 1dff17eef9465..e8a3b307733d0 100644 --- a/app/assets/javascripts/organizations/users/components/users_view.vue +++ b/app/assets/javascripts/organizations/users/components/users_view.vue @@ -1,21 +1,19 @@ <script> -import { - GlLoadingIcon, - GlKeysetPagination, - GlCollapsibleListbox, - GlTooltipDirective, -} from '@gitlab/ui'; -import { createAlert } from '~/alert'; +import { GlButton, GlLoadingIcon, GlKeysetPagination, GlTooltipDirective } from '@gitlab/ui'; import UsersTable from '~/vue_shared/components/users_table/users_table.vue'; +import UserDetailsDrawer from '~/organizations/users/components/user_details_drawer.vue'; import { FIELD_NAME, FIELD_ORGANIZATION_ROLE, FIELD_CREATED_AT, FIELD_LAST_ACTIVITY_ON, } from '~/vue_shared/components/users_table/constants'; -import { ACCESS_LEVEL_DEFAULT, ACCESS_LEVEL_OWNER } from '~/organizations/shared/constants'; +import { + ACCESS_LEVEL_DEFAULT, + ACCESS_LEVEL_OWNER, + ACCESS_LEVEL_LABEL, +} from '~/organizations/shared/constants'; import { __, s__ } from '~/locale'; -import organizationUserUpdateMutation from '../graphql/mutations/organization_user_update.mutation.graphql'; export default { name: 'UsersView', @@ -30,10 +28,11 @@ export default { GlTooltip: GlTooltipDirective, }, components: { + GlButton, GlLoadingIcon, GlKeysetPagination, - GlCollapsibleListbox, UsersTable, + UserDetailsDrawer, }, inject: ['paths'], roleListboxItems: [ @@ -70,52 +69,22 @@ export default { }, data() { return { - roleListboxLoadingStates: [], + userDetailsDrawerActiveUser: null, + userDetailsDrawerLoading: false, }; }, methods: { - async onRoleSelect(accessLevel, user) { - this.roleListboxLoadingStates.push(user.gid); - - try { - const { - data: { - organizationUserUpdate: { errors }, - }, - } = await this.$apollo.mutate({ - mutation: organizationUserUpdateMutation, - variables: { - input: { - id: user.gid, - accessLevel, - }, - }, - }); - - if (errors.length) { - createAlert({ message: errors[0] }); - - return; - } - - this.$toast.show(this.$options.i18n.successMessage); - this.$emit('role-change'); - } catch (error) { - createAlert({ message: this.$options.i18n.errorMessage, error, captureError: true }); - } finally { - this.roleListboxLoadingStates.splice(this.roleListboxLoadingStates.indexOf(user.gid), 1); - } + setUserDetailsDrawerActiveUser(user) { + this.userDetailsDrawerActiveUser = user; }, - roleListboxItemText(accessLevel) { - return this.$options.roleListboxItems.find((item) => item.value === accessLevel).text; + setUserDetailsDrawerLoading(loading) { + this.userDetailsDrawerLoading = loading; }, - isRoleListboxDisabled(user) { - return user.isLastOwner; + onRoleChange() { + this.$emit('role-change'); }, - roleListboxTooltip(user) { - return this.isRoleListboxDisabled(user) - ? this.$options.i18n.disabledRoleListboxTooltipText - : null; + userAccessLevelLabel(user) { + return ACCESS_LEVEL_LABEL[user.accessLevel.stringValue]; }, }, }; @@ -132,26 +101,25 @@ export default { :column-widths="$options.usersTable.columnWidths" > <template #organization-role="{ user }"> - <div - v-gl-tooltip="{ disabled: !roleListboxTooltip(user), title: roleListboxTooltip(user) }" - class="gl-rounded-base focus:gl-focus" - :tabindex="isRoleListboxDisabled(user) && 0" + <gl-button + class="gl-block" + variant="link" + :disabled="userDetailsDrawerLoading" + @click="setUserDetailsDrawerActiveUser(user)" > - <gl-collapsible-listbox - :disabled="isRoleListboxDisabled(user)" - :selected="user.accessLevel.stringValue" - block - toggle-class="gl-form-input-xl" - :items="$options.roleListboxItems" - :loading="roleListboxLoadingStates.includes(user.gid)" - @select="onRoleSelect($event, user)" - /> - </div> + {{ userAccessLevelLabel(user) }} + </gl-button> </template> </users-table> <div class="gl-flex gl-justify-center"> <gl-keyset-pagination v-bind="pageInfo" @prev="$emit('prev')" @next="$emit('next')" /> </div> </template> + <user-details-drawer + :user="userDetailsDrawerActiveUser" + @loading="setUserDetailsDrawerLoading" + @close="setUserDetailsDrawerActiveUser(null)" + @role-change="onRoleChange" + /> </div> </template> diff --git a/locale/gitlab.pot b/locale/gitlab.pot index fb91a0863ee94..c58c9dbdc665e 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -38938,6 +38938,9 @@ msgstr "" msgid "Organization|Organization successfully created." msgstr "" +msgid "Organization|Organization user details" +msgstr "" + msgid "Organization|Organization visibility level" msgstr "" diff --git a/spec/frontend/organizations/users/components/user_details_drawer_spec.js b/spec/frontend/organizations/users/components/user_details_drawer_spec.js new file mode 100644 index 0000000000000..f608d7b0c1e69 --- /dev/null +++ b/spec/frontend/organizations/users/components/user_details_drawer_spec.js @@ -0,0 +1,239 @@ +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import { GlCollapsibleListbox, GlDrawer } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import organizationUserUpdateResponseWithErrors from 'test_fixtures/graphql/organizations/organization_user_update.mutation.graphql_with_errors.json'; +import organizationUserUpdateResponse from 'test_fixtures/graphql/organizations/organization_user_update.mutation.graphql.json'; +import organizationUserUpdateMutation from '~/organizations/users/graphql/mutations/organization_user_update.mutation.graphql'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import { pageInfoMultiplePages } from 'jest/organizations/mock_data'; +import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; +import UserDetailsDrawer from '~/organizations/users/components/user_details_drawer.vue'; +import UserAvatar from '~/vue_shared/components/users_table/user_avatar.vue'; +import waitForPromises from 'helpers/wait_for_promises'; +import { createAlert } from '~/alert'; +import { + ACCESS_LEVEL_DEFAULT_STRING, + ACCESS_LEVEL_OWNER_STRING, +} from '~/organizations/shared/constants'; +import { MOCK_PATHS, MOCK_USERS_FORMATTED } from '../mock_data'; + +Vue.use(VueApollo); + +jest.mock('~/alert'); + +describe('UserDetailsDrawer', () => { + let wrapper; + let mockApollo; + + const mockUser = MOCK_USERS_FORMATTED[0]; + + const successfulResponseHandler = jest.fn().mockResolvedValue(organizationUserUpdateResponse); + const mockToastShow = jest.fn(); + + const findGlDrawer = () => wrapper.findComponent(GlDrawer); + const findGlCollapsibleListbox = () => wrapper.findComponent(GlCollapsibleListbox); + const findUserAvatar = () => wrapper.findComponent(UserAvatar); + + const selectRole = (value) => findGlCollapsibleListbox().vm.$emit('select', value); + + const createComponent = ({ props = {}, handler = successfulResponseHandler } = {}) => { + mockApollo = createMockApollo([[organizationUserUpdateMutation, handler]]); + + wrapper = shallowMount(UserDetailsDrawer, { + propsData: { + user: mockUser, + pageInfo: pageInfoMultiplePages, + ...props, + }, + provide: { + paths: MOCK_PATHS, + }, + apolloProvider: mockApollo, + mocks: { + $toast: { + show: mockToastShow, + }, + }, + directives: { + GlTooltip: createMockDirective('gl-tooltip'), + }, + }); + }; + + afterEach(() => { + mockApollo = null; + }); + + describe('when there is no active user', () => { + it('does not render drawer', () => { + createComponent({ props: { user: null } }); + + expect(findGlDrawer().exists()).toBe(false); + }); + }); + + describe('when there is an active user', () => { + beforeEach(() => { + createComponent(); + }); + + it('renders open drawer', () => { + expect(findGlDrawer().exists()).toBe(true); + expect(findGlDrawer().props('open')).toBe(true); + }); + + it('renders drawer title', () => { + expect(findGlDrawer().text()).toContain('Organization user details'); + }); + + it('renders user avatar', () => { + expect(findUserAvatar().props()).toMatchObject({ + user: mockUser, + adminUserPath: MOCK_PATHS.adminUser, + }); + }); + + it('renders role listbox label', () => { + expect(findGlDrawer().text()).toContain('Organization role'); + }); + + it('renders role listbox correct props', () => { + expect(findGlCollapsibleListbox().props()).toMatchObject({ + items: UserDetailsDrawer.roleListboxItems, + selected: mockUser.accessLevel.stringValue, + disabled: false, + }); + }); + + it('does not render disabled listbox tooltip', () => { + const tooltipContainer = findGlCollapsibleListbox().element.parentNode; + const tooltip = getBinding(tooltipContainer, 'gl-tooltip'); + + expect(tooltip.value.disabled).toBe(true); + expect(tooltipContainer.getAttribute('tabindex')).toBe(null); + }); + + describe('when user is last owner of organization', () => { + beforeEach(() => { + createComponent({ + props: { + loading: false, + user: { ...mockUser, isLastOwner: true }, + }, + }); + }); + + it('renders listbox as disabled', () => { + expect(findGlCollapsibleListbox().props('disabled')).toBe(true); + }); + + it('renders tooltip and makes element focusable', () => { + const tooltipContainer = findGlCollapsibleListbox().element.parentNode; + const tooltip = getBinding(tooltipContainer, 'gl-tooltip'); + + expect(tooltip.value).toEqual({ + title: 'Organizations must have at least one owner.', + disabled: false, + }); + expect(tooltipContainer.getAttribute('tabindex')).toBe('0'); + }); + }); + + describe('when selecting new role', () => { + beforeEach(() => { + createComponent(); + selectRole(ACCESS_LEVEL_DEFAULT_STRING); + }); + + it('calls GraphQL mutation with correct variables', () => { + expect(successfulResponseHandler).toHaveBeenCalledWith({ + input: { + id: mockUser.gid, + accessLevel: ACCESS_LEVEL_DEFAULT_STRING, + }, + }); + }); + + it('sets listbox to loading', () => { + expect(findGlCollapsibleListbox().props('loading')).toBe(true); + }); + + it('emits loading start event', () => { + expect(wrapper.emitted('loading')[0]).toEqual([true]); + }); + + describe('when role update is successful', () => { + beforeEach(async () => { + await waitForPromises(); + }); + + it('shows toast when GraphQL mutation is successful', () => { + expect(mockToastShow).toHaveBeenCalledWith('Organization role was updated successfully.'); + }); + + it('emits role-change event', () => { + expect(wrapper.emitted('role-change')).toHaveLength(1); + }); + + it('emits loading end event', () => { + expect(wrapper.emitted('loading')[1]).toEqual([false]); + }); + }); + + describe('when role update has a validation error', () => { + beforeEach(async () => { + const errorResponseHandler = jest + .fn() + .mockResolvedValue(organizationUserUpdateResponseWithErrors); + + createComponent({ + handler: errorResponseHandler, + props: { user: { ...mockUser, accessLevel: ACCESS_LEVEL_OWNER_STRING } }, + }); + + selectRole(ACCESS_LEVEL_DEFAULT_STRING); + await waitForPromises(); + }); + + it('creates an alert', () => { + expect(createAlert).toHaveBeenCalledWith({ + message: 'You cannot change the access of the last owner from the organization', + }); + }); + }); + + describe('when role update has a network error', () => { + const error = new Error(); + + beforeEach(async () => { + const errorResponseHandler = jest.fn().mockRejectedValue(error); + + createComponent({ + handler: errorResponseHandler, + props: { user: { ...mockUser, accessLevel: ACCESS_LEVEL_OWNER_STRING } }, + }); + + selectRole(ACCESS_LEVEL_DEFAULT_STRING); + await waitForPromises(); + }); + + it('creates an alert', () => { + expect(createAlert).toHaveBeenCalledWith({ + message: 'An error occurred updating the organization role. Please try again.', + error, + captureError: true, + }); + }); + }); + }); + + describe('when drawer is closed', () => { + it('emits close event', () => { + findGlDrawer().vm.$emit('close'); + + expect(wrapper.emitted('close')).toHaveLength(1); + }); + }); + }); +}); diff --git a/spec/frontend/organizations/users/components/users_view_spec.js b/spec/frontend/organizations/users/components/users_view_spec.js index bc2635c38d5c9..919e9f6630311 100644 --- a/spec/frontend/organizations/users/components/users_view_spec.js +++ b/spec/frontend/organizations/users/components/users_view_spec.js @@ -1,64 +1,34 @@ -import VueApollo from 'vue-apollo'; -import Vue, { nextTick } from 'vue'; -import { GlLoadingIcon, GlKeysetPagination, GlCollapsibleListbox } from '@gitlab/ui'; -import organizationUserUpdateResponse from 'test_fixtures/graphql/organizations/organization_user_update.mutation.graphql.json'; -import organizationUserUpdateResponseWithErrors from 'test_fixtures/graphql/organizations/organization_user_update.mutation.graphql_with_errors.json'; -import { mountExtended } from 'helpers/vue_test_utils_helper'; +import { GlLoadingIcon, GlKeysetPagination, GlButton } from '@gitlab/ui'; +import { shallowMount, mount } from '@vue/test-utils'; +import UserDetailsDrawer from '~/organizations/users/components/user_details_drawer.vue'; import UsersView from '~/organizations/users/components/users_view.vue'; import UsersTable from '~/vue_shared/components/users_table/users_table.vue'; -import organizationUserUpdateMutation from '~/organizations/users/graphql/mutations/organization_user_update.mutation.graphql'; -import createMockApollo from 'helpers/mock_apollo_helper'; -import { createAlert } from '~/alert'; -import waitForPromises from 'helpers/wait_for_promises'; -import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; import { pageInfoMultiplePages } from 'jest/organizations/mock_data'; +import { ACCESS_LEVEL_LABEL } from '~/organizations/shared/constants'; import { MOCK_PATHS, MOCK_USERS_FORMATTED } from '../mock_data'; -Vue.use(VueApollo); - -jest.mock('~/alert'); - describe('UsersView', () => { let wrapper; - let mockApollo; - - const successfulResponseHandler = jest.fn().mockResolvedValue(organizationUserUpdateResponse); - const mockToastShow = jest.fn(); - const createComponent = ({ propsData = {}, handler = successfulResponseHandler } = {}) => { - mockApollo = createMockApollo([[organizationUserUpdateMutation, handler]]); - - wrapper = mountExtended(UsersView, { + const createComponent = ({ mountFn = shallowMount, props = {} } = {}) => { + wrapper = mountFn(UsersView, { propsData: { loading: false, users: MOCK_USERS_FORMATTED, pageInfo: pageInfoMultiplePages, - ...propsData, + ...props, }, provide: { paths: MOCK_PATHS, }, - apolloProvider: mockApollo, - mocks: { - $toast: { - show: mockToastShow, - }, - }, - directives: { - GlTooltip: createMockDirective('gl-tooltip'), - }, }); }; const findGlLoading = () => wrapper.findComponent(GlLoadingIcon); - const findUsersTable = () => wrapper.findComponent(UsersTable); + const findGlButton = () => wrapper.findComponent(GlButton); const findGlKeysetPagination = () => wrapper.findComponent(GlKeysetPagination); - const findListbox = () => wrapper.findComponent(GlCollapsibleListbox); - const listboxSelectOwner = () => findListbox().vm.$emit('select', 'OWNER'); - - afterEach(() => { - mockApollo = null; - }); + const findUserDetailsDrawer = () => wrapper.findComponent(UserDetailsDrawer); + const findUsersTable = () => wrapper.findComponent(UsersTable); describe.each` description | loading | usersData @@ -67,7 +37,7 @@ describe('UsersView', () => { ${'when not loading and has no users'} | ${false} | ${[]} `('$description', ({ loading, usersData }) => { beforeEach(() => { - createComponent({ propsData: { loading, users: usersData } }); + createComponent({ props: { loading, users: usersData } }); }); it(`does ${loading ? '' : 'not '}render loading icon`, () => { @@ -102,142 +72,49 @@ describe('UsersView', () => { }); describe('Organization role', () => { - it('renders listbox with role options', () => { - createComponent(); + const mockUser = MOCK_USERS_FORMATTED[0]; - expect(wrapper.findComponent(GlCollapsibleListbox).props()).toMatchObject({ - items: [ - { - text: 'User', - value: 'DEFAULT', - }, - { - text: 'Owner', - value: 'OWNER', - }, - ], - selected: MOCK_USERS_FORMATTED[0].accessLevel.stringValue, - disabled: false, - }); + beforeEach(() => { + createComponent({ mountFn: mount }); }); - it('does not render tooltip', () => { - createComponent(); - - const tooltipContainer = findListbox().element.parentNode; - const tooltip = getBinding(tooltipContainer, 'gl-tooltip'); + it("render an organization role button with the user's role", () => { + const userAccessLevel = mockUser.accessLevel.stringValue; - expect(tooltip.value.disabled).toBe(true); - expect(tooltipContainer.getAttribute('tabindex')).toBe(null); + expect(findGlButton().text()).toBe(ACCESS_LEVEL_LABEL[userAccessLevel]); }); - describe('when user is last owner of organization', () => { - const [firstUser] = MOCK_USERS_FORMATTED; - - beforeEach(() => { - createComponent({ - propsData: { - loading: false, - users: [{ ...firstUser, isLastOwner: true }], - }, - }); + describe('when the organization role button is clicked', () => { + beforeEach(async () => { + await findGlButton().trigger('click'); }); - it('renders listbox as disabled', () => { - expect(findListbox().props('disabled')).toBe(true); + it("sets the user details drawer's active user to selected user", () => { + expect(findUserDetailsDrawer().props('user')).toBe(mockUser); }); - it('renders tooltip and makes element focusable', () => { - const tooltipContainer = findListbox().element.parentNode; - const tooltip = getBinding(tooltipContainer, 'gl-tooltip'); + describe('when the user details drawer is closed', () => { + it("reset the user details drawer's active user to null", async () => { + await findUserDetailsDrawer().vm.$emit('close'); - expect(tooltip.value).toEqual({ - title: 'Organizations must have at least one owner.', - disabled: false, + expect(findGlButton().props('user')).toBeUndefined(); }); - expect(tooltipContainer.getAttribute('tabindex')).toBe('0'); }); }); - describe('when role is changed', () => { - afterEach(async () => { - // clean up any unresolved GraphQL mutations - await waitForPromises(); - }); + describe('when the user details drawer is loading', () => { + it('disable the organization role button', async () => { + await findUserDetailsDrawer().vm.$emit('loading', true); - it('calls GraphQL mutation with correct variables', () => { - createComponent(); - listboxSelectOwner(); - - expect(successfulResponseHandler).toHaveBeenCalledWith({ - input: { - id: MOCK_USERS_FORMATTED[0].gid, - accessLevel: 'OWNER', - }, - }); + expect(findGlButton().props('disabled')).toBe(true); }); + }); - it('shows dropdown as loading while waiting for GraphQL mutation', async () => { - createComponent(); - listboxSelectOwner(); - - await nextTick(); - - expect(findListbox().props('loading')).toBe(true); - }); - - it('shows toast when GraphQL mutation is successful', async () => { - createComponent(); - listboxSelectOwner(); - - await waitForPromises(); - - expect(mockToastShow).toHaveBeenCalledWith('Organization role was updated successfully.'); - }); - - it('emits role-change event when GraphQL mutation is successful', async () => { - createComponent(); - listboxSelectOwner(); - - await waitForPromises(); - - expect(wrapper.emitted('role-change')).toEqual([[]]); - }); - - it('calls createAlert when GraphQL mutation has validation error', async () => { - const errorResponseHandler = jest - .fn() - .mockResolvedValue(organizationUserUpdateResponseWithErrors); - createComponent({ - handler: errorResponseHandler, - }); - - listboxSelectOwner(); - - await waitForPromises(); - - expect(createAlert).toHaveBeenCalledWith({ - message: 'You cannot change the access of the last owner from the organization', - }); - }); - - it('calls createAlert when GraphQL mutation has network error', async () => { - const error = new Error(); - const errorResponseHandler = jest.fn().mockRejectedValue(error); - - createComponent({ - handler: errorResponseHandler, - }); - - listboxSelectOwner(); - - await waitForPromises(); + describe('when the user role has been changed', () => { + it('emits role-change event', async () => { + await findUserDetailsDrawer().vm.$emit('role-change'); - expect(createAlert).toHaveBeenCalledWith({ - message: 'An error occurred updating the organization role. Please try again.', - error, - captureError: true, - }); + expect(wrapper.emitted('role-change')).toHaveLength(1); }); }); }); -- GitLab