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

Deliberation context and refactor review-point-list #215 #231

Merged
merged 21 commits into from
Oct 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
11 changes: 3 additions & 8 deletions .babelrc
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
{
"presets": [
"@babel/preset-react",
"@babel/preset-env"
],
"plugins": [
"@babel/plugin-proposal-class-properties",
"@babel/plugin-transform-regenerator"
]
"presets": ["@babel/preset-react", "@babel/preset-env"],
"plugins": ["@babel/plugin-proposal-class-properties", "@babel/plugin-transform-regenerator"],
"sourceMap": "inline"
}
3 changes: 2 additions & 1 deletion .storybook/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ const config = {
},
docs: {},
webpackFinal: async config => {
const newConfig = merge(config, webpackDevConfig)
const storyDevConfig = { ...webpackDevConfig, entry: undefined, output: undefined } // to be set by storybook
const newConfig = merge(config, storyDevConfig)
return newConfig
},
}
Expand Down
146 changes: 146 additions & 0 deletions app/components/__tests__/deliberation-context.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
// https://github.com/EnCiv/civil-pursuit/issues/215
import { deriveReducedPointList } from '../deliberation-context'

const parentId = '1001'
const data = { reducedPointList: [] }
const local = {}
describe('test deriveReducedPointList', () => {
test('initially no points in list', () => {
const { reducedPointList } = deriveReducedPointList(data, local)
expect(reducedPointList).toMatchObject([])
})
test('called with no changes should give the same ref', () => {
const oldReducedPointList = data.reducedPointList
const { reducedPointList } = deriveReducedPointList(data, local)
expect(reducedPointList).toBe(oldReducedPointList)
})
test('called with emtpy data should give the same ref', () => {
data.pointById = {}
data.groupIdsLists = []
const oldReducedPointList = data.reducedPointList
const { reducedPointList } = deriveReducedPointList(data, local)
expect(reducedPointList).toBe(oldReducedPointList)
})
test('pointIds but not groupIdsList generates a list', () => {
const points = [
{ _id: '1', subject: '1', description: 'describe 1', parentId },
{ _id: '2', subject: '2', description: 'd2', parentId },
{ _id: '3', subject: '3', description: 'd3', parentId },
{ _id: '4', subject: '4', description: 'd4', parentId },
{ _id: '5', subject: '5', description: 'd5', parentId },
{ _id: '6', subject: '6', description: 'd6', parentId },
]
data.pointById = points.reduce((pointById, point) => ((pointById[point._id] = point), pointById), {})
const { reducedPointList } = deriveReducedPointList(data, local)
expect(reducedPointList).toMatchObject(
points.map(point => ({
point,
}))
)
})
test('if a point is updated, a new pointById ref is returned, but unchanged points have unchanged refs', () => {
const oldReducedPointList = deriveReducedPointList(data, local).reducedPointList
const oldPointRefs = Object.values(data.pointById)
data.pointById['1'] = { _id: '1', subject: '1', description: 'updated d1', parentId }
data.pointById = { ...data.pointById } // needs a new ref because it changed
const { reducedPointList } = deriveReducedPointList(data, local)
expect(reducedPointList).not.toBe(oldReducedPointList)
expect(reducedPointList[0].point).not.toBe(oldPointRefs[0])
for (let i = 1; i <= 5; i++) {
expect(reducedPointList[i].point).toBe(oldPointRefs[i])
}
})
test('grouping points', () => {
const oldReducedPointList = deriveReducedPointList(data, local).reducedPointList
data.groupIdsLists = [
['1', '2'],
['3', '4', '5'],
]
const { reducedPointList } = deriveReducedPointList(data, local)
expect(reducedPointList).not.toBe(oldReducedPointList)
expect(reducedPointList).toEqual([
{
point: { _id: '1', subject: '1', description: 'updated d1', parentId },
group: [{ _id: '2', subject: '2', description: 'd2', parentId }],
},
{
point: { _id: '3', subject: '3', description: 'd3', parentId },
group: [
{ _id: '4', subject: '4', description: 'd4', parentId },
{ _id: '5', subject: '5', description: 'd5', parentId },
],
},
{ point: { _id: '6', subject: '6', description: 'd6', parentId } },
])
})
test('grouping can change', () => {
const oldReducedPointList = deriveReducedPointList(data, local).reducedPointList
const oldLastPoint = oldReducedPointList[2]
data.groupIdsLists = [
['1', '2', '4'],
['3', '5'],
]
const { reducedPointList } = deriveReducedPointList(data, local)
expect(reducedPointList).not.toBe(oldReducedPointList)
expect(reducedPointList).toEqual([
{
point: { _id: '1', subject: '1', description: 'updated d1', parentId },
group: [
{ _id: '2', subject: '2', description: 'd2', parentId },
{ _id: '4', subject: '4', description: 'd4', parentId },
],
},
{
point: { _id: '3', subject: '3', description: 'd3', parentId },
group: [{ _id: '5', subject: '5', description: 'd5', parentId }],
},
{ point: { _id: '6', subject: '6', description: 'd6', parentId } },
])
expect(oldLastPoint).toBe(reducedPointList[2])
})
test('a point in a group is updated', () => {
const oldReducedPointList = deriveReducedPointList(data, local).reducedPointList
const oldLastPoint = oldReducedPointList[2]
data.pointById['2'] = { _id: '2', subject: '2 child of 1', description: 'updated d2 in group', parentId }
data.pointById = { ...data.pointById } // needs a new ref
const { reducedPointList } = deriveReducedPointList(data, local)
expect(reducedPointList).not.toBe(oldReducedPointList)
expect(reducedPointList).toEqual([
{
point: { _id: '1', subject: '1', description: 'updated d1', parentId },
group: [
{ _id: '2', subject: '2 child of 1', description: 'updated d2 in group', parentId },
{ _id: '4', subject: '4', description: 'd4', parentId },
],
},
{
point: { _id: '3', subject: '3', description: 'd3', parentId },
group: [{ _id: '5', subject: '5', description: 'd5', parentId }],
},
{ point: { _id: '6', subject: '6', description: 'd6', parentId } },
])
expect(oldLastPoint).toBe(reducedPointList[2])
})
test('can be ungrouped', () => {
const oldReducedPointList = deriveReducedPointList(data, local).reducedPointList
const oldFirstPoint = oldReducedPointList[0]
data.groupIdsLists = [['1', '2', '4']]
const { reducedPointList } = deriveReducedPointList(data, local)
expect(reducedPointList).not.toBe(oldReducedPointList)
expect(reducedPointList).toEqual([
{
point: { _id: '1', subject: '1', description: 'updated d1', parentId },
group: [
{ _id: '2', subject: '2 child of 1', description: 'updated d2 in group', parentId },
{ _id: '4', subject: '4', description: 'd4', parentId },
],
},
{
point: { _id: '3', subject: '3', description: 'd3', parentId },
},
{ point: { _id: '5', subject: '5', description: 'd5', parentId } },
{ point: { _id: '6', subject: '6', description: 'd6', parentId } },
])
expect(oldFirstPoint).toBe(reducedPointList[0])
})
})
75 changes: 75 additions & 0 deletions app/components/deliberation-context.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import React, { createContext, useCallback, useState, useRef } from 'react'
import { merge } from 'lodash'
export const DeliberationContext = createContext({})
export default DeliberationContext

