Commit c9e9bb32 authored by Martin Wortschack's avatar Martin Wortschack

Merge branch...

Merge branch '238459-convert-group-members-list-view-from-haml-to-vue-table-and-member-avatar' into 'master'

Convert "Group" -> "Members" list view from HAML to Vue - table component

See merge request gitlab-org/gitlab!42751
parents 49b4c19e 83d7af25
<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 const initGroupMembersApp = (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,
}), }),
}); });
......
...@@ -4,7 +4,7 @@ import memberExpirationDate from '~/member_expiration_date'; ...@@ -4,7 +4,7 @@ import memberExpirationDate from '~/member_expiration_date';
import UsersSelect from '~/users_select'; import UsersSelect from '~/users_select';
import groupsSelect from '~/groups_select'; import groupsSelect from '~/groups_select';
import RemoveMemberModal from '~/vue_shared/components/remove_member_modal.vue'; import RemoveMemberModal from '~/vue_shared/components/remove_member_modal.vue';
import initGroupMembersApp from '~/groups/members'; import { initGroupMembersApp } from '~/groups/members';
function mountRemoveMemberModal() { function mountRemoveMemberModal() {
const el = document.querySelector('.js-remove-member-modal'); const el = document.querySelector('.js-remove-member-modal');
...@@ -26,10 +26,24 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -26,10 +26,24 @@ 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(
initGroupMembersApp(document.querySelector('.js-group-access-requests-list')); document.querySelector('.js-group-members-list'),
SHARED_FIELDS.concat(['source', 'granted']),
);
initGroupMembersApp(
document.querySelector('.js-group-linked-list'),
SHARED_FIELDS.concat('granted'),
);
initGroupMembersApp(
document.querySelector('.js-group-invited-members-list'),
SHARED_FIELDS.concat('invited'),
);
initGroupMembersApp(
document.querySelector('.js-group-access-requests-list'),
SHARED_FIELDS.concat('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;
......
...@@ -1319,9 +1319,15 @@ msgstr "" ...@@ -1319,9 +1319,15 @@ msgstr ""
msgid "Access expiration date" msgid "Access expiration date"
msgstr "" msgstr ""
msgid "Access expires"
msgstr ""
msgid "Access forbidden. Check your access level." msgid "Access forbidden. Check your access level."
msgstr "" msgstr ""
msgid "Access granted"
msgstr ""
msgid "Access requests" msgid "Access requests"
msgstr "" msgstr ""
...@@ -15476,6 +15482,9 @@ msgstr "" ...@@ -15476,6 +15482,9 @@ msgstr ""
msgid "Max access level" msgid "Max access level"
msgstr "" msgstr ""
msgid "Max role"
msgstr ""
msgid "Max size 15 MB" msgid "Max size 15 MB"
msgstr "" msgstr ""
...@@ -17240,6 +17249,9 @@ msgstr "" ...@@ -17240,6 +17249,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 ""
...@@ -21577,6 +21589,9 @@ msgstr "" ...@@ -21577,6 +21589,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 ""
......
import { createWrapper } from '@vue/test-utils'; import { createWrapper } from '@vue/test-utils';
import initGroupMembersApp from '~/groups/members'; import { initGroupMembersApp } from '~/groups/members';
import GroupMembersApp from '~/groups/members/components/app.vue'; import GroupMembersApp from '~/groups/members/components/app.vue';
import { membersJsonString, membersParsed } from './mock_data'; import { membersJsonString, membersParsed } from './mock_data';
...@@ -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 createStore = (state = {}) => {
return new Vuex.Store({
state: {
members: [],
tableFields: [],
...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