From f8e1d93e92459a724fdc8b16a3bb5e1b6036f3c0 Mon Sep 17 00:00:00 2001 From: Devesh Tiwari Date: Wed, 15 Sep 2021 17:12:59 +0530 Subject: [PATCH 01/18] PMP-1785 | JAN-606 --- .../components/common/PartComponent.tsx | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/assets/src/components/activities/adaptive/components/common/PartComponent.tsx b/assets/src/components/activities/adaptive/components/common/PartComponent.tsx index abb8748027c..02eecb9529c 100644 --- a/assets/src/components/activities/adaptive/components/common/PartComponent.tsx +++ b/assets/src/components/activities/adaptive/components/common/PartComponent.tsx @@ -47,6 +47,43 @@ const PartComponent: React.FC = (props) => { cancelconfigure: (props as AuthorProps).onCancelConfigure || stubHandler, }; + const handleiFrameStylingChanges = (currentStateSnapshot: Record) => { + //janus-capi-iframe is the only component that allows a user to change it's position and some other style attributes + if (props.type === 'janus-capi-iframe') { + const externalActivityStyles: CSSProperties = {}; + const sX: any = currentStateSnapshot[`stage.${props.id}.IFRAME_frameX`]; + if (sX !== undefined) { + externalActivityStyles.left = sX; + } + + const sY: any = currentStateSnapshot[`stage.${props.id}.IFRAME_frameY`]; + if (sY !== undefined) { + externalActivityStyles.top = sY; + } + + const sZ: any = currentStateSnapshot[`stage.${props.id}.IFRAME_frameZ`]; + if (sZ !== undefined) { + externalActivityStyles.zIndex = sZ; + } + + const sWidth: any = currentStateSnapshot[`stage.${props.id}.IFRAME_frameWidth`]; + if (sWidth !== undefined) { + externalActivityStyles.width = sWidth; + } + + const sHeight: any = currentStateSnapshot[`stage.${props.id}.IFRAME_frameHeight`]; + if (sHeight !== undefined) { + externalActivityStyles.height = sHeight; + } + setComponentStyle({ ...componentStyle, ...externalActivityStyles }); + + const sCssClass: any = currentStateSnapshot[`stage.${props.id}.IFRAME_frameCssClass`]; + if (sCssClass !== undefined) { + setCustomCssClass(sCssClass); + } + } + }; + const ref = useRef(null); useEffect(() => { if (!pusherContext) { @@ -64,6 +101,9 @@ const PartComponent: React.FC = (props) => { const el = ref.current; if (el) { if (el.notify) { + if (notificationType === NotificationType.CONTEXT_CHANGED) { + handleiFrameStylingChanges(e.snapshot); + } el.notify(notificationType.toString(), e); } } From 3c6219d5a22e415e18571983f6899a3f0f06ff00 Mon Sep 17 00:00:00 2001 From: Devesh Tiwari Date: Wed, 15 Sep 2021 17:15:56 +0530 Subject: [PATCH 02/18] undoing changes --- .../components/common/PartComponent.tsx | 40 ------------------- 1 file changed, 40 deletions(-) diff --git a/assets/src/components/activities/adaptive/components/common/PartComponent.tsx b/assets/src/components/activities/adaptive/components/common/PartComponent.tsx index 02eecb9529c..abb8748027c 100644 --- a/assets/src/components/activities/adaptive/components/common/PartComponent.tsx +++ b/assets/src/components/activities/adaptive/components/common/PartComponent.tsx @@ -47,43 +47,6 @@ const PartComponent: React.FC = (props) => { cancelconfigure: (props as AuthorProps).onCancelConfigure || stubHandler, }; - const handleiFrameStylingChanges = (currentStateSnapshot: Record) => { - //janus-capi-iframe is the only component that allows a user to change it's position and some other style attributes - if (props.type === 'janus-capi-iframe') { - const externalActivityStyles: CSSProperties = {}; - const sX: any = currentStateSnapshot[`stage.${props.id}.IFRAME_frameX`]; - if (sX !== undefined) { - externalActivityStyles.left = sX; - } - - const sY: any = currentStateSnapshot[`stage.${props.id}.IFRAME_frameY`]; - if (sY !== undefined) { - externalActivityStyles.top = sY; - } - - const sZ: any = currentStateSnapshot[`stage.${props.id}.IFRAME_frameZ`]; - if (sZ !== undefined) { - externalActivityStyles.zIndex = sZ; - } - - const sWidth: any = currentStateSnapshot[`stage.${props.id}.IFRAME_frameWidth`]; - if (sWidth !== undefined) { - externalActivityStyles.width = sWidth; - } - - const sHeight: any = currentStateSnapshot[`stage.${props.id}.IFRAME_frameHeight`]; - if (sHeight !== undefined) { - externalActivityStyles.height = sHeight; - } - setComponentStyle({ ...componentStyle, ...externalActivityStyles }); - - const sCssClass: any = currentStateSnapshot[`stage.${props.id}.IFRAME_frameCssClass`]; - if (sCssClass !== undefined) { - setCustomCssClass(sCssClass); - } - } - }; - const ref = useRef(null); useEffect(() => { if (!pusherContext) { @@ -101,9 +64,6 @@ const PartComponent: React.FC = (props) => { const el = ref.current; if (el) { if (el.notify) { - if (notificationType === NotificationType.CONTEXT_CHANGED) { - handleiFrameStylingChanges(e.snapshot); - } el.notify(notificationType.toString(), e); } } From 21a00cc88ae8a0547eb918377a8f97750b2d752c Mon Sep 17 00:00:00 2001 From: Devesh Tiwari Date: Fri, 3 Dec 2021 10:46:51 +0530 Subject: [PATCH 03/18] PMP-1064 | JAN-163 --- assets/src/adaptivity/scripting.ts | 24 +++++++----------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/assets/src/adaptivity/scripting.ts b/assets/src/adaptivity/scripting.ts index 9fc43b82ba0..58eb3a1924b 100644 --- a/assets/src/adaptivity/scripting.ts +++ b/assets/src/adaptivity/scripting.ts @@ -57,25 +57,15 @@ export const getExpressionStringForValue = ( // it might be CSS string, which can be decieving let actuallyAString = false; + const expressions = extractAllExpressionsFromText(val); + // A expression will not have a ';' inside it. So if there is a ';' inside it, it is CSS. + const isCSSString = expressions.filter((e) => e.includes(';')); + if (isCSSString?.length) { + actuallyAString = true; + } try { const testEnv = new Environment(env); - const evalResult = evalScript(`let foo = ${val};`, testEnv); - // when evalScript is executed successfully, evalResult.result is null. - // evalScript does not trigger catch block even though there is error and add the error in stack property. - if (evalResult?.result !== null) { - try { - //trying to check if it is a CSS string.This might not handle any advance CSS string. - const matchingCssElements = val.match( - /^(([a-z0-9\\[\]=:]+\s?)|((div|span|body.*|.box-sizing:*|.columns-container.*|background-color.*)?(#|\.){1}[a-z0-9\-_\s?:]+\s?)+)(\{[\s\S][^}]*})$/im, - ); - //matchingCssElements !== null then it means it's a CSS string so set actuallyAString=true so that it can be wrapped in "" - if (matchingCssElements) { - actuallyAString = true; - } - } catch (e) { - actuallyAString = true; - } - } + evalScript(`let foo = ${val};`, testEnv); } catch (e) { // if we have parsing error then we're guessing it's CSS actuallyAString = true; From c762557f8dfdafe1313144c6ede3751c773ba6c6 Mon Sep 17 00:00:00 2001 From: Devesh Tiwari Date: Wed, 15 Dec 2021 15:56:13 +0530 Subject: [PATCH 04/18] PMP-2358 | JAN-1175 --- .../apps/delivery/store/features/attempt/actions/savePart.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/assets/src/apps/delivery/store/features/attempt/actions/savePart.ts b/assets/src/apps/delivery/store/features/attempt/actions/savePart.ts index d0ea6d97042..8e7702afcca 100644 --- a/assets/src/apps/delivery/store/features/attempt/actions/savePart.ts +++ b/assets/src/apps/delivery/store/features/attempt/actions/savePart.ts @@ -4,6 +4,7 @@ import { defaultGlobalEnv, evalScript, getAssignStatements, + getValue, } from '../../../../../../adaptivity/scripting'; import { RootState } from '../../../rootReducer'; import { selectPreviewMode, selectSectionSlug } from '../../page/slice'; @@ -21,6 +22,7 @@ export const savePartState = createAsyncThunk( const rootState = getState() as RootState; const isPreviewMode = selectPreviewMode(rootState); const sectionSlug = selectSectionSlug(rootState); + const attemptNumber = getValue('session.attemptNumber', defaultGlobalEnv); // update redux state to match optimistically const attemptRecord = selectById(rootState, attemptGuid); @@ -29,6 +31,7 @@ export const savePartState = createAsyncThunk( if (partAttemptRecord) { const updated = { ...attemptRecord, + attemptNumber: attemptNumber, parts: attemptRecord.parts.map((p) => { const result = { ...p }; if (p.attemptGuid === partAttemptRecord.attemptGuid) { From ef39ad74a7e758cce535dfc5a62c5ce7eed1abd3 Mon Sep 17 00:00:00 2001 From: Devesh Tiwari Date: Fri, 7 Jan 2022 15:02:36 +0530 Subject: [PATCH 05/18] Templatize Text | Unit Test --- assets/test/adaptivity/scripting_test.ts | 26 ++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/assets/test/adaptivity/scripting_test.ts b/assets/test/adaptivity/scripting_test.ts index 29a80cf6352..0008714e551 100644 --- a/assets/test/adaptivity/scripting_test.ts +++ b/assets/test/adaptivity/scripting_test.ts @@ -8,6 +8,7 @@ import { getExpressionStringForValue, looksLikeJson, } from 'adaptivity/scripting'; +import { templatizeText } from 'apps/delivery/components/TextParser'; import { Environment } from 'janus-script'; describe('Scripting Interface', () => { @@ -247,6 +248,31 @@ describe('Scripting Interface', () => { expect(valuey).toBe(varValueFormat2); }); + it('should return the CSS as it is', () => { + const environment = new Environment(); + let text = + '@font-face{font-family:PTSerif;src:url(https://dev-etx.ws.asu.edu/fonts/PT%20Serif/PT_Serif-Web-Regular.ttf)}.button{white-space:normal;font-family:PTSerif,Georgia,serif;font-size:16px;font-weight:700;text-transform:none;line-height:120%;color:#E7A96B;width:calc(100% - 2px);height:auto!important;background-color:#484848;background-image:linear-gradient(rgba(0,0,0,0),rgba(0,0,0,.6));border-radius:3px;border:none;-moz-box-shadow:2px 2px rgba(0,0,0,.2);-webkit-box-shadow:2px 2px rgba(0,0,0,.2);box-shadow:2px 2px rgba(0,0,0,.2);padding:10px 20px;cursor:pointer}.button:active,.button:focus,.button:hover{background-color:#5C5C5C!important;background-image:linear-gradient(rgba(0,0,0,0),rgba(0,0,0,.6))}.button:focus,.button:hover{-moz-box-shadow:2px 2px rgba(0,0,0,.2);-webkit-box-shadow:2px 2px rgba(0,0,0,.2);box-shadow:2px 2px rgba(0,0,0,.2)}.button:active{-moz-box-shadow:inset 0 2px rgba(0,0,0,.4);-webkit-box-shadow:inset 0 2px rgba(0,0,0,.4);box-shadow:inset 0 2px rgba(0,0,0,.4);transform:translateY(1px);color:#E7A96B!important}.button:disabled{background-color:#858585;cursor:default}.button:disabled:active{-moz-box- shadow:inset 0 0 transparent;-webkit-box-shadow:inset 0 0 transparent;box-shadow:inset 0 0 transparent;color:rgba(255,255,255,.9);transform:translateY(0)}'; + let result = templatizeText(text, environment); + expect(result).toBe(text); + + text = 'stage.foo.value = {stage.foo.value}; stage.foo1.value = {stage.foo1.value};'; + evalScript( + 'let {stage.foo.value} = 1;let {stage.foo1.value}=80;let {stage.foo2.value}=50', + environment, + ); + result = templatizeText(text, environment, environment); + expect(result).toBe('stage.foo.value = 1; stage.foo1.value = 80;'); + + text = 'Lets try with variables {variables.foo}'; + evalScript('let {variables.foo} = 0.529', environment); + result = templatizeText(text, environment, environment); + expect(result).toBe('Lets try with variables 0.529'); + + text = 'Lets try with variables {variables.foo'; + result = templatizeText(text, environment); + expect(result).toBe(text); + }); + it('it should return math expression as it is', () => { const environment = new Environment(); From 331da3662967160bea08402155854e209fa5751d Mon Sep 17 00:00:00 2001 From: Devesh Tiwari Date: Fri, 7 Jan 2022 15:05:47 +0530 Subject: [PATCH 06/18] reverting changes --- assets/src/adaptivity/scripting.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/assets/src/adaptivity/scripting.ts b/assets/src/adaptivity/scripting.ts index a96b0661d20..fa1e64fb324 100644 --- a/assets/src/adaptivity/scripting.ts +++ b/assets/src/adaptivity/scripting.ts @@ -63,7 +63,6 @@ export const getExpressionStringForValue = ( if (isCSSString?.length) { actuallyAString = true; } - if (!actuallyAString) { try { const testEnv = new Environment(env); From c5d49eabf95fcb115e4b06ccf64ce965ec7dd094 Mon Sep 17 00:00:00 2001 From: Devesh Tiwari Date: Thu, 24 Feb 2022 10:59:50 +0530 Subject: [PATCH 07/18] PMP-2480 | JAN-1311 --- assets/src/adaptivity/scripting.ts | 2 +- .../delivery/components/ActivityRenderer.tsx | 12 ++++++++++-- .../delivery/layouts/deck/DeckLayoutFooter.tsx | 18 +++++++++++++++--- .../store/features/groups/actions/deck.ts | 7 ++++++- 4 files changed, 32 insertions(+), 7 deletions(-) diff --git a/assets/src/adaptivity/scripting.ts b/assets/src/adaptivity/scripting.ts index 12ab391c5ca..df5e10b8966 100644 --- a/assets/src/adaptivity/scripting.ts +++ b/assets/src/adaptivity/scripting.ts @@ -372,7 +372,7 @@ export const extractExpressionFromText = (text: string) => { // extract all expressions from a string export const extractAllExpressionsFromText = (text: string): string[] => { const expressions = []; - if (text.indexOf('{') !== -1 && text.indexOf('}') !== -1) { + if (text.toString().indexOf('{') !== -1 && text.toString().indexOf('}') !== -1) { const expr = extractExpressionFromText(text); const rest = text.substring(text.indexOf(expr) + expr.length + 1); expressions.push(expr); diff --git a/assets/src/apps/delivery/components/ActivityRenderer.tsx b/assets/src/apps/delivery/components/ActivityRenderer.tsx index 21d51fd04ca..dbce08fef42 100644 --- a/assets/src/apps/delivery/components/ActivityRenderer.tsx +++ b/assets/src/apps/delivery/components/ActivityRenderer.tsx @@ -367,7 +367,12 @@ const ActivityRenderer: React.FC = ({ } else { updatedValue = initObject.value; } - if (initObject.type !== CapiVariableTypes.MATH_EXPR) { + if ( + initObject.type !== CapiVariableTypes.MATH_EXPR && + updatedValue && + updatedValue.toString().indexOf('{') !== -1 && + updatedValue.toString().indexOf('}') !== -1 + ) { // need handle the value expression i.e. value = MISSION CONTROL: Search the surface of {q:1476902665616:794|stage.simIFrame.Globals.SelectedObject} for the astrocache. // otherwise, it will never be replace with actual value on screen updatedValue = handleValueExpression( @@ -377,7 +382,10 @@ const ActivityRenderer: React.FC = ({ ); } const evaluatedValue = - typeOfOriginalValue === 'string' && initObject.type !== CapiVariableTypes.MATH_EXPR + typeOfOriginalValue === 'string' && + initObject.type !== CapiVariableTypes.MATH_EXPR && + value.indexOf('{') !== -1 && + value.indexOf('}') !== -1 ? templatizeText(updatedValue, snapshot, defaultGlobalEnv, true) : updatedValue; acc[initObject.target] = evaluatedValue; diff --git a/assets/src/apps/delivery/layouts/deck/DeckLayoutFooter.tsx b/assets/src/apps/delivery/layouts/deck/DeckLayoutFooter.tsx index f7cf656d3cc..fbf6054837e 100644 --- a/assets/src/apps/delivery/layouts/deck/DeckLayoutFooter.tsx +++ b/assets/src/apps/delivery/layouts/deck/DeckLayoutFooter.tsx @@ -66,15 +66,27 @@ export const handleValueExpression = ( if (item.indexOf('|') > 0) { //Need to replace the opening and closing {} else the expression will look something like q.145225454.1|{stage.input.value} //it should be like {q.145225454.1|stage.input.value} - const modifiedValue = item.replace('{', '').replace('}', ''); - const partVariable = modifiedValue.split('|')[1]; + const modifiedValue = item; + const modifiedItem = modifiedValue.split('|'); + const partVariable = modifiedItem[1]; + const variableSplitter = modifiedValue.indexOf('|'); + // an expression might be like {round(({q.145225454.1|stage.input.value})/10)*10}, so we just want to replace the {q.145225454.1|stage.input.value} + // so getting the sequenceId and parts that will be used later to replace the value + const sequenceId = modifiedValue.substring( + modifiedItem[0].lastIndexOf('{'), + variableSplitter + 1, + ); + const parts = modifiedItem[1].substring(0, modifiedItem[1].indexOf('}') + 1); const variables = partVariable.split('.'); const ownerActivity = currentActivityTree?.find( (activity) => !!activity.content.partsLayout.find((p: any) => p.id === variables[1]), ); //ownerActivity is undefined for app.spr.adaptivity.something i.e. Beagle app variables if (ownerActivity) { - value = value.replace(`${item}`, `{${ownerActivity.id}|${partVariable}}`); + value = value.replace( + `${sequenceId}|${parts}`, + `{${ownerActivity.id}|${partVariable}}`, + ); } return; } diff --git a/assets/src/apps/delivery/store/features/groups/actions/deck.ts b/assets/src/apps/delivery/store/features/groups/actions/deck.ts index 6716a4a07f8..e114bfd8148 100644 --- a/assets/src/apps/delivery/store/features/groups/actions/deck.ts +++ b/assets/src/apps/delivery/store/features/groups/actions/deck.ts @@ -39,6 +39,7 @@ import { selectCurrentActivityTree, selectSequence } from '../selectors/deck'; import { getNextQBEntry, getParentBank } from './navUtils'; import { SequenceBank, SequenceEntry, SequenceEntryType } from './sequence'; import { GroupsSlice } from '../name'; +import { templatizeText } from 'apps/delivery/components/TextParser'; export const initializeActivity = createAsyncThunk( `${GroupsSlice}/deck/initializeActivity`, @@ -172,7 +173,11 @@ export const initializeActivity = createAsyncThunk( if (s.type === CapiVariableTypes.MATH_EXPR) { return { ...s, target: `${ownerActivity.id}|${s.target}` }; } - const modifiedValue = handleValueExpression(currentActivityTree, s.value, s.operator); + let modifiedValue = handleValueExpression(currentActivityTree, s.value, s.operator); + modifiedValue = + typeof modifiedValue === 'string' + ? templatizeText(modifiedValue, defaultGlobalEnv, defaultGlobalEnv, false) + : modifiedValue; if (!ownerActivity) { // shouldn't happen, but ignore I guess return { ...s, value: modifiedValue }; From a21227aafc20c06d1137d9c6ac3fb5d4d80ab09a Mon Sep 17 00:00:00 2001 From: Devesh Tiwari Date: Wed, 17 Apr 2024 16:44:30 +0530 Subject: [PATCH 08/18] MER-3138 --- .../apps/delivery/layouts/deck/LessonFinishedDialog.tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/assets/src/apps/delivery/layouts/deck/LessonFinishedDialog.tsx b/assets/src/apps/delivery/layouts/deck/LessonFinishedDialog.tsx index dc39348dfc4..51f7944f59c 100644 --- a/assets/src/apps/delivery/layouts/deck/LessonFinishedDialog.tsx +++ b/assets/src/apps/delivery/layouts/deck/LessonFinishedDialog.tsx @@ -2,7 +2,6 @@ import React, { Fragment, useCallback, useEffect, useState } from 'react'; import { useSelector } from 'react-redux'; import { ActionFailure, ActionResult, finalizePageAttempt } from 'data/persistence/page_lifecycle'; import { - selectIsGraded, selectPageSlug, selectPreviewMode, selectResourceAttemptGuid, @@ -24,7 +23,6 @@ const LessonFinishedDialog: React.FC = ({ const [redirectURL, setRedirectURL] = useState(''); const [finalizeError, setFinalizeError] = useState(null); const isPreviewMode = useSelector(selectPreviewMode); - const graded = useSelector(selectIsGraded); const revisionSlug = useSelector(selectPageSlug); const sectionSlug = useSelector(selectSectionSlug); const resourceAttemptGuid = useSelector(selectResourceAttemptGuid); @@ -39,7 +37,7 @@ const LessonFinishedDialog: React.FC = ({ return; } setIsOpen(false); - if (!graded || isPreviewMode) { + if (isPreviewMode) { window.location.reload(); } else { window.location.href = redirectURL; @@ -48,7 +46,7 @@ const LessonFinishedDialog: React.FC = ({ const handleFinalization = useCallback(async () => { setFinalizationCalled(true); - if (!isPreviewMode && graded) { + if (!isPreviewMode) { // only graded pages are finalized try { const finalizeResult = await finalizePageAttempt( @@ -86,7 +84,7 @@ const LessonFinishedDialog: React.FC = ({ } } setIsFinalized(true); - }, [sectionSlug, revisionSlug, resourceAttemptGuid, graded, isPreviewMode]); + }, [sectionSlug, revisionSlug, resourceAttemptGuid, isPreviewMode]); useEffect(() => { // TODO: maybe we should call finalization elsewhere than in this modal From f1b700f60344c09c78221cc34c2b89c5b6710866 Mon Sep 17 00:00:00 2001 From: Devesh Tiwari Date: Wed, 17 Apr 2024 16:47:45 +0530 Subject: [PATCH 09/18] Update LessonFinishedDialog.tsx --- .../apps/delivery/layouts/deck/LessonFinishedDialog.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/assets/src/apps/delivery/layouts/deck/LessonFinishedDialog.tsx b/assets/src/apps/delivery/layouts/deck/LessonFinishedDialog.tsx index 51f7944f59c..dc39348dfc4 100644 --- a/assets/src/apps/delivery/layouts/deck/LessonFinishedDialog.tsx +++ b/assets/src/apps/delivery/layouts/deck/LessonFinishedDialog.tsx @@ -2,6 +2,7 @@ import React, { Fragment, useCallback, useEffect, useState } from 'react'; import { useSelector } from 'react-redux'; import { ActionFailure, ActionResult, finalizePageAttempt } from 'data/persistence/page_lifecycle'; import { + selectIsGraded, selectPageSlug, selectPreviewMode, selectResourceAttemptGuid, @@ -23,6 +24,7 @@ const LessonFinishedDialog: React.FC = ({ const [redirectURL, setRedirectURL] = useState(''); const [finalizeError, setFinalizeError] = useState(null); const isPreviewMode = useSelector(selectPreviewMode); + const graded = useSelector(selectIsGraded); const revisionSlug = useSelector(selectPageSlug); const sectionSlug = useSelector(selectSectionSlug); const resourceAttemptGuid = useSelector(selectResourceAttemptGuid); @@ -37,7 +39,7 @@ const LessonFinishedDialog: React.FC = ({ return; } setIsOpen(false); - if (isPreviewMode) { + if (!graded || isPreviewMode) { window.location.reload(); } else { window.location.href = redirectURL; @@ -46,7 +48,7 @@ const LessonFinishedDialog: React.FC = ({ const handleFinalization = useCallback(async () => { setFinalizationCalled(true); - if (!isPreviewMode) { + if (!isPreviewMode && graded) { // only graded pages are finalized try { const finalizeResult = await finalizePageAttempt( @@ -84,7 +86,7 @@ const LessonFinishedDialog: React.FC = ({ } } setIsFinalized(true); - }, [sectionSlug, revisionSlug, resourceAttemptGuid, isPreviewMode]); + }, [sectionSlug, revisionSlug, resourceAttemptGuid, graded, isPreviewMode]); useEffect(() => { // TODO: maybe we should call finalization elsewhere than in this modal From 42ff4ee3c5724c74cee136afc71367d0f7bf4879 Mon Sep 17 00:00:00 2001 From: Devesh Tiwari Date: Fri, 4 Oct 2024 12:17:41 +0530 Subject: [PATCH 10/18] MER-3369 | MER-3414 | MER-3417 --- .../Flowchart/toolbar/FlowchartHeaderNav.tsx | 19 ++-- .../components/ScreenList/AddScreenModal.tsx | 104 +++++++++++++----- .../store/activities/actions/saveActivity.ts | 2 +- 3 files changed, 88 insertions(+), 37 deletions(-) diff --git a/assets/src/apps/authoring/components/Flowchart/toolbar/FlowchartHeaderNav.tsx b/assets/src/apps/authoring/components/Flowchart/toolbar/FlowchartHeaderNav.tsx index 56734607ee0..cc63e60c2f6 100644 --- a/assets/src/apps/authoring/components/Flowchart/toolbar/FlowchartHeaderNav.tsx +++ b/assets/src/apps/authoring/components/Flowchart/toolbar/FlowchartHeaderNav.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useRef } from 'react'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; import { OverlayTrigger, Tooltip } from 'react-bootstrap'; import { useDispatch, useSelector } from 'react-redux'; import { setCurrentSelection } from 'apps/authoring/store/parts/slice'; @@ -12,6 +12,7 @@ import { } from '../../../../delivery/store/features/activities/slice'; import { selectCurrentActivityTree, + selectCurrentSequenceId, selectSequence, } from '../../../../delivery/store/features/groups/selectors/deck'; import { @@ -110,10 +111,10 @@ export const FlowchartHeaderNav: React.FC = () => { const revisionSlug = useSelector(selectRevisionSlug); const availablePartComponents = useSelector(selectPartComponentTypes); const currentActivityTree = useSelector(selectCurrentActivityTree); - + const [newPartAddOffset, setNewPartAddOffset] = useState(0); const activities = useSelector(selectAllActivities); const sequence = useSelector(selectSequence); - + const currentSequenceId = useSelector(selectCurrentSequenceId); const dispatch = useDispatch(); const hasRedo = useSelector(selectHasRedo); @@ -140,6 +141,10 @@ export const FlowchartHeaderNav: React.FC = () => { const url = `/authoring/project/${projectSlug}/preview/${revisionSlug}`; const windowName = `preview-${projectSlug}`; + useEffect(() => { + setNewPartAddOffset(0); + }, [currentSequenceId]); + const previewLesson = useCallback(async () => { await dispatch(verifyFlowchartLesson({})); const invalidScreens = activities.filter( @@ -239,14 +244,14 @@ export const FlowchartHeaderNav: React.FC = () => { const PartClass = customElements.get(partComponent.authoring_element); if (PartClass) { // only ever add to the current activity, not a layer - + setNewPartAddOffset(newPartAddOffset + 1); const part = new PartClass() as any; const newPartData = { id: `${partComponentType}-${guid()}`, type: partComponent.delivery_element, custom: { - x: 10, - y: 10, + x: 10 * newPartAddOffset, // when new components are added, offset the location placed by 10 px + y: 10 * newPartAddOffset, // when new components are added, offset the location placed by 10 px z: 0, width: 100, height: 100, @@ -262,7 +267,7 @@ export const FlowchartHeaderNav: React.FC = () => { } } }, - [availablePartComponents, currentActivityTree, dispatch], + [availablePartComponents, currentActivityTree, dispatch, newPartAddOffset], ); return ( diff --git a/assets/src/apps/authoring/components/ScreenList/AddScreenModal.tsx b/assets/src/apps/authoring/components/ScreenList/AddScreenModal.tsx index 739529da35f..d7c47fe1c1b 100644 --- a/assets/src/apps/authoring/components/ScreenList/AddScreenModal.tsx +++ b/assets/src/apps/authoring/components/ScreenList/AddScreenModal.tsx @@ -1,4 +1,4 @@ -import React, { useCallback } from 'react'; +import React, { useCallback, useEffect } from 'react'; import { Button, Modal } from 'react-bootstrap'; import { AdvancedAuthoringModal } from '../AdvancedAuthoringModal'; import { ScreenIcon } from '../Flowchart/screen-icons/screen-icons'; @@ -34,13 +34,27 @@ export const screenTypeToTitle: Record = { export const AddScreenModal: React.FC = ({ onCancel, onCreate }) => { const [title, setTitle] = React.useState(''); + const [showValidationMessage, setShowValidationMessage] = React.useState(false); const [activeScreenType, setScreenType] = React.useState(null); - const onNext = useCallback(() => { + if (!validInput) { + setShowValidationMessage(true); + } else { + onCreate(title || 'Adaptive Screen', activeScreenType || 'blank_screen'); + } + }, [activeScreenType, onCreate, title]); + const onContinue = useCallback(() => { onCreate(title || 'Adaptive Screen', activeScreenType || 'blank_screen'); }, [activeScreenType, onCreate, title]); + const validInput = title.length > 0 && activeScreenType !== null; const isOnlyScreenTypeSelected = !title?.length && activeScreenType !== null; + const isOnlyScreenTitleSelected = title?.length && activeScreenType === null; + useEffect(() => { + if (validInput) { + setShowValidationMessage(false); + } + }, [validInput, showValidationMessage]); return ( = ({ onCancel, onCreate }) => { - - {isOnlyScreenTypeSelected && ( + + {showValidationMessage && !validInput && ( - Are you sure? | A screen title may - be helpful while creating a lesson. + Are you sure? | A{' '} + {isOnlyScreenTypeSelected + ? 'screen title ' + : isOnlyScreenTitleSelected + ? 'screen type' + : ' screen title and screen type'}{' '} + may be helpful while creating a lesson. )} - + )} + {showValidationMessage && ( + + Continue + + + + + )} ); diff --git a/assets/src/apps/authoring/store/activities/actions/saveActivity.ts b/assets/src/apps/authoring/store/activities/actions/saveActivity.ts index eef78a50080..50e895d23ff 100644 --- a/assets/src/apps/authoring/store/activities/actions/saveActivity.ts +++ b/assets/src/apps/authoring/store/activities/actions/saveActivity.ts @@ -62,7 +62,7 @@ export const saveActivity = createAsyncThunk( activity?.authoring?.parts, )[0]?.writable; // if the Part object of activity is read only then do try to write to it - if (!isActivityPartObjectWritable) { + if (isActivityPartObjectWritable) { // don't need the default part if another has been added activity.authoring.parts = activity.authoring.parts.filter( (part: any) => part.id !== '__default', From c9cf59de662e9b8a9d9ba0878514308d6637c2cb Mon Sep 17 00:00:00 2001 From: Devesh Tiwari Date: Wed, 14 Jan 2026 17:18:51 +0530 Subject: [PATCH 11/18] Update PopupAuthor.tsx --- .../parts/janus-popup/PopupAuthor.tsx | 99 ++++++++++++------- 1 file changed, 65 insertions(+), 34 deletions(-) diff --git a/assets/src/components/parts/janus-popup/PopupAuthor.tsx b/assets/src/components/parts/janus-popup/PopupAuthor.tsx index d0e7c2312ee..5aa919142fd 100644 --- a/assets/src/components/parts/janus-popup/PopupAuthor.tsx +++ b/assets/src/components/parts/janus-popup/PopupAuthor.tsx @@ -154,12 +154,25 @@ const PopupAuthor: React.FC> = (props) => { const iconSrc = getIconSrc(iconURL, defaultURL); - // Icon should always be fixed size (32x32), not resizable - const iconTriggerStyle: CSSProperties = { - width: 32, - height: 32, - flexShrink: 0, - }; + const shouldShowIcon = !hideIcon; + const shouldShowLabel = labelText && labelText.trim().length > 0; + + // Determine if iconSrc is a standard icon (data URL) or custom URL + const isStandardIcon = iconSrc && iconSrc.startsWith('data:'); + const isCustomIcon = iconSrc && !isStandardIcon; + + // Icon sizing: + // - Fixed 32x32px when label exists (regardless of icon type) + // - Resizable when no label (uses container size or min 32x32) + const shouldFixIconSize = shouldShowLabel; + const iconTriggerStyle: CSSProperties = shouldFixIconSize + ? { width: 32, height: 32, flexShrink: 0 } // Always fixed when label exists + : { + width: width && width > 0 ? width : undefined, + height: height && height > 0 ? height : undefined, + minWidth: 32, + minHeight: 32 + }; // When no label, use container size if set, otherwise allow resizing with min 32x32 // for authoring we don't actually want to hide it if (!visible) { @@ -167,16 +180,17 @@ const PopupAuthor: React.FC> = (props) => { } // Determine flex direction based on label position + // Label always appears first in DOM, so we use flexDirection and order to maintain visual positioning const getFlexDirection = () => { switch (labelPosition) { case 'left': - return 'row-reverse'; + return 'row'; // Label first in DOM, visually on left (no order needed) case 'right': - return 'row'; + return 'row'; // Label first in DOM, visually on right (use order) case 'top': - return 'column-reverse'; + return 'column'; // Label first in DOM, visually on top (no order needed) case 'bottom': - return 'column'; + return 'column'; // Label first in DOM, visually on bottom (use order) default: return 'row'; } @@ -231,10 +245,14 @@ const PopupAuthor: React.FC> = (props) => { overflow: 'hidden', textOverflow: 'ellipsis', wordWrap: 'break-word', + // Use CSS order to maintain visual positioning when label is first in DOM + order: labelPosition === 'right' || labelPosition === 'bottom' ? 2 : undefined, }; - const shouldShowIcon = !hideIcon; - const shouldShowLabel = labelText && labelText.trim().length > 0; + // Apply CSS order to icon for visual positioning + if (labelPosition === 'right' || labelPosition === 'bottom') { + iconTriggerStyle.order = 1; + } const init = useCallback(async () => { const initResult = await props.onInit({ id, responses: [] }); @@ -306,9 +324,26 @@ const PopupAuthor: React.FC> = (props) => { /> )}
+ {/* Label appears first in DOM for screen reader context */} + {shouldShowLabel && ( + { + setShowWindow(true); + }} + > + {labelText} + + )} + {/* Icon is decorative when label exists, focusable when no label */} {shouldShowIcon && ( > = (props) => { type: 'button', })} className={`info-icon`} - onDoubleClick={() => { - setShowWindow(true); - }} - aria-controls={id} - aria-haspopup="true" - aria-label={description} + {...(shouldShowLabel + ? {} // No event handlers when label exists (icon is decorative) + : { + onDoubleClick: () => { + setShowWindow(true); + }, + })} + aria-controls={shouldShowLabel ? undefined : id} + aria-haspopup={shouldShowLabel ? undefined : 'true'} + aria-label={ + shouldShowLabel + ? description // Icon is decorative, aria-label used for alt text + : description + ? `${description}, opens dialog` + : 'Additional Information, opens dialog' + } + tabIndex={shouldShowLabel ? -1 : 0} style={iconTriggerStyle} /> )} - {shouldShowLabel && ( - { - setShowWindow(true); - }} - > - {labelText} - - )}
{showWindow && } From af6abe648c3869b288fc34b38c07284d9286d773 Mon Sep 17 00:00:00 2001 From: Devesh Tiwari Date: Mon, 19 Jan 2026 09:21:28 +0530 Subject: [PATCH 12/18] Revert "Update PopupAuthor.tsx" This reverts commit c9cf59de662e9b8a9d9ba0878514308d6637c2cb. --- .../parts/janus-popup/PopupAuthor.tsx | 99 +++++++------------ 1 file changed, 34 insertions(+), 65 deletions(-) diff --git a/assets/src/components/parts/janus-popup/PopupAuthor.tsx b/assets/src/components/parts/janus-popup/PopupAuthor.tsx index 5aa919142fd..d0e7c2312ee 100644 --- a/assets/src/components/parts/janus-popup/PopupAuthor.tsx +++ b/assets/src/components/parts/janus-popup/PopupAuthor.tsx @@ -154,25 +154,12 @@ const PopupAuthor: React.FC> = (props) => { const iconSrc = getIconSrc(iconURL, defaultURL); - const shouldShowIcon = !hideIcon; - const shouldShowLabel = labelText && labelText.trim().length > 0; - - // Determine if iconSrc is a standard icon (data URL) or custom URL - const isStandardIcon = iconSrc && iconSrc.startsWith('data:'); - const isCustomIcon = iconSrc && !isStandardIcon; - - // Icon sizing: - // - Fixed 32x32px when label exists (regardless of icon type) - // - Resizable when no label (uses container size or min 32x32) - const shouldFixIconSize = shouldShowLabel; - const iconTriggerStyle: CSSProperties = shouldFixIconSize - ? { width: 32, height: 32, flexShrink: 0 } // Always fixed when label exists - : { - width: width && width > 0 ? width : undefined, - height: height && height > 0 ? height : undefined, - minWidth: 32, - minHeight: 32 - }; // When no label, use container size if set, otherwise allow resizing with min 32x32 + // Icon should always be fixed size (32x32), not resizable + const iconTriggerStyle: CSSProperties = { + width: 32, + height: 32, + flexShrink: 0, + }; // for authoring we don't actually want to hide it if (!visible) { @@ -180,17 +167,16 @@ const PopupAuthor: React.FC> = (props) => { } // Determine flex direction based on label position - // Label always appears first in DOM, so we use flexDirection and order to maintain visual positioning const getFlexDirection = () => { switch (labelPosition) { case 'left': - return 'row'; // Label first in DOM, visually on left (no order needed) + return 'row-reverse'; case 'right': - return 'row'; // Label first in DOM, visually on right (use order) + return 'row'; case 'top': - return 'column'; // Label first in DOM, visually on top (no order needed) + return 'column-reverse'; case 'bottom': - return 'column'; // Label first in DOM, visually on bottom (use order) + return 'column'; default: return 'row'; } @@ -245,14 +231,10 @@ const PopupAuthor: React.FC> = (props) => { overflow: 'hidden', textOverflow: 'ellipsis', wordWrap: 'break-word', - // Use CSS order to maintain visual positioning when label is first in DOM - order: labelPosition === 'right' || labelPosition === 'bottom' ? 2 : undefined, }; - // Apply CSS order to icon for visual positioning - if (labelPosition === 'right' || labelPosition === 'bottom') { - iconTriggerStyle.order = 1; - } + const shouldShowIcon = !hideIcon; + const shouldShowLabel = labelText && labelText.trim().length > 0; const init = useCallback(async () => { const initResult = await props.onInit({ id, responses: [] }); @@ -324,26 +306,9 @@ const PopupAuthor: React.FC> = (props) => { /> )}
- {/* Label appears first in DOM for screen reader context */} - {shouldShowLabel && ( - { - setShowWindow(true); - }} - > - {labelText} - - )} - {/* Icon is decorative when label exists, focusable when no label */} {shouldShowIcon && ( > = (props) => { type: 'button', })} className={`info-icon`} - {...(shouldShowLabel - ? {} // No event handlers when label exists (icon is decorative) - : { - onDoubleClick: () => { - setShowWindow(true); - }, - })} - aria-controls={shouldShowLabel ? undefined : id} - aria-haspopup={shouldShowLabel ? undefined : 'true'} - aria-label={ - shouldShowLabel - ? description // Icon is decorative, aria-label used for alt text - : description - ? `${description}, opens dialog` - : 'Additional Information, opens dialog' - } - tabIndex={shouldShowLabel ? -1 : 0} + onDoubleClick={() => { + setShowWindow(true); + }} + aria-controls={id} + aria-haspopup="true" + aria-label={description} style={iconTriggerStyle} /> )} + {shouldShowLabel && ( + { + setShowWindow(true); + }} + > + {labelText} + + )}
{showWindow && } From 46054613db3a49323330a2b954279b7afdb22e25 Mon Sep 17 00:00:00 2001 From: Devesh Tiwari Date: Thu, 26 Feb 2026 16:16:17 +0530 Subject: [PATCH 13/18] Update lesson_mocks.ts --- .../right_menu/lesson/lesson_mocks.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/assets/test/advanced_authoring/right_menu/lesson/lesson_mocks.ts b/assets/test/advanced_authoring/right_menu/lesson/lesson_mocks.ts index 19cf45d863d..602f3fb5467 100644 --- a/assets/test/advanced_authoring/right_menu/lesson/lesson_mocks.ts +++ b/assets/test/advanced_authoring/right_menu/lesson/lesson_mocks.ts @@ -3,10 +3,20 @@ export const transformedSchema = { defaultScreenWidth: 1000, defaultScreenHeight: 500, enableHistory: true, + displayRefreshWarningPopup: true, variables: [], logoutMessage: '', logoutPanelImageURL: '', + backgroundImageURL: '', + backgroundImageScaleContent: false, + darkModeSetting: false, + responsiveLayout: false, + grid: false, + centerpoint: false, + columnGuides: false, + rowGuides: false, }, + displayApplicationChrome: false, additionalStylesheets: [ 'default', 'https://etx-nec.s3-us-west-2.amazonaws.com/css/etx/styles/style_season.css', From 022086482d7ffd7f36d4288614b403e493a73b20 Mon Sep 17 00:00:00 2001 From: Devesh Tiwari Date: Mon, 9 Mar 2026 17:31:21 +0530 Subject: [PATCH 14/18] Update responsive-layout.scss --- assets/styles/common/responsive-layout.scss | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/assets/styles/common/responsive-layout.scss b/assets/styles/common/responsive-layout.scss index b0bfc2e3c76..adf019a38c4 100644 --- a/assets/styles/common/responsive-layout.scss +++ b/assets/styles/common/responsive-layout.scss @@ -198,11 +198,13 @@ janus-popup:has(input)[model*='question'], .responsive-item .react-draggable janus-popup:has(input) { background-size: 20px !important; - width: 40px !important; + width: fit-content !important; + min-width: fit-content; height: auto !important; - aspect-ratio: 1; input { - width: 40px !important; + width: 32px !important; + height: 32px !important; + flex-shrink: 0; } } /*------------------------------ From 171187801f2b1fde175e54a51e3327ad3981a790 Mon Sep 17 00:00:00 2001 From: Devesh Tiwari Date: Mon, 27 Apr 2026 17:31:16 +0530 Subject: [PATCH 15/18] MER-5576 --- .../parts/janus-text-flow/QuillEditor.tsx | 78 ++++++++++++++++--- .../janus-text-flow/QuillImageUploader.tsx | 47 ++++++++++- .../parts/janus-text-flow/TextFlowAuthor.tsx | 7 +- .../parts/janus-text-flow/quill-utils.ts | 6 +- 4 files changed, 120 insertions(+), 18 deletions(-) diff --git a/assets/src/components/parts/janus-text-flow/QuillEditor.tsx b/assets/src/components/parts/janus-text-flow/QuillEditor.tsx index 02a38aa8984..5d09a942088 100644 --- a/assets/src/components/parts/janus-text-flow/QuillEditor.tsx +++ b/assets/src/components/parts/janus-text-flow/QuillEditor.tsx @@ -242,6 +242,7 @@ const attachInlineCustomColorControl = ( }; const BaseImage = Quill.import('formats/image'); +const DeltaCtor = Quill.import('delta'); class ImageWithAlt extends BaseImage { static blotName = 'image'; @@ -522,6 +523,9 @@ export const QuillEditor: React.FC = ({ }, [tree]); const [delta, setDelta] = React.useState(initialDelta); const [currentQuillRange, setCurrentQuillRange] = React.useState(0); + const [editingImageIndex, setEditingImageIndex] = React.useState(null); + const [imageDialogInitialSrc, setImageDialogInitialSrc] = React.useState(''); + const [imageDialogInitialAlt, setImageDialogInitialAlt] = React.useState(''); const [showImageSelectorDailog, setShowImageSelectorDailog] = React.useState(false); const [showFIBOptionEditorDailog, setShowFIBOptionEditorDailog] = React.useState(false); const [showLinkDialog, setShowLinkDialog] = React.useState(false); @@ -664,23 +668,61 @@ export const QuillEditor: React.FC = ({ const onEditorClick = (event: MouseEvent) => { const anchor = getAnchorFromEventTarget(event.target); - if (!anchor) return; + if (anchor) { + const blot = Quill.find(anchor); + if (!blot) return; - const blot = Quill.find(anchor); - if (!blot) return; + event.preventDefault(); + event.stopPropagation(); - event.preventDefault(); - event.stopPropagation(); + const index = editor.getIndex(blot); + const length = Math.max(1, blot.length?.() || 1); - const index = editor.getIndex(blot); - const length = Math.max(1, blot.length?.() || 1); + editor.setSelection(index, length); + openLinkDialog({ index, length }, anchor.getAttribute('href') || ''); + return; + } - editor.setSelection(index, length); - openLinkDialog({ index, length }, anchor.getAttribute('href') || ''); + if (event.target instanceof HTMLImageElement) { + const imageBlot = Quill.find(event.target); + if (!imageBlot) return; + + event.preventDefault(); + event.stopPropagation(); + + const imageIndex = editor.getIndex(imageBlot); + const imageOp = editor.getContents(imageIndex, 1)?.ops?.[0]; + const imageValue = imageOp?.insert?.image; + const imageSrc = typeof imageValue === 'string' ? imageValue : imageValue?.src || ''; + const imageAlt = + typeof imageValue === 'object' + ? imageValue?.alt || imageOp?.attributes?.alt || imageOp?.insert?.alt || '' + : imageOp?.attributes?.alt || imageOp?.insert?.alt || ''; + + setEditingImageIndex(imageIndex); + setCurrentQuillRange(imageIndex); + setImageDialogInitialSrc(imageSrc); + setImageDialogInitialAlt(imageAlt); + setShowImageSelectorDailog(true); + } }; root.addEventListener('mousedown', onEditorMouseDown, true); root.addEventListener('click', onEditorClick, true); + const clipboard = editor.getModule('clipboard'); + clipboard.addMatcher('IMG', (node: HTMLImageElement) => { + const src = node.getAttribute('src'); + if (!src) { + return new DeltaCtor(); + } + + return new DeltaCtor().insert({ + image: { + src, + alt: node.getAttribute('alt') || '', + }, + }); + }); return () => { root.removeEventListener('mousedown', onEditorMouseDown, true); root.removeEventListener('click', onEditorClick, true); @@ -745,6 +787,9 @@ export const QuillEditor: React.FC = ({ } }, image: function (value: string) { + setEditingImageIndex(null); + setImageDialogInitialSrc(''); + setImageDialogInitialAlt(''); setShowImageSelectorDailog(true); setCurrentQuillRange(this.quill.getSelection()?.index || 0); }, @@ -819,9 +864,16 @@ export const QuillEditor: React.FC = ({ if (!quill?.current || !imageURL) return; const editor = quill.current.getEditor(); - const index = currentQuillRange ?? editor.getLength(); + const isEditing = editingImageIndex !== null; + const index = isEditing ? editingImageIndex : currentQuillRange ?? editor.getLength(); + if (isEditing) { + editor.deleteText(index, 1, 'user'); + } editor.insertEmbed(index, 'image', { src: imageURL, alt: imageAltText }, 'user'); + setEditingImageIndex(null); + setImageDialogInitialSrc(''); + setImageDialogInitialAlt(''); }; const handleFIBOptionsEditorSave = (Options: Array) => { @@ -845,6 +897,9 @@ export const QuillEditor: React.FC = ({ const handleImageUploaderDailogClose = () => { setShowImageSelectorDailog(false); + setEditingImageIndex(null); + setImageDialogInitialSrc(''); + setImageDialogInitialAlt(''); }; const handleFIBOptionsEditorClose = () => { @@ -1015,6 +1070,9 @@ export const QuillEditor: React.FC = ({ showImageSelectorDailog={showImageSelectorDailog} handleImageDetailsSave={handleImageDetailsSave} handleImageDailogClose={handleImageUploaderDailogClose} + initialImageSrc={imageDialogInitialSrc} + initialImageAltText={imageDialogInitialAlt} + isEditingImage={editingImageIndex !== null} > } {showFIBOptionEditorDailog && ( diff --git a/assets/src/components/parts/janus-text-flow/QuillImageUploader.tsx b/assets/src/components/parts/janus-text-flow/QuillImageUploader.tsx index e008bf43d6c..267b49973d4 100644 --- a/assets/src/components/parts/janus-text-flow/QuillImageUploader.tsx +++ b/assets/src/components/parts/janus-text-flow/QuillImageUploader.tsx @@ -5,31 +5,71 @@ interface QuillImageUploaderProps { handleImageDetailsSave: (imageSrc: string, imageAltText: string) => void; handleImageDailogClose: () => void; showImageSelectorDailog?: boolean; + initialImageSrc?: string; + initialImageAltText?: string; + isEditingImage?: boolean; } export const QuillImageUploader: React.FC = ({ handleImageDetailsSave, showImageSelectorDailog, handleImageDailogClose, + initialImageSrc = '', + initialImageAltText = '', + isEditingImage = false, }) => { const [imageURL, setImageURL] = React.useState(''); const [imageAltText, setImageAltText] = React.useState(''); + const [errorMessage, setErrorMessage] = React.useState(''); + + React.useEffect(() => { + if (!showImageSelectorDailog) { + setImageURL(''); + setImageAltText(''); + setErrorMessage(''); + return; + } + + setImageURL(initialImageSrc); + setImageAltText(initialImageAltText); + setErrorMessage(''); + }, [showImageSelectorDailog, initialImageSrc, initialImageAltText]); + const handleOnImageURLChange: ReactEventHandler = (event) => { const el = event.target as HTMLInputElement; const val = el.value; setImageURL(val); + setErrorMessage(''); }; const handleOnImageAlTextChange: ReactEventHandler = (event) => { const el = event.target as HTMLInputElement; const val = el.value; setImageAltText(val); + setErrorMessage(''); + }; + + const onSave = () => { + if (!imageURL.trim()) { + setErrorMessage('Image URL is required.'); + return; + } + + if (!imageAltText.trim()) { + setErrorMessage('Alt text is required.'); + return; + } + + handleImageDetailsSave(imageURL.trim(), imageAltText.trim()); }; + return ( { <> -

MCQ - Insert Image

+

+ {isEditingImage ? 'Edit Image' : 'Insert Image'} +

@@ -55,14 +95,13 @@ export const QuillImageUploader: React.FC = ({ style={{ width: '100%' }} />
+ {errorMessage &&
{errorMessage}
}
diff --git a/assets/src/components/parts/janus-text-flow/TextFlowAuthor.tsx b/assets/src/components/parts/janus-text-flow/TextFlowAuthor.tsx index a58a4793b6e..27c879b1454 100644 --- a/assets/src/components/parts/janus-text-flow/TextFlowAuthor.tsx +++ b/assets/src/components/parts/janus-text-flow/TextFlowAuthor.tsx @@ -16,6 +16,7 @@ export interface MarkupTree { tag: string; href?: string; src?: string; + alt?: string; target?: string; style?: any; text?: string; @@ -85,7 +86,7 @@ export const renderFlow = ( src={treeNode.src} target={treeNode.target} style={styles} - text={treeNode.text} + text={treeNode.tag === 'img' ? treeNode.alt : treeNode.text} state={state} customCssClass={treeNode.customCssClass} displayRawText={true} @@ -107,7 +108,8 @@ export const renderFlow = ( // eslint-disable-next-line react/display-name const Editor: React.FC = React.memo(({ html, tree, portal, state, projectSlug }) => { - const quillProps: { tree?: any; html?: any; 'project-slug'?: string } = {}; + const quillProps: { tree?: any; html?: any; 'project-slug'?: string; showimagecontrol?: string } = + {}; if (tree) { quillProps.tree = JSON.stringify(tree); } @@ -135,6 +137,7 @@ const Editor: React.FC = React.memo(({ html, tree, portal, state, projectSl quillProps['project-slug'] = resolvedProjectSlug; (quillProps as any).projectSlug = resolvedProjectSlug; } + quillProps.showimagecontrol = 'true'; /* console.log('E RERENDER', { html, tree, portal }); */ const E = () => (
{React.createElement(quillEditorTagName, quillProps)}
diff --git a/assets/src/components/parts/janus-text-flow/quill-utils.ts b/assets/src/components/parts/janus-text-flow/quill-utils.ts index 69112dfc55b..7c1c4cb93d6 100644 --- a/assets/src/components/parts/janus-text-flow/quill-utils.ts +++ b/assets/src/components/parts/janus-text-flow/quill-utils.ts @@ -235,13 +235,15 @@ export const convertQuillToJanus = (delta: Delta) => { const imageDetails: any = op.insert; const imageValue = imageDetails?.image; const src = typeof imageValue === 'string' ? imageValue : imageValue.src; + const altFromImageValue = typeof imageValue === 'object' ? imageValue?.alt : undefined; + const altFromLegacyInsert = imageDetails?.alt; const child: JanusMarkupNode = { tag: 'img', style: { height: '100%', width: '100%', }, - alt: `${op?.attributes?.alt || ''}`, + alt: `${altFromImageValue ?? op?.attributes?.alt ?? altFromLegacyInsert ?? ''}`, src: `${src}`, children: [], }; @@ -433,7 +435,7 @@ const processJanusChildren = (node: JanusMarkupNode, doc: Delta, parentAttrs: an lineAttrs.align = child.style.textAlign; } if (child.tag === 'img') { - doc.insert({ image: child.src, alt: child.alt }); + doc.insert({ image: { src: child.src || '', alt: child.alt || '' } }); } line.insert('\n', lineAttrs); } From f0b811a7782a82d5baadf9117a833fa969c7aa1d Mon Sep 17 00:00:00 2001 From: Devesh Tiwari Date: Tue, 28 Apr 2026 11:03:20 +0530 Subject: [PATCH 16/18] Update QuillEditor.tsx --- .../components/parts/janus-text-flow/QuillEditor.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/assets/src/components/parts/janus-text-flow/QuillEditor.tsx b/assets/src/components/parts/janus-text-flow/QuillEditor.tsx index 5d09a942088..1183eaef22a 100644 --- a/assets/src/components/parts/janus-text-flow/QuillEditor.tsx +++ b/assets/src/components/parts/janus-text-flow/QuillEditor.tsx @@ -762,16 +762,16 @@ export const QuillEditor: React.FC = ({ }, [applyColorFormat]); const customHandlers = { - textStyle: function (value: string) { + textStyle: function (this: any, value: string) { applyTextStyle(this.quill, value); }, - color: function (value: string) { + color: function (this: any, value: string) { this.quill.format('color', value, 'user'); }, - background: function (value: string) { + background: function (this: any, value: string) { this.quill.format('background', value, 'user'); }, - adaptivity: function (value: string) { + adaptivity: function (this: any, value: string) { const range = this.quill.getSelection(); let selectionValue = ''; if (range && range.length > 0) { @@ -786,14 +786,14 @@ export const QuillEditor: React.FC = ({ this.quill.deleteText(range.index + expression.length + 2, expression.length + 2); } }, - image: function (value: string) { + image: function (this: any, value: string) { setEditingImageIndex(null); setImageDialogInitialSrc(''); setImageDialogInitialAlt(''); setShowImageSelectorDailog(true); setCurrentQuillRange(this.quill.getSelection()?.index || 0); }, - insertFIBOption: function (value: string) { + insertFIBOption: function (this: any, value: string) { const range = this.quill.getSelection(); const insertIndex = range ? range.index : this.quill.getLength(); setCurrentQuillRange(insertIndex); From 78be3d8a733b226e85e32e7395abfa54fdbc3d9e Mon Sep 17 00:00:00 2001 From: Devesh Tiwari Date: Tue, 28 Apr 2026 11:19:49 +0530 Subject: [PATCH 17/18] Update QuillImageUploader.tsx --- .../components/parts/janus-text-flow/QuillImageUploader.tsx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/assets/src/components/parts/janus-text-flow/QuillImageUploader.tsx b/assets/src/components/parts/janus-text-flow/QuillImageUploader.tsx index 267b49973d4..ced3d22c2e7 100644 --- a/assets/src/components/parts/janus-text-flow/QuillImageUploader.tsx +++ b/assets/src/components/parts/janus-text-flow/QuillImageUploader.tsx @@ -52,12 +52,6 @@ export const QuillImageUploader: React.FC = ({ setErrorMessage('Image URL is required.'); return; } - - if (!imageAltText.trim()) { - setErrorMessage('Alt text is required.'); - return; - } - handleImageDetailsSave(imageURL.trim(), imageAltText.trim()); }; From 7953a09255f2a4b059259764ebd31e5ea7ef5de0 Mon Sep 17 00:00:00 2001 From: Devesh Tiwari Date: Tue, 28 Apr 2026 11:32:02 +0530 Subject: [PATCH 18/18] Update QuillImageUploader.tsx --- .../components/parts/janus-text-flow/QuillImageUploader.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/assets/src/components/parts/janus-text-flow/QuillImageUploader.tsx b/assets/src/components/parts/janus-text-flow/QuillImageUploader.tsx index ced3d22c2e7..751b91878e5 100644 --- a/assets/src/components/parts/janus-text-flow/QuillImageUploader.tsx +++ b/assets/src/components/parts/janus-text-flow/QuillImageUploader.tsx @@ -92,11 +92,7 @@ export const QuillImageUploader: React.FC = ({ {errorMessage &&
{errorMessage}
} -