Skip to content

closes #376 - Implement search and admin-filter functionality into Admin Users page #377

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 33 commits into from
Aug 12, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
17f4a1d
closes #376 - init commit, add searchbar, and filter and search options
Aug 6, 2020
7f7a931
closes #376 - change css styling to shorthand version for margin prop…
Aug 6, 2020
109a768
closes #376 - remove css styling to center text, use bootstrap classe…
Aug 6, 2020
a9bedef
closes #376 - add comment above debounce function of AdminUsersSearch…
Aug 6, 2020
3736bc1
closes #376 - fix spacing on comment above debounce function
Aug 6, 2020
efa0177
Merge branch 'master' of https://github.com/garageScript/c0d3-app int…
Aug 9, 2020
088a759
Merge branch 'master' of https://github.com/garageScript/c0d3-app int…
Aug 10, 2020
c88e279
closes 376 - remove searchbar and filter components, used internal Fi…
Aug 10, 2020
c58b3db
closes #376 - fix spacing on comments
Aug 10, 2020
661851b
closes #376 - fix spacing on comments
Aug 10, 2020
3ebcb69
closes #376 - variable name in comments
Aug 10, 2020
063b27c
closes #376 - refactor, remove unnecesssary code
Aug 10, 2020
8244fe0
closes #376 - refactor, remove unnecessary && from rowdata functoin
Aug 10, 2020
ea541be
closes #376 - refactor, turn duplicated code into variable to use
Aug 10, 2020
a7bc144
closes #376 - refactor, remove unnecessary variables in rowdata compo…
Aug 10, 2020
c06ea80
closes #376 - refactor, change property !== 'isAdmin' to property ===…
Aug 10, 2020
5e1f81b
closes #376 - remove export from split function
Aug 10, 2020
bfc7474
closes #376 - change to proper typing in adminuserssplitsearch file
Aug 10, 2020
438b34d
closes #376 - change variable names and merged 2 short components tog…
Aug 10, 2020
97c51e4
closes #376 - add export to pages/admin/users file filler variable
Aug 10, 2020
e2bb8fd
closes #376 - remove reduce and use filter->map for greater readabili…
Aug 11, 2020
8c951f5
closes #376 - fix spacing on comments inside originalWord functoin
Aug 11, 2020
a5adc01
closes #376 - change variable naming, change let to const
Aug 11, 2020
60f1a51
closes #376 - remove use of _.uniqueId() inside adminuserstable compo…
Aug 11, 2020
99242bd
closes #376 - remove unused lodash imports from files
Aug 11, 2020
442f7af
closes #376 - remove originalWord function, use substr instead, make …
Aug 11, 2020
2036c0d
closes #376 - remove originalWord function, use substr instead, make …
Aug 11, 2020
4ca9ea2
closes #376 - fix spacing on comments
Aug 11, 2020
c1a63bd
closes #376 - more detail in comments
Aug 11, 2020
096a09b
closes #376 - fix comment descriptions
Aug 11, 2020
ff25e10
closes #376 - merge split and originalcapitalization functoin together
Aug 12, 2020
8e3a177
closes #376 - remove commented out function call
Aug 12, 2020
b022017
closes #376 - remove lowerCaseSearchTerm from splitwithsearchterm fun…
Aug 12, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions components/admin/users/AdminUsersSplitSearch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import React from 'react'

/*
convert string to array, separated by and including searchTerm
Ex:
Inputs: searchTerm='bon', str = 'bonJourbon'
Output: [bon, Jour, bon]
*/
const splitWithSearchTerm = (str: string, searchTerm: string) => {
const lowerCaseSearchTerm = searchTerm.toLowerCase()
const splitArr = str.toLowerCase().split(lowerCaseSearchTerm)
// tracker is used for `str.substr` to extract characters from the original string at the correct places
let tracker = 0

const res = splitArr.reduce(
(acc: string[], word: string, splitArrIndex: number) => {
// converts words back into their original capitalization
const originalWord = str.substr(tracker, word.length)
acc.push(originalWord)
tracker += word.length

// used to prevent over-adding of searchTerm back into array
if (splitArrIndex === splitArr.length - 1) return acc
acc.push(searchTerm)
tracker += searchTerm.length
return acc
},
[]
)

return res
}

