From 740a7b8e3f495b8a120fa4dffcd7285305af4b92 Mon Sep 17 00:00:00 2001 From: Jesus Balderrama Date: Tue, 11 Nov 2025 10:42:06 -0600 Subject: [PATCH 1/8] feat(form): add validation to NumericalInput to accept only numeric values --- .../AnswerWidget/AnswerOption.jsx | 34 +++++++++++++------ .../EditProblemView/AnswerWidget/messages.js | 5 +++ .../ProblemEditor/data/OLXParser.js | 2 +- 3 files changed, 30 insertions(+), 11 deletions(-) diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/AnswerOption.jsx b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/AnswerOption.jsx index 897ff07b01..299e07e82f 100644 --- a/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/AnswerOption.jsx +++ b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/AnswerOption.jsx @@ -18,7 +18,7 @@ import { FeedbackBox } from './components/Feedback'; import * as hooks from './hooks'; import { ProblemTypeKeys } from '../../../../../data/constants/problem'; import ExpandableTextArea from '../../../../../sharedComponents/ExpandableTextArea'; -import { answerRangeFormatRegex } from '../../../data/OLXParser'; +import { answerRangeFormatRegex, numericRegex } from '../../../data/OLXParser'; const AnswerOption = ({ answer, @@ -53,6 +53,10 @@ const AnswerOption = ({ const cleanedValue = value.replace(/^\s+|\s+$/g, ''); return !cleanedValue.length || answerRangeFormatRegex.test(cleanedValue); }; + const validateAnswerNumeric = (value) => { + const cleanedValue = (value ?? '').trim(); + return !cleanedValue.length || numericRegex.test(cleanedValue); + } const getInputArea = () => { if ([ProblemTypeKeys.SINGLESELECT, ProblemTypeKeys.MULTISELECT].includes(problemType)) { @@ -70,16 +74,26 @@ const AnswerOption = ({ ); } if (problemType !== ProblemTypeKeys.NUMERIC || !answer.isAnswerRange) { + const isValidValue = validateAnswerNumeric(answer.title) return ( - + + + {!isValidValue && ( + + + + )} + + ); } // Return Answer Range View diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/messages.js b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/messages.js index d69bc876d7..c76acc4a41 100644 --- a/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/messages.js +++ b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/messages.js @@ -82,6 +82,11 @@ const messages = defineMessages({ defaultMessage: 'Error: Invalid range format. Use brackets or parentheses with values separated by a comma.', description: 'Error text describing wrong format of answer ranges', }, + AnswerNumericErrorText: { + id: 'authoring.answerwidget.answer.answerNumericErrorText', + defaultMessage: 'Error: This problem type only supports numeric answers. Did you mean to make a (Text/Math Expression) Input problem?', + description: 'Error numeric describing wrong format', + } }); export default messages; diff --git a/src/editors/containers/ProblemEditor/data/OLXParser.js b/src/editors/containers/ProblemEditor/data/OLXParser.js index 63254cffd7..9d3c9d10bb 100644 --- a/src/editors/containers/ProblemEditor/data/OLXParser.js +++ b/src/editors/containers/ProblemEditor/data/OLXParser.js @@ -96,7 +96,7 @@ export const responseKeys = [ * [] */ export const answerRangeFormatRegex = /^[([]\s*-?(?:\d+(?:\.\d+)?|\d+\/\d+)\s*,\s*-?(?:\d+(?:\.\d+)?|\d+\/\d+)\s*[)\]]$/m; - +export const numericRegex = /^[+-]?(\d+(\.\d*)?|\.\d+)$/; export const stripNonTextTags = ({ input, tag }) => { const stripedTags = {}; Object.entries(input).forEach(([key, value]) => { From d669d7e2b4f56383e52d7642f58b8579278a997d Mon Sep 17 00:00:00 2001 From: Jesus Balderrama Date: Fri, 14 Nov 2025 16:07:29 -0600 Subject: [PATCH 2/8] style(format): fix spaces and update message to camelCase --- .../EditProblemView/AnswerWidget/AnswerOption.jsx | 15 +++++++-------- .../EditProblemView/AnswerWidget/messages.js | 6 +++--- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/AnswerOption.jsx b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/AnswerOption.jsx index 299e07e82f..27e1a5ca8e 100644 --- a/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/AnswerOption.jsx +++ b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/AnswerOption.jsx @@ -56,7 +56,7 @@ const AnswerOption = ({ const validateAnswerNumeric = (value) => { const cleanedValue = (value ?? '').trim(); return !cleanedValue.length || numericRegex.test(cleanedValue); - } + }; const getInputArea = () => { if ([ProblemTypeKeys.SINGLESELECT, ProblemTypeKeys.MULTISELECT].includes(problemType)) { @@ -74,7 +74,7 @@ const AnswerOption = ({ ); } if (problemType !== ProblemTypeKeys.NUMERIC || !answer.isAnswerRange) { - const isValidValue = validateAnswerNumeric(answer.title) + const isValidValue = validateAnswerNumeric(answer.title); return ( - {!isValidValue && ( - - - - )} - + {!isValidValue && ( + + + + )} ); } diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/messages.js b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/messages.js index c76acc4a41..3dd90e1ba3 100644 --- a/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/messages.js +++ b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/messages.js @@ -82,11 +82,11 @@ const messages = defineMessages({ defaultMessage: 'Error: Invalid range format. Use brackets or parentheses with values separated by a comma.', description: 'Error text describing wrong format of answer ranges', }, - AnswerNumericErrorText: { + answerNumericErrorText: { id: 'authoring.answerwidget.answer.answerNumericErrorText', defaultMessage: 'Error: This problem type only supports numeric answers. Did you mean to make a (Text/Math Expression) Input problem?', - description: 'Error numeric describing wrong format', - } + description: 'Error message when user provides wrong format', + }, }); export default messages; From 14d2c722432f910c6dcebf40fc0a6106ff6ab885 Mon Sep 17 00:00:00 2001 From: Jesus Balderrama Date: Fri, 14 Nov 2025 16:42:32 -0600 Subject: [PATCH 3/8] fix(content): update text for clarity Co-authored-by: Kyle McCormick --- .../components/EditProblemView/AnswerWidget/messages.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/messages.js b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/messages.js index 3dd90e1ba3..c0dcec2528 100644 --- a/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/messages.js +++ b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/messages.js @@ -84,7 +84,7 @@ const messages = defineMessages({ }, answerNumericErrorText: { id: 'authoring.answerwidget.answer.answerNumericErrorText', - defaultMessage: 'Error: This problem type only supports numeric answers. Did you mean to make a (Text/Math Expression) Input problem?', + defaultMessage: 'Error: This input type only supports numeric answers. Did you mean to make a Text input or Math expression input problem?', description: 'Error message when user provides wrong format', }, }); From c24cb0da1112522c6b715429723050ed50d81ae8 Mon Sep 17 00:00:00 2001 From: Jesus Balderrama Date: Mon, 24 Nov 2025 10:41:53 -0600 Subject: [PATCH 4/8] feat(validation): validation added to numeric input with new endpoint to see if is a valid math expression --- .../AnswerWidget/AnswerOption.jsx | 12 +++----- .../EditProblemView/AnswerWidget/hooks.js | 27 ++++++++++++++++-- .../ProblemEditor/data/OLXParser.js | 1 - src/editors/data/constants/requests.ts | 1 + src/editors/data/images/numericalInput.png | Bin 7320 -> 2710 bytes src/editors/data/redux/index.ts | 1 + src/editors/data/redux/problem/reducers.ts | 1 + src/editors/data/redux/problem/selectors.ts | 1 + .../data/redux/thunkActions/problem.ts | 15 +++++++++- .../data/redux/thunkActions/requests.js | 16 +++++++++++ src/editors/data/services/cms/api.ts | 8 ++++++ src/editors/data/services/cms/urls.ts | 4 +++ 12 files changed, 75 insertions(+), 12 deletions(-) diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/AnswerOption.jsx b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/AnswerOption.jsx index 27e1a5ca8e..351932d716 100644 --- a/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/AnswerOption.jsx +++ b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/AnswerOption.jsx @@ -18,7 +18,7 @@ import { FeedbackBox } from './components/Feedback'; import * as hooks from './hooks'; import { ProblemTypeKeys } from '../../../../../data/constants/problem'; import ExpandableTextArea from '../../../../../sharedComponents/ExpandableTextArea'; -import { answerRangeFormatRegex, numericRegex } from '../../../data/OLXParser'; +import { answerRangeFormatRegex } from '../../../data/OLXParser'; const AnswerOption = ({ answer, @@ -28,6 +28,7 @@ const AnswerOption = ({ const dispatch = useDispatch(); const problemType = useSelector(selectors.problem.problemType); + const isNumericInputValid = useSelector(selectors.problem.isNumericInputValid); const images = useSelector(selectors.app.images); const isLibrary = useSelector(selectors.app.isLibrary); const learningContextId = useSelector(selectors.app.learningContextId); @@ -53,10 +54,6 @@ const AnswerOption = ({ const cleanedValue = value.replace(/^\s+|\s+$/g, ''); return !cleanedValue.length || answerRangeFormatRegex.test(cleanedValue); }; - const validateAnswerNumeric = (value) => { - const cleanedValue = (value ?? '').trim(); - return !cleanedValue.length || numericRegex.test(cleanedValue); - }; const getInputArea = () => { if ([ProblemTypeKeys.SINGLESELECT, ProblemTypeKeys.MULTISELECT].includes(problemType)) { @@ -74,9 +71,8 @@ const AnswerOption = ({ ); } if (problemType !== ProblemTypeKeys.NUMERIC || !answer.isAnswerRange) { - const isValidValue = validateAnswerNumeric(answer.title); return ( - + - {!isValidValue && ( + {!isNumericInputValid && ( diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/hooks.js b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/hooks.js index 9176d7b09a..411b9a7cc3 100644 --- a/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/hooks.js +++ b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/hooks.js @@ -5,7 +5,7 @@ import { StrictDict } from '../../../../../utils'; // should be re-thought and cleaned up to avoid this pattern. // eslint-disable-next-line import/no-self-import import * as module from './hooks'; -import { actions } from '../../../../../data/redux'; +import { actions, thunkActions } from '../../../../../data/redux'; import { ProblemTypeKeys } from '../../../../../data/constants/problem'; import { fetchEditorContent } from '../hooks'; @@ -29,6 +29,17 @@ export const setAnswer = ({ answer, hasSingleAnswer, dispatch }) => (payload) => dispatch(actions.problem.updateAnswer({ id: answer.id, hasSingleAnswer, ...payload })); }; +export const validateInputBlock = ({ + title, dispatch, +}) => { + if (!title) { + return; + } + dispatch(thunkActions.problem.validateBlockNumericInput({ + title, + })); +}; + export const setAnswerTitle = ({ answer, hasSingleAnswer, @@ -43,6 +54,11 @@ export const setAnswerTitle = ({ if (isDirty !== undefined) { dispatch(actions.problem.setDirty(isDirty)); } + + // For numeric problems, validate input on title change + if (problemType === ProblemTypeKeys.NUMERIC) { + validateInputBlock({ title, dispatch }); + } }; export const setSelectedFeedback = ({ answer, hasSingleAnswer, dispatch }) => (value) => { @@ -106,5 +122,12 @@ export const useAnswerContainer = ({ answers, updateField }) => { }; export default { - state, removeAnswer, setAnswer, setAnswerTitle, useFeedback, isSingleAnswerProblem, useAnswerContainer, + state, + removeAnswer, + setAnswer, + setAnswerTitle, + useFeedback, + isSingleAnswerProblem, + useAnswerContainer, + validateInputBlock, }; diff --git a/src/editors/containers/ProblemEditor/data/OLXParser.js b/src/editors/containers/ProblemEditor/data/OLXParser.js index 9d3c9d10bb..5f2c25f9d3 100644 --- a/src/editors/containers/ProblemEditor/data/OLXParser.js +++ b/src/editors/containers/ProblemEditor/data/OLXParser.js @@ -96,7 +96,6 @@ export const responseKeys = [ * [] */ export const answerRangeFormatRegex = /^[([]\s*-?(?:\d+(?:\.\d+)?|\d+\/\d+)\s*,\s*-?(?:\d+(?:\.\d+)?|\d+\/\d+)\s*[)\]]$/m; -export const numericRegex = /^[+-]?(\d+(\.\d*)?|\.\d+)$/; export const stripNonTextTags = ({ input, tag }) => { const stripedTags = {}; Object.entries(input).forEach(([key, value]) => { diff --git a/src/editors/data/constants/requests.ts b/src/editors/data/constants/requests.ts index f9f907bedc..85b77cb2c7 100644 --- a/src/editors/data/constants/requests.ts +++ b/src/editors/data/constants/requests.ts @@ -30,4 +30,5 @@ export const RequestKeys = StrictDict({ fetchAdvancedSettings: 'fetchAdvancedSettings', fetchVideoFeatures: 'fetchVideoFeatures', getHandlerUrl: 'getHandlerUrl', + validateBlockNumericInput: 'validateBlockNumericInput', } as const); diff --git a/src/editors/data/images/numericalInput.png b/src/editors/data/images/numericalInput.png index 535631d3b6d16fb23e6bf1ce89cbd6a788fef9c3..b4a285a5f90e2ac2ea374d20d04633021350847a 100644 GIT binary patch literal 2710 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 O0S~o}w6Gesk^ci1ntuua literal 7320 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 W$6nltB;&xrz!f74L&_!RhyMf1A(Te| diff --git a/src/editors/data/redux/index.ts b/src/editors/data/redux/index.ts index effe1a7164..66a5746a13 100644 --- a/src/editors/data/redux/index.ts +++ b/src/editors/data/redux/index.ts @@ -157,6 +157,7 @@ export interface EditorState { rawOLX: string; rawMarkdown: string; problemType: null | ProblemType | AdvancedProblemType; + isNumericInputValid: boolean; /** * Is the "markdown" editor currently active (as opposed to visual or advanced editors) * This is confusingly named, and different from `isMarkdownEditorEnabledForContext` diff --git a/src/editors/data/redux/problem/reducers.ts b/src/editors/data/redux/problem/reducers.ts index 95189dadb7..9ed320a818 100644 --- a/src/editors/data/redux/problem/reducers.ts +++ b/src/editors/data/redux/problem/reducers.ts @@ -13,6 +13,7 @@ const initialState: EditorState['problem'] = { rawMarkdown: '', isMarkdownEditorEnabled: false, problemType: null, + isNumericInputValid: true, question: '', answers: [], correctAnswerCount: 0, diff --git a/src/editors/data/redux/problem/selectors.ts b/src/editors/data/redux/problem/selectors.ts index 70d417b5e2..36c08b8fe1 100644 --- a/src/editors/data/redux/problem/selectors.ts +++ b/src/editors/data/redux/problem/selectors.ts @@ -18,6 +18,7 @@ export const simpleSelectors = { defaultSettings: mkSimpleSelector(problemData => problemData.defaultSettings), completeState: mkSimpleSelector(problemData => problemData), isDirty: mkSimpleSelector(problemData => problemData.isDirty), + isNumericInputValid: mkSimpleSelector(problemData => problemData.isNumericInputValid), }; export default simpleSelectors; diff --git a/src/editors/data/redux/thunkActions/problem.ts b/src/editors/data/redux/thunkActions/problem.ts index 24068fc908..7e533396b7 100644 --- a/src/editors/data/redux/thunkActions/problem.ts +++ b/src/editors/data/redux/thunkActions/problem.ts @@ -138,6 +138,19 @@ export const initializeProblem = (blockValue) => (dispatch, getState) => { } }; +export const validateBlockNumericInput = ({ title, ...rest }) => (dispatch) => { + dispatch(requests.validateNumericInput({ + title, + ...rest, + onSuccess: (response) => { + dispatch(actions.problem.updateField({ isNumericInputValid: response.data.is_valid })); + }, + onFailure: () => { + dispatch(actions.problem.updateField({ isNumericInputValid: false })); + }, + })); +}; + export default { - initializeProblem, switchEditor, switchToAdvancedEditor, fetchAdvancedSettings, + initializeProblem, switchEditor, switchToAdvancedEditor, fetchAdvancedSettings, validateBlockNumericInput, }; diff --git a/src/editors/data/redux/thunkActions/requests.js b/src/editors/data/redux/thunkActions/requests.js index b4e49bf0fb..6c24a23e26 100644 --- a/src/editors/data/redux/thunkActions/requests.js +++ b/src/editors/data/redux/thunkActions/requests.js @@ -485,6 +485,21 @@ export const uploadVideo = ({ data, ...rest }) => (dispatch, getState) => { })); }; +export const validateNumericInput = ({ title, ...rest }) => (dispatch, getState) => { + dispatch(module.networkRequest({ + requestKey: RequestKeys.validateBlockNumericInput, + promise: api.validateBlockNumericInput({ + blockId: selectors.app.blockId(getState()), + blockType: selectors.app.blockType(getState()), + learningContextId: selectors.app.learningContextId(getState()), + data: { formula: title }, + studioEndpointUrl: selectors.app.studioEndpointUrl(getState()), + title: selectors.app.blockTitle(getState()), + }), + ...rest, + })); +}; + export default StrictDict({ fetchBlock, fetchStudioView, @@ -507,4 +522,5 @@ export default StrictDict({ fetchVideoFeatures, uploadVideo, getHandlerlUrl, + validateNumericInput, }); diff --git a/src/editors/data/services/cms/api.ts b/src/editors/data/services/cms/api.ts index ec4cd7d760..c4bd36c4a3 100644 --- a/src/editors/data/services/cms/api.ts +++ b/src/editors/data/services/cms/api.ts @@ -390,6 +390,14 @@ export const apiMethods = { }) => get( urls.handlerUrl({ studioEndpointUrl, blockId, handlerName }), ), + validateBlockNumericInput: ({ + studioEndpointUrl, + blockId, + data, + }) => post( + urls.validateNumericInputUrl({ studioEndpointUrl, blockId }), + data, + ), }; export default apiMethods; diff --git a/src/editors/data/services/cms/urls.ts b/src/editors/data/services/cms/urls.ts index 6da1e22038..531a8ab04b 100644 --- a/src/editors/data/services/cms/urls.ts +++ b/src/editors/data/services/cms/urls.ts @@ -123,3 +123,7 @@ export const courseVideos = (({ studioEndpointUrl, learningContextId }) => ( export const handlerUrl = (({ studioEndpointUrl, blockId, handlerName }) => ( `${studioEndpointUrl}/api/xblock/v2/xblocks/${blockId}/handler_url/${handlerName}/` )) satisfies UrlFunction; + +export const validateNumericInputUrl = (({ studioEndpointUrl }) => ( + `${studioEndpointUrl}/api/courses/v1/validate/numerical-input/` +)) satisfies UrlFunction; From 49ac0700ce286e9c34bac7ed0f5353c49a76a5b0 Mon Sep 17 00:00:00 2001 From: Jesus Balderrama Date: Mon, 24 Nov 2025 18:18:28 -0600 Subject: [PATCH 5/8] fix(content): change in input validation to use react query instead of redux --- .../AnswerWidget/AnswerOption.jsx | 13 ++++++++----- .../EditProblemView/AnswerWidget/hooks.js | 19 +------------------ .../containers/ProblemEditor/data/apiHooks.ts | 19 +++++++++++++++++++ src/editors/data/redux/index.ts | 1 - src/editors/data/redux/problem/reducers.ts | 1 - src/editors/data/redux/problem/selectors.ts | 1 - .../data/redux/thunkActions/problem.ts | 15 +-------------- .../data/redux/thunkActions/requests.js | 16 ---------------- src/editors/data/services/cms/api.ts | 3 +-- 9 files changed, 30 insertions(+), 58 deletions(-) create mode 100644 src/editors/containers/ProblemEditor/data/apiHooks.ts diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/AnswerOption.jsx b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/AnswerOption.jsx index 351932d716..9607fe23e2 100644 --- a/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/AnswerOption.jsx +++ b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/AnswerOption.jsx @@ -19,6 +19,7 @@ import * as hooks from './hooks'; import { ProblemTypeKeys } from '../../../../../data/constants/problem'; import ExpandableTextArea from '../../../../../sharedComponents/ExpandableTextArea'; import { answerRangeFormatRegex } from '../../../data/OLXParser'; +import { useValidateInputBlock } from '../../../data/apiHooks'; const AnswerOption = ({ answer, @@ -28,12 +29,10 @@ const AnswerOption = ({ const dispatch = useDispatch(); const problemType = useSelector(selectors.problem.problemType); - const isNumericInputValid = useSelector(selectors.problem.isNumericInputValid); const images = useSelector(selectors.app.images); const isLibrary = useSelector(selectors.app.isLibrary); const learningContextId = useSelector(selectors.app.learningContextId); const blockId = useSelector(selectors.app.blockId); - const removeAnswer = hooks.removeAnswer({ answer, dispatch }); const setAnswer = hooks.setAnswer({ answer, hasSingleAnswer, dispatch }); const setAnswerTitle = hooks.setAnswerTitle({ @@ -45,6 +44,7 @@ const AnswerOption = ({ const setSelectedFeedback = hooks.setSelectedFeedback({ answer, hasSingleAnswer, dispatch }); const setUnselectedFeedback = hooks.setUnselectedFeedback({ answer, hasSingleAnswer, dispatch }); const { isFeedbackVisible, toggleFeedback } = hooks.useFeedback(answer); + const { data = { is_valid: true }, mutate } = useValidateInputBlock(); const staticRootUrl = isLibrary ? `${getConfig().STUDIO_BASE_URL}/library_assets/blocks/${blockId}/` @@ -72,18 +72,21 @@ const AnswerOption = ({ } if (problemType !== ProblemTypeKeys.NUMERIC || !answer.isAnswerRange) { return ( - + { + setAnswerTitle(e); + mutate({ title: e.target.value }); + }} placeholder={intl.formatMessage(messages.answerTextboxPlaceholder)} /> - {!isNumericInputValid && ( + {(!data?.is_valid ?? true) && ( diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/hooks.js b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/hooks.js index 411b9a7cc3..22d598fee8 100644 --- a/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/hooks.js +++ b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/hooks.js @@ -5,7 +5,7 @@ import { StrictDict } from '../../../../../utils'; // should be re-thought and cleaned up to avoid this pattern. // eslint-disable-next-line import/no-self-import import * as module from './hooks'; -import { actions, thunkActions } from '../../../../../data/redux'; +import { actions } from '../../../../../data/redux'; import { ProblemTypeKeys } from '../../../../../data/constants/problem'; import { fetchEditorContent } from '../hooks'; @@ -29,17 +29,6 @@ export const setAnswer = ({ answer, hasSingleAnswer, dispatch }) => (payload) => dispatch(actions.problem.updateAnswer({ id: answer.id, hasSingleAnswer, ...payload })); }; -export const validateInputBlock = ({ - title, dispatch, -}) => { - if (!title) { - return; - } - dispatch(thunkActions.problem.validateBlockNumericInput({ - title, - })); -}; - export const setAnswerTitle = ({ answer, hasSingleAnswer, @@ -54,11 +43,6 @@ export const setAnswerTitle = ({ if (isDirty !== undefined) { dispatch(actions.problem.setDirty(isDirty)); } - - // For numeric problems, validate input on title change - if (problemType === ProblemTypeKeys.NUMERIC) { - validateInputBlock({ title, dispatch }); - } }; export const setSelectedFeedback = ({ answer, hasSingleAnswer, dispatch }) => (value) => { @@ -129,5 +113,4 @@ export default { useFeedback, isSingleAnswerProblem, useAnswerContainer, - validateInputBlock, }; diff --git a/src/editors/containers/ProblemEditor/data/apiHooks.ts b/src/editors/containers/ProblemEditor/data/apiHooks.ts new file mode 100644 index 0000000000..f166d9919c --- /dev/null +++ b/src/editors/containers/ProblemEditor/data/apiHooks.ts @@ -0,0 +1,19 @@ +import { useMutation } from '@tanstack/react-query'; +import { getConfig } from '@edx/frontend-platform'; +import api from '@src/editors/data/services/cms/api'; + +const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL; + +export const useValidateInputBlock = () => useMutation({ + mutationFn: async ({ title }) => { + try { + const res = await api.validateBlockNumericInput({ studioEndpointUrl: `${getApiBaseUrl()}`, data: { formula: title } }); + return res.data; + } catch (err) { + return { + is_valid: false, + error: err.response?.data?.error ?? 'Unknown error', + }; + } + }, +}); diff --git a/src/editors/data/redux/index.ts b/src/editors/data/redux/index.ts index 66a5746a13..effe1a7164 100644 --- a/src/editors/data/redux/index.ts +++ b/src/editors/data/redux/index.ts @@ -157,7 +157,6 @@ export interface EditorState { rawOLX: string; rawMarkdown: string; problemType: null | ProblemType | AdvancedProblemType; - isNumericInputValid: boolean; /** * Is the "markdown" editor currently active (as opposed to visual or advanced editors) * This is confusingly named, and different from `isMarkdownEditorEnabledForContext` diff --git a/src/editors/data/redux/problem/reducers.ts b/src/editors/data/redux/problem/reducers.ts index 9ed320a818..95189dadb7 100644 --- a/src/editors/data/redux/problem/reducers.ts +++ b/src/editors/data/redux/problem/reducers.ts @@ -13,7 +13,6 @@ const initialState: EditorState['problem'] = { rawMarkdown: '', isMarkdownEditorEnabled: false, problemType: null, - isNumericInputValid: true, question: '', answers: [], correctAnswerCount: 0, diff --git a/src/editors/data/redux/problem/selectors.ts b/src/editors/data/redux/problem/selectors.ts index 36c08b8fe1..70d417b5e2 100644 --- a/src/editors/data/redux/problem/selectors.ts +++ b/src/editors/data/redux/problem/selectors.ts @@ -18,7 +18,6 @@ export const simpleSelectors = { defaultSettings: mkSimpleSelector(problemData => problemData.defaultSettings), completeState: mkSimpleSelector(problemData => problemData), isDirty: mkSimpleSelector(problemData => problemData.isDirty), - isNumericInputValid: mkSimpleSelector(problemData => problemData.isNumericInputValid), }; export default simpleSelectors; diff --git a/src/editors/data/redux/thunkActions/problem.ts b/src/editors/data/redux/thunkActions/problem.ts index 7e533396b7..24068fc908 100644 --- a/src/editors/data/redux/thunkActions/problem.ts +++ b/src/editors/data/redux/thunkActions/problem.ts @@ -138,19 +138,6 @@ export const initializeProblem = (blockValue) => (dispatch, getState) => { } }; -export const validateBlockNumericInput = ({ title, ...rest }) => (dispatch) => { - dispatch(requests.validateNumericInput({ - title, - ...rest, - onSuccess: (response) => { - dispatch(actions.problem.updateField({ isNumericInputValid: response.data.is_valid })); - }, - onFailure: () => { - dispatch(actions.problem.updateField({ isNumericInputValid: false })); - }, - })); -}; - export default { - initializeProblem, switchEditor, switchToAdvancedEditor, fetchAdvancedSettings, validateBlockNumericInput, + initializeProblem, switchEditor, switchToAdvancedEditor, fetchAdvancedSettings, }; diff --git a/src/editors/data/redux/thunkActions/requests.js b/src/editors/data/redux/thunkActions/requests.js index 6c24a23e26..b4e49bf0fb 100644 --- a/src/editors/data/redux/thunkActions/requests.js +++ b/src/editors/data/redux/thunkActions/requests.js @@ -485,21 +485,6 @@ export const uploadVideo = ({ data, ...rest }) => (dispatch, getState) => { })); }; -export const validateNumericInput = ({ title, ...rest }) => (dispatch, getState) => { - dispatch(module.networkRequest({ - requestKey: RequestKeys.validateBlockNumericInput, - promise: api.validateBlockNumericInput({ - blockId: selectors.app.blockId(getState()), - blockType: selectors.app.blockType(getState()), - learningContextId: selectors.app.learningContextId(getState()), - data: { formula: title }, - studioEndpointUrl: selectors.app.studioEndpointUrl(getState()), - title: selectors.app.blockTitle(getState()), - }), - ...rest, - })); -}; - export default StrictDict({ fetchBlock, fetchStudioView, @@ -522,5 +507,4 @@ export default StrictDict({ fetchVideoFeatures, uploadVideo, getHandlerlUrl, - validateNumericInput, }); diff --git a/src/editors/data/services/cms/api.ts b/src/editors/data/services/cms/api.ts index c4bd36c4a3..aceb01c763 100644 --- a/src/editors/data/services/cms/api.ts +++ b/src/editors/data/services/cms/api.ts @@ -392,10 +392,9 @@ export const apiMethods = { ), validateBlockNumericInput: ({ studioEndpointUrl, - blockId, data, }) => post( - urls.validateNumericInputUrl({ studioEndpointUrl, blockId }), + urls.validateNumericInputUrl({ studioEndpointUrl }), data, ), }; From 0b72cd93d49e13572cc27913a15abf5a3e939c15 Mon Sep 17 00:00:00 2001 From: Jesus Balderrama Date: Mon, 24 Nov 2025 18:36:16 -0600 Subject: [PATCH 6/8] fix(content): change in types to avoid ci errors --- .../components/EditProblemView/AnswerWidget/AnswerOption.jsx | 2 +- src/editors/containers/ProblemEditor/data/apiHooks.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/AnswerOption.jsx b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/AnswerOption.jsx index 9607fe23e2..2e762398ad 100644 --- a/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/AnswerOption.jsx +++ b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/AnswerOption.jsx @@ -81,7 +81,7 @@ const AnswerOption = ({ value={answer.title} onChange={(e) => { setAnswerTitle(e); - mutate({ title: e.target.value }); + mutate(e.target.value); }} placeholder={intl.formatMessage(messages.answerTextboxPlaceholder)} diff --git a/src/editors/containers/ProblemEditor/data/apiHooks.ts b/src/editors/containers/ProblemEditor/data/apiHooks.ts index f166d9919c..9e91f76440 100644 --- a/src/editors/containers/ProblemEditor/data/apiHooks.ts +++ b/src/editors/containers/ProblemEditor/data/apiHooks.ts @@ -5,11 +5,11 @@ import api from '@src/editors/data/services/cms/api'; const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL; export const useValidateInputBlock = () => useMutation({ - mutationFn: async ({ title }) => { + mutationFn: async (title) => { try { const res = await api.validateBlockNumericInput({ studioEndpointUrl: `${getApiBaseUrl()}`, data: { formula: title } }); return res.data; - } catch (err) { + } catch (err: any) { return { is_valid: false, error: err.response?.data?.error ?? 'Unknown error', From 9b57066d825f2057e8ce21904516c11c54a02d21 Mon Sep 17 00:00:00 2001 From: Jesus Balderrama Date: Tue, 25 Nov 2025 09:19:56 -0600 Subject: [PATCH 7/8] fix(content): remove unnecessary code after changing to react query --- .../components/EditProblemView/AnswerWidget/hooks.js | 8 +------- src/editors/containers/ProblemEditor/data/OLXParser.js | 1 + src/editors/data/constants/requests.ts | 1 - 3 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/hooks.js b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/hooks.js index 22d598fee8..9176d7b09a 100644 --- a/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/hooks.js +++ b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/hooks.js @@ -106,11 +106,5 @@ export const useAnswerContainer = ({ answers, updateField }) => { }; export default { - state, - removeAnswer, - setAnswer, - setAnswerTitle, - useFeedback, - isSingleAnswerProblem, - useAnswerContainer, + state, removeAnswer, setAnswer, setAnswerTitle, useFeedback, isSingleAnswerProblem, useAnswerContainer, }; diff --git a/src/editors/containers/ProblemEditor/data/OLXParser.js b/src/editors/containers/ProblemEditor/data/OLXParser.js index 5f2c25f9d3..63254cffd7 100644 --- a/src/editors/containers/ProblemEditor/data/OLXParser.js +++ b/src/editors/containers/ProblemEditor/data/OLXParser.js @@ -96,6 +96,7 @@ export const responseKeys = [ * [] */ export const answerRangeFormatRegex = /^[([]\s*-?(?:\d+(?:\.\d+)?|\d+\/\d+)\s*,\s*-?(?:\d+(?:\.\d+)?|\d+\/\d+)\s*[)\]]$/m; + export const stripNonTextTags = ({ input, tag }) => { const stripedTags = {}; Object.entries(input).forEach(([key, value]) => { diff --git a/src/editors/data/constants/requests.ts b/src/editors/data/constants/requests.ts index 85b77cb2c7..f9f907bedc 100644 --- a/src/editors/data/constants/requests.ts +++ b/src/editors/data/constants/requests.ts @@ -30,5 +30,4 @@ export const RequestKeys = StrictDict({ fetchAdvancedSettings: 'fetchAdvancedSettings', fetchVideoFeatures: 'fetchVideoFeatures', getHandlerUrl: 'getHandlerUrl', - validateBlockNumericInput: 'validateBlockNumericInput', } as const); From f9ce38107b4db61a822ee4728f5a7bc2a1560443 Mon Sep 17 00:00:00 2001 From: Jesus Balderrama Date: Fri, 28 Nov 2025 15:13:43 -0600 Subject: [PATCH 8/8] fix(content): change numeric input validation path to new url and loader added --- .../EditProblemView/AnswerWidget/AnswerOption.jsx | 6 +++++- src/editors/data/services/cms/urls.ts | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/AnswerOption.jsx b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/AnswerOption.jsx index 2e762398ad..1f9ff16e80 100644 --- a/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/AnswerOption.jsx +++ b/src/editors/containers/ProblemEditor/components/EditProblemView/AnswerWidget/AnswerOption.jsx @@ -6,6 +6,7 @@ import { Icon, IconButton, Form, + Spinner, } from '@openedx/paragon'; import { FeedbackOutline, DeleteOutline } from '@openedx/paragon/icons'; import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n'; @@ -44,7 +45,7 @@ const AnswerOption = ({ const setSelectedFeedback = hooks.setSelectedFeedback({ answer, hasSingleAnswer, dispatch }); const setUnselectedFeedback = hooks.setUnselectedFeedback({ answer, hasSingleAnswer, dispatch }); const { isFeedbackVisible, toggleFeedback } = hooks.useFeedback(answer); - const { data = { is_valid: true }, mutate } = useValidateInputBlock(); + const { data = { is_valid: true }, mutate, isPending } = useValidateInputBlock(); const staticRootUrl = isLibrary ? `${getConfig().STUDIO_BASE_URL}/library_assets/blocks/${blockId}/` @@ -91,6 +92,9 @@ const AnswerOption = ({ )} + {isPending && ( + + )} ); } diff --git a/src/editors/data/services/cms/urls.ts b/src/editors/data/services/cms/urls.ts index 531a8ab04b..28398055fa 100644 --- a/src/editors/data/services/cms/urls.ts +++ b/src/editors/data/services/cms/urls.ts @@ -125,5 +125,5 @@ export const handlerUrl = (({ studioEndpointUrl, blockId, handlerName }) => ( )) satisfies UrlFunction; export const validateNumericInputUrl = (({ studioEndpointUrl }) => ( - `${studioEndpointUrl}/api/courses/v1/validate/numerical-input/` + `${studioEndpointUrl}/api/contentstore/v2/validate/numerical-input/` )) satisfies UrlFunction;