export function DeliberationContextProvider(props) {
const [data, setData] = useState({ reducedPointList: [] })
const local = useRef({}).current // can't be in deriver becasue "Error: Rendered more hooks than during the previous render."
const upsert = useCallback(
obj => {
setData(data => {
// if something changes in a top level prop, the top level ref has to be changed so it will cause a rerender
const newData = { ...data }
Object.keys(obj).forEach(key => {
const newProp = Array.isArray(obj[key]) ? [] : {}
merge(newProp, data[key], obj[key])
newData[key] = newProp
})
deriveReducedPointList(newData, local)
return newData // spread because we need to return a new reference
})
},
[setData]
)
return <DeliberationContext.Provider value={{ data, upsert }}>{props.children}</DeliberationContext.Provider>
}

/*

reducedPointList:[
{point: pointDoc, group: [pointDoc, pointDoc, ...]},
...
]

The order of the list is not relevant. If the contents compared the same, but the order was different, the old list would be used
*/

// do two arrays have equal contents
function aEqual(a = [], b = []) {
return a.length === b.length && a.every((e, i) => e === b[i])
}
// reducedPointTable: { _id: {point, group}}

// export to test by jest -- this shouldn't be called directly
export function deriveReducedPointList(data, local) {
const { pointById, groupIdsLists } = data
if (!pointById || !groupIdsLists) return data
if (local.pointById === pointById && local.groupIdsList === groupIdsLists) return data // nothing to update
const reducedPointTable = Object.entries(pointById).reduce(
(reducedPointTable, [id, point]) => ((reducedPointTable[id] = { point }), reducedPointTable),
{}
)
let updated = false
for (const [firstId, ...groupIds] of groupIdsLists) {
reducedPointTable[firstId].group = groupIds.map(id => reducedPointTable[id].point)
groupIds.forEach(id => delete reducedPointTable[id])
}
// if there are any pointWithGroup elements in the new table, that have equal contents with those in the old reducedPointList
// then copy them over so they are unchanged
for (const pointWithGroup of data.reducedPointList) {
const ptid = pointWithGroup.point._id
if (
reducedPointTable[ptid]?.point === pointWithGroup.point &&
aEqual(reducedPointTable[ptid]?.group, pointWithGroup.group)
)
reducedPointTable[ptid] = pointWithGroup // if contentss are unchanged - unchange the ref
else updated = true
}
const newReducedPointList = Object.values(reducedPointTable)
local.pointById = pointById
local.groupIdsList = groupIdsLists
if (!(newReducedPointList.length === data.reducedPointList.length && !updated))
data.reducedPointList = newReducedPointList
return data
}
13 changes: 6 additions & 7 deletions app/components/dem-info.jsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
'use strict'
import React from 'react'
import cx from 'classnames'
import insertSheet from 'react-jss'
import { createUseStyles } from 'react-jss'

