Skip to content

Commit

Permalink
[Frontend] Support assigning users to triage columns
Browse files Browse the repository at this point in the history
  • Loading branch information
keithamoss committed Mar 17, 2019
1 parent 6bd8470 commit 2b592f1
Show file tree
Hide file tree
Showing 8 changed files with 270 additions and 9 deletions.
9 changes: 9 additions & 0 deletions frontend/src/redux/modules/reviewers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,14 @@ export const getCurrentReviewer = createSelector(
}
)

export const getReviewerById = createSelector(
[getReviewers],
(users: IReviewerUser[]) =>
memoize((userId: number | null) => {
return users.find((user: IReviewerUser) => userId === user.id)
})
)

// Action Creators

export const setCurrentReviewer = (reviewerId: number): IActionReviewersSetCurrentReviewer => ({
Expand All @@ -169,6 +177,7 @@ export enum eSocialAssignmentStatus {
export interface IReviewerUser {
id: number
initials: string
profile_image_url: string
is_accepting_assignments: boolean
name: string
username: string
Expand Down
42 changes: 39 additions & 3 deletions frontend/src/redux/modules/triage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,14 @@ import { blueGrey, green, yellow } from "@material-ui/core/colors"
import * as dotProp from "dot-prop-immutable"
import { uniq } from "lodash-es"
import { Action } from "redux"
import { IActionSocialColumnsList, IActionsTweetsLoadTweets, IActionTweetsNew, ITweetFetchColumn } from "../../websockets/actions"
import { WS_SOCIAL_COLUMNS_LIST, WS_TWEETS_LOAD_TWEETS, WS_TWEETS_NEW_TWEETS } from "../../websockets/constants"
import {
IActionSocialColumnsList,
IActionSocialColumnsUpdate,
IActionsTweetsLoadTweets,
IActionTweetsNew,
ITweetFetchColumn,
} from "../../websockets/actions"
import { WS_SOCIAL_COLUMNS_LIST, WS_SOCIAL_COLUMNS_UPDATE, WS_TWEETS_LOAD_TWEETS, WS_TWEETS_NEW_TWEETS } from "../../websockets/constants"
import { IThunkExtras } from "./interfaces"
import { eSocialAssignmentStatus, IReviewerAssignment } from "./reviewers"
import { eSocialTweetState, ISocialTweet, ISocialTweetList, ISocialTweetsAndColumnsResponse } from "./social"
Expand All @@ -20,7 +26,13 @@ const initialState: IModule = {
}

// Reducer
type IAction = IActionLoadTweets | IActionsTweetsLoadTweets | IActionTweetsNew | IActionLoadBufferedTweets | IActionSocialColumnsList
type IAction =
| IActionLoadTweets
| IActionsTweetsLoadTweets
| IActionTweetsNew
| IActionLoadBufferedTweets
| IActionSocialColumnsList
| IActionSocialColumnsUpdate
export default function reducer(state: IModule = initialState, action: IAction) {
switch (action.type) {
case LOAD_TWEETS:
Expand Down Expand Up @@ -87,6 +99,12 @@ export default function reducer(state: IModule = initialState, action: IAction)
state = dotProp.set(state, "column_tweets", columnTweets)
state = dotProp.set(state, "column_tweets_buffered", columnTweets)
return dotProp.set(state, "columns", action.columns)
case WS_SOCIAL_COLUMNS_UPDATE:
action.columns.forEach((column: ITriageColumn) => {
const columnIndex = state.columns.findIndex((c: ITriageColumn) => c.id === column.id)
state = dotProp.set(state, `columns.${columnIndex}`, column)
})
return state
default:
return state
}
Expand Down Expand Up @@ -127,6 +145,7 @@ export interface ITriageColumn {
id: number
platform: eSocialPlatformChoice
search_phrases: string[]
assigned_to: number | null
total_tweets: number
}

Expand All @@ -142,6 +161,23 @@ export function loadBufferedTweetsForColumn(columnId: number) {
}
}

export function assignTriagerToColumn(columnId: number, userId: number) {
return async (dispatch: Function, getState: Function, { api, emit }: IThunkExtras) => {
await api.get("/0.1/social_columns/assign_triager/", dispatch, {
columnId,
userId,
})
}
}

export function unassignTriagerFromColumn(columnId: number) {
return async (dispatch: Function, getState: Function, { api, emit }: IThunkExtras) => {
await api.get("/0.1/social_columns/unassign_triager/", dispatch, {
columnId,
})
}
}

// Utilities
export function getActionBarBackgroundColour(tweet: ISocialTweet, assignment: IReviewerAssignment | null) {
if (assignment !== null) {
Expand Down
121 changes: 121 additions & 0 deletions frontend/src/triage/AssignerAvatar/AssignerAvatar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { Avatar, Button, Menu, MenuItem, Theme, withStyles, WithStyles } from "@material-ui/core"
import { Person } from "@material-ui/icons"
import * as React from "react"
import { IReviewerUser } from "../../redux/modules/reviewers"

const styles = (theme: Theme) => ({
button: {
backgroundColor: "transparent !important",
},
avatar: {
marginRight: 10,
},
menuItem: {
paddingTop: 16,
paddingBottom: 16,
},
})

export interface IProps {
assignedToUser: IReviewerUser | undefined
triagers: IReviewerUser[]
onAssign: Function
onUnassign: Function
}

export interface IState {
anchorEl: HTMLElement | null
}

type TComponentProps = IProps & WithStyles
class AssignerAvatar extends React.PureComponent<TComponentProps, IState> {
private handleOpenMenu: any
private handleChooseItem: any
private handleUnassign: any
private handleClose: any

public constructor(props: TComponentProps) {
super(props)

this.state = {
anchorEl: null,
}

this.handleOpenMenu = (event: React.MouseEvent<HTMLElement>) => {
this.setState({ anchorEl: event.currentTarget })
}

this.handleChooseItem = (userId: number) => (event: React.MouseEvent<HTMLElement>) => {
this.props.onAssign(userId)
this.handleClose()
// console.log(this.state)
// this.setState({ anchorEl: null })
}

this.handleUnassign = () => {
this.props.onUnassign()
this.handleClose()
// console.log(this.state)
// this.setState({ anchorEl: null })
}

this.handleClose = () => {
this.setState({ anchorEl: null })
}
}

public render() {
const { assignedToUser, triagers, classes } = this.props
const { anchorEl } = this.state

return (
<React.Fragment>
{assignedToUser === undefined && (
<Button
aria-owns={anchorEl ? "simple-menu" : undefined}
aria-haspopup="true"
className={classes.button}
onClick={this.handleOpenMenu}
>
<Avatar className={classes.avatar}>
<Person fontSize="large" />
</Avatar>
</Button>
)}
{assignedToUser !== undefined && (
<Button
aria-owns={anchorEl ? "simple-menu" : undefined}
aria-haspopup="true"
className={classes.button}
onClick={this.handleOpenMenu}
>
<Avatar className={classes.avatar} src={assignedToUser.profile_image_url} />
</Button>
)}
<Menu id="simple-menu" anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={this.handleClose}>
{triagers.map((user: IReviewerUser) => {
if (assignedToUser === undefined || assignedToUser.id !== user.id) {
return (
<MenuItem key={user.id} className={classes.menuItem} onClick={this.handleChooseItem(user.id)}>
<Avatar className={classes.avatar} src={user.profile_image_url} />
{user.name}
</MenuItem>
)
}
return null
})}
{assignedToUser !== undefined && (
<MenuItem className={classes.menuItem} onClick={this.handleUnassign}>
<Avatar className={classes.avatar}>
<Person fontSize="large" />
</Avatar>
Unassign
</MenuItem>
)}
</Menu>
</React.Fragment>
)
}
}

export default withStyles(styles)(AssignerAvatar)
53 changes: 53 additions & 0 deletions frontend/src/triage/AssignerAvatar/AssignerAvatarContainer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import * as React from "react"
import { connect } from "react-redux"
import { IStore } from "../../redux/modules/reducer"
import { IReviewerUser } from "../../redux/modules/reviewers"
import AssignerAvatar from "./AssignerAvatar"

export interface IProps {
assignedToUser: IReviewerUser | undefined
onAssign: Function
onUnassign: Function
}

export interface IStoreProps {
triagers: IReviewerUser[]
}

export interface IDispatchProps {
onAssign: Function
onUnassign: Function
}

type TComponentProps = IProps & IStoreProps & IDispatchProps
class AssignerAvatarContainer extends React.PureComponent<TComponentProps, {}> {
public render() {
const { assignedToUser, triagers, onAssign, onUnassign } = this.props

return <AssignerAvatar assignedToUser={assignedToUser} triagers={triagers} onAssign={onAssign} onUnassign={onUnassign} />
}
}

const mapStateToProps = (state: IStore, ownProps: IProps): IStoreProps => {
const { reviewers } = state

return {
triagers: reviewers.users,
}
}

const mapDispatchToProps = (dispatch: Function, ownProps: IProps): IDispatchProps => {
return {
onAssign: (userId: number) => {
ownProps.onAssign(userId)
},
onUnassign: () => {
ownProps.onUnassign()
},
}
}

export default connect<IStoreProps, IDispatchProps, IProps, IStore>(
mapStateToProps,
mapDispatchToProps
)(AssignerAvatarContainer)
18 changes: 16 additions & 2 deletions frontend/src/triage/TweetColumnBar/TweetColumnBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import NewReleases from "@material-ui/icons/NewReleases"
import NewReleasesOutlined from "@material-ui/icons/NewReleasesOutlined"
import * as React from "react"
import { isDevEnvironment } from "../../redux/modules/app"
import { IReviewerUser } from "../../redux/modules/reviewers"
import { ITriageColumn } from "../../redux/modules/triage"
import AssignerAvatarContainer from "../AssignerAvatar/AssignerAvatarContainer"

const styles = (theme: Theme) => ({
grow: {
Expand All @@ -18,19 +20,31 @@ const styles = (theme: Theme) => ({
export interface IProps {
column: ITriageColumn
hasBufferedTweets: boolean
assignedToUser: IReviewerUser | undefined
onLoadNewTweetsForColumn: any
onAssignTriager: Function
onUnassignTriager: Function
}

export interface IState {}

type TComponentProps = IProps & WithStyles
class TweetColumnBar extends React.PureComponent<TComponentProps, IState> {
public render() {
const { column, hasBufferedTweets, onLoadNewTweetsForColumn, classes } = this.props
const {
column,
hasBufferedTweets,
assignedToUser,
onLoadNewTweetsForColumn,
onAssignTriager,
onUnassignTriager,
classes,
} = this.props

return (
<AppBar position="static">
<Toolbar variant="dense">
<Toolbar>
<AssignerAvatarContainer assignedToUser={assignedToUser} onAssign={onAssignTriager} onUnassign={onUnassignTriager} />
<Typography variant="h6" color="inherit" className={classes.grow}>
{column.search_phrases.join(", ")}
{isDevEnvironment() && ` (#${column.id})`}
Expand Down
30 changes: 26 additions & 4 deletions frontend/src/triage/TweetColumnBar/TweetColumnBarContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import * as React from "react"
import { connect } from "react-redux"
import { IStore } from "../../redux/modules/reducer"
import { ITriageColumn, loadBufferedTweetsForColumn } from "../../redux/modules/triage"
import { getReviewerById, IReviewerUser } from "../../redux/modules/reviewers"
import { assignTriagerToColumn, ITriageColumn, loadBufferedTweetsForColumn, unassignTriagerFromColumn } from "../../redux/modules/triage"
import TweetColumnBar from "./TweetColumnBar"

export interface IProps {
Expand All @@ -10,36 +11,57 @@ export interface IProps {

export interface IStoreProps {
hasBufferedTweets: boolean
assignedToUser: IReviewerUser | undefined
}

export interface IDispatchProps {
onLoadNewTweetsForColumn: Function
onAssignTriager: Function
onUnassignTriager: Function
}

type TComponentProps = IProps & IStoreProps & IDispatchProps
class TweetColumnBarContainer extends React.PureComponent<TComponentProps, {}> {
public render() {
const { column, hasBufferedTweets, onLoadNewTweetsForColumn } = this.props
const { column, hasBufferedTweets, assignedToUser, onLoadNewTweetsForColumn, onAssignTriager, onUnassignTriager } = this.props

return <TweetColumnBar column={column} hasBufferedTweets={hasBufferedTweets} onLoadNewTweetsForColumn={onLoadNewTweetsForColumn} />
return (
<TweetColumnBar
column={column}
hasBufferedTweets={hasBufferedTweets}
assignedToUser={assignedToUser}
onLoadNewTweetsForColumn={onLoadNewTweetsForColumn}
onAssignTriager={onAssignTriager}
onUnassignTriager={onUnassignTriager}
/>
)
}
}

const mapStateToProps = (state: IStore, ownProps: IProps): IStoreProps => {
const { triage } = state

const getReviewerByIdFilter = getReviewerById(state)

return {
hasBufferedTweets: triage.column_tweets_buffered[ownProps.column.id].length > 0,
assignedToUser: getReviewerByIdFilter(ownProps.column.assigned_to),
}
}

const mapDispatchToProps = (dispatch: Function): IDispatchProps => {
const mapDispatchToProps = (dispatch: Function, ownProps: IProps): IDispatchProps => {
return {
onLoadNewTweetsForColumn: (event: React.MouseEvent<HTMLElement>) => {
if ("columnid" in event.currentTarget.dataset && event.currentTarget.dataset.columnid !== undefined) {
dispatch(loadBufferedTweetsForColumn(parseInt(event.currentTarget.dataset.columnid, 10)))
}
},
onAssignTriager: (userId: number) => {
dispatch(assignTriagerToColumn(ownProps.column.id, userId))
},
onUnassignTriager: () => {
dispatch(unassignTriagerFromColumn(ownProps.column.id))
},
}
}

Expand Down
Loading

0 comments on commit 2b592f1

Please sign in to comment.