Skip to content

Commit d524129

Browse files
authored
Merge pull request #231 from EnCiv/deliberation-context
Deliberation context and refactor review-point-list #215
2 parents 2a46fde + 2c8f7e8 commit d524129

15 files changed

+1188
-363
lines changed

.babelrc

+3-8
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,5 @@
11
{
2-
"presets": [
3-
"@babel/preset-react",
4-
"@babel/preset-env"
5-
],
6-
"plugins": [
7-
"@babel/plugin-proposal-class-properties",
8-
"@babel/plugin-transform-regenerator"
9-
]
2+
"presets": ["@babel/preset-react", "@babel/preset-env"],
3+
"plugins": ["@babel/plugin-proposal-class-properties", "@babel/plugin-transform-regenerator"],
4+
"sourceMap": "inline"
105
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
// https://github.com/EnCiv/civil-pursuit/issues/215
2+
import { deriveReducedPointList } from '../deliberation-context'
3+
4+
const parentId = '1001'
5+
const data = { reducedPointList: [] }
6+
const local = {}
7+
describe('test deriveReducedPointList', () => {
8+
test('initially no points in list', () => {
9+
const { reducedPointList } = deriveReducedPointList(data, local)
10+
expect(reducedPointList).toMatchObject([])
11+
})
12+
test('called with no changes should give the same ref', () => {
13+
const oldReducedPointList = data.reducedPointList
14+
const { reducedPointList } = deriveReducedPointList(data, local)
15+
expect(reducedPointList).toBe(oldReducedPointList)
16+
})
17+
test('called with emtpy data should give the same ref', () => {
18+
data.pointById = {}
19+
data.groupIdsLists = []
20+
const oldReducedPointList = data.reducedPointList
21+
const { reducedPointList } = deriveReducedPointList(data, local)
22+
expect(reducedPointList).toBe(oldReducedPointList)
23+
})
24+
test('pointIds but not groupIdsList generates a list', () => {
25+
const points = [
26+
{ _id: '1', subject: '1', description: 'describe 1', parentId },
27+
{ _id: '2', subject: '2', description: 'd2', parentId },
28+
{ _id: '3', subject: '3', description: 'd3', parentId },
29+
{ _id: '4', subject: '4', description: 'd4', parentId },
30+
{ _id: '5', subject: '5', description: 'd5', parentId },
31+
{ _id: '6', subject: '6', description: 'd6', parentId },
32+
]
33+
data.pointById = points.reduce((pointById, point) => ((pointById[point._id] = point), pointById), {})
34+
const { reducedPointList } = deriveReducedPointList(data, local)
35+
expect(reducedPointList).toMatchObject(
36+
points.map(point => ({
37+
point,
38+
}))
39+
)
40+
})
41+
test('if a point is updated, a new pointById ref is returned, but unchanged points have unchanged refs', () => {
42+
const oldReducedPointList = deriveReducedPointList(data, local).reducedPointList
43+
const oldPointRefs = Object.values(data.pointById)
44+
data.pointById['1'] = { _id: '1', subject: '1', description: 'updated d1', parentId }
45+
data.pointById = { ...data.pointById } // needs a new ref because it changed
46+
const { reducedPointList } = deriveReducedPointList(data, local)
47+
expect(reducedPointList).not.toBe(oldReducedPointList)
48+
expect(reducedPointList[0].point).not.toBe(oldPointRefs[0])
49+
for (let i = 1; i <= 5; i++) {
50+
expect(reducedPointList[i].point).toBe(oldPointRefs[i])
51+
}
52+
})
53+
test('grouping points', () => {
54+
const oldReducedPointList = deriveReducedPointList(data, local).reducedPointList
55+
data.groupIdsLists = [
56+
['1', '2'],
57+
['3', '4', '5'],
58+
]
59+
const { reducedPointList } = deriveReducedPointList(data, local)
60+
expect(reducedPointList).not.toBe(oldReducedPointList)
61+
expect(reducedPointList).toEqual([
62+
{
63+
point: { _id: '1', subject: '1', description: 'updated d1', parentId },
64+
group: [{ _id: '2', subject: '2', description: 'd2', parentId }],
65+
},
66+
{
67+
point: { _id: '3', subject: '3', description: 'd3', parentId },
68+
group: [
69+
{ _id: '4', subject: '4', description: 'd4', parentId },
70+
{ _id: '5', subject: '5', description: 'd5', parentId },
71+
],
72+
},
73+
{ point: { _id: '6', subject: '6', description: 'd6', parentId } },
74+
])
75+
})
76+
test('grouping can change', () => {
77+
const oldReducedPointList = deriveReducedPointList(data, local).reducedPointList
78+
const oldLastPoint = oldReducedPointList[2]
79+
data.groupIdsLists = [
80+
['1', '2', '4'],
81+
['3', '5'],
82+
]
83+
const { reducedPointList } = deriveReducedPointList(data, local)
84+
expect(reducedPointList).not.toBe(oldReducedPointList)
85+
expect(reducedPointList).toEqual([
86+
{
87+
point: { _id: '1', subject: '1', description: 'updated d1', parentId },
88+
group: [
89+
{ _id: '2', subject: '2', description: 'd2', parentId },
90+
{ _id: '4', subject: '4', description: 'd4', parentId },
91+
],
92+
},
93+
{
94+
point: { _id: '3', subject: '3', description: 'd3', parentId },
95+
group: [{ _id: '5', subject: '5', description: 'd5', parentId }],
96+
},
97+
{ point: { _id: '6', subject: '6', description: 'd6', parentId } },
98+
])
99+
expect(oldLastPoint).toBe(reducedPointList[2])
100+
})
101+
test('a point in a group is updated', () => {
102+
const oldReducedPointList = deriveReducedPointList(data, local).reducedPointList
103+
const oldLastPoint = oldReducedPointList[2]
104+
data.pointById['2'] = { _id: '2', subject: '2 child of 1', description: 'updated d2 in group', parentId }
105+
data.pointById = { ...data.pointById } // needs a new ref
106+
const { reducedPointList } = deriveReducedPointList(data, local)
107+
expect(reducedPointList).not.toBe(oldReducedPointList)
108+
expect(reducedPointList).toEqual([
109+
{
110+
point: { _id: '1', subject: '1', description: 'updated d1', parentId },
111+
group: [
112+
{ _id: '2', subject: '2 child of 1', description: 'updated d2 in group', parentId },
113+
{ _id: '4', subject: '4', description: 'd4', parentId },
114+
],
115+
},
116+
{
117+
point: { _id: '3', subject: '3', description: 'd3', parentId },
118+
group: [{ _id: '5', subject: '5', description: 'd5', parentId }],
119+
},
120+
{ point: { _id: '6', subject: '6', description: 'd6', parentId } },
121+
])
122+
expect(oldLastPoint).toBe(reducedPointList[2])
123+
})
124+
test('can be ungrouped', () => {
125+
const oldReducedPointList = deriveReducedPointList(data, local).reducedPointList
126+
const oldFirstPoint = oldReducedPointList[0]
127+
data.groupIdsLists = [['1', '2', '4']]
128+
const { reducedPointList } = deriveReducedPointList(data, local)
129+
expect(reducedPointList).not.toBe(oldReducedPointList)
130+
expect(reducedPointList).toEqual([
131+
{
132+
point: { _id: '1', subject: '1', description: 'updated d1', parentId },
133+
group: [
134+
{ _id: '2', subject: '2 child of 1', description: 'updated d2 in group', parentId },
135+
{ _id: '4', subject: '4', description: 'd4', parentId },
136+
],
137+
},
138+
{
139+
point: { _id: '3', subject: '3', description: 'd3', parentId },
140+
},
141+
{ point: { _id: '5', subject: '5', description: 'd5', parentId } },
142+
{ point: { _id: '6', subject: '6', description: 'd6', parentId } },
143+
])
144+
expect(oldFirstPoint).toBe(reducedPointList[0])
145+
})
146+
})
+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import React, { createContext, useCallback, useState, useRef } from 'react'
2+
import { merge } from 'lodash'
3+
export const DeliberationContext = createContext({})
4+
export default DeliberationContext
5+
6+
export function DeliberationContextProvider(props) {
7+
const [data, setData] = useState({ reducedPointList: [] })
8+
const local = useRef({}).current // can't be in deriver becasue "Error: Rendered more hooks than during the previous render."
9+
const upsert = useCallback(
10+
obj => {
11+
setData(data => {
12+
// if something changes in a top level prop, the top level ref has to be changed so it will cause a rerender
13+
const newData = { ...data }
14+
Object.keys(obj).forEach(key => {
15+
const newProp = Array.isArray(obj[key]) ? [] : {}
16+
merge(newProp, data[key], obj[key])
17+
newData[key] = newProp
18+
})
19+
deriveReducedPointList(newData, local)
20+
return newData // spread because we need to return a new reference
21+
})
22+
},
23+
[setData]
24+
)
25+
return <DeliberationContext.Provider value={{ data, upsert }}>{props.children}</DeliberationContext.Provider>
26+
}
27+
28+
/*
29+
30+
reducedPointList:[
31+
{point: pointDoc, group: [pointDoc, pointDoc, ...]},
32+
...
33+
]
34+
35+
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
36+
*/
37+
38+
// do two arrays have equal contents
39+
function aEqual(a = [], b = []) {
40+
return a.length === b.length && a.every((e, i) => e === b[i])
41+
}
42+
// reducedPointTable: { _id: {point, group}}
43+
44+
// export to test by jest -- this shouldn't be called directly
45+
export function deriveReducedPointList(data, local) {
46+
const { pointById, groupIdsLists } = data
47+
if (!pointById || !groupIdsLists) return data
48+
if (local.pointById === pointById && local.groupIdsList === groupIdsLists) return data // nothing to update
49+
const reducedPointTable = Object.entries(pointById).reduce(
50+
(reducedPointTable, [id, point]) => ((reducedPointTable[id] = { point }), reducedPointTable),
51+
{}
52+
)
53+
let updated = false
54+
for (const [firstId, ...groupIds] of groupIdsLists) {
55+
reducedPointTable[firstId].group = groupIds.map(id => reducedPointTable[id].point)
56+
groupIds.forEach(id => delete reducedPointTable[id])
57+
}
58+
// if there are any pointWithGroup elements in the new table, that have equal contents with those in the old reducedPointList
59+
// then copy them over so they are unchanged
60+
for (const pointWithGroup of data.reducedPointList) {
61+
const ptid = pointWithGroup.point._id
62+
if (
63+
reducedPointTable[ptid]?.point === pointWithGroup.point &&
64+
aEqual(reducedPointTable[ptid]?.group, pointWithGroup.group)
65+
)
66+
reducedPointTable[ptid] = pointWithGroup // if contentss are unchanged - unchange the ref
67+
else updated = true
68+
}
69+
const newReducedPointList = Object.values(reducedPointTable)
70+
local.pointById = pointById
71+
local.groupIdsList = groupIdsLists
72+
if (!(newReducedPointList.length === data.reducedPointList.length && !updated))
73+
data.reducedPointList = newReducedPointList
74+
return data
75+
}

app/components/dem-info.jsx

+6-7
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
'use strict'
22
import React from 'react'
33
import cx from 'classnames'
4-
import insertSheet from 'react-jss'
4+
import { createUseStyles } from 'react-jss'
55

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

1011
const userState = state || ''
@@ -49,7 +50,7 @@ function calculateAge(birthdayStr) {
4950
return age
5051
}
5152

52-
const demInfoStyles = {
53+
const useStylesFromThemeFunction = createUseStyles(theme => ({
5354
infoText: {
5455
fontFamily: 'Inter',
5556
fontSize: '1rem',
@@ -59,6 +60,4 @@ const demInfoStyles = {
5960
textAlign: 'left',
6061
color: '#5D5D5C',
6162
},
62-
}
63-
64-
export default insertSheet(demInfoStyles)(DemInfo)
63+
}))

app/components/ranking.jsx

+7-3
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,13 @@ export default function Ranking(props) {
1919
let [response, setResponse] = useState(responseOptions.includes(defaultValue) ? defaultValue : '')
2020
useEffect(() => {
2121
if (defaultValue) {
22-
if (!responseOptions.includes(defaultValue)) onDone && onDone({ valid: false, value: '' })
23-
} else {
24-
setResponse(undefined)
22+
if (!responseOptions.includes(defaultValue)) {
23+
setResponse(undefined)
24+
onDone && onDone({ valid: false, value: '' })
25+
} else {
26+
setResponse(defaultValue)
27+
onDone && onDone({ valid: true, value: defaultValue })
28+
}
2529
}
2630
}, [defaultValue])
2731

app/components/review-point-list.jsx

-73
This file was deleted.

0 commit comments

Comments
 (0)