Commit c69ec009 authored by Jarek Ostrowski's avatar Jarek Ostrowski Committed by Enrique Alcantara

Update leave group modal to gl-modal

MR: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/41817
parent 1383abb6
......@@ -3,9 +3,8 @@
import $ from 'jquery';
import 'vendor/jquery.scrollTo';
import { GlLoadingIcon } from '@gitlab/ui';
import { s__, sprintf } from '~/locale';
import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
import { GlLoadingIcon, GlModal } from '@gitlab/ui';
import { __, s__, sprintf } from '~/locale';
import { HIDDEN_CLASS } from '~/lib/utils/constants';
import { getParameterByName } from '~/lib/utils/common_utils';
import { mergeUrlParams } from '~/lib/utils/url_utility';
......@@ -16,8 +15,8 @@ import groupsComponent from './groups.vue';
export default {
components: {
DeprecatedModal,
groupsComponent,
GlModal,
GlLoadingIcon,
},
props: {
......@@ -49,13 +48,30 @@ export default {
isLoading: true,
isSearchEmpty: false,
searchEmptyMessage: '',
showModal: false,
groupLeaveConfirmationMessage: '',
targetGroup: null,
targetParentGroup: null,
};
},
computed: {
primaryProps() {
return {
text: __('Leave group'),
attributes: [{ variant: 'warning' }, { category: 'primary' }],
};
},
cancelProps() {
return {
text: __('Cancel'),
};
},
groupLeaveConfirmationMessage() {
if (!this.targetGroup) {
return '';
}
return sprintf(s__('GroupsTree|Are you sure you want to leave the "%{fullName}" group?'), {
fullName: this.targetGroup.fullName,
});
},
groups() {
return this.store.getGroups();
},
......@@ -171,27 +187,17 @@ export default {
}
},
showLeaveGroupModal(group, parentGroup) {
const { fullName } = group;
this.targetGroup = group;
this.targetParentGroup = parentGroup;
this.showModal = true;
this.groupLeaveConfirmationMessage = sprintf(
s__('GroupsTree|Are you sure you want to leave the "%{fullName}" group?'),
{ fullName },
);
},
hideLeaveGroupModal() {
this.showModal = false;
},
leaveGroup() {
this.showModal = false;
this.targetGroup.isBeingRemoved = true;
this.service
.leaveGroup(this.targetGroup.leavePath)
.then(res => {
$.scrollTo(0);
this.store.removeGroup(this.targetGroup, this.targetParentGroup);
Flash(res.data.notice, 'notice');
this.$toast.show(res.data.notice);
})
.catch(err => {
let message = COMMON_STR.FAILURE;
......@@ -245,21 +251,21 @@ export default {
class="loading-animation prepend-top-20"
/>
<groups-component
v-if="!isLoading"
v-else
:groups="groups"
:search-empty="isSearchEmpty"
:search-empty-message="searchEmptyMessage"
:page-info="pageInfo"
:action="action"
/>
<deprecated-modal
v-show="showModal"
:primary-button-label="__('Leave')"
<gl-modal
modal-id="leave-group-modal"
:title="__('Are you sure?')"
:text="groupLeaveConfirmationMessage"
kind="warning"
@cancel="hideLeaveGroupModal"
@submit="leaveGroup"
/>
:action-primary="primaryProps"
:action-cancel="cancelProps"
@primary="leaveGroup"
>
{{ groupLeaveConfirmationMessage }}
</gl-modal>
</div>
</template>
<script>
import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { GlTooltipDirective, GlButton, GlModalDirective } from '@gitlab/ui';
import eventHub from '../event_hub';
import { COMMON_STR } from '../constants';
export default {
components: {
GlIcon,
GlButton,
},
directives: {
GlTooltip: GlTooltipDirective,
GlModal: GlModalDirective,
},
props: {
parentGroup: {
......@@ -44,28 +45,28 @@ export default {
<template>
<div class="controls d-flex justify-content-end">
<a
<gl-button
v-if="group.canLeave"
v-gl-tooltip.top
:href="group.leavePath"
v-gl-modal.leave-group-modal
:title="leaveBtnTitle"
:aria-label="leaveBtnTitle"
data-testid="leave-group-btn"
class="leave-group btn btn-xs no-expand gl-text-gray-500 gl-ml-5"
@click.prevent="onLeaveGroup"
>
<gl-icon name="leave" class="position-top-0" />
</a>
<a
size="small"
icon="leave"
class="leave-group gl-ml-3"
@click.stop="onLeaveGroup"
/>
<gl-button
v-if="group.canEdit"
v-gl-tooltip.top
:href="group.editPath"
:title="editBtnTitle"
:aria-label="editBtnTitle"
data-testid="edit-group-btn"
class="edit-group btn btn-xs no-expand gl-text-gray-500 gl-ml-5"
>
<gl-icon name="settings" class="position-top-0 align-middle" />
</a>
size="small"
icon="pencil"
class="edit-group gl-ml-3"
/>
</div>
</template>
import Vue from 'vue';
import { GlToast } from '@gitlab/ui';
import { parseBoolean } from '~/lib/utils/common_utils';
import Translate from '../vue_shared/translate';
import GroupFilterableList from './groups_filterable_list';
......@@ -31,6 +32,8 @@ export default (containerId = 'js-groups-tree', endpoint, action = '') => {
Vue.component('group-folder', groupFolderComponent);
Vue.component('group-item', groupItemComponent);
Vue.use(GlToast);
// eslint-disable-next-line no-new
new Vue({
el,
......
---
title: Update leave group modal to gl-modal
merge_request: 41817
author:
type: changed
......@@ -2,6 +2,8 @@ import '~/flash';
import $ from 'jquery';
import Vue from 'vue';
import AxiosMockAdapter from 'axios-mock-adapter';
import { GlModal, GlLoadingIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
import appComponent from '~/groups/components/app.vue';
......@@ -23,26 +25,38 @@ import {
mockPageInfo,
} from '../mock_data';
const createComponent = (hideProjects = false) => {
const Component = Vue.extend(appComponent);
const $toast = {
show: jest.fn(),
};
describe('AppComponent', () => {
let wrapper;
let vm;
let mock;
let getGroupsSpy;
const store = new GroupsStore(false);
const service = new GroupsService(mockEndpoint);
const createShallowComponent = (hideProjects = false) => {
store.state.pageInfo = mockPageInfo;
return new Component({
wrapper = shallowMount(appComponent, {
propsData: {
store,
service,
hideProjects,
},
mocks: {
$toast,
},
});
};
vm = wrapper.vm;
};
describe('AppComponent', () => {
let vm;
let mock;
let getGroupsSpy;
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
beforeEach(() => {
mock = new AxiosMockAdapter(axios);
......@@ -50,20 +64,12 @@ describe('AppComponent', () => {
Vue.component('group-folder', groupFolderComponent);
Vue.component('group-item', groupItemComponent);
vm = createComponent();
createShallowComponent();
getGroupsSpy = jest.spyOn(vm.service, 'getGroups');
return vm.$nextTick();
});
describe('computed', () => {
beforeEach(() => {
vm.$mount();
});
afterEach(() => {
vm.$destroy();
});
describe('groups', () => {
it('should return list of groups from store', () => {
jest.spyOn(vm.store, 'getGroups').mockImplementation(() => {});
......@@ -88,14 +94,6 @@ describe('AppComponent', () => {
});
describe('methods', () => {
beforeEach(() => {
vm.$mount();
});
afterEach(() => {
vm.$destroy();
});
describe('fetchGroups', () => {
it('should call `getGroups` with all the params provided', () => {
return vm
......@@ -284,29 +282,15 @@ describe('AppComponent', () => {
it('updates props which show modal confirmation dialog', () => {
const group = { ...mockParentGroupItem };
expect(vm.showModal).toBe(false);
expect(vm.groupLeaveConfirmationMessage).toBe('');
vm.showLeaveGroupModal(group, mockParentGroupItem);
expect(vm.showModal).toBe(true);
expect(vm.groupLeaveConfirmationMessage).toBe(
`Are you sure you want to leave the "${group.fullName}" group?`,
);
});
});
describe('hideLeaveGroupModal', () => {
it('hides modal confirmation which is shown before leaving the group', () => {
const group = { ...mockParentGroupItem };
vm.showLeaveGroupModal(group, mockParentGroupItem);
expect(vm.showModal).toBe(true);
vm.hideLeaveGroupModal();
expect(vm.showModal).toBe(false);
});
});
describe('leaveGroup', () => {
let groupItem;
let childGroupItem;
......@@ -324,18 +308,16 @@ describe('AppComponent', () => {
const notice = `You left the "${childGroupItem.fullName}" group.`;
jest.spyOn(vm.service, 'leaveGroup').mockResolvedValue({ data: { notice } });
jest.spyOn(vm.store, 'removeGroup');
jest.spyOn(window, 'Flash').mockImplementation(() => {});
jest.spyOn($, 'scrollTo').mockImplementation(() => {});
vm.leaveGroup();
expect(vm.showModal).toBe(false);
expect(vm.targetGroup.isBeingRemoved).toBe(true);
expect(vm.service.leaveGroup).toHaveBeenCalledWith(vm.targetGroup.leavePath);
return waitForPromises().then(() => {
expect($.scrollTo).toHaveBeenCalledWith(0);
expect(vm.store.removeGroup).toHaveBeenCalledWith(vm.targetGroup, vm.targetParentGroup);
expect(window.Flash).toHaveBeenCalledWith(notice, 'notice');
expect($toast.show).toHaveBeenCalledWith(notice);
});
});
......@@ -417,8 +399,7 @@ describe('AppComponent', () => {
it('should bind event listeners on eventHub', () => {
jest.spyOn(eventHub, '$on').mockImplementation(() => {});
const newVm = createComponent();
newVm.$mount();
createShallowComponent();
return vm.$nextTick().then(() => {
expect(eventHub.$on).toHaveBeenCalledWith('fetchPage', expect.any(Function));
......@@ -426,25 +407,20 @@ describe('AppComponent', () => {
expect(eventHub.$on).toHaveBeenCalledWith('showLeaveGroupModal', expect.any(Function));
expect(eventHub.$on).toHaveBeenCalledWith('updatePagination', expect.any(Function));
expect(eventHub.$on).toHaveBeenCalledWith('updateGroups', expect.any(Function));
newVm.$destroy();
});
});
it('should initialize `searchEmptyMessage` prop with correct string when `hideProjects` is `false`', () => {
const newVm = createComponent();
newVm.$mount();
createShallowComponent();
return vm.$nextTick().then(() => {
expect(newVm.searchEmptyMessage).toBe('No groups or projects matched your search');
newVm.$destroy();
expect(vm.searchEmptyMessage).toBe('No groups or projects matched your search');
});
});
it('should initialize `searchEmptyMessage` prop with correct string when `hideProjects` is `true`', () => {
const newVm = createComponent(true);
newVm.$mount();
createShallowComponent(true);
return vm.$nextTick().then(() => {
expect(newVm.searchEmptyMessage).toBe('No groups matched your search');
newVm.$destroy();
expect(vm.searchEmptyMessage).toBe('No groups matched your search');
});
});
});
......@@ -453,9 +429,8 @@ describe('AppComponent', () => {
it('should unbind event listeners on eventHub', () => {
jest.spyOn(eventHub, '$off').mockImplementation(() => {});
const newVm = createComponent();
newVm.$mount();
newVm.$destroy();
createShallowComponent();
wrapper.destroy();
return vm.$nextTick().then(() => {
expect(eventHub.$off).toHaveBeenCalledWith('fetchPage', expect.any(Function));
......@@ -468,19 +443,10 @@ describe('AppComponent', () => {
});
describe('template', () => {
beforeEach(() => {
vm.$mount();
});
afterEach(() => {
vm.$destroy();
});
it('should render loading icon', () => {
vm.isLoading = true;
return vm.$nextTick().then(() => {
expect(vm.$el.querySelector('.loading-animation')).toBeDefined();
expect(vm.$el.querySelector('span').getAttribute('aria-label')).toBe('Loading groups');
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
});
});
......@@ -493,15 +459,13 @@ describe('AppComponent', () => {
});
it('renders modal confirmation dialog', () => {
vm.groupLeaveConfirmationMessage = 'Are you sure you want to leave the "foo" group?';
vm.showModal = true;
return vm.$nextTick().then(() => {
const modalDialogEl = vm.$el.querySelector('.modal');
createShallowComponent();
expect(modalDialogEl).not.toBe(null);
expect(modalDialogEl.querySelector('.modal-title').innerText.trim()).toBe('Are you sure?');
expect(modalDialogEl.querySelector('.btn.btn-warning').innerText.trim()).toBe('Leave');
});
const findGlModal = wrapper.find(GlModal);
expect(findGlModal.exists()).toBe(true);
expect(findGlModal.attributes('title')).toBe('Are you sure?');
expect(findGlModal.props('actionPrimary').text).toBe('Leave group');
});
});
});
import { shallowMount } from '@vue/test-utils';
import { GlIcon } from '@gitlab/ui';
import ItemActions from '~/groups/components/item_actions.vue';
import eventHub from '~/groups/event_hub';
import { mockParentGroupItem, mockChildren } from '../mock_data';
......@@ -20,68 +19,72 @@ describe('ItemActions', () => {
};
afterEach(() => {
if (wrapper) {
wrapper.destroy();
wrapper = null;
}
});
const findEditGroupBtn = () => wrapper.find('[data-testid="edit-group-btn"]');
const findEditGroupIcon = () => findEditGroupBtn().find(GlIcon);
const findLeaveGroupBtn = () => wrapper.find('[data-testid="leave-group-btn"]');
const findLeaveGroupIcon = () => findLeaveGroupBtn().find(GlIcon);
describe('template', () => {
it('renders component template correctly', () => {
createComponent();
expect(wrapper.classes()).toContain('controls');
});
let group;
it('renders "Edit group" button with correct attribute values', () => {
const group = {
beforeEach(() => {
group = {
...mockParentGroupItem,
canEdit: true,
canLeave: true,
};
createComponent({ group });
expect(findEditGroupBtn().exists()).toBe(true);
expect(findEditGroupBtn().classes()).toContain('no-expand');
expect(findEditGroupBtn().attributes('href')).toBe(group.editPath);
expect(findEditGroupBtn().attributes('aria-label')).toBe('Edit group');
expect(findEditGroupBtn().attributes('title')).toBe('Edit group');
expect(findEditGroupIcon().exists()).toBe(true);
expect(findEditGroupIcon().props('name')).toBe('settings');
});
describe('`canLeave` is true', () => {
const group = {
...mockParentGroupItem,
canLeave: true,
};
it('renders component template correctly', () => {
createComponent();
beforeEach(() => {
createComponent({ group });
expect(wrapper.classes()).toContain('controls');
});
it('renders "Leave this group" button with correct attribute values', () => {
expect(findLeaveGroupBtn().exists()).toBe(true);
expect(findLeaveGroupBtn().classes()).toContain('no-expand');
expect(findLeaveGroupBtn().attributes('href')).toBe(group.leavePath);
expect(findLeaveGroupBtn().attributes('aria-label')).toBe('Leave this group');
expect(findLeaveGroupBtn().attributes('title')).toBe('Leave this group');
expect(findLeaveGroupIcon().exists()).toBe(true);
expect(findLeaveGroupIcon().props('name')).toBe('leave');
it('renders "Edit group" button with correct attribute values', () => {
const button = findEditGroupBtn();
expect(button.exists()).toBe(true);
expect(button.props('icon')).toBe('pencil');
expect(button.attributes('aria-label')).toBe('Edit group');
});
it('emits event on "Leave this group" button click', () => {
jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
it('renders "Leave this group" button with correct attribute values', () => {
const button = findLeaveGroupBtn();
expect(button.exists()).toBe(true);
expect(button.props('icon')).toBe('leave');
expect(button.attributes('aria-label')).toBe('Leave this group');
});
findLeaveGroupBtn().trigger('click');
it('emits `showLeaveGroupModal` event in the event hub', () => {
jest.spyOn(eventHub, '$emit');
findLeaveGroupBtn().vm.$emit('click', { stopPropagation: () => {} });
expect(eventHub.$emit).toHaveBeenCalledWith('showLeaveGroupModal', group, parentGroup);
});
});
it('does not render leave button if group can not be left', () => {
createComponent({
group: {
...mockParentGroupItem,
canLeave: false,
},
});
expect(findLeaveGroupBtn().exists()).toBe(false);
});
it('does not render edit button if group can not be edited', () => {
createComponent({
group: {
...mockParentGroupItem,
canEdit: false,
},
});
expect(findEditGroupBtn().exists()).toBe(false);
});
});
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment