Skip to content
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

Feature/163 implement expandable table rows for data points #168

Merged
111 changes: 41 additions & 70 deletions components/data-points/data-points.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import { CircularProgress, IconButton, Box, Tooltip } from '@material-ui/core'
import { useCallback, useEffect, useMemo, useReducer, useState } from 'react'
import { useCallback, useEffect, useMemo, useReducer } from 'react'
import { useGlobal } from '../../context/global-context'
import {
dataPointsReducer,
DataPointsState,
} from '../../reducers/data-points-reducer'
import {
DataPointType,
TableDataPoint,
Expand All @@ -21,6 +17,10 @@ import { TitleCard } from '../title-card/title-card'
import useStyles from './data-points.style'
import DownloadCSVButton from '../download-csv-button'
import UploadCSVButton from '../upload-csv-button'
import {
dataPointsReducer,
DataPointsState,
} from '../../reducers/data-points-reducer'

type DataPointProps = {
valueVariables: ValueVariableType[]
Expand All @@ -30,8 +30,6 @@ type DataPointProps = {
onUpdateDataPoints: (dataPoints: DataPointType[][]) => void
}

type UpdateFnType = (rowIndex: number, ...args: any[]) => void

export default function DataPoints(props: DataPointProps) {
const {
valueVariables,
Expand All @@ -43,9 +41,7 @@ export default function DataPoints(props: DataPointProps) {
const classes = useStyles()
const [state, dispatch] = useReducer(dataPointsReducer, {
rows: [],
prevRows: [],
changed: false,
hasTempChange: false,
})
const isLoadingState = state.rows.length === 0
const global = useGlobal()
Expand All @@ -57,8 +53,16 @@ export default function DataPoints(props: DataPointProps) {
)

const buildCombinedVariables = useCallback((): CombinedVariableType[] => {
return (valueVariables as CombinedVariableType[]).concat(
categoricalVariables as CombinedVariableType[]
return (
valueVariables.map(v => ({
...v,
tooltip: `[${v.min}, ${v.max}]`,
})) as CombinedVariableType[]
).concat(
categoricalVariables.map(v => ({
...v,
tooltip: `${v.options.length} options`,
})) as CombinedVariableType[]
)
}, [categoricalVariables, valueVariables])

Expand All @@ -70,51 +74,45 @@ export default function DataPoints(props: DataPointProps) {
name: variable.name,
value: variable.options ? variable.options[0] : '',
options: variable.options,
tooltip: variable.tooltip,
}
})
.concat(
scoreNames.map(s => ({
name: s,
value: '0',
value: '',
options: undefined,
tooltip: undefined,
}))
),
isEditMode: true,
isNew: true,
}
}, [buildCombinedVariables, scoreNames])

const toggleEditMode = (rowIndex: number) => {
dispatch({ type: 'DATA_POINTS_TABLE_EDIT_TOGGLED', payload: rowIndex })
}
const rowAdded = (row: TableDataRow) =>
dispatch({
type: 'rowAdded',
payload: row,
})

const cancelEdit = (rowIndex: number) => {
dispatch({ type: 'DATA_POINTS_TABLE_EDIT_CANCELLED', payload: rowIndex })
}
const rowDeleted = (rowIndex: number) =>
dispatch({
type: 'rowDeleted',
payload: rowIndex,
})

const edit = (rowIndex: number, editValue: string, itemIndex: number) => {
const rowEdited = (rowIndex: number, row: TableDataRow) =>
dispatch({
type: 'DATA_POINTS_TABLE_EDITED',
type: 'rowEdited',
payload: {
itemIndex,
rowIndex,
value: editValue,
row,
},
})
}

const deleteRow = (rowIndex: number) => {
dispatch({ type: 'DATA_POINTS_TABLE_ROW_DELETED', payload: rowIndex })
}

const addRow = (emptyRow: TableDataRow) => {
dispatch({ type: 'DATA_POINTS_TABLE_ROW_ADDED', payload: emptyRow })
}

const buildState = useCallback(
(dataPoints: DataPointType[][]): DataPointsState => {
const combinedVariables: CombinedVariableType[] = buildCombinedVariables()
const emptyRow: TableDataRow = buildEmptyRow()
const dataPointRows: TableDataRow[] = dataPoints
.map(item => {
const rowData: DataPointType[] = item.filter(
Expand All @@ -127,46 +125,27 @@ export default function DataPoints(props: DataPointProps) {
name: v.name,
value: v.value.toString(),
options: combinedVariables[idx]?.options,
tooltip: combinedVariables[idx]?.tooltip,
}
})
const scores: TableDataPoint[] = item
.filter(dp => scoreNames.includes(dp.name))
.map(score => ({ name: score.name, value: score.value as string }))
return {
dataPoints: vars.concat(scores),
isEditMode: false,
isNew: false,
}
})
.concat(emptyRow as any)
.concat(buildEmptyRow())

return {
rows: dataPointRows,
prevRows: dataPointRows,
changed: false,
hasTempChange: false,
}
},
[buildCombinedVariables, buildEmptyRow, scoreNames]
[buildCombinedVariables, scoreNames, buildEmptyRow]
)

const onEditConfirm = (row: TableDataRow, rowIndex: number) => {
if (row.isNew) {
addRow(buildEmptyRow())
} else {
updateRow(rowIndex, toggleEditMode)
}
}

const updateRow = (index: number, updateFn: UpdateFnType, ...args: any[]) => {
const rowIndex = newestFirst ? state.rows.length - 1 - index : index
updateFn(rowIndex, ...args)
}

const updateTable = (dataPoints: DataPointType[][]) => {
dispatch({ type: 'setInitialState', payload: buildState(dataPoints) })
}

useEffect(() => {
dispatch({ type: 'setInitialState', payload: buildState(dataPoints) })
}, [valueVariables, categoricalVariables, scoreNames, buildState, dataPoints])
Expand Down Expand Up @@ -203,10 +182,7 @@ export default function DataPoints(props: DataPointProps) {
title={
<>
<Box display="flex" justifyContent="space-between">
<Box>
Data points
<br />
</Box>
Data points
<Box>
<DownloadCSVButton light />
<UploadCSVButton
Expand Down Expand Up @@ -244,22 +220,17 @@ export default function DataPoints(props: DataPointProps) {
{buildCombinedVariables().length > 0 && !isLoadingState && (
<Box className={classes.tableContainer}>
<EditableTable
newestFirst={global.state.dataPointsNewestFirst}
rows={
(newestFirst
? [...state.rows].reverse()
: state.rows) as TableDataRow[]
}
onEdit={(editValue: string, rowIndex: number, itemIndex: number) =>
updateRow(rowIndex, edit, editValue, itemIndex)
}
onEditConfirm={(row: TableDataRow, rowIndex: number) =>
onEditConfirm(row, rowIndex)
: [...state.rows]) as TableDataRow[]
}
onEditCancel={(rowIndex: number) => updateRow(rowIndex, cancelEdit)}
onToggleEditMode={(rowIndex: number) =>
updateRow(rowIndex, toggleEditMode)
onRowAdded={(row: TableDataRow) => rowAdded(row)}
onRowDeleted={(rowIndex: number) => rowDeleted(rowIndex)}
onRowEdited={(rowIndex: number, row: TableDataRow) =>
rowEdited(rowIndex, row)
}
onDelete={(rowIndex: number) => updateRow(rowIndex, deleteRow)}
/>
</Box>
)}
Expand Down
7 changes: 6 additions & 1 deletion components/editable-table/editable-table-cell.style.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import { makeStyles } from '@material-ui/core'
import { tableBorder } from '../../theme/theme'

export const useStyles = makeStyles(theme => ({
cell: {
editCell: {
minWidth: 48,
},
cell: {
border: 'none',
borderTop: tableBorder,
},
}))

export default useStyles
51 changes: 36 additions & 15 deletions components/editable-table/editable-table-cell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,50 @@ import {
Select,
TableCell,
TextField,
Tooltip,
} from '@material-ui/core'
import { ChangeEvent } from 'react'
import { ChangeEvent, CSSProperties } from 'react'
import useStyles from './editable-table-cell.style'

type EditableTableCellProps = {
value: string
isEditMode: boolean
options?: string[]
onChange: (value: string) => void
onChange?: (value: string) => void
tooltip?: string
style?: CSSProperties
}

export function EditableTableCell(props: EditableTableCellProps) {
const { value, isEditMode, options, onChange } = props
export function EditableTableCell({
value,
isEditMode,
options,
onChange,
tooltip,
style,
}: EditableTableCellProps) {
const classes = useStyles()

const textField = (
<TextField
size="small"
value={value}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
onChange('' + e.target.value)
}
/>
)

return (
<>
{isEditMode ? (
<TableCell className={classes.cell}>
<TableCell className={classes.editCell} style={{ ...style }}>
{options && options.length > 0 ? (
<FormControl>
<Select
value={value}
onChange={(e: ChangeEvent<any>) =>
onChange(e.target.value as string)
onChange={(e: ChangeEvent<HTMLInputElement>) =>
onChange(e.target.value)
}
displayEmpty
inputProps={{ 'aria-label': 'select value' }}
Expand All @@ -41,17 +60,19 @@ export function EditableTableCell(props: EditableTableCellProps) {
</Select>
</FormControl>
) : (
<TextField
size="small"
value={value}
onChange={(e: ChangeEvent) =>
onChange('' + (e.target as HTMLInputElement).value)
}
/>
<>
{tooltip !== undefined ? (
<Tooltip title={tooltip}>{textField}</Tooltip>
) : (
<>{textField}</>
)}
</>
)}
</TableCell>
) : (
<TableCell>{value}</TableCell>
<TableCell className={classes.cell} style={{ ...style }}>
{value}
</TableCell>
)}
</>
)
Expand Down
46 changes: 46 additions & 0 deletions components/editable-table/editable-table-collapsed-row.style.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { makeStyles } from '@material-ui/core'
import { colors, tableBorder } from '../../theme/theme'

export const useStyles = makeStyles(theme => ({
buttonContainer: {
whiteSpace: 'nowrap',
float: 'right',
},
cell: {
color: colors.silver,
paddingRight: '8px',
border: 'none',
borderTop: tableBorder,
},
editCell: {
paddingRight: '0px',
border: 'none',
borderTop: tableBorder,
},
row: {
'&:hover': {
background: '#fdfdfd',
},
'&:hover td': {
background: '#f4f4f4',
},
'&:hover td:first-of-type': {
background: 'white',
},
'&:hover td:last-of-type': {
background: 'white',
},
},
newRow: {
paddingRight: 0,
border: 'none',
borderTop: tableBorder,
},
emptyCell: {
border: 'none',
width: 16,
padding: 0,
},
}))

export default useStyles
Loading