export const AdminUsersSplitSearch = (str: string, searchTerm: string) => {
// make all lowercase now to ensure both lower and uppercase characters can be searched for
const lowerCaseSearchTerm = searchTerm.toLowerCase()

// convert string to array with searchTerm included
const splitArr = splitWithSearchTerm(str, searchTerm)

// highlight search Term
const res = splitArr.map((word: string, key: number) => {
const bgColor =
word.toLowerCase() === lowerCaseSearchTerm ? 'rgb(84, 64, 216, .25)' : ''
return (
<span key={word + searchTerm + key} style={{ backgroundColor: bgColor }}>
{word}
</span>
)
})

return res
}
151 changes: 94 additions & 57 deletions components/admin/users/AdminUsersTable.tsx
Original file line number Diff line number Diff line change
@@ -1,135 +1,172 @@
import React from 'react'
import { useMutation } from '@apollo/react-hooks'
import _ from 'lodash'
import { Button } from '../../theme/Button'
import changeAdminRights from '../../../graphql/queries/changeAdminRights'
import { User } from '../../../graphql'
import { AdminUsersSplitSearch } from './AdminUsersSplitSearch'
import { filter } from '../../../pages/admin/users'

type UsersListProps = {
users: User[]
searchOption: filter
setUsers: React.Dispatch<React.SetStateAction<User[]>>
}

type RowDataProps = {
user: any
users: User[]
setUsers: React.Dispatch<React.SetStateAction<User[]>>
index: number
usersIndex: number
option: string
searchTerm: string
}

type AdminOptionProps = {
isAdmin: boolean
users: User[]
setUsers: React.Dispatch<React.SetStateAction<User[]>>
index: number
usersIndex: number
id: string | null | undefined
}

type UsersTableProps = {
users: User[]
searchOption: filter
setUsers: React.Dispatch<React.SetStateAction<User[]>>
}

export const headerValues = ['ID', 'Username', 'Name', 'Email', 'Admin']
export const headerTitles = ['ID', 'Username', 'Name', 'Email', 'Admin']
export const userProperties = ['id', 'username', 'name', 'email', 'isAdmin']

const TableHeaders: React.FC = () => {
const head = headerValues.map((property, key) => (
<th key={key} style={{ fontSize: '1.5rem' }}>
{property}
</th>
))

return (
<thead>
<tr style={{ textAlign: 'center' }}>{head}</tr>
</thead>
)
}

const AdminOption: React.FC<AdminOptionProps> = ({
isAdmin,
setUsers,
index,
usersIndex,
users,
id
}) => {
const [changeRights] = useMutation(changeAdminRights)

const newAdminRights = isAdmin ? 'false' : 'true'

const mutationVariable = {
variables: {
id: parseInt(id + ''),
status: isAdmin ? 'false' : 'true'
status: newAdminRights
}
}

const changeButton = async () => {
await changeRights(mutationVariable)
const newUsers = users && [...users]
if (newUsers) {
newUsers[index].isAdmin = isAdmin ? 'false' : 'true'
}
const newUsers = [...users]
newUsers[usersIndex].isAdmin = newAdminRights
setUsers(newUsers)
}

return (
<Button type={isAdmin ? 'danger' : 'success'} onClick={changeButton}>
{(isAdmin ? 'Remove' : 'Grant') + ' Admin Rights'}
{isAdmin ? 'Remove' : 'Add'}
</Button>
)
}

