feat: users pagination & searching users by role

This commit is contained in:
Sergey Yarkov 2021-09-18 18:35:57 +03:00
parent 3ab6cafb06
commit a4f3aebec5
11 changed files with 192 additions and 103 deletions

View File

@ -12,6 +12,7 @@ const UserBadge: React.FC<{ roles: IUserRole[] }> = ({ roles }) => {
if (userHasRoles(roles, [UserRoleEnum.ADMIN])) return { name: 'Administrator', color: 'purple' };
if (userHasRoles(roles, [UserRoleEnum.TEACHER])) return { name: 'Teacher', color: 'orange' };
if (userHasRoles(roles, [UserRoleEnum.STUDENT])) return { name: 'Student', color: 'blue' };
if (roles.length === 0) return { name: 'Guest', color: 'gray' };
};
const role = getCurrentRole();

View File

@ -1,10 +1,17 @@
import React from 'react';
import { ProfilePageViewStatusContextType } from 'types';
import { createContext } from 'react';
import { ProfilePageViewStatusContextType, UsersPageContextType } from 'types';
import RootStore from '../stores/RootStore';
export const StoreContext = React.createContext<RootStore | undefined>(undefined);
export const StoreContext = createContext<RootStore | undefined>(undefined);
export const ProfilePageViewContext = React.createContext<ProfilePageViewStatusContextType>({
export const ProfilePageViewContext = createContext<ProfilePageViewStatusContextType>({
statusPageView: 'default',
setStatusPageView: () => {},
});
export const UsersPageContext = createContext<UsersPageContextType>({
searchingRole: undefined,
setSearchingRole: () => {},
loading: false,
setLoading: () => {},
});

View File

@ -112,6 +112,9 @@ export interface IUserRole {
export interface IApiRespose<T> {
data: T;
meta?: {
/**
* The server can return the pagination meta data
*/
pagination?: IPaginationMeta;
};
message: string;

View File

@ -75,6 +75,7 @@ const ProfilePage: React.FC<IPageProps> = () => {
{statusPageView.status === 'default' && (
<Box maxW='900px'>
<Heading as='h1'>My account</Heading>
<Text mt='2'>Update you contacts information or password here.</Text>
<Flex
alignItems='center'
mt='20'

View File

@ -2,10 +2,12 @@ import { ChevronLeftIcon, ChevronRightIcon } from '@chakra-ui/icons';
import { Flex, Box, Stack, Text } from '@chakra-ui/layout';
import { Button } from '@chakra-ui/button';
import { IPaginationMeta, IUser } from 'interfaces';
import React, { useState } from 'react';
import React, { useContext } from 'react';
import UserItem from './UserItem';
import { useRootStore } from 'hooks/useRootStore';
import { useErrorHandler } from 'react-error-boundary';
import { UsersPageContextType } from 'types';
import { UsersPageContext } from 'contexts';
type UserListPropsType = {
users: IUser[];
@ -14,21 +16,18 @@ type UserListPropsType = {
const UserList: React.FC<UserListPropsType> = ({ users, pagination }) => {
const { userStore } = useRootStore();
const { searchingRole, loading, setLoading } = useContext<UsersPageContextType>(UsersPageContext);
const handleError = useErrorHandler();
const [loading, setLoading] = useState<boolean>(false);
const pagesCount = Math.round(pagination.total / pagination.per_page);
const [pages] = useState<number>(pagesCount < 1 ? 1 : pagesCount);
const [currentPage, setCurrentPage] = useState<number>(pagination.current_page);
const pagesCount = Math.ceil(pagination.total / pagination.per_page);
const nextPage = async () => {
if (currentPage === pages) return;
try {
setLoading(true);
await userStore.loadUsersData({
page: currentPage + 1,
page: pagination.current_page + 1,
limit: pagination.per_page,
role: searchingRole,
});
setCurrentPage(p => p + 1);
} catch (error: any) {
handleError(error);
} finally {
@ -37,14 +36,13 @@ const UserList: React.FC<UserListPropsType> = ({ users, pagination }) => {
};
const prevPage = async () => {
if (currentPage <= 1) return;
try {
setLoading(true);
await userStore.loadUsersData({
page: currentPage - 1,
page: pagination.current_page - 1,
limit: pagination.per_page,
role: searchingRole,
});
setCurrentPage(p => p - 1);
} catch (error: any) {
handleError(error);
} finally {
@ -52,19 +50,13 @@ const UserList: React.FC<UserListPropsType> = ({ users, pagination }) => {
}
};
if (pagination.total === 0) {
return (
<Box textAlign='center' mt='10' userSelect='none'>
<Text color='gray.500'>There are no users in the system</Text>
</Box>
);
}
return (
<>
{pagination.total !== 0 ? (
<>
<Box>
<Flex mt='7' p='0 10px' fontWeight='bold' alignItems='center' justifyContent='space-between'>
<Text>Total users: ({pagination.total})</Text>
<Text>Total: ({pagination.total})</Text>
<Text>Actions</Text>
</Flex>
{loading ? (
@ -81,18 +73,34 @@ const UserList: React.FC<UserListPropsType> = ({ users, pagination }) => {
</Box>
<Flex mt='auto' mb='16' flexDirection={{ base: 'column', sm: 'column', md: 'row' }}>
<Box textAlign={{ base: 'center', sm: 'center', md: 'left' }} mb={{ base: '2', md: '0' }}>
Page <b>{currentPage}</b> of {pages}
Page <b>{pagination.current_page}</b> of {pagesCount}
</Box>
<Flex ml='auto' mr='auto' alignItems='center' sx={{ gap: '30px' }}>
<Button onClick={prevPage} disabled={currentPage <= 1} variant='link' leftIcon={<ChevronLeftIcon />}>
<Button
onClick={prevPage}
disabled={pagination.current_page <= 1}
variant='link'
leftIcon={<ChevronLeftIcon />}
>
Previous page
</Button>
<Button onClick={nextPage} disabled={currentPage === pages} variant='link' rightIcon={<ChevronRightIcon />}>
<Button
onClick={nextPage}
disabled={pagination.current_page === pagesCount}
variant='link'
rightIcon={<ChevronRightIcon />}
>
Next page
</Button>
</Flex>
</Flex>
</>
) : (
<Box textAlign='center' mt='10' userSelect='none'>
<Text color='gray.500'>Cannot find any users.</Text>
</Box>
)}
</>
);
};

View File

@ -0,0 +1,53 @@
import React, { useContext } from 'react';
import { Flex, Select, Input, InputGroup, InputLeftElement } from '@chakra-ui/react';
import { SearchIcon } from '@chakra-ui/icons';
import { useRootStore } from 'hooks/useRootStore';
import { UserRoleEnum } from 'enums';
import { useErrorHandler } from 'react-error-boundary';
import { UsersPageContext } from 'contexts';
import { UsersPageContextType } from 'types';
type SelectRoleStateType = UserRoleEnum | 'any';
const UserSearch: React.FC = () => {
const { userStore } = useRootStore();
const { searchingRole, setSearchingRole, setLoading } = useContext<UsersPageContextType>(UsersPageContext);
const { pagination } = userStore;
const handleError = useErrorHandler();
if (pagination === undefined) return null;
const onSearchRole: React.ChangeEventHandler<HTMLSelectElement> | undefined = async e => {
try {
setLoading(true);
const value = e.target.value as SelectRoleStateType;
await userStore.loadUsersData({
page: 1,
limit: pagination.per_page,
role: value,
});
setSearchingRole(value);
} catch (error) {
handleError(error);
} finally {
setLoading(false);
}
};
return (
<Flex flexBasis='600px'>
<Select value={searchingRole} onChange={onSearchRole} w='full' mr='2'>
<option value='any'>Any role</option>
<option value={UserRoleEnum.ADMIN}>Administrator</option>
<option value={UserRoleEnum.TEACHER}>Teacher</option>
<option value={UserRoleEnum.STUDENT}>Student</option>
</Select>
<InputGroup maxW='400px' w='full'>
<InputLeftElement pointerEvents='none' children={<SearchIcon color='gray.300' />} />
<Input placeholder='Search for a user...' />
</InputGroup>
</Flex>
);
};
export default UserSearch;

View File

@ -0,0 +1,3 @@
import UserSearch from './UserSearch';
export default UserSearch;

View File

@ -1,19 +1,19 @@
import React, { useEffect } from 'react';
import React, { useEffect, useState } from 'react';
import { observer } from 'mobx-react';
import { Select } from '@chakra-ui/react';
import { Box, Flex, Button, Heading } from '@chakra-ui/react';
import { Input, InputGroup, InputLeftElement } from '@chakra-ui/react';
import { AddIcon, SearchIcon } from '@chakra-ui/icons';
import { Box, Flex, Button, Heading, Text } from '@chakra-ui/react';
import { AddIcon } from '@chakra-ui/icons';
import { FaFileExcel } from 'react-icons/fa';
/**
* Types
*/
import { IPageProps } from 'interfaces';
import { SearchingRoleStateType } from 'types';
/**
* Components
*/
import UserSearch from './components/UserSearch';
import UserList from './components/UserList';
import LoadingPage from './components/LoadingPage';
@ -24,36 +24,34 @@ import { useRootStore } from 'hooks/useRootStore';
import { useErrorHandler } from 'react-error-boundary';
import { useLocation } from 'react-router';
/**
* Context
*/
import { UsersPageContext } from 'contexts';
/**
* Users Page
*/
const UsersPage: React.FC<IPageProps> = ({ title }) => {
const { userStore } = useRootStore();
const [searchingRole, setSearchingRole] = useState<SearchingRoleStateType>(undefined);
const [loading, setLoading] = useState<boolean>(false);
const handleError = useErrorHandler();
const location = useLocation();
useEffect(() => {
userStore.loadUsersData({ page: 1, limit: 2 }).catch(error => handleError(error));
userStore.loadUsersData().catch(error => handleError(error));
}, [handleError, userStore, location.search]);
return (
<UsersPageContext.Provider value={{ searchingRole, setSearchingRole, loading, setLoading }}>
<Box h='full'>
<Heading as='h1'>User management</Heading>
<Text mt='2'>You can add or delete your users on this page.</Text>
{userStore.users !== null ? (
<Flex mt='5' flexDir='column' h='full'>
<Flex justifyContent='space-between' sx={{ gap: '10px' }} flexWrap='wrap'>
<Flex flexBasis='600px'>
<Select defaultValue='any' w='full' mr='2'>
<option value='any'>Any role</option>
<option value='administrator'>Administrator</option>
<option value='teacher'>Teacher</option>
<option value='student'>Student</option>
</Select>
<InputGroup maxW='400px' w='full'>
<InputLeftElement pointerEvents='none' children={<SearchIcon color='gray.300' />} />
<Input placeholder='Search for a user...' />
</InputGroup>
</Flex>
<UserSearch />
<Flex sx={{ gap: '10px' }}>
<Button variant='outline' color='blue.500' leftIcon={<AddIcon />}>
Create new
@ -63,14 +61,13 @@ const UsersPage: React.FC<IPageProps> = ({ title }) => {
</Button>
</Flex>
</Flex>
{userStore.users.meta.pagination && (
<UserList users={userStore.users.data} pagination={userStore.users.meta.pagination} />
)}
{userStore.pagination && <UserList users={userStore.users} pagination={userStore.pagination} />}
</Flex>
) : (
<LoadingPage />
)}
</Box>
</UsersPageContext.Provider>
);
};

View File

@ -1,8 +1,9 @@
import { AxiosInstance } from 'axios';
import { IApiRespose, IMe, IUser, IUserContacts } from 'interfaces';
import * as helpers from 'helpers';
import { UserRoleEnum } from 'enums';
export type FetchUsersParamsType = { page?: number; limit?: number };
export type FetchUsersParamsType = { page?: number; limit?: number; role?: UserRoleEnum | 'any' };
export default class UserService {
public api: AxiosInstance;
@ -17,11 +18,12 @@ export default class UserService {
}
public async fetchAll(params?: FetchUsersParamsType): Promise<IApiRespose<IUser[]>> {
const { page, limit } = params || {};
const { page = 1, limit = 1, role } = params || {};
const result = await this.api.get('/v1/users', {
params: {
page,
limit,
role: role === 'any' ? undefined : role,
},
});
return result.data;

View File

@ -19,7 +19,9 @@ export default class UserStore {
public me: IMe | null = null;
public users: { data: IUser[]; meta: { pagination?: IPaginationMeta } } | null = null;
public users: IUser[] | null = null;
public pagination: IPaginationMeta | undefined;
constructor(root: RootStore, api: AxiosInstance) {
this.root = root;
@ -33,12 +35,8 @@ export default class UserStore {
console.log(`[${this.constructor.name}]: ${result.message}`, result);
runInAction(() => {
this.users = {
data: result.data,
meta: {
pagination: result.meta?.pagination,
},
};
this.users = result.data;
this.pagination = result.meta && result.meta.pagination;
});
return result;
@ -70,7 +68,7 @@ export default class UserStore {
});
return result;
} catch (error) {
} catch (error: any) {
throw error;
}
}
@ -92,7 +90,7 @@ export default class UserStore {
});
return result;
} catch (error) {
} catch (error: any) {
throw error;
}
}
@ -108,7 +106,7 @@ export default class UserStore {
try {
const result = await this.userService.updatePassword(oldPassword, newPassword);
return result;
} catch (error) {
} catch (error: any) {
throw error;
}
}
@ -123,7 +121,7 @@ export default class UserStore {
try {
const result = await this.userService.updateEmail(email);
return result;
} catch (error) {
} catch (error: any) {
throw error;
}
}
@ -144,7 +142,7 @@ export default class UserStore {
}
});
return result;
} catch (error) {
} catch (error: any) {
throw error;
}
}

16
src/types/index.d.ts vendored
View File

@ -1,4 +1,5 @@
import { UserRoleEnum } from 'enums';
import React from 'react';
import { IconType } from 'react-icons/lib';
export type LinkType = {
@ -27,7 +28,22 @@ export type ProfilePageStatusType = {
confirmEmailData?: ConfirmEmailContainerDataType;
};
};
export type ProfilePageViewStatusContextType = {
statusPageView: ProfilePageStatusType['status'];
setStatusPageView: React.Dispatch<React.SetStateAction<ProfilePageStatusType>>;
};
export type SearchingRoleStateType = UserRoleEnum | 'any' | undefined;
export type UsersPageContextType = {
/**
* Search users by user role
*/
searchingRole: SearchingRoleStateType;
setSearchingRole: React.Dispatch<React.SetStateAction<SearchingRoleStateType>>;
/**
* Set loading state while loading users
*/
loading: boolean;
setLoading: React.Dispatch<React.SetStateAction<boolean>>;
};