Skip to content
Snippets Groups Projects
Commit 2f41bb92 authored by Jure's avatar Jure
Browse files

feat: pagination in users manager/overview

parent cfadcbc2
No related branches found
No related tags found
No related merge requests found
Showing
with 240 additions and 36 deletions
...@@ -2,7 +2,7 @@ import VisibilitySensor from 'react-visibility-sensor' ...@@ -2,7 +2,7 @@ import VisibilitySensor from 'react-visibility-sensor'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import Spinner from '../Spinner' import Spinner from '../shared/Spinner'
import { HasNextPage, NextPageButton } from './style' import { HasNextPage, NextPageButton } from './style'
const NextPageButtonWrapper = props => { const NextPageButtonWrapper = props => {
......
...@@ -7,7 +7,7 @@ import { useMutation } from '@apollo/react-hooks' ...@@ -7,7 +7,7 @@ import { useMutation } from '@apollo/react-hooks'
// import compose from 'recompose/compose'; // import compose from 'recompose/compose';
// import { connect } from 'react-redux'; // import { connect } from 'react-redux';
import Icon from '../Messages/Icon' import Icon from '../../../shared/Icon'
// import { addToastWithTimeout } from 'src/actions/toasts'; // import { addToastWithTimeout } from 'src/actions/toasts';
// import { openModal } from 'src/actions/modals'; // import { openModal } from 'src/actions/modals';
// import { replyToMessage } from 'src/actions/message'; // import { replyToMessage } from 'src/actions/message';
......
...@@ -6,7 +6,7 @@ import gql from 'graphql-tag' ...@@ -6,7 +6,7 @@ import gql from 'graphql-tag'
import { useQuery } from '@apollo/react-hooks' import { useQuery } from '@apollo/react-hooks'
import { useDropzone } from 'react-dropzone' import { useDropzone } from 'react-dropzone'
import Spinner from '../../Spinner' import Spinner from '../../shared/Spinner'
import ChangeUsername from './ChangeUsername' import ChangeUsername from './ChangeUsername'
import { BigProfileImage } from './ProfileImage' import { BigProfileImage } from './ProfileImage'
import PageWithHeader from './PageWithHeader' import PageWithHeader from './PageWithHeader'
......
import React from 'react'
import { Action } from '@pubsweet/ui'
import styled, { css } from 'styled-components'
import { th, grid } from '@pubsweet/ui-toolkit'
// TODO: Standardize shared components
import Icon from '../../shared/Icon'
const Page = styled.div`
// height: ${grid(3)};
// padding: ${grid(1)};
line-height: ${grid(5)};
${props =>
props.active &&
css`
color: ${th('colorPrimary')};
`}
`
const Pagination = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
padding: ${grid(2)} ${grid(3)};
`
const PaginationInfo = styled.div``
const Paginators = styled.div`
display: inline-flex;
`
const PreviousButton = styled.div`
border: 1px solid ${th('colorFurniture')};
svg {
stroke: ${th('colorFurniture')};
}
button {
padding: ${grid(1)};
line-height: ${grid(5)};
width: ${grid(5)};
cursor: pointer;
svg {
stroke: ${th('colorText')};
}
}
`
const NextButton = styled(PreviousButton)`
margin-left: -1px;
button {
&:before {
margin-left: -1px;
}
}
`
const Paginator = styled.div`
margin-left: -1px;
border: 1px solid ${th('colorFurniture')};
color: ${th('colorFurniture')};
text-align: center;
width: ${grid(5)};
svg {
stroke: ${th('colorFurniture')};
}
button {
width: ${grid(5)};
line-height: ${grid(5)};
cursor: pointer;
color: ${th('colorText')};
&:before {
margin-left: -1px;
}
}
`
const PaginationContainer = ({ setPage, limit, page, totalCount }) => {
const Previous = () => (
<PreviousButton>
<Action disabled={page <= 1} onClick={() => setPage(page - 1)}>
<Icon noPadding>chevron_left</Icon>
</Action>
</PreviousButton>
)
const Next = () => {
const lastPage = page >= pages.length
return (
<NextButton>
<Action disabled={lastPage} onClick={() => setPage(page + 1)}>
<Icon noPadding>chevron_right</Icon>
</Action>
</NextButton>
)
}
const PageNumber = ({ pageNumber }) => {
const active = page === pageNumber
return (
<Paginator>
<Page active={active}>
{active && <>{pageNumber}</>}
{!active && (
<Action onClick={() => setPage(pageNumber)}>{pageNumber}</Action>
)}
</Page>
</Paginator>
)
}
// e.g. Get [1,2,3] from totalCount 9, limit 3
const pages = [...new Array(Math.ceil(totalCount / limit)).keys()].map(
p => p + 1,
)
const firstResult = (page - 1) * limit + 1
const lastResult = Math.min((page - 1) * limit + limit, totalCount)
return (
<Pagination>
<PaginationInfo>
Showing {firstResult} to {lastResult} of {totalCount} results
</PaginationInfo>
<Paginators>
<Previous />
{pages.map(pageNumber => (
<PageNumber pageNumber={pageNumber} />
))}
<Next />
</Paginators>
</Pagination>
)
}
export default PaginationContainer
...@@ -3,7 +3,8 @@ import gql from 'graphql-tag' ...@@ -3,7 +3,8 @@ import gql from 'graphql-tag'
import { useMutation } from '@apollo/react-hooks' import { useMutation } from '@apollo/react-hooks'
import { Action } from '@pubsweet/ui' import { Action } from '@pubsweet/ui'
import { UserAvatar } from '../../component-avatar/src' import { UserAvatar } from '../../component-avatar/src'
import { Row, Cell, LastCell, UserCombo } from './style' import { Row, Cell, LastCell, UserCombo, Primary, Secondary, UserInfo } from './style'
import { convertTimestampToDate } from '../../../shared/time-formatting'
const DELETE_USER = gql` const DELETE_USER = gql`
mutation($id: ID) { mutation($id: ID) {
...@@ -18,8 +19,16 @@ const User = ({ user }) => { ...@@ -18,8 +19,16 @@ const User = ({ user }) => {
return ( return (
<Row> <Row>
<Cell><UserCombo><UserAvatar user={user}/>{user.username} {user.email}</UserCombo></Cell> <Cell>
<Cell>{user.created}</Cell> <UserCombo>
<UserAvatar user={user} />
<UserInfo>
<Primary>{user.username}</Primary>
<Secondary>{user.email || '(via ORCID)'}</Secondary>
</UserInfo>
</UserCombo>
</Cell>
<Cell>{convertTimestampToDate(user.created)}</Cell>
<Cell>{user.admin ? 'yes' : ''}</Cell> <Cell>{user.admin ? 'yes' : ''}</Cell>
<LastCell> <LastCell>
<Action onClick={() => deleteUser({ variables: { id: user.id } })}> <Action onClick={() => deleteUser({ variables: { id: user.id } })}>
......
import React, { useState } from 'react' import React, { useState } from 'react'
import gql from 'graphql-tag' import gql from 'graphql-tag'
import { useQuery } from '@apollo/react-hooks' import { useQuery } from '@apollo/react-hooks'
import { Heading, Action } from '@pubsweet/ui' import { Heading } from '@pubsweet/ui'
import User from './User' import User from './User'
import { Container, Table, Header } from './style' import { Container, Table, Header } from './style'
import Spinner from '../../Spinner' import Spinner from '../../shared/Spinner'
import Pagination from './Pagination'
const GET_USERS = gql` const GET_USERS = gql`
query Users( query Users(
...@@ -15,13 +16,16 @@ const GET_USERS = gql` ...@@ -15,13 +16,16 @@ const GET_USERS = gql`
$limit: Int $limit: Int
) { ) {
users(sort: $sort, filter: $filter, offset: $offset, limit: $limit) { users(sort: $sort, filter: $filter, offset: $offset, limit: $limit) {
id totalCount
username users {
admin id
email username
profilePicture admin
online email
created profilePicture
online
created
}
} }
} }
` `
...@@ -56,23 +60,25 @@ const UsersManager = () => { ...@@ -56,23 +60,25 @@ const UsersManager = () => {
const [sortName, setSortName] = useState('created') const [sortName, setSortName] = useState('created')
const [sortDirection, setSortDirection] = useState('DESC') const [sortDirection, setSortDirection] = useState('DESC')
const [page, setPage] = useState(1) const [page, setPage] = useState(1)
const limit = 15 const limit = 10
const sort = sortName && sortDirection && `${sortName}_${sortDirection}` const sort = sortName && sortDirection && `${sortName}_${sortDirection}`
const { loading, error, data } = useQuery(GET_USERS, { const { loading, error, data } = useQuery(GET_USERS, {
variables: { variables: {
sort, sort,
offset: (page - 1) * limit, offset: (page - 1) * limit,
limit limit,
}, },
}) })
if (loading) return <Spinner/> if (loading) return <Spinner />
if (error) return `Error! ${error.message}` if (error) return `Error! ${error.message}`
const { users, totalCount } = data.users
return ( return (
<Container> <Container>
<Heading level={1}>List of users</Heading> <Heading level={1}>Users</Heading>
<Table> <Table>
<Header> <Header>
<tr> <tr>
...@@ -83,13 +89,12 @@ const UsersManager = () => { ...@@ -83,13 +89,12 @@ const UsersManager = () => {
</tr> </tr>
</Header> </Header>
<tbody> <tbody>
{data.users.map((user, key) => ( {users.map((user, key) => (
<User key={user.id} number={key + 1} user={user} /> <User key={user.id} number={key + 1} user={user} />
))} ))}
</tbody> </tbody>
</Table> </Table>
{ page > 1 && <><Action onClick={() => setPage(page - 1)}>Previous</Action>&nbsp;</> } <Pagination setPage={setPage} limit={limit} page={page} totalCount={totalCount} />
{ data.users.length === limit && <Action onClick={() => setPage(page + 1)}>Next</Action> }
</Container> </Container>
) )
} }
......
import styled from 'styled-components' import styled, { css } from 'styled-components'
import { Action } from '@pubsweet/ui' import { Action } from '@pubsweet/ui'
import { th, grid } from '@pubsweet/ui-toolkit' import { th, grid } from '@pubsweet/ui-toolkit'
...@@ -6,7 +6,6 @@ export const Table = styled.table` ...@@ -6,7 +6,6 @@ export const Table = styled.table`
width: 100%; width: 100%;
border-radius: ${th('borderRadius')}; border-radius: ${th('borderRadius')};
border-collapse: collapse; border-collapse: collapse;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
font-size: ${th('fontSizeBaseSmall')}; font-size: ${th('fontSizeBaseSmall')};
td { td {
...@@ -18,10 +17,10 @@ export const Header = styled.thead` ...@@ -18,10 +17,10 @@ export const Header = styled.thead`
font-variant: all-small-caps; font-variant: all-small-caps;
border-bottom: 1px solid ${th('colorFurniture')}; border-bottom: 1px solid ${th('colorFurniture')};
background: ${th('colorBackgroundHue')}; background-color: ${th('colorBackgroundHue')};
th { th {
padding: ${grid(2)} ${grid(3)}; padding: ${grid(1)} ${grid(3)};
} }
` `
...@@ -30,13 +29,19 @@ export const Container = styled.div` ...@@ -30,13 +29,19 @@ export const Container = styled.div`
` `
export const Row = styled.tr` export const Row = styled.tr`
height: ${grid(6)}; max-height: ${grid(8)};
border-bottom: 1px solid ${th('colorFurniture')}; border-bottom: 1px solid ${th('colorFurniture')};
&:hover {
background-color: ${th('colorBackgroundHue')};
}
` `
export const Cell = styled.td` export const Cell = styled.td`
padding: ${grid(2)} ${grid(3)}; padding-bottom: ${grid(2)};
padding-top: calc(${grid(2)} - 1px);
padding-left: ${grid(3)};
padding-right: ${grid(3)};
button { button {
font-size: ${th('fontSizeBaseSmall')}; font-size: ${th('fontSizeBaseSmall')};
} }
...@@ -44,9 +49,22 @@ export const Cell = styled.td` ...@@ -44,9 +49,22 @@ export const Cell = styled.td`
export const UserCombo = styled.div` export const UserCombo = styled.div`
display: flex; display: flex;
line-height: ${grid(5)}; line-height: ${grid(2.5)};
align-items: center;
` `
export const LastCell = styled(Cell)` export const LastCell = styled(Cell)`
text-align: right; text-align: right;
` `
export const Primary = styled.div`
font-weight: 500;
`
export const Secondary = styled.div`
color: ${th('colorTextPlaceholder')};
`
export const UserInfo = styled.div`
margin-left: ${grid(1)};
`
File moved
...@@ -19,10 +19,13 @@ const secondary = css` ...@@ -19,10 +19,13 @@ const secondary = css`
&[disabled] { &[disabled] {
color: ${th('colorTextPlaceholder')}; color: ${th('colorTextPlaceholder')};
cursor: default;
&:hover { &:hover {
background: none; background: none;
} }
&:hover:before {
visibility: hidden;
}
} }
` `
......
...@@ -28,9 +28,12 @@ height: 100% ...@@ -28,9 +28,12 @@ height: 100%
width: 100% width: 100%
} }
*, *:before, *:after {
box-sizing: inherit;
}
* { * {
border: 0; border: 0;
box-sizing: inherit;
-webkit-font-smoothing: auto; -webkit-font-smoothing: auto;
font-weight: inherit; font-weight: inherit;
margin: 0; margin: 0;
......
...@@ -18,7 +18,7 @@ const cokoTheme = { ...@@ -18,7 +18,7 @@ const cokoTheme = {
colorBackground: 'white', colorBackground: 'white',
colorPrimary: '#0B65CB', colorPrimary: '#0B65CB',
colorSecondary: '#E7E7E7', colorSecondary: '#E7E7E7',
colorFurniture: '#CCC', colorFurniture: '#E8E8E8',
colorBorder: '#AAA', colorBorder: '#AAA',
colorBackgroundHue: '#f9fafb', colorBackgroundHue: '#f9fafb',
colorSuccess: '#008800', colorSuccess: '#008800',
......
<svg width="340" height="340" viewBox="0 0 340 340" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0)">
<rect width="340" height="340" fill="white"/>
<rect width="340" height="340" fill="#E8E8E8"/>
<path d="M259 254.091C259 303.294 281 410 170 410C59 410 81 303.294 81 254.091C81 204.887 120.847 165 170 165C219.153 165 259 204.887 259 254.091Z" fill="white"/>
<ellipse cx="170" cy="96" rx="56" ry="55" fill="white"/>
</g>
<defs>
<clipPath id="clip0">
<rect width="340" height="340" fill="white"/>
</clipPath>
</defs>
</svg>
...@@ -5,7 +5,7 @@ const { getPubsub } = pubsubManager ...@@ -5,7 +5,7 @@ const { getPubsub } = pubsubManager
// Fires immediately when the message is created // Fires immediately when the message is created
const MESSAGE_CREATED = 'MESSAGE_CREATED' const MESSAGE_CREATED = 'MESSAGE_CREATED'
const Message = require('../message') const Message = require('./message')
const resolvers = { const resolvers = {
Query: { Query: {
...@@ -85,6 +85,7 @@ const typeDefs = ` ...@@ -85,6 +85,7 @@ const typeDefs = `
type PageInfo { type PageInfo {
startCursor: String startCursor: String
hasPreviousPage: Boolean hasPreviousPage: Boolean
hasNextPage: Boolean
} }
type MessagesRelay { type MessagesRelay {
......
...@@ -15,6 +15,8 @@ const resolvers = { ...@@ -15,6 +15,8 @@ const resolvers = {
query.where({ admin: true }) query.where({ admin: true })
} }
const totalCount = await query.resultSize()
if (sort) { if (sort) {
// e.g. 'created_DESC' into 'created' and 'DESC' arguments // e.g. 'created_DESC' into 'created' and 'DESC' arguments
query.orderBy(...sort.split('_')) query.orderBy(...sort.split('_'))
...@@ -28,7 +30,12 @@ const resolvers = { ...@@ -28,7 +30,12 @@ const resolvers = {
query.offset(offset) query.offset(offset)
} }
return query const users = await query
return {
totalCount,
users,
}
// return ctx.connectors.User.fetchAll(where, ctx, { eager }) // return ctx.connectors.User.fetchAll(where, ctx, { eager })
}, },
// Authentication // Authentication
...@@ -152,10 +159,15 @@ const resolvers = { ...@@ -152,10 +159,15 @@ const resolvers = {
const typeDefs = ` const typeDefs = `
extend type Query { extend type Query {
user(id: ID, username: String): User user(id: ID, username: String): User
users(sort: UsersSort, offset: Int, limit: Int, filter: UsersFilter): [User] users(sort: UsersSort, offset: Int, limit: Int, filter: UsersFilter): PaginatedUsers
searchUsers(teamId: ID, query: String): [User] searchUsers(teamId: ID, query: String): [User]
} }
type PaginatedUsers {
totalCount: Int
users: [User]
}
extend type Mutation { extend type Mutation {
createUser(input: UserInput): User createUser(input: UserInput): User
deleteUser(id: ID): User deleteUser(id: ID): User
......
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