const RowData: React.FC<RowDataProps> = ({ user, users, setUsers, index }) => {
const RowData: React.FC<RowDataProps> = ({
user,
users,
setUsers,
usersIndex,
searchTerm,
option
}) => {
option = option.toLowerCase()

const data = userProperties.map((property: string, key: number) => {
let value = user[property]

const displayOption =
property !== 'isAdmin' ? (
value
) : (
if (searchTerm && property === option) {
value = AdminUsersSplitSearch(value, searchTerm)
}

if (property === 'isAdmin')
value = (
<AdminOption
isAdmin={user[property] === 'true'}
setUsers={setUsers}
index={index}
usersIndex={usersIndex}
users={users}
id={users && users[index].id}
id={users[usersIndex].id}
/>
)

return (
<td style={{ verticalAlign: 'middle' }} key={key}>
{displayOption}
<td className="align-middle" key={key}>
{value}
</td>
)
})

return <>{data}</>
}

const UsersList: React.FC<UsersListProps> = ({ users, setUsers }) => {
const list =
users &&
users.reduce((acc: any[], user: any, usersIndex: number) => {
acc.push(
<tr key={usersIndex} style={{ textAlign: 'center' }}>
<RowData
user={user}
setUsers={setUsers}
index={usersIndex}
users={users}
/>
</tr>
)
const UsersList: React.FC<UsersListProps> = ({
users,
setUsers,
searchOption
}) => {
const { searchTerm, admin } = searchOption
let { option } = searchOption
option = option.toLowerCase()

// usersIndex is needed for the RowData component to function properly
const usersListIndex: any = []

return acc
}, [])
// remove all users from list that are not going to be rendered
const list: User[] = users.filter((user: any, usersIndex: number) => {
let bool = true

return <tbody>{list}</tbody>
if (searchTerm) bool = (user[option] || '').includes(searchTerm)
if (bool && admin === 'Non-Admins') bool = user.isAdmin === 'false'
if (bool && admin === 'Admins') bool = user.isAdmin === 'true'

bool && usersListIndex.push(usersIndex)
return bool
})

const usersList = list.map((user: User, key: number) => {
return (
<tr key={key} className="text-center">
<RowData
user={user}
setUsers={setUsers}
usersIndex={usersListIndex.shift()}
users={users}
option={option}
searchTerm={searchTerm}
/>
</tr>
)
})

return <tbody>{usersList}</tbody>
}

export const UsersTable: React.FC<UsersTableProps> = ({ users, setUsers }) => (
<table className="table table-striped">
<TableHeaders />
<UsersList users={users} setUsers={setUsers} />
</table>
)
export const UsersTable: React.FC<UsersTableProps> = ({
users,
setUsers,
searchOption
}) => {
const head = headerTitles.map((title, key) => <th key={key}>{title}</th>)

return (
<table className="table table-striped">
<thead>
<tr className="text-center">{head}</tr>
</thead>
<UsersList
users={users}
setUsers={setUsers}
searchOption={searchOption}
/>
</table>
)
}
83 changes: 73 additions & 10 deletions pages/admin/users.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,93 @@
import React, { useState } from 'react'
import allUsers from '../../graphql/queries/allUsers'
import { UsersTable } from '../../components/admin/users/AdminUsersTable'
import {
UsersTable,
headerTitles
} from '../../components/admin/users/AdminUsersTable'
import { User, withGetApp, GetAppProps } from '../../graphql/index'
import { AdminLayout } from '../../components/admin/AdminLayout'
import withQueryLoader, {
QueryDataProps
} from '../../containers/withQueryLoader'

const titleStyle: React.CSSProperties | undefined = {
fontSize: '6rem',
textAlign: 'center',
fontWeight: 'bold'
}
import { FilterButtons } from '../../components/FilterButtons'
import _ from 'lodash'

type AllUsersData = {
allUsers: User[]
}

export type filter = {
option: string
admin: string
searchTerm: string
}

const initialSearchOptions: filter = {
option: 'Username',
admin: 'None',
searchTerm: ''
}

const adminFilters = ['Admins', 'Non-Admins', 'None']

const searchHeaders = [...headerTitles]

searchHeaders.length = 4

const AdminUsers: React.FC<QueryDataProps<AllUsersData>> = ({ queryData }) => {
const [searchOption, setSearchOption] = useState<filter>(initialSearchOptions)
const [users, setUsers] = useState<User[]>(queryData.allUsers)
/*
The reason debounce is used here is to prevent page rerenders on every keystroke.
If there is a rerender on every keystroke, the CPU consumption is high and
creates a perception of the page being slow and sluggish.
Therefore, we only rerender when the user stops typing.
*/
const run = _.debounce(setSearchOption, 500)

const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newSearchOption = {
...searchOption,
searchTerm: e.target.value
}
run(newSearchOption)
}

const changeFilter = (str: string, type: string) => {
const newSearchOption: any = { ...searchOption }
newSearchOption[type] = str
setSearchOption(newSearchOption)
}

return (
<div className="d-flex flex-column col-12">
<span className="text-primary" style={titleStyle}>
<h1 className="text-primary text-center font-weight-bold display-1">
Users
</span>
<UsersTable users={users} setUsers={setUsers} />
</h1>
<div className="mb-2">
<FilterButtons
options={searchHeaders}
onClick={(value: string) => changeFilter(value, 'option')}
currentOption={searchOption.option}
>
Search By:
</FilterButtons>
</div>
<input type="text" className="form-control" onChange={handleChange} />
<div className="mt-2 mb-2">
<FilterButtons
options={adminFilters}
onClick={(value: string) => changeFilter(value, 'admin')}
currentOption={searchOption.admin}
>
Filter By:
</FilterButtons>
</div>
<UsersTable
users={users}
setUsers={setUsers}
searchOption={searchOption}
/>
</div>
)
}
Expand Down