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