From 2c02a976025726a308748490263a8b08a0d85c58 Mon Sep 17 00:00:00 2001 From: David Fridley Date: Sat, 26 Oct 2024 18:39:42 -0700 Subject: [PATCH 01/23] moving files --- app/components/{compare-reasons.jsx => steps/compare-whys.js} | 2 +- app/components/tournament.js | 2 +- stories/compare-reasons.stories.jsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename app/components/{compare-reasons.jsx => steps/compare-whys.js} (98%) diff --git a/app/components/compare-reasons.jsx b/app/components/steps/compare-whys.js similarity index 98% rename from app/components/compare-reasons.jsx rename to app/components/steps/compare-whys.js index b0d4cf0f0..a9f7f9332 100644 --- a/app/components/compare-reasons.jsx +++ b/app/components/steps/compare-whys.js @@ -3,7 +3,7 @@ 'use strict' import React, { useEffect, useState } from 'react' import { createUseStyles } from 'react-jss' -import PairCompare from './pair-compare' +import PairCompare from '../pair-compare' import { H, Level } from 'react-accessible-headings' function CompareReasons(props) { diff --git a/app/components/tournament.js b/app/components/tournament.js index 09a7e13ab..a09ef9816 100644 --- a/app/components/tournament.js +++ b/app/components/tournament.js @@ -12,7 +12,7 @@ import GroupingStep from './grouping-step' import RankStep from './rank-step' import ReviewPointList from './steps/rerank' import WhyStep from './why-step' -import CompareReasons from './compare-reasons' +import CompareReasons from './steps/compare-whys' import Intermission from './intermission' const WebComponents = { diff --git a/stories/compare-reasons.stories.jsx b/stories/compare-reasons.stories.jsx index 4127f8782..4ebaee407 100644 --- a/stories/compare-reasons.stories.jsx +++ b/stories/compare-reasons.stories.jsx @@ -1,4 +1,4 @@ -import CompareReasons from '../app/components/compare-reasons' +import CompareReasons from '../app/components/steps/compare-whys' import { onDoneDecorator, onDoneResult } from './common' import { within, userEvent } from '@storybook/test' import expect from 'expect' From 805a89897b58c851e3d64d20ba42a79bf7665da5 Mon Sep 17 00:00:00 2001 From: David Fridley Date: Sat, 26 Oct 2024 18:51:18 -0700 Subject: [PATCH 02/23] refactoring props --- app/components/steps/compare-whys.js | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/app/components/steps/compare-whys.js b/app/components/steps/compare-whys.js index a9f7f9332..76d2c4bb7 100644 --- a/app/components/steps/compare-whys.js +++ b/app/components/steps/compare-whys.js @@ -6,8 +6,16 @@ import { createUseStyles } from 'react-jss' import PairCompare from '../pair-compare' import { H, Level } from 'react-accessible-headings' +// [{point: {}, whys:[{}], ranks:[{}]}] function CompareReasons(props) { - const { pointList = [], side = '', onDone = () => {}, className, ...otherProps } = props + const { + pointWithWhyRankListList = [], + pointList = [], + side = '', + onDone = () => {}, + className, + ...otherProps + } = props const classes = useStyles() const [completedPoints, setCompletedPoints] = useState(new Set()) const [percentDone, setPercentDone] = useState(0) @@ -41,14 +49,15 @@ function CompareReasons(props) { return (
- {pointList.map((headlinePoint, idx) => ( + {pointWithWhyRankListList.map(({ point, whys, ranks }, idx) => (
Please choose the most convincing explanation for... - {headlinePoint.subject} + {point.subject} handlePairCompare(value, idx)} /> From a82bfc4d5c8302e0fd7f63020df637b5ba8c9be2 Mon Sep 17 00:00:00 2001 From: David Fridley Date: Sun, 3 Nov 2024 12:03:27 -0800 Subject: [PATCH 03/23] refactoring props and story --- app/components/pair-compare.jsx | 65 ++++++--- app/components/steps/compare-whys.js | 8 +- stories/compare-reasons.stories.jsx | 207 ++++++++++++++++++++++++++- 3 files changed, 258 insertions(+), 22 deletions(-) diff --git a/app/components/pair-compare.jsx b/app/components/pair-compare.jsx index 9b648c9ea..cadf8a997 100644 --- a/app/components/pair-compare.jsx +++ b/app/components/pair-compare.jsx @@ -1,13 +1,26 @@ // https://github.com/EnCiv/civil-pursuit/issues/53 +// https://github.com/EnCiv/civil-pursuit/issues/200 'use strict' import React, { useEffect, useRef, useState } from 'react' import Point from './point' import { createUseStyles } from 'react-jss' import { SecondaryButton } from './button.jsx' +import ObjectId from 'bson-objectid' function PairCompare(props) { - const { pointList = [], onDone = () => {}, mainPoint = { subject: '', description: '' }, ...otherProps } = props + const { + whyRankList = [], + onDone = () => {}, + mainPoint = { subject: '', description: '' }, + discussionId, + round, + ...otherProps + } = props + + // if all points are ranked, show the (first) one ranked most important, go to start over state + // otherwise compare the list + // // idxLeft and idxRight can swap places at any point - they are simply pointers to the current two elements const [idxLeft, setIdxLeft] = useState(0) @@ -28,7 +41,7 @@ function PairCompare(props) { useEffect(() => { if (isSelectionComplete()) { - setSelectedPoint(pointList[idxLeft] ? pointList[idxLeft] : pointList[idxRight]) + setSelectedPoint(whyRankList[idxLeft] ? whyRankList[idxLeft] : whyRankList[idxRight]) } }, [pointsIdxCounter]) @@ -45,9 +58,27 @@ function PairCompare(props) { } }, [selectedPoint]) + function rankItNeutral() { + // this one gets a neutral vote + if (whyRankList[pointsIdxCounter].rank) { + const value = { ...whyRankList[pointsIdxCounter].rank, category: 'neutral' } + setTimeout(() => onDone({ valid: false, value })) + } else { + const value = { + _id: ObjectId(), + category: 'neutral', + parentId: whyRankList[pointsIdxCounter].why._id, + stage: 'why', + discussionId, + round, + } + } + } + const handleLeftPointClick = () => { + rankItNeutral() // prevent transitions from firing on last comparison - if (idxRight >= pointList.length - 1 || idxLeft >= pointList.length - 1) { + if (idxRight >= whyRankList.length - 1 || idxLeft >= whyRankList.length - 1) { if (idxLeft >= idxRight) { setIdxRight(idxLeft + 1) } else { @@ -89,7 +120,7 @@ function PairCompare(props) { const handleRightPointClick = () => { // prevent transitions from firing on last comparison - if (idxRight >= pointList.length - 1 || idxLeft >= pointList.length - 1) { + if (idxRight >= whyRankList.length - 1 || idxLeft >= whyRankList.length - 1) { if (idxLeft >= idxRight) { setIdxLeft(idxLeft + 1) } else { @@ -152,17 +183,17 @@ function PairCompare(props) { } const isSelectionComplete = () => { - return pointsIdxCounter >= pointList.length + return pointsIdxCounter >= whyRankList.length } const nextIndex = idxLeft > idxRight ? idxLeft + 1 : idxRight + 1 const hiddenEmptyLeftPoint = const hiddenTransitioningLeftPoint = ( - + ) const hiddenEmptyRightPoint = const hiddenTransitioningRightPoint = ( - + ) return ( @@ -173,17 +204,17 @@ function PairCompare(props) {
{`${ - pointsIdxCounter <= pointList.length ? pointsIdxCounter : pointList.length - } out of ${pointList.length}`} + pointsIdxCounter <= whyRankList.length ? pointsIdxCounter : whyRankList.length + } out of ${whyRankList.length}`}
- {pointsIdxCounter < pointList.length && ( + {pointsIdxCounter < whyRankList.length && (
{isRightTransitioning ? hiddenTransitioningLeftPoint : hiddenEmptyLeftPoint}
)} - {pointsIdxCounter < pointList.length && ( + {pointsIdxCounter < whyRankList.length && (
{isLeftTransitioning ? hiddenTransitioningRightPoint : hiddenEmptyRightPoint}
@@ -191,26 +222,26 @@ function PairCompare(props) {
- {idxLeft < pointList.length && ( + {idxLeft < whyRankList.length && ( )} - {idxRight < pointList.length && ( + {idxRight < whyRankList.length && ( )}
diff --git a/app/components/steps/compare-whys.js b/app/components/steps/compare-whys.js index 76d2c4bb7..938620a92 100644 --- a/app/components/steps/compare-whys.js +++ b/app/components/steps/compare-whys.js @@ -1,4 +1,5 @@ // https://github.com/EnCiv/civil-pursuit/issues/77 +// https://github.com/EnCiv/civil-pursuit/issues/200 'use strict' import React, { useEffect, useState } from 'react' @@ -6,7 +7,7 @@ import { createUseStyles } from 'react-jss' import PairCompare from '../pair-compare' import { H, Level } from 'react-accessible-headings' -// [{point: {}, whys:[{}], ranks:[{}]}] +// pointWithWhyRankListList = [{point: {}, whyRankList: [why:{}, rank:{}]] function CompareReasons(props) { const { pointWithWhyRankListList = [], @@ -49,15 +50,14 @@ function CompareReasons(props) { return (
- {pointWithWhyRankListList.map(({ point, whys, ranks }, idx) => ( + {pointWithWhyRankListList.map(({ point, whyRankList }, idx) => (
Please choose the most convincing explanation for... {point.subject} handlePairCompare(value, idx)} /> diff --git a/stories/compare-reasons.stories.jsx b/stories/compare-reasons.stories.jsx index 4ebaee407..888a37904 100644 --- a/stories/compare-reasons.stories.jsx +++ b/stories/compare-reasons.stories.jsx @@ -1,3 +1,5 @@ +// https://github.com/EnCiv/civil-pursuit/issues/200 + import CompareReasons from '../app/components/steps/compare-whys' import { onDoneDecorator, onDoneResult } from './common' import { within, userEvent } from '@storybook/test' @@ -9,6 +11,208 @@ export default { decorators: [onDoneDecorator], } +const pointWithWhyRankListList = [ + { + point: { _id: '1', subject: 'subject 1', description: 'describe 1' }, + whyRankList: [ + { + why: { + _id: '2', + subject: '1 is less than 2', + description: '2 is why because', + parentId: '1', + category: 'most', + }, + }, + { + why: { + _id: '3', + subject: '1 is less than 3', + description: '3 is why because', + parentId: '1', + category: 'most', + }, + }, + { + why: { + _id: '4', + subject: '1 is less than 4', + description: '4 is why because', + parentId: '1', + category: 'most', + }, + }, + { + why: { + _id: '5', + subject: '1 is less than 5', + description: '5 is why because', + parentId: '1', + category: 'most', + }, + }, + { + why: { + _id: '6', + subject: '1 is less than 6', + description: '6 is why because', + parentId: '1', + category: 'most', + }, + }, + ], + }, + { + point: { _id: '21', subject: 'subject 20', description: 'describe 20' }, + whyRankList: [ + { + why: { + _id: '22', + subject: '21 is less than 2', + description: '2 is why because', + parentId: '21', + category: 'most', + }, + }, + { + why: { + _id: '23', + subject: '21 is less than 3', + description: '3 is why because', + parentId: '21', + category: 'most', + }, + }, + { + why: { + _id: '24', + subject: '21 is less than 4', + description: '4 is why because', + parentId: '21', + category: 'most', + }, + }, + { + why: { + _id: '25', + subject: '21 is less than 5', + description: '5 is why because', + parentId: '21', + category: 'most', + }, + }, + { + why: { + _id: '26', + subject: '21 is less than 6', + description: '6 is why because', + parentId: '21', + category: 'most', + }, + }, + ], + }, + { + point: { _id: '31', subject: 'subject 30', description: 'describe 30' }, + whyRankList: [ + { + why: { + _id: '32', + subject: '21 is less than 2', + description: '2 is why because', + parentId: '31', + category: 'most', + }, + }, + { + why: { + _id: '33', + subject: '21 is less than 3', + description: '3 is why because', + parentId: '31', + category: 'most', + }, + }, + { + why: { + _id: '34', + subject: '21 is less than 4', + description: '4 is why because', + parentId: '31', + category: 'most', + }, + }, + { + why: { + _id: '35', + subject: '21 is less than 5', + description: '5 is why because', + parentId: '31', + category: 'most', + }, + }, + { + why: { + _id: '36', + subject: '21 is less than 6', + description: '6 is why because', + parentId: '31', + category: 'most', + }, + }, + ], + }, + { + point: { _id: '41', subject: 'subject 40', description: 'describe 40' }, + whyRankList: [ + { + why: { + _id: '42', + subject: '21 is less than 2', + description: '2 is why because', + parentId: '41', + category: 'least', + }, + }, + { + why: { + _id: '43', + subject: '21 is less than 3', + description: '3 is why because', + parentId: '41', + category: 'least', + }, + }, + { + why: { + _id: '44', + subject: '21 is less than 4', + description: '4 is why because', + parentId: '41', + category: 'least', + }, + }, + { + why: { + _id: '45', + subject: '21 is less than 5', + description: '5 is why because', + parentId: '41', + category: 'least', + }, + }, + { + why: { + _id: '46', + subject: '21 is less than 6', + description: '6 is why because', + parentId: '41', + category: 'least', + }, + }, + ], + }, +] const pointOne = { subject: 'Point 1', description: 'This is the first point' } const pointTwo = { subject: 'Point 2', description: 'This is the second point' } const pointThree = { subject: 'Point 3', description: 'This is the third point' } @@ -33,6 +237,7 @@ const pointList = [ { subject: 'Headline Issue #1', description: 'Description for Headline Issue #1', + pointWithWhyRankListList, reasonPoints: { most: [pointOne, pointTwo, pointThree, pointFour, pointFive], least: [pointSix, pointSeven, pointEight, pointNine, pointTen], @@ -58,7 +263,7 @@ const pointList = [ export const threePointLists = { args: { - pointList, + pointWithWhyRankListList, side: 'most', }, } From b56584db2407cc6a7f112ce75e43b5849612e177 Mon Sep 17 00:00:00 2001 From: David Fridley Date: Sun, 3 Nov 2024 14:04:59 -0800 Subject: [PATCH 04/23] width 240 --- stories/compare-reasons.stories.jsx | 259 ++++------------------------ 1 file changed, 29 insertions(+), 230 deletions(-) diff --git a/stories/compare-reasons.stories.jsx b/stories/compare-reasons.stories.jsx index 888a37904..f92ba2e49 100644 --- a/stories/compare-reasons.stories.jsx +++ b/stories/compare-reasons.stories.jsx @@ -5,211 +5,47 @@ import { onDoneDecorator, onDoneResult } from './common' import { within, userEvent } from '@storybook/test' import expect from 'expect' -export default { - component: CompareReasons, - args: {}, - decorators: [onDoneDecorator], -} +export default { component: CompareReasons, args: {}, decorators: [onDoneDecorator] } const pointWithWhyRankListList = [ { point: { _id: '1', subject: 'subject 1', description: 'describe 1' }, whyRankList: [ - { - why: { - _id: '2', - subject: '1 is less than 2', - description: '2 is why because', - parentId: '1', - category: 'most', - }, - }, - { - why: { - _id: '3', - subject: '1 is less than 3', - description: '3 is why because', - parentId: '1', - category: 'most', - }, - }, - { - why: { - _id: '4', - subject: '1 is less than 4', - description: '4 is why because', - parentId: '1', - category: 'most', - }, - }, - { - why: { - _id: '5', - subject: '1 is less than 5', - description: '5 is why because', - parentId: '1', - category: 'most', - }, - }, - { - why: { - _id: '6', - subject: '1 is less than 6', - description: '6 is why because', - parentId: '1', - category: 'most', - }, - }, + { why: { _id: '2', subject: '1 is less than 2', description: '2 is why because', parentId: '1', category: 'most' } }, + { why: { _id: '3', subject: '1 is less than 3', description: '3 is why because', parentId: '1', category: 'most' } }, + { why: { _id: '4', subject: '1 is less than 4', description: '4 is why because', parentId: '1', category: 'most' } }, + { why: { _id: '5', subject: '1 is less than 5', description: '5 is why because', parentId: '1', category: 'most' } }, + { why: { _id: '6', subject: '1 is less than 6', description: '6 is why because', parentId: '1', category: 'most' } }, ], }, { point: { _id: '21', subject: 'subject 20', description: 'describe 20' }, whyRankList: [ - { - why: { - _id: '22', - subject: '21 is less than 2', - description: '2 is why because', - parentId: '21', - category: 'most', - }, - }, - { - why: { - _id: '23', - subject: '21 is less than 3', - description: '3 is why because', - parentId: '21', - category: 'most', - }, - }, - { - why: { - _id: '24', - subject: '21 is less than 4', - description: '4 is why because', - parentId: '21', - category: 'most', - }, - }, - { - why: { - _id: '25', - subject: '21 is less than 5', - description: '5 is why because', - parentId: '21', - category: 'most', - }, - }, - { - why: { - _id: '26', - subject: '21 is less than 6', - description: '6 is why because', - parentId: '21', - category: 'most', - }, - }, + { why: { _id: '22', subject: '21 is less than 2', description: '2 is why because', parentId: '21', category: 'most' } }, + { why: { _id: '23', subject: '21 is less than 3', description: '3 is why because', parentId: '21', category: 'most' } }, + { why: { _id: '24', subject: '21 is less than 4', description: '4 is why because', parentId: '21', category: 'most' } }, + { why: { _id: '25', subject: '21 is less than 5', description: '5 is why because', parentId: '21', category: 'most' } }, + { why: { _id: '26', subject: '21 is less than 6', description: '6 is why because', parentId: '21', category: 'most' } }, ], }, { point: { _id: '31', subject: 'subject 30', description: 'describe 30' }, whyRankList: [ - { - why: { - _id: '32', - subject: '21 is less than 2', - description: '2 is why because', - parentId: '31', - category: 'most', - }, - }, - { - why: { - _id: '33', - subject: '21 is less than 3', - description: '3 is why because', - parentId: '31', - category: 'most', - }, - }, - { - why: { - _id: '34', - subject: '21 is less than 4', - description: '4 is why because', - parentId: '31', - category: 'most', - }, - }, - { - why: { - _id: '35', - subject: '21 is less than 5', - description: '5 is why because', - parentId: '31', - category: 'most', - }, - }, - { - why: { - _id: '36', - subject: '21 is less than 6', - description: '6 is why because', - parentId: '31', - category: 'most', - }, - }, + { why: { _id: '32', subject: '21 is less than 2', description: '2 is why because', parentId: '31', category: 'most' } }, + { why: { _id: '33', subject: '21 is less than 3', description: '3 is why because', parentId: '31', category: 'most' } }, + { why: { _id: '34', subject: '21 is less than 4', description: '4 is why because', parentId: '31', category: 'most' } }, + { why: { _id: '35', subject: '21 is less than 5', description: '5 is why because', parentId: '31', category: 'most' } }, + { why: { _id: '36', subject: '21 is less than 6', description: '6 is why because', parentId: '31', category: 'most' } }, ], }, { point: { _id: '41', subject: 'subject 40', description: 'describe 40' }, whyRankList: [ - { - why: { - _id: '42', - subject: '21 is less than 2', - description: '2 is why because', - parentId: '41', - category: 'least', - }, - }, - { - why: { - _id: '43', - subject: '21 is less than 3', - description: '3 is why because', - parentId: '41', - category: 'least', - }, - }, - { - why: { - _id: '44', - subject: '21 is less than 4', - description: '4 is why because', - parentId: '41', - category: 'least', - }, - }, - { - why: { - _id: '45', - subject: '21 is less than 5', - description: '5 is why because', - parentId: '41', - category: 'least', - }, - }, - { - why: { - _id: '46', - subject: '21 is less than 6', - description: '6 is why because', - parentId: '41', - category: 'least', - }, - }, + { why: { _id: '42', subject: '21 is less than 2', description: '2 is why because', parentId: '41', category: 'least' } }, + { why: { _id: '43', subject: '21 is less than 3', description: '3 is why because', parentId: '41', category: 'least' } }, + { why: { _id: '44', subject: '21 is less than 4', description: '4 is why because', parentId: '41', category: 'least' } }, + { why: { _id: '45', subject: '21 is less than 5', description: '5 is why because', parentId: '41', category: 'least' } }, + { why: { _id: '46', subject: '21 is less than 6', description: '6 is why because', parentId: '41', category: 'least' } }, ], }, ] @@ -238,51 +74,20 @@ const pointList = [ subject: 'Headline Issue #1', description: 'Description for Headline Issue #1', pointWithWhyRankListList, - reasonPoints: { - most: [pointOne, pointTwo, pointThree, pointFour, pointFive], - least: [pointSix, pointSeven, pointEight, pointNine, pointTen], - }, - }, - { - subject: 'Headline Issue #2', - description: 'Description for Headline Issue #2', - reasonPoints: { - most: [pointEleven, pointTwelve], - least: [pointThirteen, pointFourteen], - }, - }, - { - subject: 'Headline Issue #3', - description: 'Description for Headline Issue #3', - reasonPoints: { - most: [pointFifteen, pointSixteen], - least: [pointSeventeen, pointEighteen], - }, + reasonPoints: { most: [pointOne, pointTwo, pointThree, pointFour, pointFive], least: [pointSix, pointSeven, pointEight, pointNine, pointTen] }, }, + { subject: 'Headline Issue #2', description: 'Description for Headline Issue #2', reasonPoints: { most: [pointEleven, pointTwelve], least: [pointThirteen, pointFourteen] } }, + { subject: 'Headline Issue #3', description: 'Description for Headline Issue #3', reasonPoints: { most: [pointFifteen, pointSixteen], least: [pointSeventeen, pointEighteen] } }, ] -export const threePointLists = { - args: { - pointWithWhyRankListList, - side: 'most', - }, -} +export const threePointLists = { args: { pointWithWhyRankListList, side: 'most' } } -export const emptyPointList = { - args: { - pointList: [], - }, -} +export const emptyPointList = { args: { pointList: [] } } -export const emptyArgs = { - args: {}, -} +export const emptyArgs = { args: {} } export const twoPointListsPlayThrough = { - args: { - pointList: pointList.slice(1, 3), - side: 'least', - }, + args: { pointList: pointList.slice(1, 3), side: 'least' }, play: async ({ canvasElement }) => { const canvas = within(canvasElement) const pointThirteen = canvas.getByText('Point 13') @@ -290,12 +95,6 @@ export const twoPointListsPlayThrough = { await userEvent.click(pointThirteen) await userEvent.click(pointSeventeen) - expect(onDoneResult(canvas)).toMatchObject({ - count: 5, - onDoneResult: { - valid: true, - value: 100, - }, - }) + expect(onDoneResult(canvas)).toMatchObject({ count: 5, onDoneResult: { valid: true, value: 100 } }) }, } From e36a3a7a419e3ccf060ee87d593436fdb956401e Mon Sep 17 00:00:00 2001 From: David Fridley Date: Tue, 5 Nov 2024 10:27:08 -0800 Subject: [PATCH 05/23] whyRankList and jump to done if all ranked --- app/components/pair-compare.jsx | 162 +++++++++++++------------------ stories/pair-compare.stories.jsx | 41 ++++++-- 2 files changed, 103 insertions(+), 100 deletions(-) diff --git a/app/components/pair-compare.jsx b/app/components/pair-compare.jsx index cadf8a997..0bf4b1271 100644 --- a/app/components/pair-compare.jsx +++ b/app/components/pair-compare.jsx @@ -9,74 +9,81 @@ import { SecondaryButton } from './button.jsx' import ObjectId from 'bson-objectid' function PairCompare(props) { - const { - whyRankList = [], - onDone = () => {}, - mainPoint = { subject: '', description: '' }, - discussionId, - round, - ...otherProps - } = props - - // if all points are ranked, show the (first) one ranked most important, go to start over state - // otherwise compare the list - // + const { whyRankList = [], onDone = () => {}, mainPoint = { subject: '', description: '' }, discussionId, round, ...otherProps } = props // idxLeft and idxRight can swap places at any point - they are simply pointers to the current two elements const [idxLeft, setIdxLeft] = useState(0) const [idxRight, setIdxRight] = useState(1) - const isInitialRender = useRef(true) - const visibleRightPointRef = useRef(null) const visibleLeftPointRef = useRef(null) const hiddenLeftPointRef = useRef(null) const hiddenRightPointRef = useRef(null) const [pointsIdxCounter, setPointsIdxCounter] = useState(1) - const [selectedPoint, setSelectedPoint] = useState(null) const [isRightTransitioning, setIsRightTransitioning] = useState(false) const [isLeftTransitioning, setIsLeftTransitioning] = useState(false) const classes = useStyles() - - useEffect(() => { - if (isSelectionComplete()) { - setSelectedPoint(whyRankList[idxLeft] ? whyRankList[idxLeft] : whyRankList[idxRight]) - } - }, [pointsIdxCounter]) - + const [ranksByParentId, setRanksByParentId] = useState(whyRankList.reduce((ranksByParentId, whyRank) => (whyRank.rank && (ranksByParentId[whyRank.rank.parentId] = whyRank.rank), ranksByParentId), {})) + // if all points are ranked, show the (first) one ranked most important, go to start over state + // otherwise compare the list useEffect(() => { - if (isInitialRender.current) { - isInitialRender.current = false - return - } - - if (selectedPoint) { - onDone({ valid: true, value: selectedPoint }) - } else { - onDone({ valid: false, value: null }) + let selectedIdx + whyRankList.forEach((whyRank, i) => { + if (whyRank.rank) { + ranksByParentId[whyRank.rank.parentId] = whyRank.rank + if (whyRank.rank.category === 'most') selectedIdx = i + } + }) + if (Object.keys(ranksByParentId).length === whyRankList.length) { + if (pointsIdxCounter !== whyRankList.length) { + let selectedRank = null + // skip if an update from above after the user has completed ranking - likely thisis the initial render + setPointsIdxCounter(whyRankList.length) + if (typeof selectedIdx === 'number') { + setIdxLeft(selectedIdx) // idx could be 0 + selectedRank = whyRankList[selectedIdx].rank + } else setIdxLeft(whyRankList.length + 1) + setIdxRight(whyRankList.length) + setTimeout(() => onDone({ valid: true, value: selectedRank })) + } } - }, [selectedPoint]) + }, [whyRankList]) - function rankItNeutral() { + function rankIdxCategory(idx, category) { // this one gets a neutral vote - if (whyRankList[pointsIdxCounter].rank) { - const value = { ...whyRankList[pointsIdxCounter].rank, category: 'neutral' } - setTimeout(() => onDone({ valid: false, value })) - } else { - const value = { - _id: ObjectId(), - category: 'neutral', - parentId: whyRankList[pointsIdxCounter].why._id, - stage: 'why', - discussionId, - round, + const value = ranksByParentId[whyRankList[idx]._id] + ? { ...ranksByParentId[whyRankList[idx]._id], category } + : { + _id: ObjectId().toString(), + category, + parentId: whyRankList[idx].why._id, + stage: 'why', + discussionId, + round, + } + ranksByParentId[value.parentId] = value + setTimeout(() => onDone({ valid: category === 'most', value })) + } + + function incrementPointsIdxCounter(increment) { + setPointsIdxCounter(pointsIdxCounter => { + pointsIdxCounter += increment + if (pointsIdxCounter >= whyRankList.length + 1) { + // neither and no other choices + setTimeout(() => onDone({ valid: true, value: null })) //done but no winner + return pointsIdxCounter + } else if (pointsIdxCounter >= whyRankList.length) { + // selectionComplete ? + const selectedIdx = whyRankList[idxLeft] ? idxLeft : idxRight + rankIdxCategory(selectedIdx, 'most') } - } + return pointsIdxCounter + }) } const handleLeftPointClick = () => { - rankItNeutral() + rankIdxCategory(idxRight, 'neutral') // prevent transitions from firing on last comparison if (idxRight >= whyRankList.length - 1 || idxLeft >= whyRankList.length - 1) { if (idxLeft >= idxRight) { @@ -84,7 +91,7 @@ function PairCompare(props) { } else { setIdxRight(idxRight + 1) } - setPointsIdxCounter(pointsIdxCounter + 1) + incrementPointsIdxCounter(1) return } @@ -110,8 +117,7 @@ function PairCompare(props) { } else { setIdxRight(idxRight + 1) } - - setPointsIdxCounter(pointsIdxCounter + 1) + incrementPointsIdxCounter(1) Object.assign(visiblePointRight.style, { position: '', transition: 'none', transform: '' }) Object.assign(hiddenPointRight.style, { position: '', transition: 'none', transform: '' }) @@ -119,6 +125,7 @@ function PairCompare(props) { } const handleRightPointClick = () => { + rankIdxCategory(idxLeft, 'neutral') // prevent transitions from firing on last comparison if (idxRight >= whyRankList.length - 1 || idxLeft >= whyRankList.length - 1) { if (idxLeft >= idxRight) { @@ -126,7 +133,7 @@ function PairCompare(props) { } else { setIdxLeft(idxRight + 1) } - setPointsIdxCounter(pointsIdxCounter + 1) + incrementPointsIdxCounter(1) return } @@ -153,7 +160,7 @@ function PairCompare(props) { setIdxLeft(idxRight + 1) } - setPointsIdxCounter(pointsIdxCounter + 1) + incrementPointsIdxCounter(1) Object.assign(visiblePointLeft.style, { position: '', transition: 'none', transform: '' }) Object.assign(hiddenPointLeft.style, { position: '', transition: 'none', transform: '' }) @@ -161,7 +168,8 @@ function PairCompare(props) { } const handleNeitherButton = () => { - if (selectedPoint) return + rankIdxCategory(idxLeft, 'neutral') + rankIdxCategory(idxRight, 'neutral') if (idxLeft >= idxRight) { setIdxRight(idxLeft + 1) @@ -171,7 +179,7 @@ function PairCompare(props) { setIdxRight(idxRight + 2) } - setPointsIdxCounter(pointsIdxCounter + 2) + incrementPointsIdxCounter(2) } const handleStartOverButton = () => { @@ -188,13 +196,9 @@ function PairCompare(props) { const nextIndex = idxLeft > idxRight ? idxLeft + 1 : idxRight + 1 const hiddenEmptyLeftPoint = - const hiddenTransitioningLeftPoint = ( - - ) + const hiddenTransitioningLeftPoint = const hiddenEmptyRightPoint = - const hiddenTransitioningRightPoint = ( - - ) + const hiddenTransitioningRightPoint = return (
@@ -203,55 +207,27 @@ function PairCompare(props) {
{mainPoint.description}
- {`${ - pointsIdxCounter <= whyRankList.length ? pointsIdxCounter : whyRankList.length - } out of ${whyRankList.length}`} + {`${pointsIdxCounter <= whyRankList.length ? pointsIdxCounter : whyRankList.length} out of ${whyRankList.length}`}
- {pointsIdxCounter < whyRankList.length && ( -
- {isRightTransitioning ? hiddenTransitioningLeftPoint : hiddenEmptyLeftPoint} -
- )} - {pointsIdxCounter < whyRankList.length && ( -
- {isLeftTransitioning ? hiddenTransitioningRightPoint : hiddenEmptyRightPoint} -
- )} + {pointsIdxCounter < whyRankList.length &&
{isRightTransitioning ? hiddenTransitioningLeftPoint : hiddenEmptyLeftPoint}
} + {pointsIdxCounter < whyRankList.length &&
{isLeftTransitioning ? hiddenTransitioningRightPoint : hiddenEmptyRightPoint}
}
{idxLeft < whyRankList.length && ( - )} {idxRight < whyRankList.length && ( - )}
-
- {!isSelectionComplete() ? ( - Neither - ) : ( - Start Over - )} -
+
{!isSelectionComplete() ? Neither : Start Over}
) diff --git a/stories/pair-compare.stories.jsx b/stories/pair-compare.stories.jsx index 9ed83d063..a2fbcbf32 100644 --- a/stories/pair-compare.stories.jsx +++ b/stories/pair-compare.stories.jsx @@ -10,20 +10,47 @@ export default { decorators: [onDoneDecorator], } -const pointOne = { subject: 'Point 1', description: 'This is the first point' } -const pointTwo = { subject: 'Point 2', description: 'This is the second point' } -const pointThree = { subject: 'Point 3', description: 'This is the third point' } -const pointFour = { subject: 'Point 4', description: 'This is the fourth point' } -const pointFive = { subject: 'Point 5', description: 'This is the fifth point' } -const pointSix = { subject: 'Point 6', description: 'This is the sixth point' } +const pointOne = { _id: '1', subject: 'Point 1', description: 'This is the first point' } +const pointTwo = { _id: '2', subject: 'Point 2', description: 'This is the second point' } +const pointThree = { _id: '3', subject: 'Point 3', description: 'This is the third point' } +const pointFour = { _id: '4', subject: 'Point 4', description: 'This is the fourth point' } +const pointFive = { _id: '5', subject: 'Point 5', description: 'This is the fifth point' } +const pointSix = { _id: '6', subject: 'Point 6', description: 'This is the sixth point' } +const whyRankList = [ + { why: { _id: '1', subject: 'Point 1', description: 'This is the first point' } }, + { why: { _id: '2', subject: 'Point 2', description: 'This is the second point' } }, + { why: { _id: '3', subject: 'Point 3', description: 'This is the third point' } }, + { why: { _id: '4', subject: 'Point 4', description: 'This is the fourth point' } }, + { why: { _id: '5', subject: 'Point 5', description: 'This is the fifth point' } }, + { why: { _id: '6', subject: 'Point 6', description: 'This is the sixth point' } }, +] + +const rankedWhyRankList = [ + { why: { _id: '1', subject: 'Point 1', description: 'This is the first point' }, rank: { _id: '11', parentId: '1', stage: 'why', category: 'most' } }, + { why: { _id: '2', subject: 'Point 2', description: 'This is the second point' }, rank: { _id: '12', parentId: '2', stage: 'why', category: 'neutral' } }, + { why: { _id: '3', subject: 'Point 3', description: 'This is the third point' }, rank: { _id: '13', parentId: '3', stage: 'why', category: 'neutral' } }, + { why: { _id: '4', subject: 'Point 4', description: 'This is the fourth point' }, rank: { _id: '14', parentId: '4', stage: 'why', category: 'neutral' } }, + { why: { _id: '5', subject: 'Point 5', description: 'This is the fifth point' }, rank: { _id: '15', parentId: '5', stage: 'why', category: 'neutral' } }, + { why: { _id: '6', subject: 'Point 6', description: 'This is the sixth point' }, rank: { _id: '16', parentId: '6', stage: 'why', category: 'neutral' } }, +] export const sixPoints = { args: { mainPoint: { subject: 'Global Warming', description: 'Climate change and global warming', }, - pointList: [pointOne, pointTwo, pointThree, pointFour, pointFive, pointSix], + whyRankList, + }, +} + +export const sixPointsRanked = { + args: { + mainPoint: { + subject: 'Global Warming', + description: 'Climate change and global warming', + }, + whyRankList: rankedWhyRankList, }, } From f3f4ac5e41780d6791654f46c3a71744abe01e79 Mon Sep 17 00:00:00 2001 From: David Fridley Date: Tue, 5 Nov 2024 20:28:38 -0800 Subject: [PATCH 06/23] transitioning by state, yes / no buttons --- app/components/pair-compare.jsx | 125 +++++++++++++++++-------------- stories/pair-compare.stories.jsx | 65 +++++++++------- 2 files changed, 105 insertions(+), 85 deletions(-) diff --git a/app/components/pair-compare.jsx b/app/components/pair-compare.jsx index 0bf4b1271..c617a235c 100644 --- a/app/components/pair-compare.jsx +++ b/app/components/pair-compare.jsx @@ -2,11 +2,12 @@ // https://github.com/EnCiv/civil-pursuit/issues/200 'use strict' -import React, { useEffect, useRef, useState } from 'react' +import React, { useEffect, useState } from 'react' import Point from './point' import { createUseStyles } from 'react-jss' import { SecondaryButton } from './button.jsx' import ObjectId from 'bson-objectid' +import cx from 'classnames' function PairCompare(props) { const { whyRankList = [], onDone = () => {}, mainPoint = { subject: '', description: '' }, discussionId, round, ...otherProps } = props @@ -14,12 +15,8 @@ function PairCompare(props) { // idxLeft and idxRight can swap places at any point - they are simply pointers to the current two elements const [idxLeft, setIdxLeft] = useState(0) const [idxRight, setIdxRight] = useState(1) - - const visibleRightPointRef = useRef(null) - const visibleLeftPointRef = useRef(null) - const hiddenLeftPointRef = useRef(null) - const hiddenRightPointRef = useRef(null) - + const [nextLeftPoint, setNextLeftPoint] = useState(null) + const [nextRightPoint, setNextRightPoint] = useState(null) const [pointsIdxCounter, setPointsIdxCounter] = useState(1) const [isRightTransitioning, setIsRightTransitioning] = useState(false) const [isLeftTransitioning, setIsLeftTransitioning] = useState(false) @@ -35,7 +32,12 @@ function PairCompare(props) { if (whyRank.rank.category === 'most') selectedIdx = i } }) - if (Object.keys(ranksByParentId).length === whyRankList.length) { + if (whyRankList.length == 1) { + if (whyRankList[0].rank) { + setPointsIdxCounter(2) + setTimeout(() => onDone({ valid: true, value: whyRankList[0].rank })) + } + } else if (Object.keys(ranksByParentId).length === whyRankList.length) { if (pointsIdxCounter !== whyRankList.length) { let selectedRank = null // skip if an update from above after the user has completed ranking - likely thisis the initial render @@ -50,8 +52,9 @@ function PairCompare(props) { } }, [whyRankList]) + // send up the rank, and track it locally function rankIdxCategory(idx, category) { - // this one gets a neutral vote + if (idx >= whyRankList.length) return // if only one to rank, this could be out of bounds const value = ranksByParentId[whyRankList[idx]._id] ? { ...ranksByParentId[whyRankList[idx]._id], category } : { @@ -63,7 +66,9 @@ function PairCompare(props) { round, } ranksByParentId[value.parentId] = value - setTimeout(() => onDone({ valid: category === 'most', value })) + const ranks = Object.values(ranksByParentId) + const valid = ranks.length === whyRankList.length && ranks.every(rank => rank.category) + setTimeout(() => onDone({ valid, value })) } function incrementPointsIdxCounter(increment) { @@ -96,19 +101,9 @@ function PairCompare(props) { } setIsLeftTransitioning(true) - const visiblePointRight = visibleRightPointRef.current - const hiddenPointRight = hiddenRightPointRef.current - - Object.assign(visiblePointRight.style, { - position: 'relative', - transform: 'translateX(200%)', - transition: 'transform 0.5s linear', - }) - Object.assign(hiddenPointRight.style, { - position: 'absolute', - transform: 'translateY(8.25rem)', - transition: 'transform 0.5s linear', - }) + if (pointsIdxCounter + 1 < whyRankList.length) { + setNextRightPoint(whyRankList[pointsIdxCounter + 1].why) + } setTimeout(() => { setIsLeftTransitioning(false) @@ -118,9 +113,7 @@ function PairCompare(props) { setIdxRight(idxRight + 1) } incrementPointsIdxCounter(1) - - Object.assign(visiblePointRight.style, { position: '', transition: 'none', transform: '' }) - Object.assign(hiddenPointRight.style, { position: '', transition: 'none', transform: '' }) + setNextRightPoint(null) }, 500) } @@ -137,33 +130,21 @@ function PairCompare(props) { return } + if (pointsIdxCounter + 1 < whyRankList.length) { + setNextLeftPoint(whyRankList[pointsIdxCounter + 1].why) + } setIsRightTransitioning(true) - const visiblePointLeft = visibleLeftPointRef.current - const hiddenPointLeft = hiddenLeftPointRef.current - - Object.assign(visiblePointLeft.style, { - position: 'relative', - transform: 'translateX(-200%)', - transition: 'transform 0.5s linear', - }) - Object.assign(hiddenPointLeft.style, { - position: 'absolute', - transform: 'translateY(8.25rem)', - transition: 'transform 0.5s linear', - }) setTimeout(() => { setIsRightTransitioning(false) + setNextLeftPoint(null) + if (nextLeftPoint) setNextLeftPoint(null) if (idxLeft >= idxRight) { setIdxLeft(idxLeft + 1) } else { setIdxLeft(idxRight + 1) } - incrementPointsIdxCounter(1) - - Object.assign(visiblePointLeft.style, { position: '', transition: 'none', transform: '' }) - Object.assign(hiddenPointLeft.style, { position: '', transition: 'none', transform: '' }) }, 500) } @@ -178,28 +159,30 @@ function PairCompare(props) { setIdxLeft(idxRight + 1) setIdxRight(idxRight + 2) } - incrementPointsIdxCounter(2) } const handleStartOverButton = () => { + Object.values(ranksByParentId).forEach(rank => (rank.category = undefined)) // reset the categories so we can start again onDone({ valid: false, value: null }) setIdxRight(1) setIdxLeft(0) setPointsIdxCounter(1) - setSelectedPoint(null) + } + + const handleYes = () => { + rankIdxCategory(Math.min(idxLeft, idxRight), 'most') + setPointsIdxCounter(Math.max(idxLeft, idxRight) + 1) + } + const handleNo = () => { + rankIdxCategory(Math.min(idxLeft, idxRight), 'neutral') + setPointsIdxCounter(Math.max(idxLeft, idxRight) + 1) } const isSelectionComplete = () => { return pointsIdxCounter >= whyRankList.length } - const nextIndex = idxLeft > idxRight ? idxLeft + 1 : idxRight + 1 - const hiddenEmptyLeftPoint = - const hiddenTransitioningLeftPoint = - const hiddenEmptyRightPoint = - const hiddenTransitioningRightPoint = - return (
@@ -211,23 +194,35 @@ function PairCompare(props) {
- {pointsIdxCounter < whyRankList.length &&
{isRightTransitioning ? hiddenTransitioningLeftPoint : hiddenEmptyLeftPoint}
} - {pointsIdxCounter < whyRankList.length &&
{isLeftTransitioning ? hiddenTransitioningRightPoint : hiddenEmptyRightPoint}
} +
= whyRankList.length - 1 && classes.hidden)}> + +
+
= whyRankList.length - 1 && classes.hidden)}> + +
{idxLeft < whyRankList.length && ( - )} {idxRight < whyRankList.length && ( - )}
-
{!isSelectionComplete() ? Neither : Start Over}
+ {(idxLeft < whyRankList.length && idxRight < whyRankList.length) || ranksByParentId[whyRankList[Math.min(idxLeft, idxRight)]?.why._id]?.category ? ( +
{!isSelectionComplete() ? Neither : Start Over}
+ ) : ( +
+ Yes +
+ No +
+ )}
) @@ -268,6 +263,9 @@ const useStyles = createUseStyles(theme => ({ marginBottom: '1rem', clipPath: 'xywh(0 0 100% 500%)', }, + hidden: { + display: 'none', + }, hiddenPoint: { width: '30%', }, @@ -301,6 +299,21 @@ const useStyles = createUseStyles(theme => ({ justifyContent: 'center', margin: '2rem auto', }, + transitioningDown: { + position: 'absolute', + transform: 'translateY(8.25rem)', + transition: 'transform 0.5s linear', + }, + transitioningLeft: { + position: 'relative', + transform: 'translateX(-200%)', + transition: 'transform 0.5s linear', + }, + transitioningRight: { + position: 'relative', + transform: 'translateX(200%)', + transition: 'transform 0.5s linear', + }, })) const sharedStatusBadgeStyle = () => ({ diff --git a/stories/pair-compare.stories.jsx b/stories/pair-compare.stories.jsx index a2fbcbf32..ef887eb94 100644 --- a/stories/pair-compare.stories.jsx +++ b/stories/pair-compare.stories.jsx @@ -1,6 +1,6 @@ import PairCompare from '../app/components/pair-compare' -import { onDoneDecorator, onDoneResult } from './common' -import { within, userEvent } from '@storybook/test' +import { onDoneDecorator, onDoneResult, asyncSleep } from './common' +import { within, userEvent, waitFor } from '@storybook/test' import expect from 'expect' import React from 'react' @@ -10,13 +10,6 @@ export default { decorators: [onDoneDecorator], } -const pointOne = { _id: '1', subject: 'Point 1', description: 'This is the first point' } -const pointTwo = { _id: '2', subject: 'Point 2', description: 'This is the second point' } -const pointThree = { _id: '3', subject: 'Point 3', description: 'This is the third point' } -const pointFour = { _id: '4', subject: 'Point 4', description: 'This is the fourth point' } -const pointFive = { _id: '5', subject: 'Point 5', description: 'This is the fifth point' } -const pointSix = { _id: '6', subject: 'Point 6', description: 'This is the sixth point' } - const whyRankList = [ { why: { _id: '1', subject: 'Point 1', description: 'This is the first point' } }, { why: { _id: '2', subject: 'Point 2', description: 'This is the second point' } }, @@ -70,55 +63,69 @@ export const onePoint = { subject: 'Global Warming', description: 'Climate change and global warming', }, - pointList: [pointOne], + whyRankList: [whyRankList[0]], }, } +export const onePointRanked = { + args: { + mainPoint: { + subject: 'Global Warming', + description: 'Climate change and global warming', + }, + whyRankList: [rankedWhyRankList[0]], + }, +} export const twoPoints = { args: { mainPoint: { subject: 'Global Warming', description: 'Climate change and global warming', }, - pointList: [pointOne, pointTwo], + whyRankList: [whyRankList[0], whyRankList[1]], }, } +export const threePoints = { + args: { + mainPoint: { + subject: 'Global Warming', + description: 'Climate change and global warming', + }, + whyRankList: [whyRankList[0], whyRankList[1], whyRankList[2]], + }, +} export const onDoneTest = { args: { mainPoint: { subject: 'Global Warming', description: 'Climate change and global warming', }, - pointList: [pointOne, pointTwo, pointThree, pointFour], + whyRankList: [whyRankList[0], whyRankList[1], whyRankList[2], whyRankList[3]], }, play: async ({ canvasElement }) => { const canvas = within(canvasElement) const Point1 = canvas.getByText('Point 1') await userEvent.click(Point1) - - setTimeout(() => { - // wait for transition to occur - const Point3 = canvas.getByText('Point 3') - userEvent.click(Point3) - }, 500) - - setTimeout(() => { - const Point4 = canvas.getByText('Point 4') - userEvent.click(Point4) - }, 1300) - - setTimeout(() => { + await asyncSleep(500) + const Point3 = canvas.getByText('Point 3') + await userEvent.click(Point3) + await asyncSleep(500) + const Point4 = canvas.getByText('Point 4') + await userEvent.click(Point4) + waitFor(() => { expect(onDoneResult(canvas)).toMatchObject({ - count: 1, + count: 3, onDoneResult: { valid: true, value: { - description: 'This is the fourth point', - subject: 'Point 4', + // _id will be auto generated + category: 'most', + parentId: '4', + stage: 'why', }, }, }) - }, 1500) + }) }, } From 001fa01d8cfa3b556a5590150e1e4a82423080fe Mon Sep 17 00:00:00 2001 From: David Fridley Date: Wed, 6 Nov 2024 12:30:27 -0800 Subject: [PATCH 07/23] Yes/No case, and tests, factored out pointIdxCounter state var --- app/components/pair-compare.jsx | 134 +++++++++---------------- stories/common.js | 3 +- stories/pair-compare.stories.jsx | 161 ++++++++++++++++++++++++++++++- 3 files changed, 202 insertions(+), 96 deletions(-) diff --git a/app/components/pair-compare.jsx b/app/components/pair-compare.jsx index c617a235c..311eae2a0 100644 --- a/app/components/pair-compare.jsx +++ b/app/components/pair-compare.jsx @@ -17,11 +17,10 @@ function PairCompare(props) { const [idxRight, setIdxRight] = useState(1) const [nextLeftPoint, setNextLeftPoint] = useState(null) const [nextRightPoint, setNextRightPoint] = useState(null) - const [pointsIdxCounter, setPointsIdxCounter] = useState(1) const [isRightTransitioning, setIsRightTransitioning] = useState(false) const [isLeftTransitioning, setIsLeftTransitioning] = useState(false) const classes = useStyles() - const [ranksByParentId, setRanksByParentId] = useState(whyRankList.reduce((ranksByParentId, whyRank) => (whyRank.rank && (ranksByParentId[whyRank.rank.parentId] = whyRank.rank), ranksByParentId), {})) + const [ranksByParentId, neverSetRanksByParentId] = useState(whyRankList.reduce((ranksByParentId, whyRank) => (whyRank.rank && (ranksByParentId[whyRank.rank.parentId] = whyRank.rank), ranksByParentId), {})) // if all points are ranked, show the (first) one ranked most important, go to start over state // otherwise compare the list useEffect(() => { @@ -32,23 +31,14 @@ function PairCompare(props) { if (whyRank.rank.category === 'most') selectedIdx = i } }) - if (whyRankList.length == 1) { - if (whyRankList[0].rank) { - setPointsIdxCounter(2) - setTimeout(() => onDone({ valid: true, value: whyRankList[0].rank })) - } - } else if (Object.keys(ranksByParentId).length === whyRankList.length) { - if (pointsIdxCounter !== whyRankList.length) { - let selectedRank = null - // skip if an update from above after the user has completed ranking - likely thisis the initial render - setPointsIdxCounter(whyRankList.length) - if (typeof selectedIdx === 'number') { - setIdxLeft(selectedIdx) // idx could be 0 - selectedRank = whyRankList[selectedIdx].rank - } else setIdxLeft(whyRankList.length + 1) - setIdxRight(whyRankList.length) - setTimeout(() => onDone({ valid: true, value: selectedRank })) - } + const ranks = Object.values(ranksByParentId) + if (ranks.length === whyRankList.length && ranks.length > 0 && ranks.every(rank => rank.category)) { + // skip if an update from above after the user has completed ranking - likely this is the initial render + if (typeof selectedIdx === 'number') { + setIdxLeft(selectedIdx) // idx could be 0 + } else setIdxLeft(whyRankList.length) + setIdxRight(whyRankList.length) + setTimeout(() => onDone({ valid: true, value: undefined })) } }, [whyRankList]) @@ -71,80 +61,41 @@ function PairCompare(props) { setTimeout(() => onDone({ valid, value })) } - function incrementPointsIdxCounter(increment) { - setPointsIdxCounter(pointsIdxCounter => { - pointsIdxCounter += increment - if (pointsIdxCounter >= whyRankList.length + 1) { - // neither and no other choices - setTimeout(() => onDone({ valid: true, value: null })) //done but no winner - return pointsIdxCounter - } else if (pointsIdxCounter >= whyRankList.length) { - // selectionComplete ? - const selectedIdx = whyRankList[idxLeft] ? idxLeft : idxRight - rankIdxCategory(selectedIdx, 'most') - } - return pointsIdxCounter - }) - } - const handleLeftPointClick = () => { rankIdxCategory(idxRight, 'neutral') - // prevent transitions from firing on last comparison - if (idxRight >= whyRankList.length - 1 || idxLeft >= whyRankList.length - 1) { - if (idxLeft >= idxRight) { - setIdxRight(idxLeft + 1) - } else { - setIdxRight(idxRight + 1) - } - incrementPointsIdxCounter(1) + if (Math.max(idxLeft, idxRight) + 1 >= whyRankList.length) { + // they've all been ranked + rankIdxCategory(idxLeft, 'most') + setIdxRight(whyRankList.length) return } - + const nextRightIdx = Math.min(idxLeft >= idxRight ? idxLeft + 1 : idxRight + 1, whyRankList.length) setIsLeftTransitioning(true) - if (pointsIdxCounter + 1 < whyRankList.length) { - setNextRightPoint(whyRankList[pointsIdxCounter + 1].why) - } + setNextRightPoint(whyRankList[nextRightIdx].why) setTimeout(() => { setIsLeftTransitioning(false) - if (idxLeft >= idxRight) { - setIdxRight(idxLeft + 1) - } else { - setIdxRight(idxRight + 1) - } - incrementPointsIdxCounter(1) setNextRightPoint(null) + setIdxRight(nextRightIdx) }, 500) } const handleRightPointClick = () => { rankIdxCategory(idxLeft, 'neutral') - // prevent transitions from firing on last comparison - if (idxRight >= whyRankList.length - 1 || idxLeft >= whyRankList.length - 1) { - if (idxLeft >= idxRight) { - setIdxLeft(idxLeft + 1) - } else { - setIdxLeft(idxRight + 1) - } - incrementPointsIdxCounter(1) + if (Math.max(idxLeft, idxRight) + 1 >= whyRankList.length) { + // they've all been ranked + rankIdxCategory(idxRight, 'most') + setIdxLeft(whyRankList.length) return } - - if (pointsIdxCounter + 1 < whyRankList.length) { - setNextLeftPoint(whyRankList[pointsIdxCounter + 1].why) - } + const nextLeftIdx = Math.min(idxLeft >= idxRight ? idxLeft + 1 : idxRight + 1, whyRankList.length) + setNextLeftPoint(whyRankList[nextLeftIdx].why) setIsRightTransitioning(true) setTimeout(() => { setIsRightTransitioning(false) setNextLeftPoint(null) - if (nextLeftPoint) setNextLeftPoint(null) - if (idxLeft >= idxRight) { - setIdxLeft(idxLeft + 1) - } else { - setIdxLeft(idxRight + 1) - } - incrementPointsIdxCounter(1) + setIdxLeft(nextLeftIdx) }, 500) } @@ -153,13 +104,12 @@ function PairCompare(props) { rankIdxCategory(idxRight, 'neutral') if (idxLeft >= idxRight) { - setIdxRight(idxLeft + 1) - setIdxLeft(idxLeft + 2) + setIdxRight(Math.min(idxLeft + 1, whyRankList.length)) + setIdxLeft(Math.min(idxLeft + 2, whyRankList.length)) } else { - setIdxLeft(idxRight + 1) - setIdxRight(idxRight + 2) + setIdxLeft(Math.min(idxRight + 1, whyRankList.length)) + setIdxRight(Math.min(idxRight + 2, whyRankList.length)) } - incrementPointsIdxCounter(2) } const handleStartOverButton = () => { @@ -167,22 +117,27 @@ function PairCompare(props) { onDone({ valid: false, value: null }) setIdxRight(1) setIdxLeft(0) - setPointsIdxCounter(1) } const handleYes = () => { - rankIdxCategory(Math.min(idxLeft, idxRight), 'most') - setPointsIdxCounter(Math.max(idxLeft, idxRight) + 1) + if (idxLeft < whyRankList.length) { + rankIdxCategory(idxLeft, 'most') + } else if (idxRight < whyRankList.length) { + rankIdxCategory(idxRight, 'most') + } } const handleNo = () => { - rankIdxCategory(Math.min(idxLeft, idxRight), 'neutral') - setPointsIdxCounter(Math.max(idxLeft, idxRight) + 1) - } - - const isSelectionComplete = () => { - return pointsIdxCounter >= whyRankList.length + if (idxLeft < whyRankList.length) { + rankIdxCategory(idxLeft, 'neutral') + } else if (idxRight < whyRankList.length) { + rankIdxCategory(idxRight, 'neutral') + } } + const pointsIdxCounter = Math.max(idxLeft, idxRight) + const isSelectionComplete = pointsIdxCounter >= whyRankList.length + const ranks = isSelectionComplete && Object.values(ranksByParentId) // isSelectionComple here so we don't do work if not needed + const allRanked = isSelectionComplete && ranks.length === whyRankList.length && ranks.every(rank => rank.category) // isSelectionComple here so we don't do work if not needed return (
@@ -190,7 +145,7 @@ function PairCompare(props) {
{mainPoint.description}
- {`${pointsIdxCounter <= whyRankList.length ? pointsIdxCounter : whyRankList.length} out of ${whyRankList.length}`} + {`${pointsIdxCounter <= whyRankList.length ? pointsIdxCounter : whyRankList.length} out of ${whyRankList.length}`}
@@ -201,7 +156,6 @@ function PairCompare(props) {
-
{idxLeft < whyRankList.length && ( )}
- {(idxLeft < whyRankList.length && idxRight < whyRankList.length) || ranksByParentId[whyRankList[Math.min(idxLeft, idxRight)]?.why._id]?.category ? ( -
{!isSelectionComplete() ? Neither : Start Over}
+ {!isSelectionComplete || allRanked ? ( +
{!isSelectionComplete ? Neither : Start Over}
) : (
Yes diff --git a/stories/common.js b/stories/common.js index 359cfb3d3..f1332eff8 100644 --- a/stories/common.js +++ b/stories/common.js @@ -116,7 +116,8 @@ export function RenderStory(props) { export function onDoneDecorator(Story, context) { const [result, setResult] = useState({ count: 0 }) const onDone = useCallback(res => { - setResult({ count: result.count + 1, onDoneResult: res }) + // two succesive calls to onDone from the same user event will not increment the count twice, unless we use the set function approach + setResult(result => ({ count: result.count + 1, onDoneResult: res })) }) context.args.onDone = onDone return ( diff --git a/stories/pair-compare.stories.jsx b/stories/pair-compare.stories.jsx index ef887eb94..e86ca4d2e 100644 --- a/stories/pair-compare.stories.jsx +++ b/stories/pair-compare.stories.jsx @@ -57,7 +57,7 @@ export const empty = { pointList: [], } -export const onePoint = { +export const onePointCanBeYesStartOverNo = { args: { mainPoint: { subject: 'Global Warming', @@ -65,9 +65,55 @@ export const onePoint = { }, whyRankList: [whyRankList[0]], }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement) + const Yes = canvas.getByText('Yes') + await userEvent.click(Yes) + await waitFor(() => { + expect(onDoneResult(canvas)).toMatchObject({ + count: 1, + onDoneResult: { + valid: true, + value: { + // _id will be auto generated + category: 'most', + parentId: '1', + stage: 'why', + }, + }, + }) + }) + const StartOver = canvas.getByText('Start Over') + await userEvent.click(StartOver) + await waitFor(() => { + expect(onDoneResult(canvas)).toMatchObject({ + count: 2, + onDoneResult: { + valid: false, + value: null, + }, + }) + }) + const No = canvas.getByText('No') + await userEvent.click(No) + await waitFor(() => { + expect(onDoneResult(canvas)).toMatchObject({ + count: 3, + onDoneResult: { + valid: true, + value: { + // _id will be auto generated + category: 'neutral', + parentId: '1', + stage: 'why', + }, + }, + }) + }) + }, } -export const onePointRanked = { +export const onePointRankedGetsOnDone = { args: { mainPoint: { subject: 'Global Warming', @@ -75,6 +121,17 @@ export const onePointRanked = { }, whyRankList: [rankedWhyRankList[0]], }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement) + await waitFor(() => { + expect(onDoneResult(canvas)).toMatchObject({ + count: 1, + onDoneResult: { + valid: true, + }, + }) + }) + }, } export const twoPoints = { args: { @@ -86,7 +143,7 @@ export const twoPoints = { }, } -export const threePoints = { +export const UserChoosesNoPoint = { args: { mainPoint: { subject: 'Global Warming', @@ -94,7 +151,58 @@ export const threePoints = { }, whyRankList: [whyRankList[0], whyRankList[1], whyRankList[2]], }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement) + const Neither = canvas.getByText('Neither') + // don't await users event so not to miss the onDone calls from the same event + userEvent.click(Neither) + await waitFor(() => { + expect(onDoneResult(canvas)).toMatchObject({ + count: 1, + onDoneResult: { + valid: false, + value: { + // _id will be auto generated + category: 'neutral', + parentId: '1', + stage: 'why', + }, + }, + }) + }) + await waitFor(() => { + expect(onDoneResult(canvas)).toMatchObject({ + count: 2, + onDoneResult: { + valid: false, + value: { + // _id will be auto generated + category: 'neutral', + parentId: '2', + stage: 'why', + }, + }, + }) + }) + const No = canvas.getByText('No') + await userEvent.click(No) + await waitFor(() => { + expect(onDoneResult(canvas)).toMatchObject({ + count: 3, + onDoneResult: { + valid: true, + value: { + // _id will be auto generated + category: 'neutral', + parentId: '3', + stage: 'why', + }, + }, + }) + }) + }, } + export const onDoneTest = { args: { mainPoint: { @@ -107,15 +215,58 @@ export const onDoneTest = { const canvas = within(canvasElement) const Point1 = canvas.getByText('Point 1') await userEvent.click(Point1) + await waitFor(() => { + expect(onDoneResult(canvas)).toMatchObject({ + count: 1, + onDoneResult: { + valid: false, + value: { + // _id will be auto generated + category: 'neutral', + parentId: '2', + stage: 'why', + }, + }, + }) + }) await asyncSleep(500) const Point3 = canvas.getByText('Point 3') await userEvent.click(Point3) + await waitFor(() => { + expect(onDoneResult(canvas)).toMatchObject({ + count: 2, + onDoneResult: { + valid: false, + value: { + // _id will be auto generated + category: 'neutral', + parentId: '1', + stage: 'why', + }, + }, + }) + }) await asyncSleep(500) const Point4 = canvas.getByText('Point 4') - await userEvent.click(Point4) - waitFor(() => { + /* don't await - there are two onDone updates in succession and if we await the user event we miss the first one */ + userEvent.click(Point4) + await waitFor(() => { expect(onDoneResult(canvas)).toMatchObject({ count: 3, + onDoneResult: { + valid: false, + value: { + // _id will be auto generated + category: 'neutral', + parentId: '3', + stage: 'why', + }, + }, + }) + }) + await waitFor(() => { + expect(onDoneResult(canvas)).toMatchObject({ + count: 4, onDoneResult: { valid: true, value: { From ff95f776f8b83031fa496dcd15992217c778a4ea Mon Sep 17 00:00:00 2001 From: David Fridley Date: Wed, 6 Nov 2024 20:05:55 -0800 Subject: [PATCH 08/23] updated for pointWithWhyRankListList --- app/components/steps/compare-whys.js | 23 +++------- stories/compare-reasons.stories.jsx | 69 ++++++++++++---------------- 2 files changed, 35 insertions(+), 57 deletions(-) diff --git a/app/components/steps/compare-whys.js b/app/components/steps/compare-whys.js index 938620a92..f2dc4d7b0 100644 --- a/app/components/steps/compare-whys.js +++ b/app/components/steps/compare-whys.js @@ -9,20 +9,13 @@ import { H, Level } from 'react-accessible-headings' // pointWithWhyRankListList = [{point: {}, whyRankList: [why:{}, rank:{}]] function CompareReasons(props) { - const { - pointWithWhyRankListList = [], - pointList = [], - side = '', - onDone = () => {}, - className, - ...otherProps - } = props + const { pointWithWhyRankListList = [], side = '', onDone = () => {}, className, ...otherProps } = props const classes = useStyles() const [completedPoints, setCompletedPoints] = useState(new Set()) const [percentDone, setPercentDone] = useState(0) useEffect(() => { - if (completedPoints.size === pointList.length) { + if (completedPoints.size === pointWithWhyRankListList.length) { onDone({ valid: true, value: percentDone }) } else { onDone({ valid: false, value: percentDone }) @@ -30,11 +23,11 @@ function CompareReasons(props) { }, [completedPoints, percentDone]) useEffect(() => { - if (pointList.length === 0) setPercentDone(100) + if (pointWithWhyRankListList.length === 0) setPercentDone(100) else { - setPercentDone(Number(((completedPoints.size / pointList.length) * 100).toFixed(2))) + setPercentDone(Number(((completedPoints.size / pointWithWhyRankListList.length) * 100).toFixed(2))) } - }, [completedPoints, pointList]) + }, [completedPoints, pointWithWhyRankListList]) const handlePairCompare = ({ valid, value }, idx) => { setCompletedPoints(prevPoints => { @@ -55,11 +48,7 @@ function CompareReasons(props) { Please choose the most convincing explanation for... {point.subject} - handlePairCompare(value, idx)} - /> + handlePairCompare(value, idx)} />
))} diff --git a/stories/compare-reasons.stories.jsx b/stories/compare-reasons.stories.jsx index f92ba2e49..29a643eb5 100644 --- a/stories/compare-reasons.stories.jsx +++ b/stories/compare-reasons.stories.jsx @@ -1,8 +1,8 @@ // https://github.com/EnCiv/civil-pursuit/issues/200 import CompareReasons from '../app/components/steps/compare-whys' -import { onDoneDecorator, onDoneResult } from './common' -import { within, userEvent } from '@storybook/test' +import { asyncSleep, onDoneDecorator, onDoneResult } from './common' +import { within, userEvent, waitFor } from '@storybook/test' import expect from 'expect' export default { component: CompareReasons, args: {}, decorators: [onDoneDecorator] } @@ -49,52 +49,41 @@ const pointWithWhyRankListList = [ ], }, ] -const pointOne = { subject: 'Point 1', description: 'This is the first point' } -const pointTwo = { subject: 'Point 2', description: 'This is the second point' } -const pointThree = { subject: 'Point 3', description: 'This is the third point' } -const pointFour = { subject: 'Point 4', description: 'This is the fourth point' } -const pointFive = { subject: 'Point 5', description: 'This is the fifth point' } -const pointSix = { subject: 'Point 6', description: 'This is the sixth point' } -const pointSeven = { subject: 'Point 7', description: 'This is the seventh point' } -const pointEight = { subject: 'Point 8', description: 'This is the eighth point' } -const pointNine = { subject: 'Point 9', description: 'This is the ninth point' } -const pointTen = { subject: 'Point 10', description: 'This is the tenth point' } - -const pointEleven = { subject: 'Point 11', description: 'This is the eleventh point' } -const pointTwelve = { subject: 'Point 12', description: 'This is the twelfth point' } -const pointThirteen = { subject: 'Point 13', description: 'This is the thirteenth point' } -const pointFourteen = { subject: 'Point 14', description: 'This is the fourteenth point' } -const pointFifteen = { subject: 'Point 15', description: 'This is the fifteenth point' } -const pointSixteen = { subject: 'Point 16', description: 'This is the sixteenth point' } -const pointSeventeen = { subject: 'Point 17', description: 'This is the seventeenth point' } -const pointEighteen = { subject: 'Point 18', description: 'This is the eighteenth point' } - -const pointList = [ - { - subject: 'Headline Issue #1', - description: 'Description for Headline Issue #1', - pointWithWhyRankListList, - reasonPoints: { most: [pointOne, pointTwo, pointThree, pointFour, pointFive], least: [pointSix, pointSeven, pointEight, pointNine, pointTen] }, - }, - { subject: 'Headline Issue #2', description: 'Description for Headline Issue #2', reasonPoints: { most: [pointEleven, pointTwelve], least: [pointThirteen, pointFourteen] } }, - { subject: 'Headline Issue #3', description: 'Description for Headline Issue #3', reasonPoints: { most: [pointFifteen, pointSixteen], least: [pointSeventeen, pointEighteen] } }, -] export const threePointLists = { args: { pointWithWhyRankListList, side: 'most' } } -export const emptyPointList = { args: { pointList: [] } } +export const emptyPointList = { args: { pointWithWhyRankListList: [] } } export const emptyArgs = { args: {} } export const twoPointListsPlayThrough = { - args: { pointList: pointList.slice(1, 3), side: 'least' }, + args: { pointWithWhyRankListList: [pointWithWhyRankListList[0], pointWithWhyRankListList[1]], side: 'least' }, play: async ({ canvasElement }) => { const canvas = within(canvasElement) - const pointThirteen = canvas.getByText('Point 13') - const pointSeventeen = canvas.getByText('Point 17') - - await userEvent.click(pointThirteen) - await userEvent.click(pointSeventeen) - expect(onDoneResult(canvas)).toMatchObject({ count: 5, onDoneResult: { valid: true, value: 100 } }) + await waitFor(() => { + expect(onDoneResult(canvas)).toMatchObject({ count: 1, onDoneResult: { valid: false, value: 0 } }) + }) + const one = canvas.getByText('1 is less than 2') + await userEvent.click(one) + await asyncSleep(500) + await userEvent.click(one) + await asyncSleep(500) + await userEvent.click(one) + await asyncSleep(500) + await userEvent.click(one) + await waitFor(() => { + expect(onDoneResult(canvas)).toMatchObject({ count: 7, onDoneResult: { valid: false, value: 50 } }) + }) + const two = canvas.getByText('21 is less than 2') + await userEvent.click(two) + await asyncSleep(500) + await userEvent.click(two) + await asyncSleep(500) + await userEvent.click(two) + await asyncSleep(500) + await userEvent.click(two) + await waitFor(() => { + expect(onDoneResult(canvas)).toMatchObject({ count: 13, onDoneResult: { valid: true, value: 100 } }) + }) }, } From 783edc1700047e9cdc726a9e1aec40ca7b002bf2 Mon Sep 17 00:00:00 2001 From: David Fridley Date: Tue, 12 Nov 2024 09:47:35 -0800 Subject: [PATCH 09/23] working on deriver --- app/components/steps/compare-whys.js | 53 ++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/app/components/steps/compare-whys.js b/app/components/steps/compare-whys.js index f2dc4d7b0..8b0b10b96 100644 --- a/app/components/steps/compare-whys.js +++ b/app/components/steps/compare-whys.js @@ -84,3 +84,56 @@ const useStyles = createUseStyles(theme => ({ })) export default CompareReasons + +// pointWithWhyRankByWhyIdByPointId={id: {point, whyRankByWhyId: {id: {why, rank}}}} + +export function derivePointWithWhyRankListLisyByCategory(data, category) { + const local = useRef({ pointWithWhyRankListList: {}, pointWithWhyRankByWhyIdByPointId: {} }).current + const { reducedPointList, randomWhyById, whyRankByParentId } = data + const { pointWithWhyRankListList, pointWithWhyRankByWhyIdByPointId } = local + let updatedPoints = {} + if (local.reducedPointList !== reducedPointList) { + for (const pointGroup of reducedPointList) { + if (!pointWithWhyRankByWhyIdByPointId[pointGroup.point._id]?.point !== pointGroup.point) { + if (!pointWithWhyRankByWhyIdByPointId[pointGroup.point._id]) pointWithWhyRankByWhyIdByPointId[pointGroup.point._id] = { whyRankByWhyId: {} } + pointWithWhyRankByWhyIdByPointId[pointGroup.point._id].point = pointGroup.point + updatedPoints[pointGroup.point._id] = true + } + } + local.reducedPointList = reducedPointList + } + if (local.randomWhyById !== randomWhyById || local.whyRankByParentId !== whyRankByParentId) { + for (const why of Object.values(randomWhyById)) { + if (!pointWithWhyRankByWhyIdByPointId[why.parentId]) continue // a why's parent not here + if (pointWithWhyRankByWhyIdByPointId[why.parentId].whyRankByWhyId[why._id].why !== why) { + pointWithWhyRankByWhyIdByPointId[why.parentId].whyRankByWhyId[why._id] = { + why, + rank: (pointWithWhyRankByWhyIdByPointId[why.parentId].whyRankByWhyId[why._id].rank = whyRankByParentId[why._id]), + } + updatedPoints[why.parentId] = true + } else if (pointWithWhyRankByWhyIdByPointId[why.parentId].whyRankByWhyId[why._id].rank !== whyRankByParentId[why._id]) { + pointWithWhyRankByWhyIdByPointId[why.parentId].whyRankByWhyId[why._id].rank = { why, rank: whyRankByParentId[why._id] } + updatedPoints[why.parentId] = true + } + } + local.randomWhyById = randomWhyById + local.whyRankByParentId = whyRankByParentId + } + //const newPointWithWhyRankListList=Object.values(pointWithWhyRankByWhyIdByPointId).map(pointWithWhyRankByParentId=>({point: pointWithWhyRankByParentId.point, whyRanks: Object.values(pointWithWhyRankByParentId.whyRankByParentId)})) + const newPointWithWhyRankListList = [] + const updated = false + for (const pointWithWhyRankList of pointWithWhyRankListList) { + const pointId = pointWithWhyRankList.point._id + if (updatedPoints[pointId]) { + newPointWithWhyRankListList.push({ point: pointWithWhyRankByWhyIdByPointId[pointId].point, whyRanks: Object.values(pointWithWhyRankByWhyIdByPointId[pointId].whyRankByWhyId) }) + updated = true + } else newPointWithWhyRankListList.push(pointWithWhyRankList) + delete updatedPoints[pointWithWhyRankList.point._id] + } + for (const pointId of Object.keys(updatedPoints)) { + newPointWithWhyRankListList.push({ point: pointWithWhyRankByWhyIdByPointId[pointId].point, whyRanks: Object.values(pointWithWhyRankByWhyIdByPointId[pointId].whyRankByWhyId) }) + updated = true + } + if (updated) local.pointWithWhyRankListList = newPointWithWhyRankListList + return local.pointWithWhyRankListList +} From 25edcf241af2b196c6f7e140f55e8e8e77f8d2ea Mon Sep 17 00:00:00 2001 From: David Fridley Date: Thu, 14 Nov 2024 11:48:09 -0800 Subject: [PATCH 10/23] deriver and test --- .../steps/__tests__/compare-whys.js | 148 ++++++++++++++++++ app/components/steps/compare-whys.js | 22 +-- 2 files changed, 160 insertions(+), 10 deletions(-) create mode 100644 app/components/steps/__tests__/compare-whys.js diff --git a/app/components/steps/__tests__/compare-whys.js b/app/components/steps/__tests__/compare-whys.js new file mode 100644 index 000000000..00c68b085 --- /dev/null +++ b/app/components/steps/__tests__/compare-whys.js @@ -0,0 +1,148 @@ +// https://github.com/EnCiv/civil-pursuit/issues/215 +import { derivePointWithWhyRankListLisyByCategory } from '../compare-whys' +jest.mock('react', () => { + const obj = {} + return { + // every call to useRef in this file will use the state from the first call plus updates along the way + // the real React code throws because it's not inside a render + useRef: jest.fn(val => (typeof obj.current === 'undefined' && (obj.current = val), obj)), + // mocking these so it will build - they aren't used + createElement: jest.fn(), + createContext: jest.fn(), + forwardRef: jest.fn(), + useEffect: jest.fn(), + useState: jest.fn(), + default: jest.fn(), + } +}) +// mock these so it will build - they aren't used +jest.mock('react-jss', () => { + return { + createUseStyles: jest.fn(), + } +}) +// Button uses this one +jest.mock('@codastic/react-positioning-portal', () => { + return { + PositioningPortal: jest.fn(), + } +}) +// Points and others use LEVEL and H from here +jest.mock('react-accessible-headings', () => { + return { + H: jest.fn(), + Level: jest.fn(), + } +}) +const data = {} +describe('test derivePoinMostsLeastsRankList', () => { + test('no data yields no chanage', () => { + const { pointWithWhyRankListList } = derivePointWithWhyRankListLisyByCategory(data, 'most') + expect(pointWithWhyRankListList).toBe(undefined) + }) + test('can insert initial data', () => { + data.reducedPointList = [{ point: { _id: '1', subject: '1', description: '1' } }, { point: { _id: '2', subject: '2', description: '2' } }, { point: { _id: '3', subject: '3', description: '3' } }] + const { pointWithWhyRankListList } = derivePointWithWhyRankListLisyByCategory(data, 'most') + expect(pointWithWhyRankListList).toMatchObject(data.reducedPointList) + }) + test("ref doesn't change if data doesn't change", () => { + const { pointWithWhyRankListList } = derivePointWithWhyRankListLisyByCategory(data, 'most') + const newRef = derivePointWithWhyRankListLisyByCategory(data, 'most').pointWithWhyRankListList + expect(newRef).toBe(pointWithWhyRankListList) + }) + test('ref does change if a point changes', () => { + const pointWithWhyRankListList = derivePointWithWhyRankListLisyByCategory(data) + data.reducedPointList[1] = { point: { ...data.reducedPointList[1].point } } + data.reducedPointList = [...data.reducedPointList] + const newRef = derivePointWithWhyRankListLisyByCategory(data, 'most') + expect(newRef).not.toBe(pointWithWhyRankListList) + expect(newRef).toEqual(pointWithWhyRankListList) + }) + test('works with random ranked whys', () => { + data.randomWhyById = { + 4: { _id: '4', subject: '1.4 random most', description: '1.4 random most', parentId: '1', category: 'most' }, + 5: { _id: '5', subject: '1.5 random least', description: '1.5 random least', parentId: '1', category: 'most' }, + 6: { _id: '6', subject: '2.6 random most', description: '2.6 random most', parentId: '2', category: 'most' }, + 7: { _id: '7', subject: '2.7 random least', description: '2.7 random least', parentId: '2', category: 'most' }, + } + const { pointWithWhyRankListList } = derivePointWithWhyRankListLisyByCategory(data, 'most') + const calculatedReviewPoints = data.reducedPointList.map(({ point }) => { + // if there are no mosts or leasts, the entry should not be present + const mosts = Object.values(data.randomWhyById).filter(why => why.parentId === point._id && why.category === 'most') + const result = { point } + if (mosts.length) + result.whyRanks = mosts.map(why => ({ + why, + })) + return result + }) + expect(pointWithWhyRankListList).toMatchObject(calculatedReviewPoints) + }) + test("ref doesn't change if point and random whys doesn't change", () => { + const { pointWithWhyRankListList } = derivePointWithWhyRankListLisyByCategory(data, 'most') + const newRef = derivePointWithWhyRankListLisyByCategory(data, 'most').pointWithWhyRankListList + expect(newRef).toBe(pointWithWhyRankListList) + }) + test('ref does change if a point changes when there are whys', () => { + const { pointWithWhyRankListList } = derivePointWithWhyRankListLisyByCategory(data, 'most') + data.reducedPointList[1] = { point: { ...data.reducedPointList[1].point } } + data.reducedPointList = [...data.reducedPointList] + const newRef = derivePointWithWhyRankListLisyByCategory(data, 'most').pointWithWhyRankListList + expect(newRef).not.toBe(pointWithWhyRankListList) + expect(newRef).toEqual(pointWithWhyRankListList) + }) + test('ref does change if a why is changed', () => { + const { pointWithWhyRankListList } = derivePointWithWhyRankListLisyByCategory(data, 'most') + const item1 = pointWithWhyRankListList[1] + data.randomWhyById['6'] = { ...data.randomWhyById['6'] } + data.randomWhyById = { ...data.randomWhyById } + const newRef = derivePointWithWhyRankListLisyByCategory(data, 'most').pointWithWhyRankListList + expect(newRef).not.toBe(pointWithWhyRankListList) + expect(newRef).toEqual(pointWithWhyRankListList) + expect(newRef[1]).toEqual(item1) + }) + test('works with ranks added', () => { + data.whyRankByParentId = { + 4: { _id: '8', category: 'most', parentId: '4', stage: 'why' }, + 5: { _id: '9', category: 'neutral', parentId: '5', stage: 'why' }, + 6: { _id: '10', category: 'neutral', parentId: '6', stage: 'why' }, + } + const { pointWithWhyRankListList } = derivePointWithWhyRankListLisyByCategory(data, 'most') + console.info(JSON.stringify(pointWithWhyRankListList, null, 2)) + expect(pointWithWhyRankListList).toMatchObject([ + { + point: { _id: '1', subject: '1', description: '1' }, + whyRanks: [ + { + why: { _id: '4', subject: '1.4 random most', description: '1.4 random most', parentId: '1', category: 'most' }, + rank: { _id: '8', category: 'most', parentId: '4', stage: 'why' }, + }, + { + why: { _id: '5', subject: '1.5 random least', description: '1.5 random least', parentId: '1', category: 'most' }, + rank: { _id: '9', category: 'neutral', parentId: '5', stage: 'why' }, + }, + ], + }, + { + point: { + _id: '2', + subject: '2', + description: '2', + }, + whyRanks: [ + { + why: { _id: '6', subject: '2.6 random most', description: '2.6 random most', parentId: '2', category: 'most' }, + rank: { _id: '10', category: 'neutral', parentId: '6', stage: 'why' }, + }, + { + why: { _id: '7', subject: '2.7 random least', description: '2.7 random least', parentId: '2', category: 'most' }, + }, + ], + }, + { + point: { _id: '3', subject: '3', description: '3' }, + whyRanks: [], + }, + ]) + }) +}) diff --git a/app/components/steps/compare-whys.js b/app/components/steps/compare-whys.js index 8b0b10b96..35efc053a 100644 --- a/app/components/steps/compare-whys.js +++ b/app/components/steps/compare-whys.js @@ -2,7 +2,7 @@ // https://github.com/EnCiv/civil-pursuit/issues/200 'use strict' -import React, { useEffect, useState } from 'react' +import React, { useEffect, useState, useRef } from 'react' import { createUseStyles } from 'react-jss' import PairCompare from '../pair-compare' import { H, Level } from 'react-accessible-headings' @@ -88,7 +88,8 @@ export default CompareReasons // pointWithWhyRankByWhyIdByPointId={id: {point, whyRankByWhyId: {id: {why, rank}}}} export function derivePointWithWhyRankListLisyByCategory(data, category) { - const local = useRef({ pointWithWhyRankListList: {}, pointWithWhyRankByWhyIdByPointId: {} }).current + // pointWithWhyRankListList shouldn't default to [], it should be undefined until data is fetched from the server. But then, [] is ok + const local = useRef({ pointWithWhyRankListList: undefined, pointWithWhyRankByWhyIdByPointId: {} }).current const { reducedPointList, randomWhyById, whyRankByParentId } = data const { pointWithWhyRankListList, pointWithWhyRankByWhyIdByPointId } = local let updatedPoints = {} @@ -104,15 +105,16 @@ export function derivePointWithWhyRankListLisyByCategory(data, category) { } if (local.randomWhyById !== randomWhyById || local.whyRankByParentId !== whyRankByParentId) { for (const why of Object.values(randomWhyById)) { + if (why.category !== category) continue if (!pointWithWhyRankByWhyIdByPointId[why.parentId]) continue // a why's parent not here - if (pointWithWhyRankByWhyIdByPointId[why.parentId].whyRankByWhyId[why._id].why !== why) { + if (pointWithWhyRankByWhyIdByPointId[why.parentId]?.whyRankByWhyId[why._id]?.why !== why) { pointWithWhyRankByWhyIdByPointId[why.parentId].whyRankByWhyId[why._id] = { why, - rank: (pointWithWhyRankByWhyIdByPointId[why.parentId].whyRankByWhyId[why._id].rank = whyRankByParentId[why._id]), + rank: whyRankByParentId?.[why._id], } updatedPoints[why.parentId] = true - } else if (pointWithWhyRankByWhyIdByPointId[why.parentId].whyRankByWhyId[why._id].rank !== whyRankByParentId[why._id]) { - pointWithWhyRankByWhyIdByPointId[why.parentId].whyRankByWhyId[why._id].rank = { why, rank: whyRankByParentId[why._id] } + } else if (pointWithWhyRankByWhyIdByPointId[why.parentId].whyRankByWhyId[why._id].rank !== whyRankByParentId?.[why._id]) { + pointWithWhyRankByWhyIdByPointId[why.parentId].whyRankByWhyId[why._id].rank = whyRankByParentId[why._id] updatedPoints[why.parentId] = true } } @@ -121,19 +123,19 @@ export function derivePointWithWhyRankListLisyByCategory(data, category) { } //const newPointWithWhyRankListList=Object.values(pointWithWhyRankByWhyIdByPointId).map(pointWithWhyRankByParentId=>({point: pointWithWhyRankByParentId.point, whyRanks: Object.values(pointWithWhyRankByParentId.whyRankByParentId)})) const newPointWithWhyRankListList = [] - const updated = false - for (const pointWithWhyRankList of pointWithWhyRankListList) { + let updated = false + for (const pointWithWhyRankList of pointWithWhyRankListList ?? []) { const pointId = pointWithWhyRankList.point._id if (updatedPoints[pointId]) { newPointWithWhyRankListList.push({ point: pointWithWhyRankByWhyIdByPointId[pointId].point, whyRanks: Object.values(pointWithWhyRankByWhyIdByPointId[pointId].whyRankByWhyId) }) updated = true } else newPointWithWhyRankListList.push(pointWithWhyRankList) - delete updatedPoints[pointWithWhyRankList.point._id] + delete updatedPoints[pointId] } for (const pointId of Object.keys(updatedPoints)) { newPointWithWhyRankListList.push({ point: pointWithWhyRankByWhyIdByPointId[pointId].point, whyRanks: Object.values(pointWithWhyRankByWhyIdByPointId[pointId].whyRankByWhyId) }) updated = true } if (updated) local.pointWithWhyRankListList = newPointWithWhyRankListList - return local.pointWithWhyRankListList + return { pointWithWhyRankListList: local.pointWithWhyRankListList } } From 699a492340068d6ab609cf60524adbe7684fd7b8 Mon Sep 17 00:00:00 2001 From: David Fridley Date: Mon, 25 Nov 2024 10:28:01 -0800 Subject: [PATCH 11/23] refactoring in progresss --- app/components/ranking.jsx | 48 +++++++++++++++------------- app/components/steps/compare-whys.js | 42 ++++++++++++++++++++++-- stories/compare-reasons.stories.jsx | 4 +-- 3 files changed, 66 insertions(+), 28 deletions(-) diff --git a/app/components/ranking.jsx b/app/components/ranking.jsx index 1d572c4f7..d3b346f2f 100644 --- a/app/components/ranking.jsx +++ b/app/components/ranking.jsx @@ -13,6 +13,27 @@ const selectedOption = ( const unselectedOption = +// table to map from data model properties, to the Rank Strings shown in the UI +const toRankString = { + undefined: '', + most: 'Most', + least: 'Least', + neutral: 'Neutral', +} + +// table to map from UI's Rank Strings to the data model prpoerty names +const rankStringToCategory = Object.entries(toRankString).reduce((rS2C, [key, value]) => { + if (key === 'undefined') return rS2C // rankStringToCategory[''] will be undefined + rS2C[value] = key + return rS2C +}, {}) + +function Rank(props) { + const { onDone, rank, ...otherProps } = props + const handleOnDone = result => ({ valid: result.valid, value: rankStringToCategory[result.value] }) + return +} + export default function Ranking(props) { //Isolate props and set initial state const { disabled, defaultValue, className, onDone, ...otherProps } = props @@ -38,36 +59,17 @@ export default function Ranking(props) { } setResponse(e.target.value) if (!onDone) { - return console.warn( - `Unhandled rank selection: ${e.target.value}. Please pass a handler function via the onDone prop.` - ) + return console.warn(`Unhandled rank selection: ${e.target.value}. Please pass a handler function via the onDone prop.`) } else onDone({ valid: true, value: e.target.value }) } return ( -
+
{responseOptions.map(option => { return (