function DemInfo(props) {
const { state, dob, party, classes, className, ...otherProps } = props
export default function DemInfo(props) {
const { state, dob, party, className, ...otherProps } = props
const classes = useStylesFromThemeFunction()
if (!(state && dob && party)) return null // if no data, render not

const userState = state || ''
Expand Down Expand Up @@ -49,7 +50,7 @@ function calculateAge(birthdayStr) {
return age
}

const demInfoStyles = {
const useStylesFromThemeFunction = createUseStyles(theme => ({
infoText: {
fontFamily: 'Inter',
fontSize: '1rem',
Expand All @@ -59,6 +60,4 @@ const demInfoStyles = {
textAlign: 'left',
color: '#5D5D5C',
},
}

export default insertSheet(demInfoStyles)(DemInfo)
}))
10 changes: 7 additions & 3 deletions app/components/ranking.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,13 @@ export default function Ranking(props) {
let [response, setResponse] = useState(responseOptions.includes(defaultValue) ? defaultValue : '')
useEffect(() => {
if (defaultValue) {
if (!responseOptions.includes(defaultValue)) onDone && onDone({ valid: false, value: '' })
} else {
setResponse(undefined)
if (!responseOptions.includes(defaultValue)) {
setResponse(undefined)
onDone && onDone({ valid: false, value: '' })
} else {
setResponse(defaultValue)
onDone && onDone({ valid: true, value: defaultValue })
}
}
}, [defaultValue])

Expand Down
73 changes: 0 additions & 73 deletions app/components/review-point-list.jsx

This file was deleted.

Loading