web/satellite: project members frontend selection caching added (#3238)

This commit is contained in:
Nikolay Yurchenko 2019-10-15 16:22:41 +03:00 committed by GitHub
parent f1867a954b
commit 875c7dfe72
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 92 additions and 31 deletions

View File

@ -35,9 +35,10 @@
is-white="true" is-white="true"
:on-press="onClearSelection" :on-press="onClearSelection"
/> />
<span class="header-selected-members__info-text"><b>{{selectedProjectMembersCount}}</b> users selected</span>
</div> </div>
<div class="header-after-delete-click" v-if="areSelectedProjectMembersBeingDeleted"> <div class="header-after-delete-click" v-if="areSelectedProjectMembersBeingDeleted">
<span class="header-after-delete-click__delete-confirmation">Are you sure you want to delete {{selectedProjectMembersCount}} {{userCountTitle}}?</span> <span class="header-after-delete-click__delete-confirmation">Are you sure you want to delete <b>{{selectedProjectMembersCount}}</b> {{userCountTitle}}?</span>
<div class="header-after-delete-click__button-area"> <div class="header-after-delete-click__button-area">
<VButton <VButton
class="button deletion" class="button deletion"
@ -71,7 +72,7 @@ import VButton from '@/components/common/VButton.vue';
import VHeader from '@/components/common/VHeader.vue'; import VHeader from '@/components/common/VHeader.vue';
import AddUserPopup from '@/components/team/AddUserPopup.vue'; import AddUserPopup from '@/components/team/AddUserPopup.vue';
import { ProjectMember, ProjectMemberHeaderState } from '@/types/projectMembers'; import { ProjectMemberHeaderState } from '@/types/projectMembers';
import { APP_STATE_ACTIONS, NOTIFICATION_ACTIONS, PM_ACTIONS } from '@/utils/constants/actionNames'; import { APP_STATE_ACTIONS, NOTIFICATION_ACTIONS, PM_ACTIONS } from '@/utils/constants/actionNames';
declare interface ClearSearch { declare interface ClearSearch {
@ -119,22 +120,20 @@ export default class HeaderArea extends Vue {
this.$store.dispatch(PM_ACTIONS.CLEAR_SELECTION); this.$store.dispatch(PM_ACTIONS.CLEAR_SELECTION);
this.isDeleteClicked = false; this.isDeleteClicked = false;
this.$emit('onSuccessAction');
this.$refs.headerComponent.clearSearch(); this.$refs.headerComponent.clearSearch();
} }
public async onDelete(): Promise<void> { public async onDelete(): Promise<void> {
const projectMemberEmails: string[] = this.$store.getters.selectedProjectMembers.map((member: ProjectMember) => {
return member.user.email;
});
try { try {
await this.$store.dispatch(PM_ACTIONS.DELETE, projectMemberEmails); await this.$store.dispatch(PM_ACTIONS.DELETE);
} catch (error) { } catch (error) {
this.$store.dispatch(NOTIFICATION_ACTIONS.ERROR, `Error while deleting users from projectMembers. ${error.message}`); this.$store.dispatch(NOTIFICATION_ACTIONS.ERROR, `Error while deleting users from projectMembers. ${error.message}`);
return; return;
} }
this.$emit('onSuccessAction');
this.$store.dispatch(NOTIFICATION_ACTIONS.SUCCESS, 'Members was successfully removed from project'); this.$store.dispatch(NOTIFICATION_ACTIONS.SUCCESS, 'Members was successfully removed from project');
this.isDeleteClicked = false; this.isDeleteClicked = false;
@ -212,6 +211,11 @@ export default class HeaderArea extends Vue {
align-items: flex-end; align-items: flex-end;
height: 85px; height: 85px;
justify-content: center; justify-content: center;
&__info-text {
margin-left: 25px;
line-height: 48px;
}
} }
.button { .button {

View File

@ -6,7 +6,8 @@
<div class="team-area__header"> <div class="team-area__header">
<HeaderArea <HeaderArea
:header-state="headerState" :header-state="headerState"
:selected-project-members-count="selectedProjectMembers.length" :selected-project-members-count="selectedProjectMembersLength"
@onSuccessAction="resetPaginator"
/> />
</div> </div>
<div class="team-area__container" id="team-container" v-if="isTeamAreaShown"> <div class="team-area__container" id="team-container" v-if="isTeamAreaShown">
@ -99,7 +100,7 @@ export default class ProjectMembersArea extends Vue {
} }
public onMemberClick(member: ProjectMember): void { public onMemberClick(member: ProjectMember): void {
this.$store.dispatch(PM_ACTIONS.TOGGLE_SELECTION, member.user.id); this.$store.dispatch(PM_ACTIONS.TOGGLE_SELECTION, member);
} }
public get projectMembers(): ProjectMember[] { public get projectMembers(): ProjectMember[] {
@ -122,12 +123,12 @@ export default class ProjectMembersArea extends Vue {
return this.$store.state.projectMembersModule.page.pageCount; return this.$store.state.projectMembersModule.page.pageCount;
} }
public get selectedProjectMembers(): ProjectMember[] { public get selectedProjectMembersLength(): number {
return this.$store.getters.selectedProjectMembers; return this.$store.state.projectMembersModule.selectedProjectMembersEmails.length;
} }
public get headerState(): number { public get headerState(): number {
if (this.selectedProjectMembers.length > 0) { if (this.selectedProjectMembersLength > 0) {
return ProjectMemberHeaderState.ON_SELECT; return ProjectMemberHeaderState.ON_SELECT;
} }
@ -159,6 +160,10 @@ export default class ProjectMembersArea extends Vue {
await this.$store.dispatch(NOTIFICATION_ACTIONS.ERROR, `Unable to fetch project members. ${error.message}`); await this.$store.dispatch(NOTIFICATION_ACTIONS.ERROR, `Unable to fetch project members. ${error.message}`);
} }
this.resetPaginator();
}
public resetPaginator(): void {
if (this.totalPageCount > 1) { if (this.totalPageCount > 1) {
this.$refs.pagination.resetPageIndex(); this.$refs.pagination.resetPageIndex();
} }

View File

@ -36,6 +36,7 @@ const {
class ProjectMembersState { class ProjectMembersState {
public cursor: ProjectMemberCursor = new ProjectMemberCursor(); public cursor: ProjectMemberCursor = new ProjectMemberCursor();
public page: ProjectMembersPage = new ProjectMembersPage(); public page: ProjectMembersPage = new ProjectMembersPage();
public selectedProjectMembersEmails: string[] = [];
} }
export function makeProjectMembersModule(api: ProjectMembersApi): StoreModule<ProjectMembersState> { export function makeProjectMembersModule(api: ProjectMembersApi): StoreModule<ProjectMembersState> {
@ -44,6 +45,13 @@ export function makeProjectMembersModule(api: ProjectMembersApi): StoreModule<Pr
mutations: { mutations: {
[FETCH](state: ProjectMembersState, page: ProjectMembersPage) { [FETCH](state: ProjectMembersState, page: ProjectMembersPage) {
state.page = page; state.page = page;
state.page.projectMembers = state.page.projectMembers.map(member => {
if (state.selectedProjectMembersEmails.includes(member.user.email)) {
member.isSelected = true;
}
return member;
});
}, },
[SET_PAGE](state: ProjectMembersState, page: number) { [SET_PAGE](state: ProjectMembersState, page: number) {
state.cursor.page = page; state.cursor.page = page;
@ -60,17 +68,23 @@ export function makeProjectMembersModule(api: ProjectMembersApi): StoreModule<Pr
[CLEAR](state: ProjectMembersState) { [CLEAR](state: ProjectMembersState) {
state.cursor = new ProjectMemberCursor(); state.cursor = new ProjectMemberCursor();
state.page = new ProjectMembersPage(); state.page = new ProjectMembersPage();
state.selectedProjectMembersEmails = [];
}, },
[TOGGLE_SELECTION](state: ProjectMembersState, projectMemberId: string) { [TOGGLE_SELECTION](state: ProjectMembersState, projectMember: ProjectMember) {
state.page.projectMembers = state.page.projectMembers.map((projectMember: ProjectMember) => { if (!state.selectedProjectMembersEmails.includes(projectMember.user.email)) {
if (projectMember.user.id === projectMemberId) { projectMember.isSelected = true;
projectMember.isSelected = !projectMember.isSelected; state.selectedProjectMembersEmails.push(projectMember.user.email);
}
return projectMember; return;
}
projectMember.isSelected = false;
state.selectedProjectMembersEmails = state.selectedProjectMembersEmails.filter(projectMemberEmail => {
return projectMemberEmail !== projectMember.user.email;
}); });
}, },
[CLEAR_SELECTION](state: ProjectMembersState) { [CLEAR_SELECTION](state: ProjectMembersState) {
state.selectedProjectMembersEmails = [];
state.page.projectMembers = state.page.projectMembers.map((projectMember: ProjectMember) => { state.page.projectMembers = state.page.projectMembers.map((projectMember: ProjectMember) => {
projectMember.isSelected = false; projectMember.isSelected = false;
@ -84,10 +98,12 @@ export function makeProjectMembersModule(api: ProjectMembersApi): StoreModule<Pr
await api.add(projectId, emails); await api.add(projectId, emails);
}, },
deleteProjectMembers: async function ({rootGetters}: any, projectMemberEmails: string[]): Promise<void> { deleteProjectMembers: async function ({rootGetters, state, commit}: any): Promise<void> {
const projectId = rootGetters.selectedProject.id; const projectId = rootGetters.selectedProject.id;
await api.delete(projectId, projectMemberEmails); await api.delete(projectId, state.selectedProjectMembersEmails);
commit(CLEAR_SELECTION);
}, },
fetchProjectMembers: async function ({commit, rootGetters, state}: any, page: number): Promise<ProjectMembersPage> { fetchProjectMembers: async function ({commit, rootGetters, state}: any, page: number): Promise<ProjectMembersPage> {
const projectID = rootGetters.selectedProject.id; const projectID = rootGetters.selectedProject.id;
@ -112,15 +128,17 @@ export function makeProjectMembersModule(api: ProjectMembersApi): StoreModule<Pr
clearProjectMembers: function ({commit}) { clearProjectMembers: function ({commit}) {
commit(CLEAR); commit(CLEAR);
}, },
toggleProjectMemberSelection: function ({commit}: any, projectMemberId: string) { toggleProjectMemberSelection: function ({commit}: any, projectMember: ProjectMember) {
commit(TOGGLE_SELECTION, projectMemberId); commit(TOGGLE_SELECTION, projectMember);
}, },
clearProjectMemberSelection: function ({commit}: any) { clearProjectMemberSelection: function ({commit}: any) {
commit(CLEAR_SELECTION); commit(CLEAR_SELECTION);
}, },
}, },
getters: { getters: {
selectedProjectMembers: (state: any) => state.page.projectMembers.filter((member: ProjectMember) => member.isSelected), selectedProjectMembers: (state: ProjectMembersState) =>
state.page.projectMembers.filter((member: ProjectMember) =>
state.selectedProjectMembersEmails.includes(member.user.email)),
}, },
}; };
} }

View File

@ -93,8 +93,8 @@ export class ProjectMember {
public joinedAt: string; public joinedAt: string;
public isSelected: boolean; public isSelected: boolean;
public constructor(fullName: string, shortName: string, email: string, joinedAt: string, id?: string) { public constructor(fullName: string, shortName: string, email: string, joinedAt: string, id: string = '') {
this.user = new User(id || '', fullName, shortName, email); this.user = new User(id, fullName, shortName, email);
this.joinedAt = joinedAt; this.joinedAt = joinedAt;
this.isSelected = false; this.isSelected = false;
} }

View File

@ -25,7 +25,7 @@ exports[`Team HeaderArea renders correctly with 1 selected user and delete click
<vheader-stub placeholder="Team Members" search="function () { [native code] }"> <vheader-stub placeholder="Team Members" search="function () { [native code] }">
<!----> <!---->
<!----> <!---->
<div class="header-after-delete-click"><span class="header-after-delete-click__delete-confirmation">Are you sure you want to delete 1 user?</span> <div class="header-after-delete-click"><span class="header-after-delete-click__delete-confirmation">Are you sure you want to delete <b>1</b> user?</span>
<div class="header-after-delete-click__button-area"> <div class="header-after-delete-click__button-area">
<vbutton-stub label="Delete" width="122px" height="48px" onpress="function () { [native code] }" class="button deletion"></vbutton-stub> <vbutton-stub label="Delete" width="122px" height="48px" onpress="function () { [native code] }" class="button deletion"></vbutton-stub>
<vbutton-stub label="Cancel" width="122px" height="48px" iswhite="true" onpress="function () { [native code] }" class="button"></vbutton-stub> <vbutton-stub label="Cancel" width="122px" height="48px" iswhite="true" onpress="function () { [native code] }" class="button"></vbutton-stub>
@ -46,7 +46,7 @@ exports[`Team HeaderArea renders correctly with 2 selected users and delete clic
<vheader-stub placeholder="Team Members" search="function () { [native code] }"> <vheader-stub placeholder="Team Members" search="function () { [native code] }">
<!----> <!---->
<!----> <!---->
<div class="header-after-delete-click"><span class="header-after-delete-click__delete-confirmation">Are you sure you want to delete 2 users?</span> <div class="header-after-delete-click"><span class="header-after-delete-click__delete-confirmation">Are you sure you want to delete <b>2</b> users?</span>
<div class="header-after-delete-click__button-area"> <div class="header-after-delete-click__button-area">
<vbutton-stub label="Delete" width="122px" height="48px" onpress="function () { [native code] }" class="button deletion"></vbutton-stub> <vbutton-stub label="Delete" width="122px" height="48px" onpress="function () { [native code] }" class="button deletion"></vbutton-stub>
<vbutton-stub label="Cancel" width="122px" height="48px" iswhite="true" onpress="function () { [native code] }" class="button"></vbutton-stub> <vbutton-stub label="Cancel" width="122px" height="48px" iswhite="true" onpress="function () { [native code] }" class="button"></vbutton-stub>
@ -86,7 +86,7 @@ exports[`Team HeaderArea renders correctly with selected users 1`] = `
<!----> <!---->
<div class="header-selected-members"> <div class="header-selected-members">
<vbutton-stub label="Delete" width="122px" height="48px" onpress="function () { [native code] }" class="button deletion"></vbutton-stub> <vbutton-stub label="Delete" width="122px" height="48px" onpress="function () { [native code] }" class="button deletion"></vbutton-stub>
<vbutton-stub label="Cancel" width="122px" height="48px" iswhite="true" onpress="function () { [native code] }" class="button"></vbutton-stub> <vbutton-stub label="Cancel" width="122px" height="48px" iswhite="true" onpress="function () { [native code] }" class="button"></vbutton-stub> <span class="header-selected-members__info-text"><b>2</b> users selected</span>
</div> </div>
<!----> <!---->
</vheader-stub> </vheader-stub>

View File

@ -79,9 +79,24 @@ describe('mutations', () => {
}); });
it('toggle selection', function () { it('toggle selection', function () {
store.commit(PROJECT_MEMBER_MUTATIONS.TOGGLE_SELECTION, projectMember1.user.id); const testProjectMembersPage = new ProjectMembersPage();
testProjectMembersPage.projectMembers = [projectMember1];
testProjectMembersPage.totalCount = 1;
testProjectMembersPage.pageCount = 1;
store.commit(PROJECT_MEMBER_MUTATIONS.TOGGLE_SELECTION, projectMember1);
expect(state.page.projectMembers[0].isSelected).toBe(true); expect(state.page.projectMembers[0].isSelected).toBe(true);
expect(state.selectedProjectMembersEmails.length).toBe(1);
store.commit(PROJECT_MEMBER_MUTATIONS.FETCH, testProjectMembersPage);
expect(state.selectedProjectMembersEmails.length).toBe(1);
store.commit(PROJECT_MEMBER_MUTATIONS.TOGGLE_SELECTION, projectMember1);
expect(state.page.projectMembers[0].isSelected).toBe(false);
expect(state.selectedProjectMembersEmails.length).toBe(0);
}); });
it('clear selection', function () { it('clear selection', function () {
@ -90,6 +105,8 @@ describe('mutations', () => {
state.page.projectMembers.forEach((pm: ProjectMember) => { state.page.projectMembers.forEach((pm: ProjectMember) => {
expect(pm.isSelected).toBe(false); expect(pm.isSelected).toBe(false);
}); });
expect(state.selectedProjectMembersEmails.length).toBe(0);
}); });
it('clear store', function () { it('clear store', function () {
@ -100,6 +117,7 @@ describe('mutations', () => {
expect(state.cursor.order).toBe(ProjectMemberOrderBy.NAME); expect(state.cursor.order).toBe(ProjectMemberOrderBy.NAME);
expect(state.cursor.orderDirection).toBe(SortDirection.ASCENDING); expect(state.cursor.orderDirection).toBe(SortDirection.ASCENDING);
expect(state.page.projectMembers.length).toBe(0); expect(state.page.projectMembers.length).toBe(0);
expect(state.selectedProjectMembersEmails.length).toBe(0);
}); });
}); });
@ -243,9 +261,25 @@ describe('actions', async () => {
); );
await store.dispatch(PM_ACTIONS.FETCH, FIRST_PAGE); await store.dispatch(PM_ACTIONS.FETCH, FIRST_PAGE);
store.dispatch(PM_ACTIONS.TOGGLE_SELECTION, projectMember1.user.id); store.dispatch(PM_ACTIONS.TOGGLE_SELECTION, projectMember1);
expect(state.page.projectMembers[0].isSelected).toBe(true); expect(state.page.projectMembers[0].isSelected).toBe(true);
expect(state.selectedProjectMembersEmails.length).toBe(1);
store.dispatch(PM_ACTIONS.TOGGLE_SELECTION, projectMember2);
expect(state.page.projectMembers[1].isSelected).toBe(true);
expect(state.selectedProjectMembersEmails.length).toBe(2);
await store.dispatch(PM_ACTIONS.FETCH, FIRST_PAGE);
expect(state.page.projectMembers[1].isSelected).toBe(true);
expect(state.selectedProjectMembersEmails.length).toBe(2);
store.dispatch(PM_ACTIONS.TOGGLE_SELECTION, projectMember1);
expect(state.page.projectMembers[0].isSelected).toBe(false);
expect(state.selectedProjectMembersEmails.length).toBe(1);
}); });
it('clear selection', function () { it('clear selection', function () {
@ -269,7 +303,6 @@ describe('actions', async () => {
describe('getters', () => { describe('getters', () => {
const selectedProjectMember = new ProjectMember('testFullName2', 'testShortName2', 'test2@example.com', 'now2', '2'); const selectedProjectMember = new ProjectMember('testFullName2', 'testShortName2', 'test2@example.com', 'now2', '2');
selectedProjectMember.isSelected = true;
it('selected project members', function () { it('selected project members', function () {
const testProjectMembersPage = new ProjectMembersPage(); const testProjectMembersPage = new ProjectMembersPage();
@ -278,6 +311,7 @@ describe('getters', () => {
testProjectMembersPage.pageCount = 1; testProjectMembersPage.pageCount = 1;
store.commit(PROJECT_MEMBER_MUTATIONS.FETCH, testProjectMembersPage); store.commit(PROJECT_MEMBER_MUTATIONS.FETCH, testProjectMembersPage);
store.commit(PROJECT_MEMBER_MUTATIONS.TOGGLE_SELECTION, selectedProjectMember);
const retrievedProjectMembers = store.getters.selectedProjectMembers; const retrievedProjectMembers = store.getters.selectedProjectMembers;