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>
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 const initGroupMembersApp = (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,
}),
});
......
......@@ -4,7 +4,7 @@ import memberExpirationDate from '~/member_expiration_date';
import UsersSelect from '~/users_select';
import groupsSelect from '~/groups_select';
import RemoveMemberModal from '~/vue_shared/components/remove_member_modal.vue';
import initGroupMembersApp from '~/groups/members';
import { initGroupMembersApp } from '~/groups/members';
function mountRemoveMemberModal() {
const el = document.querySelector('.js-remove-member-modal');
......@@ -26,10 +26,24 @@ 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.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 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;
......
......@@ -1319,9 +1319,15 @@ msgstr ""
msgid "Access expiration date"
msgstr ""
msgid "Access expires"
msgstr ""
msgid "Access forbidden. Check your access level."
msgstr ""
msgid "Access granted"
msgstr ""
msgid "Access requests"
msgstr ""
......@@ -15476,6 +15482,9 @@ msgstr ""
msgid "Max access level"
msgstr ""
msgid "Max role"
msgstr ""
msgid "Max size 15 MB"
msgstr ""
......@@ -17240,6 +17249,9 @@ msgstr ""
msgid "No matching results for \"%{query}\""
msgstr ""
msgid "No members found"
msgstr ""
msgid "No merge requests found"
msgstr ""
......@@ -21577,6 +21589,9 @@ msgstr ""
msgid "Request to link SAML account must be authorized"
msgstr ""
msgid "Requested"
msgstr ""
msgid "Requested %{time_ago}"
msgstr ""
......
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 { membersJsonString, membersParsed } from './mock_data';
......@@ -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 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