Commit b7f857c1 authored by peterhegman's avatar peterhegman

Add group members table component

Part of a larger initiative to convert the group members page from
HAML to Vue
parent 1be01d92
<script>
import MembersTable from '~/vue_shared/components/members/table/members_table.vue';
export default {
name: 'GroupMembersApp',
components: { MembersTable },
};
</script>
<template>
<span>
<!-- Temporary empty template -->
</span>
<members-table />
</template>
......@@ -4,7 +4,7 @@ import App from './components/app.vue';
import membersModule from '~/vuex_shared/modules/members';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
export default el => {
export default (el, tableFields) => {
if (!el) {
return () => {};
}
......@@ -18,6 +18,7 @@ export default el => {
members: convertObjectPropsToCamelCase(JSON.parse(members), { deep: true }),
sourceId: parseInt(groupId, 10),
currentUserId: gon.current_user_id || null,
tableFields,
}),
});
......
......@@ -26,10 +26,25 @@ document.addEventListener('DOMContentLoaded', () => {
memberExpirationDate('.js-access-expiration-date-groups');
mountRemoveMemberModal();
initGroupMembersApp(document.querySelector('.js-group-members-list'));
initGroupMembersApp(document.querySelector('.js-group-linked-list'));
initGroupMembersApp(document.querySelector('.js-group-invited-members-list'));
initGroupMembersApp(document.querySelector('.js-group-access-requests-list'));
const SHARED_FIELDS = ['account', 'expires', 'maxRole', 'expiration', 'actions'];
initGroupMembersApp(document.querySelector('.js-group-members-list'), [
...SHARED_FIELDS,
'source',
'granted',
]);
initGroupMembersApp(document.querySelector('.js-group-linked-list'), [
...SHARED_FIELDS,
'granted',
]);
initGroupMembersApp(document.querySelector('.js-group-invited-members-list'), [
...SHARED_FIELDS,
'invited',
]);
initGroupMembersApp(document.querySelector('.js-group-access-requests-list'), [
...SHARED_FIELDS,
'requested',
]);
new Members(); // eslint-disable-line no-new
new UsersSelect(); // eslint-disable-line no-new
......
import { __ } from '~/locale';
export const FIELDS = [
{
key: 'account',
label: __('Account'),
},
{
key: 'source',
label: __('Source'),
thClass: 'col-meta',
tdClass: 'col-meta',
},
{
key: 'granted',
label: __('Access Granted'),
thClass: 'col-meta',
tdClass: 'col-meta',
},
{
key: 'invited',
label: __('Invited'),
thClass: 'col-meta',
tdClass: 'col-meta',
},
{
key: 'requested',
label: __('Requested'),
thClass: 'col-meta',
tdClass: 'col-meta',
},
{
key: 'expires',
label: __('Access Expires'),
thClass: 'col-meta',
tdClass: 'col-meta',
},
{
key: 'maxRole',
label: __('Max Role'),
thClass: 'col-meta',
tdClass: 'col-meta',
},
{
key: 'expiration',
label: __('Expiration'),
thClass: 'col-expiration',
tdClass: 'col-expiration',
},
{
key: 'actions',
thClass: 'col-actions',
tdClass: 'col-actions',
},
];
<script>
import { mapState } from 'vuex';
import { GlTable } from '@gitlab/ui';
import { FIELDS } from '../constants';
import initUserPopovers from '~/user_popovers';
export default {
name: 'MembersTable',
components: {
GlTable,
},
computed: {
...mapState(['members', 'tableFields']),
filteredFields() {
return FIELDS.filter(field => this.tableFields.includes(field.key));
},
},
mounted() {
initUserPopovers(this.$el.querySelectorAll('.js-user-link'));
},
};
</script>
<template>
<gl-table
class="members-table"
head-variant="white"
stacked="lg"
:fields="filteredFields"
:items="members"
primary-key="id"
thead-class="border-bottom"
:empty-text="__('No members found')"
show-empty
>
<template #cell(source)>
<!-- Temporarily empty -->
</template>
<template #head(actions)="{ label }">
<span data-testid="col-actions" class="gl-sr-only">{{ label }}</span>
</template>
</gl-table>
</template>
export default ({ members, sourceId, currentUserId }) => ({
export default ({ members, sourceId, currentUserId, tableFields }) => ({
members,
sourceId,
currentUserId,
tableFields,
});
......@@ -209,6 +209,23 @@
}
}
.members-table {
@include media-breakpoint-up(lg) {
.col-meta {
width: px-to-rem(150px);
}
.col-expiration {
width: px-to-rem(200px);
}
.col-actions {
width: px-to-rem(50px);
}
}
}
.card-mobile {
.content-list.members-list li {
display: block;
......
......@@ -1300,6 +1300,12 @@ msgstr ""
msgid "Acceptable for use in this project"
msgstr ""
msgid "Access Expires"
msgstr ""
msgid "Access Granted"
msgstr ""
msgid "Access Tokens"
msgstr ""
......@@ -15448,6 +15454,9 @@ msgstr ""
msgid "Max Project Import requests per minute per user"
msgstr ""
msgid "Max Role"
msgstr ""
msgid "Max access level"
msgstr ""
......@@ -17209,6 +17218,9 @@ msgstr ""
msgid "No matching results for \"%{query}\""
msgstr ""
msgid "No members found"
msgstr ""
msgid "No merge requests found"
msgstr ""
......@@ -21540,6 +21552,9 @@ msgstr ""
msgid "Request to link SAML account must be authorized"
msgstr ""
msgid "Requested"
msgstr ""
msgid "Requested %{time_ago}"
msgstr ""
......
......@@ -9,7 +9,7 @@ describe('initGroupMembersApp', () => {
let wrapper;
const setup = () => {
vm = initGroupMembersApp(el);
vm = initGroupMembersApp(el, ['account']);
wrapper = createWrapper(vm);
};
......@@ -63,4 +63,10 @@ describe('initGroupMembersApp', () => {
expect(vm.$store.state.members).toEqual(membersParsed);
});
it('sets `tableFields` in Vuex store', () => {
setup();
expect(vm.$store.state.tableFields).toEqual(['account']);
});
});
import { mount, createLocalVue, createWrapper } from '@vue/test-utils';
import Vuex from 'vuex';
import {
getByText as getByTextHelper,
getByTestId as getByTestIdHelper,
} from '@testing-library/dom';
import MembersTable from '~/vue_shared/components/members/table/members_table.vue';
import * as initUserPopovers from '~/user_popovers';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('MemberList', () => {
let wrapper;
const defaultState = { members: [], tableFields: [] };
const createStore = (state = {}) => {
return new Vuex.Store({
state: {
...defaultState,
...state,
},
});
};
const createComponent = state => {
wrapper = mount(MembersTable, {
localVue,
store: createStore(state),
stubs: ['member-avatar'],
});
};
const getByText = (text, options) =>
createWrapper(getByTextHelper(wrapper.element, text, options));
const getByTestId = (id, options) =>
createWrapper(getByTestIdHelper(wrapper.element, id, options));
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('fields', () => {
it.each`
field | label
${'source'} | ${'Source'}
${'granted'} | ${'Access Granted'}
${'invited'} | ${'Invited'}
${'requested'} | ${'Requested'}
${'expires'} | ${'Access Expires'}
${'maxRole'} | ${'Max Role'}
${'expiration'} | ${'Expiration'}
`('renders the $label field', ({ field, label }) => {
createComponent({
tableFields: [field],
});
expect(getByText(label, { selector: '[role="columnheader"]' }).exists()).toBe(true);
});
it('renders "Actions" field for screen readers', () => {
createComponent({ tableFields: ['actions'] });
const actionField = getByTestId('col-actions');
expect(actionField.exists()).toBe(true);
expect(actionField.classes('gl-sr-only')).toBe(true);
});
});
describe('when `members` is an empty array', () => {
it('displays a "No members found" message', () => {
createComponent();
expect(getByText('No members found').exists()).toBe(true);
});
});
it('initializes user popovers when mounted', () => {
const initUserPopoversMock = jest.spyOn(initUserPopovers, 'default');
createComponent();
expect(initUserPopoversMock).toHaveBeenCalled();
});
});
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