From 84235c13c40014c1e7ce1c75e53219702f2c8826 Mon Sep 17 00:00:00 2001 From: Joshua Melville Date: Mon, 11 Dec 2023 21:17:18 +0200 Subject: [PATCH] attempt to fix prompt issues by replacing withPrompts hoc with functional component and hook --- lib/interviewer/behaviours/withPrompt.js | 180 +++++++++--------- lib/interviewer/components/Prompts.js | 6 +- .../containers/Interfaces/NameGenerator.js | 49 ++--- lib/interviewer/containers/NodePanel.js | 2 - lib/interviewer/containers/NodePanels.js | 10 +- .../containers/withExternalData.js | 6 - lib/interviewer/ducks/modules/session.js | 1 + lib/interviewer/selectors/prop.js | 5 +- lib/interviewer/selectors/session.ts | 4 +- lib/interviewer/utils/loadExternalData.js | 1 - lib/ui/components/Prompts/Prompts.js | 10 +- 11 files changed, 133 insertions(+), 141 deletions(-) diff --git a/lib/interviewer/behaviours/withPrompt.js b/lib/interviewer/behaviours/withPrompt.js index c3184fb64..d6b1ac0ac 100644 --- a/lib/interviewer/behaviours/withPrompt.js +++ b/lib/interviewer/behaviours/withPrompt.js @@ -1,12 +1,12 @@ -import React, { Component } from 'react'; -import { connect } from 'react-redux'; +import React, { Component, useEffect } from 'react'; +import { connect, useDispatch, useSelector } from 'react-redux'; import { bindActionCreators } from 'redux'; import PropTypes from 'prop-types'; import { actionCreators as sessionActions } from '../ducks/modules/session'; import { getAllVariableUUIDsByEntity, getProtocolStages } from '../selectors/protocol'; import { get } from '../utils/lodash-replacements'; import { processProtocolSortRule } from '../utils/createSorter'; -import { getPromptIndex } from '../selectors/session'; +import { getIsFirstPrompt, getIsLastPrompt, getPromptIndex, getPrompts } from '../selectors/session'; /** * Convert sort rules to new format. See `processProtocolSortRule` for details. @@ -15,7 +15,7 @@ import { getPromptIndex } from '../selectors/session'; * @returns {Array} * @private */ -const processSortRules = (prompts, codebookVariables) => { +const processSortRules = (prompts = [], codebookVariables) => { const sortProperties = ['bucketSortOrder', 'binSortOrder']; return prompts.map((prompt) => { @@ -31,97 +31,103 @@ const processSortRules = (prompts, codebookVariables) => { }); }; -export default function withPrompt(WrappedComponent) { - class WithPrompt extends Component { - get prompts() { - const { - codebookVariables, - } = this.props; - - const prompts = get(this.props, ['stage', 'prompts'], []); - const processedPrompts = processSortRules(prompts, codebookVariables); - return processedPrompts; - } - - get promptsCount() { - return this.prompts.length; - } - - isFirstPrompt = () => { - const { promptIndex } = this.props; - return promptIndex === 0; - } - - isLastPrompt = () => { - const { promptIndex } = this.props; - const lastPromptIndex = this.promptsCount - 1; - return promptIndex === lastPromptIndex; - } - - promptForward = () => { - const { updatePrompt, promptIndex } = this.props; - - updatePrompt( - (this.promptsCount + promptIndex + 1) % this.promptsCount, - ); - } - - promptBackward = () => { - const { updatePrompt, promptIndex } = this.props; - - updatePrompt( - (this.promptsCount + promptIndex - 1) % this.promptsCount, - ); - } - - prompt() { - const { promptIndex } = this.props; - - return get(this.prompts, promptIndex); - } - - render() { - const { promptIndex, codebookVariables, ...rest } = this.props; - - return ( - - ); - } - } +const withPrompt = (WrappedComponent) => { + const WithPrompt = (props) => { + const dispatch = useDispatch(); + const updatePrompt = (promptIndex) => dispatch(sessionActions.updatePrompt(promptIndex)); + + const codebookVariables = useSelector(getAllVariableUUIDsByEntity); + const prompts = useSelector(getPrompts); + const { ...rest } = props; + + const processedPrompts = processSortRules(prompts, codebookVariables); + + const isFirstPrompt = useSelector(getIsFirstPrompt); + const isLastPrompt = useSelector(getIsLastPrompt); + const promptIndex = useSelector(getPromptIndex); + + const promptForward = () => { + updatePrompt((promptIndex + 1) % processedPrompts.length); + }; + + const promptBackward = () => { + updatePrompt((promptIndex - 1 + processedPrompts.length) % processedPrompts.length); + }; + + const prompt = () => { + console.log('calling prompt()', props, promptIndex); + return get(processedPrompts, promptIndex); + }; + + // useEffect(() => { + // let promptIndex = props.promptId; + + // if (promptIndex === undefined) { + // promptIndex = getPromptIndex(state); + // } + + // // Dispatch an action to update the promptIndex + // // This assumes you have an action to update the promptIndex in your sessionActions + // dispatch(sessionActions.updatePromptIndex(promptIndex)); + // }, [props.promptId]); + + return ( + + ); + }; + WithPrompt.propTypes = { stage: PropTypes.object.isRequired, promptIndex: PropTypes.number, updatePrompt: PropTypes.func.isRequired, + promptId: PropTypes.number, }; - WithPrompt.defaultProps = { - promptIndex: 0, + return WithPrompt; +}; + +export const usePrompts = () => { + const dispatch = useDispatch(); + const updatePrompt = (promptIndex) => dispatch(sessionActions.updatePrompt(promptIndex)); + + const codebookVariables = useSelector(getAllVariableUUIDsByEntity); + const prompts = useSelector(getPrompts); + + const processedPrompts = processSortRules(prompts, codebookVariables); + + const isFirstPrompt = useSelector(getIsFirstPrompt); + const isLastPrompt = useSelector(getIsLastPrompt); + const promptIndex = useSelector(getPromptIndex); + + const promptForward = () => { + updatePrompt((promptIndex + 1) % processedPrompts.length); }; - function mapStateToProps(state, ownProps) { - let promptIndex = ownProps.promptId; - if (promptIndex === undefined) { - promptIndex = getPromptIndex(state); - } - return { - promptIndex, - stage: ownProps.stage || getProtocolStages(state)[ownProps.currentStep], - codebookVariables: getAllVariableUUIDsByEntity(state), - }; - } + const promptBackward = () => { + updatePrompt((promptIndex - 1 + processedPrompts.length) % processedPrompts.length); + }; - function mapDispatchToProps(dispatch) { - return { - updatePrompt: bindActionCreators(sessionActions.updatePrompt, dispatch), - }; - } + const currentPrompt = () => { + return processedPrompts[promptIndex] ?? null; + }; - return connect(mapStateToProps, mapDispatchToProps)(WithPrompt); + return { + promptIndex, + currentPrompt: currentPrompt(), + prompts, + promptForward, + promptBackward, + isLastPrompt, + isFirstPrompt, + }; } + + +export default withPrompt; \ No newline at end of file diff --git a/lib/interviewer/components/Prompts.js b/lib/interviewer/components/Prompts.js index dc76522fc..9cfdb05fa 100644 --- a/lib/interviewer/components/Prompts.js +++ b/lib/interviewer/components/Prompts.js @@ -1,11 +1,13 @@ import React from 'react'; import UIPrompts from '~/lib/ui/components/Prompts/Prompts'; import { useSelector } from 'react-redux'; +import { usePrompts } from '../behaviours/withPrompt'; -const Prompts = (props) => { +const Prompts = () => { + const { currentPrompt, prompts } = usePrompts(); const speakable = useSelector((state) => state.deviceSettings.enableExperimentalTTS); - return ; + return ; }; export default Prompts; diff --git a/lib/interviewer/containers/Interfaces/NameGenerator.js b/lib/interviewer/containers/Interfaces/NameGenerator.js index bd8277777..398b8cbc5 100644 --- a/lib/interviewer/containers/Interfaces/NameGenerator.js +++ b/lib/interviewer/containers/Interfaces/NameGenerator.js @@ -1,5 +1,4 @@ import React, { useEffect, useRef, useState } from 'react'; -import { compose } from 'redux'; import { useDispatch, useSelector } from 'react-redux'; import PropTypes from 'prop-types'; import { @@ -8,7 +7,7 @@ import { import { createPortal } from 'react-dom'; import { entityAttributesProperty, entityPrimaryKeyProperty } from '@codaco/shared-consts'; import Prompts from '../../components/Prompts'; -import withPrompt from '../../behaviours/withPrompt'; +import { usePrompts } from '../../behaviours/withPrompt'; import { actionCreators as sessionActions } from '../../ducks/modules/session'; import { getStageNodeCount, getNetworkNodesForPrompt } from '../../selectors/interface'; import { getPromptModelData as getPromptNodeModelData, getNodeIconName } from '../../selectors/name-generator'; @@ -28,12 +27,11 @@ import { getAdditionalAttributesSelector } from '../../selectors/prop'; const NameGenerator = (props) => { const { registerBeforeNext, - prompt, + onComplete, stage, } = props; const { - prompts, form, quickAdd, behaviours, @@ -42,6 +40,13 @@ const NameGenerator = (props) => { const interfaceRef = useRef(null); + const { + currentPrompt, + isFirstPrompt, + isLastPrompt, + prompts + } = usePrompts(); + const [selectedNode, setSelectedNode] = useState(null); const [showMinWarning, setShowMinWarning] = useState(false); @@ -72,18 +77,12 @@ const NameGenerator = (props) => { if (stageNodeCount >= minNodes) { setShowMinWarning(false); } - }, [stageNodeCount]); + }, [stageNodeCount, minNodes]); // Prevent leaving the stage if the minimum number of nodes has not been met const handleBeforeLeaving = (direction, destination) => { - const { - isFirstPrompt, - isLastPrompt, - onComplete, - } = props; - - const isLeavingStage = (isFirstPrompt() && direction === -1) - || (isLastPrompt() && direction === 1); + const isLeavingStage = (isFirstPrompt && direction === -1) + || (isLastPrompt && direction === 1); // Implementation quirk that destination is only provided when navigation // is triggered by Stages Menu. Use this to skip message if user has @@ -111,7 +110,7 @@ const NameGenerator = (props) => { if (has(node, 'promptIDs')) { addNodeToPrompt( node[entityPrimaryKeyProperty], - prompt.id, + currentPrompt.id, { ...newNodeAttributes }, ); } else { @@ -133,23 +132,22 @@ const NameGenerator = (props) => { setSelectedNode(node); }; + console.log('rendering NameGenerator', currentPrompt, prompts) + return (
- +
- +
get(meta, 'itemType', null) === 'NEW_NODE'} itemType="EXISTING_NODE" @@ -204,15 +202,8 @@ const NameGenerator = (props) => { ); }; +export default NameGenerator; + NameGenerator.propTypes = { - prompt: PropTypes.object.isRequired, stage: PropTypes.object.isRequired, }; - -export default compose( - withPrompt, -)(NameGenerator); - -export { - NameGenerator as UnconnectedNameGenerator, -}; diff --git a/lib/interviewer/containers/NodePanel.js b/lib/interviewer/containers/NodePanel.js index 90391dc12..a12b5bd9d 100644 --- a/lib/interviewer/containers/NodePanel.js +++ b/lib/interviewer/containers/NodePanel.js @@ -78,8 +78,6 @@ class NodePanel extends PureComponent { ...nodeListProps } = this.props; - console.log('NodePanel', this.props); - return ( { protocolCodebook, } = getSessionMeta(state); - console.log({ - protocolUID, - assetManifest, - protocolCodebook, - }); - return { protocolUID, assetManifest, diff --git a/lib/interviewer/ducks/modules/session.js b/lib/interviewer/ducks/modules/session.js index b0fb8a56c..db82d808c 100644 --- a/lib/interviewer/ducks/modules/session.js +++ b/lib/interviewer/ducks/modules/session.js @@ -126,6 +126,7 @@ const getReducer = (network) => (state = initialState, action = {}) => { [action.sessionId]: withTimestamp({ ...state[action.sessionId], currentStep: action.currentStep, + promptIndex: 0, }), }; } diff --git a/lib/interviewer/selectors/prop.js b/lib/interviewer/selectors/prop.js index d31182a43..6d1f15393 100644 --- a/lib/interviewer/selectors/prop.js +++ b/lib/interviewer/selectors/prop.js @@ -1,5 +1,4 @@ import { createSelector } from "@reduxjs/toolkit"; -import { get } from "~/utils/lodash-replacements"; const asKeyValue = (acc, { variable, value }) => ({ ...acc, @@ -7,9 +6,9 @@ const asKeyValue = (acc, { variable, value }) => ({ }); export const getAdditionalAttributes = (stage, prompt) => { - const stageAttributes = get(stage, 'additionalAttributes', []) + const stageAttributes = (stage?.additionalAttributes ?? []) .reduce(asKeyValue, {}); - const promptAttributes = get(prompt, 'additionalAttributes', []) + const promptAttributes = (prompt?.additionalAttributes ?? []) .reduce(asKeyValue, {}); return { diff --git a/lib/interviewer/selectors/session.ts b/lib/interviewer/selectors/session.ts index 026207201..df162d650 100644 --- a/lib/interviewer/selectors/session.ts +++ b/lib/interviewer/selectors/session.ts @@ -55,13 +55,13 @@ export const getCurrentStage = createSelector( export const getPromptIndex = createSelector( getActiveSession, - (session) => session?.promptIndex ?? 0, + (session: Session) => session?.promptIndex ?? 0, ); export const getCurrentPrompt = createSelector( getCurrentStage, getPromptIndex, - (stage, promptIndex) => stage?.prompts?.[promptIndex], + (stage: Stage, promptIndex: number) => stage?.prompts?.[promptIndex], ); export const getCaseId = createDeepEqualSelector( diff --git a/lib/interviewer/utils/loadExternalData.js b/lib/interviewer/utils/loadExternalData.js index ddbcaad2a..cbd936e0b 100644 --- a/lib/interviewer/utils/loadExternalData.js +++ b/lib/interviewer/utils/loadExternalData.js @@ -61,7 +61,6 @@ const loadExternalData = async (fileName, url) => { nodes = json.nodes ?? []; } - console.log('returning', nodes) return { nodes }; } catch (e) { diff --git a/lib/ui/components/Prompts/Prompts.js b/lib/ui/components/Prompts/Prompts.js index a233490aa..db30aaeca 100644 --- a/lib/ui/components/Prompts/Prompts.js +++ b/lib/ui/components/Prompts/Prompts.js @@ -10,18 +10,20 @@ import Pips from './Pips'; */ const Prompts = (props) => { const { - currentPrompt, + currentPromptId, prompts, speakable = false, } = props; const prevPromptRef = useRef(); - const currentIndex = findIndex(prompts, (prompt) => prompt.id === currentPrompt); + console.log('prompts', { currentPromptId, prompts }) + + const currentIndex = findIndex(prompts, (prompt) => prompt.id === currentPromptId); useEffect(() => { prevPromptRef.current = currentIndex; - }, [currentPrompt, currentIndex]); + }, [currentPromptId, currentIndex]); const backwards = useMemo(() => currentIndex < prevPromptRef.current, [currentIndex]); @@ -56,7 +58,7 @@ const Prompts = (props) => { Prompts.propTypes = { prompts: PropTypes.any.isRequired, - currentPrompt: PropTypes.string.isRequired, + currentPromptId: PropTypes.string.isRequired, speakable: PropTypes.bool, };