Satellite web Endless scroll and 'Sort by' for Team Members page. (#999)

* Implemented endless scroll and 'sort by' for Team Members page.
* Implemented actions name constants usage instead raw strings.
This commit is contained in:
Bogdan Artemenko 2019-01-10 16:44:15 +02:00 committed by GitHub
parent 34125fe614
commit 625ae46ae5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 227 additions and 83 deletions

View File

@ -66,14 +66,14 @@ func graphqlProject(service *satellite.Service, types Types) *graphql.Object {
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
project, _ := p.Source.(*satellite.Project)
offs, _ := p.Args[offset].(int64)
offs, _ := p.Args[offset].(int)
lim, _ := p.Args[limit].(int)
search, _ := p.Args[search].(string)
order, _ := p.Args[order].(int8)
order, _ := p.Args[order].(int)
pagination := satellite.Pagination{
Limit: lim,
Offset: offs,
Offset: int64(offs),
Search: search,
Order: satellite.ProjectMemberOrder(order),
}

View File

@ -30,7 +30,9 @@ import { APP_STATE_ACTIONS } from '@/utils/constants/actionNames';
'deleteAccountPopup',
'addTeamMemberPopupButton',
'addTeamMemberPopup',
'addTeamMemberPopupButtonSVG'
'addTeamMemberPopupButtonSVG',
'sortTeamMemberByDropdown',
'sortTeamMemberByDropdownButton',
]
};
},

View File

