Commit dd5bb1d4 authored by Vitaly Slobodin's avatar Vitaly Slobodin

Merge branch '327648-group-project-members-migrate-pagination-to-vue' into 'master'

Refactor members pagination from HAML to Vue

See merge request gitlab-org/gitlab!59707
parents 61dd1268 d3edd8d9
<script>
import { GlTable, GlBadge } from '@gitlab/ui';
import { GlTable, GlBadge, GlPagination } from '@gitlab/ui';
import { mapState } from 'vuex';
import MembersTableCell from 'ee_else_ce/members/components/table/members_table_cell.vue';
import { canOverride, canRemove, canResend, canUpdate } from 'ee_else_ce/members/utils';
import { mergeUrlParams } from '~/lib/utils/url_utility';
import initUserPopovers from '~/user_popovers';
import { FIELDS } from '../../constants';
import RemoveGroupLinkModal from '../modals/remove_group_link_modal.vue';
......@@ -19,6 +20,7 @@ export default {
components: {
GlTable,
GlBadge,
GlPagination,
MemberAvatar,
CreatedAt,
ExpiresAt,
......@@ -43,6 +45,9 @@ export default {
tableAttrs(state) {
return state[this.namespace].tableAttrs;
},
pagination(state) {
return state[this.namespace].pagination;
},
}),
filteredFields() {
return FIELDS.filter(
......@@ -59,6 +64,11 @@ export default {
userIsLoggedIn() {
return this.currentUserId !== null;
},
showPagination() {
const { paramName, currentPage, perPage, totalItems } = this.pagination;
return paramName && currentPage && perPage && totalItems;
},
},
mounted() {
initUserPopovers(this.$el.querySelectorAll('.js-user-link'));
......@@ -99,6 +109,11 @@ export default {
...(member?.id && { 'data-testid': `members-table-row-${member.id}` }),
};
},
paginationLinkGenerator(page) {
const { params = {}, paramName } = this.pagination;
return mergeUrlParams({ ...params, [paramName]: page }, window.location.href);
},
},
};
</script>
......@@ -179,6 +194,18 @@ export default {
<span data-testid="col-actions" class="gl-sr-only">{{ label }}</span>
</template>
</gl-table>
<gl-pagination
v-if="showPagination"
:value="pagination.currentPage"
:per-page="pagination.perPage"
:total-items="pagination.totalItems"
:link-gen="paginationLinkGenerator"
:prev-text="__('Prev')"
:next-text="__('Next')"
:label-next-page="__('Go to next page')"
:label-prev-page="__('Go to previous page')"
align="center"
/>
<remove-group-link-modal />
<ldap-override-confirmation-modal />
</div>
......
export default ({
members,
pagination,
tableFields,
tableAttrs,
tableSortableFields,
......@@ -8,6 +9,7 @@ export default ({
filteredSearchBar,
}) => ({
members,
pagination,
tableFields,
tableAttrs,
tableSortableFields,
......
......@@ -105,10 +105,14 @@ export const buildSortHref = ({
export const canOverride = () => false;
export const parseDataAttributes = (el) => {
const { members, sourceId, memberPath, canManageMembers } = el.dataset;
const { members, pagination, sourceId, memberPath, canManageMembers } = el.dataset;
return {
members: convertObjectPropsToCamelCase(JSON.parse(members), { deep: true }),
pagination: convertObjectPropsToCamelCase(JSON.parse(pagination || '{}'), {
deep: true,
ignoreKeyNames: ['params'],
}),
sourceId: parseInt(sourceId, 10),
memberPath,
canManageMembers: parseBoolean(canManageMembers),
......
......@@ -22,9 +22,10 @@ module Groups::GroupMembersHelper
end
# Overridden in `ee/app/helpers/ee/groups/group_members_helper.rb`
def group_members_list_data_attributes(group, members)
def group_members_list_data_attributes(group, members, pagination = {})
{
members: members_data_json(group, members),
pagination: members_pagination_data_json(members, pagination),
member_path: group_group_member_path(group, ':id'),
source_id: group.id,
can_manage_members: can?(current_user, :admin_group_member, group).to_s
......@@ -32,8 +33,11 @@ module Groups::GroupMembersHelper
end
def group_group_links_list_data_attributes(group)
group_links = group.shared_with_group_links
{
members: group_group_links_data_json(group.shared_with_group_links),
members: group_group_links_data_json(group_links),
pagination: members_pagination_data_json(group_links),
member_path: group_group_link_path(group, ':id'),
source_id: group.id
}
......
......@@ -65,4 +65,14 @@ module MembersHelper
'group and any subresources'
end
def members_pagination_data_json(members, pagination = {})
{
current_page: members.respond_to?(:current_page) ? members.current_page : nil,
per_page: members.respond_to?(:limit_value) ? members.limit_value : nil,
total_items: members.respond_to?(:total_count) ? members.total_count : members.count,
param_name: pagination[:param_name] || nil,
params: pagination[:params] || {}
}.to_json
end
end
......@@ -35,9 +35,10 @@ module Projects::ProjectMembersHelper
MemberSerializer.new.represent(members, { current_user: current_user, group: project.group, source: project }).to_json
end
def project_members_list_data_attributes(project, members)
def project_members_list_data_attributes(project, members, pagination = {})
{
members: project_members_data_json(project, members),
pagination: members_pagination_data_json(members, pagination),
member_path: project_project_member_path(project, ':id'),
source_id: project.id,
can_manage_members: can_manage_project_members?(project).to_s
......@@ -47,6 +48,7 @@ module Projects::ProjectMembersHelper
def project_group_links_list_data_attributes(project, group_links)
{
members: project_group_links_data_json(group_links),
pagination: members_pagination_data_json(group_links),
member_path: project_group_link_path(project, ':id'),
source_id: project.id,
can_manage_members: can_manage_project_members?(project).to_s
......
......@@ -62,10 +62,9 @@
%span.badge.gl-tab-counter-badge.badge-muted.badge-pill.gl-badge.sm= @requesters.count
.tab-content
#tab-members.tab-pane{ class: ('active' unless invited_active) }
.js-group-members-list{ data: group_members_list_data_attributes(@group, @members) }
.js-group-members-list{ data: group_members_list_data_attributes(@group, @members, { param_name: :page, params: { invited_members_page: nil, search_invited: nil } }) }
.loading
.spinner.spinner-md
= paginate @members, theme: 'gitlab', params: { invited_members_page: nil, search_invited: nil }
- if @group.shared_with_group_links.any?
#tab-groups.tab-pane
.js-group-group-links-list{ data: group_group_links_list_data_attributes(@group) }
......@@ -73,10 +72,9 @@
.spinner.spinner-md
- if show_invited_members
#tab-invited-members.tab-pane{ class: ('active' if invited_active) }
.js-group-invited-members-list{ data: group_members_list_data_attributes(@group, @invited_members) }
.js-group-invited-members-list{ data: group_members_list_data_attributes(@group, @invited_members, { param_name: :invited_members_page, params: { page: nil } }) }
.loading
.spinner.spinner-md
= paginate @invited_members, param_name: 'invited_members_page', theme: 'gitlab', params: { page: nil }
- if show_access_requests
#tab-access-requests.tab-pane
.js-group-access-requests-list{ data: group_members_list_data_attributes(@group, @requesters) }
......
......@@ -75,10 +75,9 @@
%span.badge.gl-tab-counter-badge.badge-muted.badge-pill.gl-badge.sm= @requesters.count
.tab-content
#tab-members.tab-pane{ class: ('active' unless groups_tab_active?) }
.js-project-members-list{ data: project_members_list_data_attributes(@project, @project_members) }
.js-project-members-list{ data: project_members_list_data_attributes(@project, @project_members, { param_name: :page, params: { search_groups: nil } }) }
.loading
.spinner.spinner-md
= paginate @project_members, theme: "gitlab", params: { search_groups: nil }
- if show_groups?(@group_links)
#tab-groups.tab-pane{ class: ('active' if groups_tab_active?) }
.js-project-group-links-list{ data: project_group_links_list_data_attributes(@project, @group_links) }
......
......@@ -9,7 +9,7 @@ module EE::Groups::GroupMembersHelper
end
override :group_members_list_data_attributes
def group_members_list_data_attributes(group, _members)
def group_members_list_data_attributes(group, _members, _pagination = {})
super.merge!({
ldap_override_path: override_group_group_member_path(group, ':id')
})
......
......@@ -23,6 +23,7 @@ describe('MemberList', () => {
table: { 'data-qa-selector': 'members_list' },
tr: { 'data-qa-selector': 'member_row' },
},
pagination: {},
...state,
},
},
......
......@@ -5,6 +5,8 @@ import {
inheritedMember,
membersJsonString,
members,
paginationJsonString,
pagination,
} from 'jest/members/mock_data';
describe('Members Utils', () => {
......@@ -59,6 +61,7 @@ describe('Members Utils', () => {
beforeEach(() => {
el = document.createElement('div');
el.setAttribute('data-members', membersJsonString);
el.setAttribute('data-pagination', paginationJsonString);
el.setAttribute('data-source-id', '234');
el.setAttribute('data-can-manage-members', 'true');
el.setAttribute(
......@@ -74,6 +77,7 @@ describe('Members Utils', () => {
it('correctly parses the data attributes', () => {
expect(parseDataAttributes(el)).toEqual({
members,
pagination,
sourceId: 234,
canManageMembers: true,
ldapOverridePath: '/groups/ldap-group/-/group_members/:id/override',
......
......@@ -15064,9 +15064,15 @@ msgstr ""
msgid "Go to metrics"
msgstr ""
msgid "Go to next page"
msgstr ""
msgid "Go to parent"
msgstr ""
msgid "Go to previous page"
msgstr ""
msgid "Go to project"
msgstr ""
......
import { GlBadge, GlTable } from '@gitlab/ui';
import { GlBadge, GlPagination, GlTable } from '@gitlab/ui';
import {
getByText as getByTextHelper,
getByTestId as getByTestIdHelper,
......@@ -6,6 +6,7 @@ import {
} from '@testing-library/dom';
import { mount, createLocalVue, createWrapper } from '@vue/test-utils';
import Vuex from 'vuex';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import CreatedAt from '~/members/components/table/created_at.vue';
import ExpirationDatepicker from '~/members/components/table/expiration_datepicker.vue';
import ExpiresAt from '~/members/components/table/expires_at.vue';
......@@ -16,7 +17,13 @@ import MembersTable from '~/members/components/table/members_table.vue';
import RoleDropdown from '~/members/components/table/role_dropdown.vue';
import { MEMBER_TYPES } from '~/members/constants';
import * as initUserPopovers from '~/user_popovers';
import { member as memberMock, directMember, invite, accessRequest } from '../../mock_data';
import {
member as memberMock,
directMember,
invite,
accessRequest,
pagination,
} from '../../mock_data';
const localVue = createLocalVue();
localVue.use(Vuex);
......@@ -36,6 +43,7 @@ describe('MembersTable', () => {
table: { 'data-qa-selector': 'members_list' },
tr: { 'data-qa-selector': 'member_row' },
},
pagination,
...state,
},
},
......@@ -66,6 +74,8 @@ describe('MembersTable', () => {
});
};
const url = 'https://localhost/foo-bar/-/project_members';
const getByText = (text, options) =>
createWrapper(getByTextHelper(wrapper.element, text, options));
......@@ -78,6 +88,14 @@ describe('MembersTable', () => {
`[data-label="${tableCellLabel}"][role="cell"]`,
);
const findPagination = () => extendedWrapper(wrapper.find(GlPagination));
const expectCorrectLinkToPage2 = () => {
expect(findPagination().findByText('2', { selector: 'a' }).attributes('href')).toBe(
`${url}?page=2`,
);
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
......@@ -219,4 +237,80 @@ describe('MembersTable', () => {
expect(findTable().find('tbody tr').attributes('data-qa-selector')).toBe('member_row');
});
describe('when required pagination data is provided', () => {
beforeEach(() => {
delete window.location;
});
it('renders `gl-pagination` component with correct props', () => {
window.location = new URL(url);
createComponent();
const glPagination = findPagination();
expect(glPagination.exists()).toBe(true);
expect(glPagination.props()).toMatchObject({
value: pagination.currentPage,
perPage: pagination.perPage,
totalItems: pagination.totalItems,
prevText: 'Prev',
nextText: 'Next',
labelNextPage: 'Go to next page',
labelPrevPage: 'Go to previous page',
align: 'center',
});
});
it('uses `pagination.paramName` to generate the pagination links', () => {
window.location = new URL(url);
createComponent({
pagination: {
currentPage: 1,
perPage: 5,
totalItems: 10,
paramName: 'page',
},
});
expectCorrectLinkToPage2();
});
it('removes any url params defined as `null` in the `params` attribute', () => {
window.location = new URL(`${url}?search_groups=foo`);
createComponent({
pagination: {
currentPage: 1,
perPage: 5,
totalItems: 10,
paramName: 'page',
params: { search_groups: null },
},
});
expectCorrectLinkToPage2();
});
});
describe.each`
attribute | value
${'paramName'} | ${null}
${'currentPage'} | ${null}
${'perPage'} | ${null}
${'totalItems'} | ${0}
`('when pagination.$attribute is $value', ({ attribute, value }) => {
it('does not render `gl-pagination`', () => {
createComponent({
pagination: {
...pagination,
[attribute]: value,
},
});
expect(findPagination().exists()).toBe(false);
});
});
});
......@@ -2,7 +2,7 @@ import { createWrapper } from '@vue/test-utils';
import MembersApp from '~/members/components/app.vue';
import { MEMBER_TYPES } from '~/members/constants';
import { initMembersApp } from '~/members/index';
import { membersJsonString, members } from './mock_data';
import { membersJsonString, members, paginationJsonString, pagination } from './mock_data';
describe('initMembersApp', () => {
let el;
......@@ -24,6 +24,7 @@ describe('initMembersApp', () => {
beforeEach(() => {
el = document.createElement('div');
el.setAttribute('data-members', membersJsonString);
el.setAttribute('data-pagination', paginationJsonString);
el.setAttribute('data-source-id', '234');
el.setAttribute('data-can-manage-members', 'true');
el.setAttribute('data-member-path', '/groups/foo-bar/-/group_members/:id');
......@@ -50,6 +51,12 @@ describe('initMembersApp', () => {
expect(vm.$store.state[MEMBER_TYPES.user].members).toEqual(members);
});
it('parses and sets `pagination` in Vuex store', () => {
setup();
expect(vm.$store.state[MEMBER_TYPES.user].pagination).toEqual(pagination);
});
it('sets `tableFields` in Vuex store', () => {
setup();
......
......@@ -79,3 +79,19 @@ export const directMember = { ...member, isDirectMember: true };
export const inheritedMember = { ...member, isDirectMember: false };
export const member2faEnabled = { ...member, user: { ...member.user, twoFactorEnabled: true } };
export const paginationJsonString = JSON.stringify({
current_page: 1,
per_page: 5,
total_items: 10,
param_name: 'page',
params: { search_groups: null },
});
export const pagination = {
currentPage: 1,
perPage: 5,
totalItems: 10,
paramName: 'page',
params: { search_groups: null },
};
......@@ -22,6 +22,8 @@ import {
invite,
membersJsonString,
members,
paginationJsonString,
pagination,
} from './mock_data';
const IS_CURRENT_USER_ID = 123;
......@@ -259,6 +261,7 @@ describe('Members Utils', () => {
beforeEach(() => {
el = document.createElement('div');
el.setAttribute('data-members', membersJsonString);
el.setAttribute('data-pagination', paginationJsonString);
el.setAttribute('data-source-id', '234');
el.setAttribute('data-can-manage-members', 'true');
});
......@@ -270,6 +273,7 @@ describe('Members Utils', () => {
it('correctly parses the data attributes', () => {
expect(parseDataAttributes(el)).toEqual({
members,
pagination,
sourceId: 234,
canManageMembers: true,
});
......
......@@ -70,7 +70,7 @@ RSpec.describe Groups::GroupMembersHelper do
end
describe '#group_members_list_data_attributes' do
let(:group_member) { create(:group_member, group: group, created_by: current_user) }
let_it_be(:group_members) { create_list(:group_member, 2, group: group, created_by: current_user) }
before do
allow(helper).to receive(:group_group_member_path).with(group, ':id').and_return('/groups/foo-bar/-/group_members/:id')
......@@ -78,13 +78,45 @@ RSpec.describe Groups::GroupMembersHelper do
end
it 'returns expected hash' do
expect(helper.group_members_list_data_attributes(group, present_members([group_member]))).to include({
members: helper.members_data_json(group, present_members([group_member])),
expect(helper.group_members_list_data_attributes(group, present_members(group_members))).to include({
members: helper.members_data_json(group, present_members(group_members)),
member_path: '/groups/foo-bar/-/group_members/:id',
source_id: group.id,
can_manage_members: 'true'
})
end
context 'when pagination is not available' do
it 'sets `pagination` attribute to expected json' do
expect(helper.group_members_list_data_attributes(group, present_members(group_members))[:pagination]).to match({
current_page: nil,
per_page: nil,
total_items: 2,
param_name: nil,
params: {}
}.to_json)
end
end
context 'when pagination is available' do
let(:collection) { Kaminari.paginate_array(group_members).page(1).per(1) }
it 'sets `pagination` attribute to expected json' do
expect(
helper.group_members_list_data_attributes(
group,
present_members(collection),
{ param_name: :page, params: { search_groups: nil } }
)[:pagination]
).to match({
current_page: 1,
per_page: 1,
total_items: 2,
param_name: :page,
params: { search_groups: nil }
}.to_json)
end
end
end
describe '#group_group_links_list_data_attributes' do
......@@ -96,6 +128,13 @@ RSpec.describe Groups::GroupMembersHelper do
it 'returns expected hash' do
expect(helper.group_group_links_list_data_attributes(shared_group)).to include({
pagination: {
current_page: nil,
per_page: nil,
total_items: 1,
param_name: nil,
params: {}
}.to_json,
members: helper.group_group_links_data_json(shared_group.shared_with_group_links),
member_path: '/groups/foo-bar/-/group_links/:id',
source_id: shared_group.id
......
......@@ -147,7 +147,7 @@ RSpec.describe Projects::ProjectMembersHelper do
end
describe 'project members' do
let_it_be(:project_members) { create_list(:project_member, 1, project: project) }
let_it_be(:project_members) { create_list(:project_member, 2, project: project) }
describe '#project_members_data_json' do
it 'matches json schema' do
......@@ -170,6 +170,38 @@ RSpec.describe Projects::ProjectMembersHelper do
can_manage_members: 'true'
})
end
context 'when pagination is not available' do
it 'sets `pagination` attribute to expected json' do
expect(helper.project_members_list_data_attributes(project, present_members(project_members))[:pagination]).to match({
current_page: nil,
per_page: nil,
total_items: 2,
param_name: nil,
params: {}
}.to_json)
end
end
context 'when pagination is available' do
let(:collection) { Kaminari.paginate_array(project_members).page(1).per(1) }
it 'sets `pagination` attribute to expected json' do
expect(
helper.project_members_list_data_attributes(
project,
present_members(collection),
{ param_name: :page, params: { search_groups: nil } }
)[:pagination]
).to match({
current_page: 1,
per_page: 1,
total_items: 2,
param_name: :page,
params: { search_groups: nil }
}.to_json)
end
end
end
end
......@@ -193,6 +225,13 @@ RSpec.describe Projects::ProjectMembersHelper do
it 'returns expected hash' do
expect(helper.project_group_links_list_data_attributes(project, project_group_links)).to include({
members: helper.project_group_links_data_json(project_group_links),
pagination: {
current_page: nil,
per_page: nil,
total_items: 1,
param_name: nil,
params: {}
}.to_json,
member_path: '/foo-bar/-/group_links/:id',
source_id: project.id,
can_manage_members: 'true'
......
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