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> <script>
import MembersTable from '~/vue_shared/components/members/table/members_table.vue';
export default { export default {
name: 'GroupMembersApp', name: 'GroupMembersApp',
components: { MembersTable },
}; };
</script> </script>
<template> <template>
<span> <members-table />
<!-- Temporary empty template -->
</span>
</template> </template>
...@@ -4,7 +4,7 @@ import App from './components/app.vue'; ...@@ -4,7 +4,7 @@ import App from './components/app.vue';
import membersModule from '~/vuex_shared/modules/members'; import membersModule from '~/vuex_shared/modules/members';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
export default el => { export default (el, tableFields) => {
if (!el) { if (!el) {
return () => {}; return () => {};
} }
...@@ -18,6 +18,7 @@ export default el => { ...@@ -18,6 +18,7 @@ export default el => {
members: convertObjectPropsToCamelCase(JSON.parse(members), { deep: true }), members: convertObjectPropsToCamelCase(JSON.parse(members), { deep: true }),
sourceId: parseInt(groupId, 10), sourceId: parseInt(groupId, 10),
currentUserId: gon.current_user_id || null, currentUserId: gon.current_user_id || null,
tableFields,
}), }),
}); });
......
...@@ -26,10 +26,25 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -26,10 +26,25 @@ document.addEventListener('DOMContentLoaded', () => {
memberExpirationDate('.js-access-expiration-date-groups'); memberExpirationDate('.js-access-expiration-date-groups');
mountRemoveMemberModal(); mountRemoveMemberModal();
initGroupMembersApp(document.querySelector('.js-group-members-list')); const SHARED_FIELDS = ['account', 'expires', 'maxRole', 'expiration', 'actions'];
initGroupMembersApp(document.querySelector('.js-group-linked-list'));
initGroupMembersApp(document.querySelector('.js-group-invited-members-list')); initGroupMembersApp(document.querySelector('.js-group-members-list'), [
initGroupMembersApp(document.querySelector('.js-group-access-requests-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 Members(); // eslint-disable-line no-new
new UsersSelect(); // 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, members,
sourceId, sourceId,
currentUserId, currentUserId,
tableFields,
}); });
...@@ -209,6 +209,23 @@ ...@@ -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 { .card-mobile {
.content-list.members-list li { .content-list.members-list li {
display: block; display: block;
......
...@@ -1300,6 +1300,12 @@ msgstr "" ...@@ -1300,6 +1300,12 @@ msgstr ""
msgid "Acceptable for use in this project" msgid "Acceptable for use in this project"
msgstr "" msgstr ""
msgid "Access Expires"
msgstr ""
msgid "Access Granted"
msgstr ""
msgid "Access Tokens" msgid "Access Tokens"
msgstr "" msgstr ""
...@@ -15448,6 +15454,9 @@ msgstr "" ...@@ -15448,6 +15454,9 @@ msgstr ""
msgid "Max Project Import requests per minute per user" msgid "Max Project Import requests per minute per user"
msgstr "" msgstr ""
msgid "Max Role"
msgstr ""
msgid "Max access level" msgid "Max access level"
msgstr "" msgstr ""
...@@ -17209,6 +17218,9 @@ msgstr "" ...@@ -17209,6 +17218,9 @@ msgstr ""
msgid "No matching results for \"%{query}\"" msgid "No matching results for \"%{query}\""
msgstr "" msgstr ""
msgid "No members found"
msgstr ""
msgid "No merge requests found" msgid "No merge requests found"
msgstr "" msgstr ""
...@@ -21540,6 +21552,9 @@ msgstr "" ...@@ -21540,6 +21552,9 @@ msgstr ""
msgid "Request to link SAML account must be authorized" msgid "Request to link SAML account must be authorized"
msgstr "" msgstr ""
msgid "Requested"
msgstr ""
msgid "Requested %{time_ago}" msgid "Requested %{time_ago}"
msgstr "" msgstr ""
......
...@@ -9,7 +9,7 @@ describe('initGroupMembersApp', () => { ...@@ -9,7 +9,7 @@ describe('initGroupMembersApp', () => {
let wrapper; let wrapper;
const setup = () => { const setup = () => {
vm = initGroupMembersApp(el); vm = initGroupMembersApp(el, ['account']);
wrapper = createWrapper(vm); wrapper = createWrapper(vm);
}; };
...@@ -63,4 +63,10 @@ describe('initGroupMembersApp', () => { ...@@ -63,4 +63,10 @@ describe('initGroupMembersApp', () => {
expect(vm.$store.state.members).toEqual(membersParsed); 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