@ -3,6 +3,7 @@
import apollo from '@/utils/apolloManager';
import gql from 'graphql-tag';
import { ProjectMemberSortByEnum } from '@/utils/constants/ProjectMemberSortEnum';
// Performs graqhQL request.
// Throws an exception if error occurs
@ -78,11 +79,11 @@ export async function deleteProjectMembersRequest(projectID: string, emails: str
// Performs graqhQL request.
// Throws an exception if error occurs
export async function fetchProjectMembersRequest(projectID: string, limit: string, offset: string): Promise<RequestResponse<TeamMemberModel[]>> {
export async function fetchProjectMembersRequest(projectID: string, limit: string, offset: string, sortBy: ProjectMemberSortByEnum, searchQuery: string): Promise<RequestResponse<TeamMemberModel[]>> {
let result: RequestResponse<TeamMemberModel[]> = {
errorMessage: '',
isSuccess: false,
data: []
errorMessage: '',
isSuccess: false,
data: []
};
try {
@ -93,7 +94,7 @@ export async function fetchProjectMembersRequest(projectID: string, limit: strin
project(
id: "${projectID}",
) {
members(limit: ${limit}, offset: ${offset}) {
members(limit: ${limit}, offset: ${offset}, order: ${sortBy}, search: "${searchQuery}") {
user {
id,
firstName,

View File

@ -31,16 +31,17 @@ import { APP_STATE_ACTIONS, PROJETS_ACTIONS, NOTIFICATION_ACTIONS, PM_ACTIONS }
},
methods: {
onProjectSelected: async function (projectID: string): Promise<void> {
this.$store.dispatch(PROJETS_ACTIONS.SELECT, projectID);
this.$store.dispatch(APP_STATE_ACTIONS.TOGGLE_PROJECTS);
this.$store.dispatch(PROJETS_ACTIONS.SELECT, projectID);
this.$store.dispatch(APP_STATE_ACTIONS.TOGGLE_PROJECTS);
if (!this.$store.getters.selectedProject.id) return;
if (!this.$store.getters.selectedProject.id) return;
const response = await this.$store.dispatch(PM_ACTIONS.FETCH, {limit: 20, offset: 0});
this.$store.dispatch(PM_ACTIONS.SET_SEARCH_QUERY, '');
if (response.isSuccess) return;
const response = await this.$store.dispatch(PM_ACTIONS.FETCH);
if (response.isSuccess) return;
this.$store.dispatch(NOTIFICATION_ACTIONS.ERROR, 'Unable to fetch project members');
this.$store.dispatch(NOTIFICATION_ACTIONS.ERROR, 'Unable to fetch project members');
}
},
}

View File

@ -3,10 +3,10 @@
<template>
<div>
<div v-if="projectMembers.length > 0" class="team-header">
<div class="team-header">
<HeaderArea/>
</div>
<div v-if="projectMembers.length > 0" class="team-container">
<div id="scrollable_team_container" v-if="projectMembers.length > 0" v-on:scroll="handleScroll" class="team-container">
<div class="team-container__content">
<div v-for="(member, index) in projectMembers" v-on:click="onMemberClick(member)" v-bind:key="index">
<TeamMemberItem
@ -21,10 +21,9 @@
</div>
</div>
<EmptyState
v-if="projectMembers.length === 0"
mainTitle="Invite Team Members"
additionalText="You need to click the button “+” in the left corner"
:imageSource="emptyImage" />
v-if="projectMembers.length === 0"
mainTitle="No results found"
:imageSource="emptyImage" />
</div>
</template>
@ -35,17 +34,38 @@ import HeaderArea from '@/components/team/headerArea/HeaderArea.vue';
import Footer from '@/components/team/footerArea/Footer.vue';
import EmptyState from '@/components/common/EmptyStateArea.vue';
import { EMPTY_STATE_IMAGES } from '@/utils/constants/emptyStatesImages';
import { PM_ACTIONS } from '@/utils/constants/actionNames';
import { NOTIFICATION_ACTIONS, PM_ACTIONS } from '@/utils/constants/actionNames';
@Component({
data: function () {
return {
emptyImage: EMPTY_STATE_IMAGES.TEAM
emptyImage: EMPTY_STATE_IMAGES.TEAM,
isFetchInProgress: false,
};
},
methods: {
onMemberClick: function (member: any) {
this.$store.dispatch(PM_ACTIONS.TOGGLE_SELECTION, member.user.id);
},
handleScroll: async function () {
const documentElement = document.getElementById('scrollable_team_container');
if (!documentElement) {
return;
}
const isAtBottom = documentElement.scrollTop + documentElement.clientHeight === documentElement.scrollHeight;
if (!isAtBottom || this.$data.isFetchInProgress) return;
this.$data.isFetchInProgress = true;
const response = await this.$store.dispatch(PM_ACTIONS.FETCH);
this.$data.isFetchInProgress = false;
if (response.isSuccess) return;
this.$store.dispatch(NOTIFICATION_ACTIONS.ERROR, 'Unable to fetch project members');
},
},
computed: {
@ -54,7 +74,7 @@ import { PM_ACTIONS } from '@/utils/constants/actionNames';
},
selectedProjectMembers: function () {
return this.$store.getters.selectedProjectMembers;
}
},
},
components: {
TeamMemberItem,
@ -71,6 +91,7 @@ export default class TeamArea extends Vue {
<style scoped lang="scss">
.team-header {
position: fixed;
top: 100px;
padding: 55px 30px 25px 64px;
max-width: 79.7%;
width: 100%;

View File

@ -4,7 +4,7 @@
<template>
<div class="user-container">
<div class="user-container__avatar">
<h1>m</h1>
<h1>{{projectMember.user.firstName.slice(0,1)}}</h1>
</div>
<p class="user-container__user-name">{{`${projectMember.user.firstName} ${projectMember.user.lastName}`}}</p>
<p class="user-container__user-email">{{projectMember.user.email}}</p>

View File

@ -5,7 +5,7 @@
<div class="search-container">
<div class="search-container__wrap">
<label class="search-container__wrap__input">
<input placeholder="Search Users" type="text">
<input v-on:input="processSearchQuery" v-model="searchQuery" placeholder="Search Users" type="text">
</label>
</div>
</div>
@ -13,8 +13,25 @@
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import { NOTIFICATION_ACTIONS, PM_ACTIONS } from '@/utils/constants/actionNames';
@Component({})
@Component({
data:function () {
return {
searchQuery:''
};
},
methods: {
processSearchQuery: async function () {
this.$store.dispatch(PM_ACTIONS.SET_SEARCH_QUERY, this.$data.searchQuery);
const response = await this.$store.dispatch(PM_ACTIONS.FETCH);
if (response.isSuccess) return;
this.$store.dispatch(NOTIFICATION_ACTIONS.ERROR, 'Unable to fetch project members');
},
}
})
export default class SearchArea extends Vue {
}

View File

@ -3,26 +3,33 @@
<template>
<!-- To close popup we need to use method onCloseClick -->
<div class="sort-dropdown-choice-container">
<div class="sort-dropdown-choice-container" id="sortTeamMemberByDropdown">
<div class="sort-dropdown-overflow-container">
<!-- TODO: add selection logic onclick -->
<div class="sort-dropdown-item-container" v-on:click="onSortUsersClick">
<div class="sort-dropdown-item-container" v-on:click="onSortUsersClick(sortByEnum.EMAIL)">
<h2>Sort by email</h2>
</div>
<div class="sort-dropdown-item-container" v-on:click="onSortUsersClick">
<div class="sort-dropdown-item-container" v-on:click="onSortUsersClick(sortByEnum.CREATED_AT)">
<h2>Sort by date</h2>
</div>
<div class="sort-dropdown-item-container" v-on:click="onSortUsersClick(sortByEnum.NAME)">
<h2>Sort by name</h2>
</div>
</div>
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import { ProjectMemberSortByEnum } from '@/utils/constants/ProjectMemberSortEnum';
import { APP_STATE_ACTIONS, NOTIFICATION_ACTIONS, PM_ACTIONS } from '@/utils/constants/actionNames';
@Component(
{
data: function () {
return {};
return {
sortByEnum: ProjectMemberSortByEnum,
};
},
props: {
onClose: {
@ -31,10 +38,17 @@ import { Component, Vue } from 'vue-property-decorator';
},
methods: {
onCloseClick: function (): void {
this.$emit('onClose');
this.$store.dispatch(APP_STATE_ACTIONS.TOGGLE_SORT_PM_BY_DROPDOWN);
},
onSortUsersClick: function (): void {
this.$emit('onClose');
onSortUsersClick: async function (sortBy: ProjectMemberSortByEnum) {
this.$store.dispatch(PM_ACTIONS.SET_SORT_BY, sortBy);
this.$store.dispatch(APP_STATE_ACTIONS.TOGGLE_SORT_PM_BY_DROPDOWN);
const response = await this.$store.dispatch(PM_ACTIONS.FETCH);
if (response.isSuccess) return;
this.$store.dispatch(NOTIFICATION_ACTIONS.ERROR, 'Unable to fetch project members');
}
},
}

View File

@ -2,10 +2,10 @@
// See LICENSE for copying information.
<template>
<div class="sort-container" >
<div class="sort-container" id="sortTeamMemberByDropdownButton">
<!-- TODO: fix dd styles on hover -->
<div class="sort-toggle-container" v-on:click="toggleSelection" >
<h1 class="sort-toggle-container__sort-name">Sort by name</h1>
<h1 class="sort-toggle-container__sort-name">Sort by {{sortOption}}</h1>
<div class="sort-toggle-container__expander-area">
<img v-if="!isChoiceShown" src="../../../../static/images/register/BlueExpand.svg" />
<img v-if="isChoiceShown" src="../../../../static/images/register/BlueHide.svg" />
@ -16,22 +16,38 @@
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import SortDropdown from './SortDropdown.vue';
import { Component, Vue } from 'vue-property-decorator';
import SortDropdown from './SortDropdown.vue';
import { mapState } from 'vuex';
import { ProjectMemberSortByEnum } from '@/utils/constants/ProjectMemberSortEnum';
import { APP_STATE_ACTIONS } from '@/utils/constants/actionNames';
@Component(
@Component(
{
data: function () {
return {
userName: this.$store.getters.userName,
isChoiceShown: false
};
},
methods: {
toggleSelection: function (): void {
this.$data.isChoiceShown = !this.$data.isChoiceShown;
this.$store.dispatch(APP_STATE_ACTIONS.TOGGLE_SORT_PM_BY_DROPDOWN);
}
},
computed: mapState({
sortOption: (state: any) => {
switch (state.projectMembersModule.searchParameters.sortBy) {
case ProjectMemberSortByEnum.EMAIL:
return 'email';
case ProjectMemberSortByEnum.CREATED_AT:
return 'date';
default: // ProjectMemberSortByEnum.NAME
return 'name';
}
},
isChoiceShown: (state: any) => state.appStateModule.appState.isSortProjectMembersByPopupShown
}),
components: {
SortDropdown
}

View File

@ -13,6 +13,7 @@ export const appStateModule = {
isAccountDropdownShown: false,
isDeleteProjectPopupShown: false,
isDeleteAccountPopupShown: false,
isSortProjectMembersByPopupShown: false,
},
},
mutations: {
@ -45,6 +46,10 @@ export const appStateModule = {
[APP_STATE_MUTATIONS.TOGGLE_DELETE_ACCOUNT_DROPDOWN](state: any): void {
state.appState.isDeleteAccountPopupShown = !state.appState.isDeleteAccountPopupShown;
},
// Mutation changing 'sort project members by' popup visibility
[APP_STATE_MUTATIONS.TOGGLE_SORT_PM_BY_DROPDOWN](state: any): void {
state.appState.isSortProjectMembersByPopupShown = !state.appState.isSortProjectMembersByPopupShown;
},
// Mutation that closes each popup/dropdown
[APP_STATE_MUTATIONS.CLOSE_ALL](state: any): void {
@ -54,6 +59,7 @@ export const appStateModule = {
state.appState.isAccountDropdownShown = false;
state.appState.isDeleteProjectPopupShown = false;
state.appState.isDeleteAccountPopupShown = false;
state.appState.isSortProjectMembersByPopupShown = false;
},
},
actions: {
@ -100,6 +106,13 @@ export const appStateModule = {
commit(APP_STATE_MUTATIONS.TOGGLE_DELETE_ACCOUNT_DROPDOWN);
},
toggleSortProjectMembersByPopup: function ({commit, state}: any): void {
if (!state.appState.isSortProjectMembersByPopupShown) {
commit(APP_STATE_MUTATIONS.CLOSE_ALL);
}
commit(APP_STATE_MUTATIONS.TOGGLE_SORT_PM_BY_DROPDOWN);
},
closePopups: function ({commit}: any): void {
commit(APP_STATE_MUTATIONS.CLOSE_ALL);
},

View File

@ -7,10 +7,19 @@ import {
deleteProjectMembersRequest,
fetchProjectMembersRequest
} from '@/api/projectMembers';
import { ProjectMemberSortByEnum } from '@/utils/constants/ProjectMemberSortEnum';
export const projectMembersModule = {
state: {
projectMembers: [],
searchParameters: {
sortBy: ProjectMemberSortByEnum.NAME,
searchQuery: ''
},
pagination: {
offset: 0,
limit: 20,
}
},
mutations: {
[PROJECT_MEMBER_MUTATIONS.DELETE](state: any, projectMemberEmails: string[]) {
@ -38,18 +47,33 @@ export const projectMembersModule = {
return projectMember;
});
},
[PROJECT_MEMBER_MUTATIONS.FETCH](state: any, teamMembers: any[]) {
state.projectMembers = teamMembers;
},
[PROJECT_MEMBER_MUTATIONS.CLEAR](state: any) {
state.projectMembers = [];
},
[PROJECT_MEMBER_MUTATIONS.FETCH](state: any, teamMembers: any[]) {
state.projectMembers = state.projectMembers.concat(teamMembers);
},
[PROJECT_MEMBER_MUTATIONS.CLEAR](state: any) {
state.projectMembers = [];
},
[PROJECT_MEMBER_MUTATIONS.CHANGE_SORT_ORDER](state: any, sortBy: ProjectMemberSortByEnum) {
state.searchParameters.sortBy = sortBy;
},
[PROJECT_MEMBER_MUTATIONS.SET_SEARCH_QUERY](state: any, searchQuery: string) {
state.searchParameters.searchQuery = searchQuery;
},
[PROJECT_MEMBER_MUTATIONS.ADD_OFFSET](state: any) {
state.pagination.offset += state.pagination.limit;
},
[PROJECT_MEMBER_MUTATIONS.CLEAR_OFFSET](state: any) {
state.pagination.offset = 0;
}
},
actions: {
addProjectMembers: async function ({rootGetters}: any, emails: string[]): Promise<RequestResponse<null>> {
const projectId = rootGetters.selectedProject.id;
return await addProjectMembersRequest(projectId, emails);
const response = await addProjectMembersRequest(projectId, emails);
return response;
},
deleteProjectMembers: async function ({commit, rootGetters}: any, projectMemberEmails: string[]): Promise<RequestResponse<null>> {
const projectId = rootGetters.selectedProject.id;
@ -68,22 +92,40 @@ export const projectMembersModule = {
clearProjectMemberSelection: function ({commit}: any) {
commit(PROJECT_MEMBER_MUTATIONS.CLEAR_SELECTION);
},
fetchProjectMembers: async function ({commit, rootGetters}: any, limitoffset: any): Promise<RequestResponse<TeamMemberModel[]>> {
fetchProjectMembers: async function ({commit, state, rootGetters}: any): Promise<RequestResponse<TeamMemberModel[]>> {
const projectId = rootGetters.selectedProject.id;
const response = await fetchProjectMembersRequest(projectId, limitoffset.limit, limitoffset.offset);
const response = await fetchProjectMembersRequest(projectId, state.pagination.limit, state.pagination.offset,
state.searchParameters.sortBy, state.searchParameters.searchQuery);
if (response.isSuccess) {
commit(PROJECT_MEMBER_MUTATIONS.FETCH, response.data);
}
if (response.data.length > 0) {
commit(PROJECT_MEMBER_MUTATIONS.ADD_OFFSET);
}
return response;
},
setProjectMembersSortingBy: function ({commit, dispatch}, sortBy: ProjectMemberSortByEnum) {
commit(PROJECT_MEMBER_MUTATIONS.CHANGE_SORT_ORDER, sortBy);
commit(PROJECT_MEMBER_MUTATIONS.CLEAR);
commit(PROJECT_MEMBER_MUTATIONS.CLEAR_OFFSET);
},
setProjectMembersSearchQuery: function ({commit, dispatch}, searchQuery: string) {
commit(PROJECT_MEMBER_MUTATIONS.SET_SEARCH_QUERY, searchQuery);
commit(PROJECT_MEMBER_MUTATIONS.CLEAR);
commit(PROJECT_MEMBER_MUTATIONS.CLEAR_OFFSET);
},
clearProjectMembers: function ({commit}: any) {
commit(PROJECT_MEMBER_MUTATIONS.CLEAR);
commit(PROJECT_MEMBER_MUTATIONS.CLEAR);
},
clearProjectMembersOffset: function ({commit}) {
commit(PROJECT_MEMBER_MUTATIONS.CLEAR_OFFSET);
}
},
getters: {
projectMembers: (state: any) => state.projectMembers,
selectedProjectMembers: (state: any) => state.projectMembers.filter((member: any) => member.isSelected)
selectedProjectMembers: (state: any) => state.projectMembers.filter((member: any) => member.isSelected),
},
};

View File

@ -24,6 +24,10 @@ export const PROJECT_MEMBER_MUTATIONS = {
ADD: 'ADD_MEMBERS',
DELETE: 'DELETE_MEMBERS',
CLEAR: 'CLEAR_MEMBERS',
CHANGE_SORT_ORDER: 'CHANGE_SORT_ORDER',
SET_SEARCH_QUERY: 'SET_SEARCH_QUERY',
CLEAR_OFFSET: 'CLEAR_OFFSET',
ADD_OFFSET:'ADD_OFFSET',
};
export const NOTIFICATION_MUTATIONS = {
@ -40,5 +44,6 @@ export const APP_STATE_MUTATIONS = {
TOGGLE_ACCOUNT_DROPDOWN: 'TOGGLE_ACCOUNT_DROPDOWN',
TOGGLE_DELETE_PROJECT_DROPDOWN: 'TOGGLE_DELETE_PROJECT_DROPDOWN',
TOGGLE_DELETE_ACCOUNT_DROPDOWN: 'TOGGLE_DELETE_ACCOUNT_DROPDOWN',
TOGGLE_SORT_PM_BY_DROPDOWN: 'TOGGLE_SORT_PM_BY_DROPDOWN',
CLOSE_ALL: 'CLOSE_ALL',
};

View File

@ -0,0 +1,8 @@
// Copyright (C) 2018 Storj Labs, Inc.
// See LICENSE for copying information.
export enum ProjectMemberSortByEnum {
NAME = 1,
EMAIL,
CREATED_AT,
}

View File

@ -2,46 +2,50 @@
// See LICENSE for copying information.
export const APP_STATE_ACTIONS = {
TOGGLE_TEAM_MEMBERS: 'toggleAddTeamMembersPopup',
TOGGLE_NEW_PROJ : 'toggleNewProjectPopup',
TOGGLE_PROJECTS: 'toggleProjectsDropdown',
TOGGLE_ACCOUNT: 'toggleAccountDropdown',
TOGGLE_DEL_PROJ: 'toggleDeleteProjectPopup',
TOGGLE_DEL_ACCOUNT: 'toggleDeleteAccountPopup',
CLOSE_POPUPS: 'closePopups',
TOGGLE_TEAM_MEMBERS: 'toggleAddTeamMembersPopup',
TOGGLE_NEW_PROJ: 'toggleNewProjectPopup',
TOGGLE_PROJECTS: 'toggleProjectsDropdown',
TOGGLE_ACCOUNT: 'toggleAccountDropdown',
TOGGLE_DEL_PROJ: 'toggleDeleteProjectPopup',
TOGGLE_DEL_ACCOUNT: 'toggleDeleteAccountPopup',
TOGGLE_SORT_PM_BY_DROPDOWN: 'toggleSortProjectMembersByPopup',
CLOSE_POPUPS: 'closePopups',
};
export const NOTIFICATION_ACTIONS = {
SUCCESS: 'success',
ERROR: 'error',
NOTIFY: 'notify',
DELETE: 'deleteNotification',
PAUSE: 'pauseNotification',
RESUME: 'resumeNotification',
SUCCESS: 'success',
ERROR: 'error',
NOTIFY: 'notify',
DELETE: 'deleteNotification',
PAUSE: 'pauseNotification',
RESUME: 'resumeNotification',
};
export const PM_ACTIONS = {
ADD: 'addProjectMembers',
DELETE: 'deleteProjectMembers',
TOGGLE_SELECTION: 'toggleProjectMemberSelection',
CLEAR_SELECTION: 'clearProjectMemberSelection',
FETCH: 'fetchProjectMembers',
CLEAR: 'clearProjectMembers'
ADD: 'addProjectMembers',
DELETE: 'deleteProjectMembers',
TOGGLE_SELECTION: 'toggleProjectMemberSelection',
CLEAR_SELECTION: 'clearProjectMemberSelection',
FETCH: 'fetchProjectMembers',
CLEAR: 'clearProjectMembers',
SET_SEARCH_QUERY: 'setProjectMembersSearchQuery',
SET_SORT_BY: 'setProjectMembersSortingBy',
CLEAR_OFFSET: 'clearProjectMembersOffset'
};
export const PROJETS_ACTIONS = {
FETCH: 'fetchProjects',
CREATE: 'createProject',
SELECT: 'selectProject',
UPDATE: 'updateProject',
DELETE: 'deleteProject',
CLEAR: 'clearProjects',
FETCH: 'fetchProjects',
CREATE: 'createProject',
SELECT: 'selectProject',
UPDATE: 'updateProject',
DELETE: 'deleteProject',
CLEAR: 'clearProjects',
};
export const USER_ACTIONS = {
UPDATE: 'updateAccount',
CHANGE_PASSWORD: 'changePassword',
DELETE: 'deleteAccount',
GET: 'getUser',
CLEAR: 'clearUser',
UPDATE: 'updateAccount',
CHANGE_PASSWORD: 'changePassword',
DELETE: 'deleteAccount',
GET: 'getUser',
CLEAR: 'clearUser',
};