From 44f548c388ba609e680f372e60b8ec793d157588 Mon Sep 17 00:00:00 2001 From: Arpan Gupta Date: Thu, 13 Jul 2023 13:16:00 +0530 Subject: [PATCH 01/37] Issue #42 feat: Added an interface for match the following type question --- .../src/lib/interfaces/MtfForm.ts | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 projects/questionset-editor-library/src/lib/interfaces/MtfForm.ts diff --git a/projects/questionset-editor-library/src/lib/interfaces/MtfForm.ts b/projects/questionset-editor-library/src/lib/interfaces/MtfForm.ts new file mode 100644 index 000000000..489d3f99e --- /dev/null +++ b/projects/questionset-editor-library/src/lib/interfaces/MtfForm.ts @@ -0,0 +1,56 @@ +import * as _ from "lodash-es"; + +export class MtfOption { + constructor(public leftOption: string, public rightOption: string) {} +} + +export interface MtfData { + question: string; + options: Array; + answer?: object; + learningOutcome?: string; + complexityLevel?: string; + maxScore?: number; +} + +export interface MtfConfig { + templateId?: string; + numberOfOptions?: number; + maximumOptions?: number; + +} + +export class MtfForm { + public question: string; + public options: Array; + public templateId: string; + public answer: object; + public learningOutcome?: string; + public complexityLevel?: string; + public maxScore?: number; + public maximumOptions; + public numberOfOptions; + + constructor({question, options, answer, learningOutcome, complexityLevel, maxScore,}: MtfData,{ templateId, numberOfOptions, maximumOptions }: MtfConfig) { + this.question = question; + this.options = options || []; + this.templateId = templateId; + this.answer = answer || {}; + this.learningOutcome = learningOutcome; + this.complexityLevel = complexityLevel; + this.numberOfOptions = numberOfOptions || 2; + this.maximumOptions = maximumOptions || 4; + this.maxScore = maxScore; + if (!this.options || !this.options.length) { + _.times(this.numberOfOptions, index => this.options.push(new MtfOption("",""))); + } + } + + addOptions() { + this.options.push(new MtfOption("", "")); + } + + deleteOptions(position: number) { + this.options.splice(position, 1); + } +} From cefe2a612bf9589b72fc40514ce1413e6412240f Mon Sep 17 00:00:00 2001 From: Arpan Gupta Date: Fri, 14 Jul 2023 11:21:13 +0530 Subject: [PATCH 02/37] Used optional chaining --- .../questionset-editor-library/src/lib/interfaces/MtfForm.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/questionset-editor-library/src/lib/interfaces/MtfForm.ts b/projects/questionset-editor-library/src/lib/interfaces/MtfForm.ts index 489d3f99e..5b2aab549 100644 --- a/projects/questionset-editor-library/src/lib/interfaces/MtfForm.ts +++ b/projects/questionset-editor-library/src/lib/interfaces/MtfForm.ts @@ -41,7 +41,7 @@ export class MtfForm { this.numberOfOptions = numberOfOptions || 2; this.maximumOptions = maximumOptions || 4; this.maxScore = maxScore; - if (!this.options || !this.options.length) { + if (!this.options?.length) { _.times(this.numberOfOptions, index => this.options.push(new MtfOption("",""))); } } From 0cc5c5038f894afff83e73a644da2b7db806688a Mon Sep 17 00:00:00 2001 From: Arpan Gupta Date: Sat, 15 Jul 2023 12:24:41 +0530 Subject: [PATCH 03/37] updated answer type to string as per schema --- .../src/lib/interfaces/MtfForm.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/projects/questionset-editor-library/src/lib/interfaces/MtfForm.ts b/projects/questionset-editor-library/src/lib/interfaces/MtfForm.ts index 5b2aab549..9634569bc 100644 --- a/projects/questionset-editor-library/src/lib/interfaces/MtfForm.ts +++ b/projects/questionset-editor-library/src/lib/interfaces/MtfForm.ts @@ -7,7 +7,7 @@ export class MtfOption { export interface MtfData { question: string; options: Array; - answer?: object; + answer?: string; learningOutcome?: string; complexityLevel?: string; maxScore?: number; @@ -24,7 +24,7 @@ export class MtfForm { public question: string; public options: Array; public templateId: string; - public answer: object; + public answer: string; public learningOutcome?: string; public complexityLevel?: string; public maxScore?: number; @@ -35,19 +35,19 @@ export class MtfForm { this.question = question; this.options = options || []; this.templateId = templateId; - this.answer = answer || {}; + this.answer = answer; this.learningOutcome = learningOutcome; this.complexityLevel = complexityLevel; this.numberOfOptions = numberOfOptions || 2; this.maximumOptions = maximumOptions || 4; this.maxScore = maxScore; if (!this.options?.length) { - _.times(this.numberOfOptions, index => this.options.push(new MtfOption("",""))); + _.times(this.numberOfOptions, index => this.options.push(new MtfOption('', ''))); } } addOptions() { - this.options.push(new MtfOption("", "")); + this.options.push(new MtfOption('', '')); } deleteOptions(position: number) { From 803e8769f977ab27fd6dbb251b49f0e09b4dbc99 Mon Sep 17 00:00:00 2001 From: Arpan Gupta Date: Fri, 21 Jul 2023 09:38:22 +0530 Subject: [PATCH 04/37] Add Match The Following Question in question primary categories --- .../src/lib/services/config/editor.config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/questionset-editor-library/src/lib/services/config/editor.config.json b/projects/questionset-editor-library/src/lib/services/config/editor.config.json index 195ea0ad1..20990e723 100644 --- a/projects/questionset-editor-library/src/lib/services/config/editor.config.json +++ b/projects/questionset-editor-library/src/lib/services/config/editor.config.json @@ -17,7 +17,7 @@ "accepted": "mp4, webm" } }, - "questionPrimaryCategories": ["Multiple Choice Question", "Subjective Question"], + "questionPrimaryCategories": ["Multiple Choice Question", "Subjective Question", "Match The Following Question"], "contentPrimaryCategories": ["Course Assessment", "eTextbook", "Explanation Content", "Learning Resource", "Practice Question Set"], "readQuestionFields": "body,primaryCategory,mimeType,qType,answer,templateId,responseDeclaration,interactionTypes,interactions,name,solutions,editorState,media,remarks,evidence,hints,instructions,outcomeDeclaration,", "omitFalseyProperties":["topic", "topicsIds", "targetTopicIds", "keywords"], From fff10952ce0566291e433db3b6d8121c2376392c Mon Sep 17 00:00:00 2001 From: Arpan Gupta Date: Fri, 21 Jul 2023 11:21:08 +0530 Subject: [PATCH 05/37] Added Match The Following Question --- src/app/data.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/data.ts b/src/app/data.ts index e57fe1055..ecf3a4dac 100644 --- a/src/app/data.ts +++ b/src/app/data.ts @@ -135,7 +135,8 @@ export const questionSetEditorConfig = { children: { Question: [ 'Multiple Choice Question', - 'Subjective Question' + 'Subjective Question', + 'Match The Following Question', ] }, addFromLibrary: false, From f93a37331c43f2b4589234b1a428cd0fe686c3b4 Mon Sep 17 00:00:00 2001 From: Arpan Gupta Date: Fri, 21 Jul 2023 21:08:31 +0530 Subject: [PATCH 06/37] Issue #42 feat: Function for preparing Match the following body --- .../components/options/options.component.ts | 44 +++++++++++++++++-- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/projects/questionset-editor-library/src/lib/components/options/options.component.ts b/projects/questionset-editor-library/src/lib/components/options/options.component.ts index daf368fc5..c2a058124 100644 --- a/projects/questionset-editor-library/src/lib/components/options/options.component.ts +++ b/projects/questionset-editor-library/src/lib/components/options/options.component.ts @@ -15,6 +15,7 @@ export class OptionsComponent implements OnInit, OnChanges { @Input() showFormError; @Input() sourcingSettings; @Input() questionPrimaryCategory; + @Input() questionInteractionType; @Input() mapping = []; @Input() isReadOnlyMode; @Input() maxScore; @@ -75,8 +76,16 @@ export class OptionsComponent implements OnInit, OnChanges { } editorDataHandler(event?) { - const body = this.prepareMcqBody(this.editorState); - this.editorDataOutput.emit({ body, mediaobj: event ? event.mediaobj : undefined }); + let body: any; + if (this.questionInteractionType === 'choice') { + body = this.prepareMcqBody(this.editorState); + } else if (this.questionInteractionType === 'match') { + body = this.prepareMtfBody(this.editorState); + } + this.editorDataOutput.emit({ + body, + mediaobj: event ? event.mediaobj : undefined, + }); } prepareMcqBody(editorState) { @@ -119,11 +128,40 @@ export class OptionsComponent implements OnInit, OnChanges { return metadata; } + prepareMtfBody(editorState) { + let metadata: any; + const correctAnswer = editorState.answer; + let options: any; + if (!_.isEmpty(correctAnswer)) { + options = _.reduce(this.editorState.options,function (acc, obj) { + acc.leftOption.push(obj.leftOption); + acc.rightOption.push(obj.rightOption); + return acc; + },{ leftOption: [], rightOption: [] } + ); + } + console.log(options); + console.log(editorState.answer); + metadata = { + templateId: this.templateType, + name: this.questionPrimaryCategory || 'Match The Following Question', + responseDeclaration: this.getResponseDeclaration(editorState), + outcomeDeclaration: this.getOutcomeDeclaration(), + interactionTypes: ['match'], + interactions: this.getInteractions(editorState.options), + editorState: { + options, + }, + qType: 'MTF', + primaryCategory: this.questionPrimaryCategory || "Match The Following Question", + }; + return metadata; + } getResponseDeclaration(editorState) { const responseDeclaration = { response1: { cardinality: this.getCardinality(), - type: 'integer', + type: this.questionInteractionType === 'choice' ? 'integer' : 'map', correctResponse: { value: editorState.answer, }, From fd99b99bf434ce8253cea0c4bd406d4714810b54 Mon Sep 17 00:00:00 2001 From: Arpan Gupta Date: Fri, 21 Jul 2023 21:22:43 +0530 Subject: [PATCH 07/37] Added some button labels --- .../src/lib/services/config/label.config.json | 376 +++++++++--------- 1 file changed, 190 insertions(+), 186 deletions(-) diff --git a/projects/questionset-editor-library/src/lib/services/config/label.config.json b/projects/questionset-editor-library/src/lib/services/config/label.config.json index 29e13cd2d..6ecd46581 100644 --- a/projects/questionset-editor-library/src/lib/services/config/label.config.json +++ b/projects/questionset-editor-library/src/lib/services/config/label.config.json @@ -50,192 +50,196 @@ "add_page_numbers_to_questions_btn_label": "Pagination" }, "lbl":{ - "search":"Search", - "subject":"Subject", - "medium":"Medium", - "gradeLevel":"Class", - "contentType":"Content Type", - "reset":"Reset", - "apply":"apply", - "filterText":"Change Filters", - "Questiondetails":"Question details", - "selectContent":"Select content", - "noMatchingContent":"Sorry there is no matching content", - "changeFilterMessage":"Changing filter helps you find more content", - "changeFilter":"Change filters", - "whereDoYouWantToAddThisContent":"Where do you want to add this content?", - "selectContentToAdd":"Use search and filters above to find more content", - "addedToCollection":"Added to collection", - "changingFilters":"Changing filters make you find more content", - "ChangeFilters":"Change filters", - "addFromLibrary":"Add from Library", - "showContentAddedToCollection":"Show content added to collection", - "addContent":"Add content", - "sortBy":"Sort By", - "sortlabel":"A - Z", - "viewOnOrigin":"View Content on consumption", - "answers":"Answers", - "answersRequired":"Answer is required", - "answersPopupText":"Please provide an answer for the question. Check preview to understand how it would look.", - "selectImage":"Select Image", - "myImages":"My Images", - "allImage":"All Image", - "uploadAndUse":"Upload and Use", - "chooseOrDragImage":"Choose or drag and drop your image here", - "chooseOrDragVideo":"Choose or drag and drop your video here", - "uploadFromComputer":"Upload from Computer", - "upload":"Upload", - "maxFileSize":"Max File size:", - "allowedFileTypes":"Allowed file types are:", - "maximumAllowedFileSize":"Maximum allowed file size:", - "copyRightsAndLicense":"Copyright & License", - "dropChooseFile":"Drop or choose file to upload before entering the details", - "charactersLeft":"Characters left:", - "myVideos":"My Video(s)", - "allVideos":"All Video(s)", - "selectVideo":"Select Video", - "searchPlaceholder":"Search...", - "addAnImage":"Add an image", - "name":"Name", - "copyRightsAndYear":"Copyright & Yrar", - "license":"License", - "author":"Author", - "grade":"Grade", - "board":"Board", - "audience":"Audience", - "copyRight":"Copyright", - "licensedBy":"Licensed by", - "attributions":"Attributions", - "requestForQrCode":"Request for QR Codes", - "confirmDeleteContent":"Confirm Delete Content", - "confirmDeleteNode":"Are you sure want to delete the selected Node?", - "comments":"Comments", - "reviewComments":"Review Comments", - "questionSetPreview":"Question Set Preview", - "numberToolarge": "This number is too large for the request", - "acceptTerms":"Accepting Terms & Conditions", - "iAgreeSubmit":"I agree that by submitting / publishing this Content,", - "iconfirmContent":"I confirm that this Content complies with prescribed guidelines, including the Terms of Use and Content Policy and that I consent to publish it under the", - "createCommonFramework":"Creative Commons Framework in", - "accordance":"accordance with the", - "contentPolicy":"Content Policy.", - "privacyRights":"I have made sure that I do not violate others’ copyright or privacy rights." , - "viewComments":"View Comments", - "addReviewComments":"Add Review Comments", - "enterYourComments":"Enter your comments", - "publishCollection":"Publish ${objectType}", - "confirmPublishCollection":"Are you sure you want to publish this ${objectType}?", - "fillComments":"Fill comments", - "searchLibrary":"Search Library", - "options":"Options", - "optionsPopupText":"Please Preview to check how layout looks. Layout is responsive to the resolution of your device", - "selectOneAns":"Select one correct answer", - "fillThisOption":"Fill this option", - "reduceSize":"Please reduce the size", - "correctAns":"Correct answer", - "addOption":"Add option", - "question":"Question", - "pageNumber":"Page No", - "confirmQuestionNotSaved":"This question will not be saved, are you sure you want to go back to questionset?", - "video":"Video", - "textImage":"Text+Image", - "chooseType":"Choose type", - "solution":"Solution", - "optional":"(Optional)", - "questionRequired":"Question is required", - "addingTo":"Adding To", - "selectTemplate":"Select a template to get started", - "createNew":"Create new", - "selectLayout":"Select Layout", - "vertical":"Vertical", - "grid":"Grid", - "horizontal":"Horizontal", - "folders":"Folders", - "createHierarchyCsv":"Create folders using csv file", - "downloadFoldersInCSV":"Download folders as csv file", - "uploadUpdateCSV":"Update folder metadata using csv file", - "downloadSampleHierarchyCSv":"Upload csv file as per the given sample to create folders", - "makeSureFile":"Make sure that:", - "allColumnsAreAvailable":"The file has all the required columns", - "hasAllMandatoryColumn":"The file has all the required values filled in as per the format", - "noDuplicateRow":"There are no duplicate rows (with exactly same folder levels) in the file", - "downloadSampleCSV":"Download sample csv file", - "dragAndDropCSV":"Drag and Drop files to upload", - "selectFileToUpload":"Select file to upload", - "uploadEntries":"File accepted - csv", - "Or":"Or", - "pleaseWait":"Please wait", - "validateCsvFile":"Validating CSV file", - "hierarchyValidationError":"Error in processing the file", - "followingErrors":"Following errors are found in the file. Please correct and upload again", - "reUploadCSV":"Upload file again", - "hierarchyValidation":"Hierarchy Validation", - "hierarchyAdded":"Folders have been successfully created. Please close the dialog", - "hierarchyUpdated":"Folder metadata has been successfully updated. Please close the dialog", - "successful":"Successful !", - "csvDownloadInstruction":"Please make sure that this is the file downloaded using the “Download folders as csv file” option and changes are made to it", - "collaborators": "Collaborators", - "addCollaborators": "Add Collaborators", - "manageCollaborators": "Manage Collaborators", - "sliderValue":"Slider Value", - "left":"Left", - "stepSize":"Step size", - "right":"Right", - "translation":"Translation", - "publishchecklistTitle": "Please confirm that ALL the following items are verified (by ticking the check-boxes) before you can publish:", - "bulkUploadErrorMessage" : "The metadata file has following errors. Please check and upload again", - "bulkUploadQuestion": "Bulk Upload Question", - "lastUploaded": "Last uploaded", - "bulkInProgress": "Bulk upload in progress", - "viewDetails": "View Details", - "downloadSampleMetadataCsvFileAndCreate": "Download sample metadata CSV file and create your own metadata file using the format", - "makeSureYourFile": "Make sure your file", - "allColumnsAreAvailableShownFormat": "All columns are available as shown in format.", - "hasAllMandatoryColumnsFilledAsMarkedInFormat": "Has all mandatory columns filled, as marked in the format", - "hasNoDuplicateUrlsFilepathColumn": "Has no duplicate URLs in the filepath column", - "downloadSampleMetadataCsv": "Download sample metadata CSV", - "processingDroppedFiles" : "Processing dropped files...", - "retry": "retry", - "dragAndDrop": "Drag and Drop file", - "or" : "or", - "selectFile": "Select file", - "uploadCSVXlEntries": "Upload csv upto 300 entries", - "no": "No", - "yes": "Yes", - "cancel": "Cancel", - "ok": "OK", - "validatingCSVFile": "Validating CSV file", - "metadataFileValidationFailed": "Metadata file validation failed.", - "metadataFollowingError": "The metadata file has following errors. Please check and upload again", - "uploadingYourContentFromCSV": "Uploading your question from CSV", - "uploadFail" : "Upload failed", - "uploadSuccessful" : "Upload successful", - "uploadRemaining" : "Upload remaining", - "bulkUploadComplete" : "Bulk upload complete!", - "contentUploaded" : "Question uploaded", - "downloadReport": "Download report", - "next": "Next", - "back": "Back", - "close": "Close", - "bulkUploadNoticeLine1": "Uploading in bulk may take some time, click on", - "bulkUploadNoticeLine2": "to continue using Vidyadaan.", - "bulkUploadNoticeLine3": "Your upload will keep running in the background.", - "alreadyContentPresent": "Already present in this folder", - "loaderHeading": "Please wait", - "loaderMessage": "We are fetching details.", - "slidervalue": "Set the minimum and maximum values with which the slider would start and end respectively.", - "stepSizeInfo": "The step size would define the gap/jump between two values on the slider.", - "minSizeInfo": "The minimum slider value", - "maxSizeInfo": "The maximum slider value", - "questionsetAddFromLibraryItemLabel": "question", - "questionsetAddFromLibraryCollectionLabel": "question set", - "marks": "Marks", - "shuffleOnMessage": "Each question will carry equal weightage of 1 mark when using Shuffle. To provide different weightage to individual questions please turn off Shuffle.", - "editingConsentNote": "I confirm that I am allowing my reviewer to make edits to the content and metadata of the content. I consent my reviewer make changes, if any and publish the contain thereafter.", - "acceptBothConsentNote": "Agree to both conditions", - "totalScore": "Total Score", - "qualityReview": "If the score is less than or equal to 15 please re-evaluate the question and send back for corrections." - }, + "search": "Search", + "subject": "Subject", + "medium": "Medium", + "gradeLevel": "Class", + "contentType": "Content Type", + "reset": "Reset", + "apply": "apply", + "filterText": "Change Filters", + "Questiondetails": "Question details", + "selectContent": "Select content", + "noMatchingContent": "Sorry there is no matching content", + "changeFilterMessage": "Changing filter helps you find more content", + "changeFilter": "Change filters", + "whereDoYouWantToAddThisContent": "Where do you want to add this content?", + "selectContentToAdd": "Use search and filters above to find more content", + "addedToCollection": "Added to collection", + "changingFilters": "Changing filters make you find more content", + "ChangeFilters": "Change filters", + "addFromLibrary": "Add from Library", + "showContentAddedToCollection": "Show content added to collection", + "addContent": "Add content", + "sortBy": "Sort By", + "sortlabel": "A - Z", + "viewOnOrigin": "View Content on consumption", + "answers": "Answers", + "answersRequired": "Answer is required", + "answersPopupText": "Please provide an answer for the question. Check preview to understand how it would look.", + "selectImage": "Select Image", + "myImages": "My Images", + "allImage": "All Image", + "uploadAndUse": "Upload and Use", + "chooseOrDragImage": "Choose or drag and drop your image here", + "chooseOrDragVideo": "Choose or drag and drop your video here", + "uploadFromComputer": "Upload from Computer", + "upload": "Upload", + "maxFileSize": "Max File size:", + "allowedFileTypes": "Allowed file types are:", + "maximumAllowedFileSize": "Maximum allowed file size:", + "copyRightsAndLicense": "Copyright & License", + "dropChooseFile": "Drop or choose file to upload before entering the details", + "charactersLeft": "Characters left:", + "myVideos": "My Video(s)", + "allVideos": "All Video(s)", + "selectVideo": "Select Video", + "searchPlaceholder": "Search...", + "addAnImage": "Add an image", + "name": "Name", + "copyRightsAndYear": "Copyright & Yrar", + "license": "License", + "author": "Author", + "grade": "Grade", + "board": "Board", + "audience": "Audience", + "copyRight": "Copyright", + "licensedBy": "Licensed by", + "attributions": "Attributions", + "requestForQrCode": "Request for QR Codes", + "confirmDeleteContent": "Confirm Delete Content", + "confirmDeleteNode": "Are you sure want to delete the selected Node?", + "comments": "Comments", + "reviewComments": "Review Comments", + "questionSetPreview": "Question Set Preview", + "numberToolarge": "This number is too large for the request", + "acceptTerms": "Accepting Terms & Conditions", + "iAgreeSubmit": "I agree that by submitting / publishing this Content,", + "iconfirmContent": "I confirm that this Content complies with prescribed guidelines, including the Terms of Use and Content Policy and that I consent to publish it under the", + "createCommonFramework": "Creative Commons Framework in", + "accordance": "accordance with the", + "contentPolicy": "Content Policy.", + "privacyRights": "I have made sure that I do not violate others’ copyright or privacy rights.", + "viewComments": "View Comments", + "addReviewComments": "Add Review Comments", + "enterYourComments": "Enter your comments", + "publishCollection": "Publish ${objectType}", + "confirmPublishCollection": "Are you sure you want to publish this ${objectType}?", + "fillComments": "Fill comments", + "searchLibrary": "Search Library", + "options": "Options", + "optionsPopupText": "Please Preview to check how layout looks. Layout is responsive to the resolution of your device", + "selectOneAns": "Select one correct answer", + "fillThisOption": "Fill this option", + "reduceSize": "Please reduce the size", + "correctAns": "Correct answer", + "correctMatch": "Check if all match pairs are correct", + "addOption": "Add option", + "addPair": "Add pair", + "question": "Question", + "pageNumber": "Page No", + "setAnswers": "Set your answers", + "addQuestionAnswerPairText": "Add question-answer pairs to your question. Answers will be shuffled automatically", + "confirmQuestionNotSaved": "This question will not be saved, are you sure you want to go back to questionset?", + "video": "Video", + "textImage": "Text+Image", + "chooseType": "Choose type", + "solution": "Solution", + "optional": "(Optional)", + "questionRequired": "Question is required", + "addingTo": "Adding To", + "selectTemplate": "Select a template to get started", + "createNew": "Create new", + "selectLayout": "Select Layout", + "vertical": "Vertical", + "grid": "Grid", + "horizontal": "Horizontal", + "folders": "Folders", + "createHierarchyCsv": "Create folders using csv file", + "downloadFoldersInCSV": "Download folders as csv file", + "uploadUpdateCSV": "Update folder metadata using csv file", + "downloadSampleHierarchyCSv": "Upload csv file as per the given sample to create folders", + "makeSureFile": "Make sure that:", + "allColumnsAreAvailable": "The file has all the required columns", + "hasAllMandatoryColumn": "The file has all the required values filled in as per the format", + "noDuplicateRow": "There are no duplicate rows (with exactly same folder levels) in the file", + "downloadSampleCSV": "Download sample csv file", + "dragAndDropCSV": "Drag and Drop files to upload", + "selectFileToUpload": "Select file to upload", + "uploadEntries": "File accepted - csv", + "Or": "Or", + "pleaseWait": "Please wait", + "validateCsvFile": "Validating CSV file", + "hierarchyValidationError": "Error in processing the file", + "followingErrors": "Following errors are found in the file. Please correct and upload again", + "reUploadCSV": "Upload file again", + "hierarchyValidation": "Hierarchy Validation", + "hierarchyAdded": "Folders have been successfully created. Please close the dialog", + "hierarchyUpdated": "Folder metadata has been successfully updated. Please close the dialog", + "successful": "Successful !", + "csvDownloadInstruction": "Please make sure that this is the file downloaded using the “Download folders as csv file” option and changes are made to it", + "collaborators": "Collaborators", + "addCollaborators": "Add Collaborators", + "manageCollaborators": "Manage Collaborators", + "sliderValue": "Slider Value", + "left": "Left", + "stepSize": "Step size", + "right": "Right", + "translation": "Translation", + "publishchecklistTitle": "Please confirm that ALL the following items are verified (by ticking the check-boxes) before you can publish:", + "bulkUploadErrorMessage": "The metadata file has following errors. Please check and upload again", + "bulkUploadQuestion": "Bulk Upload Question", + "lastUploaded": "Last uploaded", + "bulkInProgress": "Bulk upload in progress", + "viewDetails": "View Details", + "downloadSampleMetadataCsvFileAndCreate": "Download sample metadata CSV file and create your own metadata file using the format", + "makeSureYourFile": "Make sure your file", + "allColumnsAreAvailableShownFormat": "All columns are available as shown in format.", + "hasAllMandatoryColumnsFilledAsMarkedInFormat": "Has all mandatory columns filled, as marked in the format", + "hasNoDuplicateUrlsFilepathColumn": "Has no duplicate URLs in the filepath column", + "downloadSampleMetadataCsv": "Download sample metadata CSV", + "processingDroppedFiles": "Processing dropped files...", + "retry": "retry", + "dragAndDrop": "Drag and Drop file", + "or": "or", + "selectFile": "Select file", + "uploadCSVXlEntries": "Upload csv upto 300 entries", + "no": "No", + "yes": "Yes", + "cancel": "Cancel", + "ok": "OK", + "validatingCSVFile": "Validating CSV file", + "metadataFileValidationFailed": "Metadata file validation failed.", + "metadataFollowingError": "The metadata file has following errors. Please check and upload again", + "uploadingYourContentFromCSV": "Uploading your question from CSV", + "uploadFail": "Upload failed", + "uploadSuccessful": "Upload successful", + "uploadRemaining": "Upload remaining", + "bulkUploadComplete": "Bulk upload complete!", + "contentUploaded": "Question uploaded", + "downloadReport": "Download report", + "next": "Next", + "back": "Back", + "close": "Close", + "bulkUploadNoticeLine1": "Uploading in bulk may take some time, click on", + "bulkUploadNoticeLine2": "to continue using Vidyadaan.", + "bulkUploadNoticeLine3": "Your upload will keep running in the background.", + "alreadyContentPresent": "Already present in this folder", + "loaderHeading": "Please wait", + "loaderMessage": "We are fetching details.", + "slidervalue": "Set the minimum and maximum values with which the slider would start and end respectively.", + "stepSizeInfo": "The step size would define the gap/jump between two values on the slider.", + "minSizeInfo": "The minimum slider value", + "maxSizeInfo": "The maximum slider value", + "questionsetAddFromLibraryItemLabel": "question", + "questionsetAddFromLibraryCollectionLabel": "question set", + "marks": "Marks", + "shuffleOnMessage": "Each question will carry equal weightage of 1 mark when using Shuffle. To provide different weightage to individual questions please turn off Shuffle.", + "editingConsentNote": "I confirm that I am allowing my reviewer to make edits to the content and metadata of the content. I consent my reviewer make changes, if any and publish the contain thereafter.", + "acceptBothConsentNote": "Agree to both conditions", + "totalScore": "Total Score", + "qualityReview": "If the score is less than or equal to 15 please re-evaluate the question and send back for corrections." + }, "err":{ "somethingWentWrong":"Something went wrong", "contentNotFoundonOrigin": "The content not found in consumption", From c0e88fd401f4e1b0ce118435175ed78bec4d3d10 Mon Sep 17 00:00:00 2001 From: Arpan Gupta Date: Sat, 22 Jul 2023 20:45:46 +0530 Subject: [PATCH 08/37] Issue #42 feat: Added support if questionInteraction type is match --- .../components/question/question.component.ts | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/projects/questionset-editor-library/src/lib/components/question/question.component.ts b/projects/questionset-editor-library/src/lib/components/question/question.component.ts index cb1e50d5d..56103a3c5 100644 --- a/projects/questionset-editor-library/src/lib/components/question/question.component.ts +++ b/projects/questionset-editor-library/src/lib/components/question/question.component.ts @@ -2,6 +2,7 @@ import { Component, EventEmitter, Input, OnInit, Output, AfterViewInit, ViewEnca import * as _ from 'lodash-es'; import { v4 as uuidv4 } from 'uuid'; import { McqForm } from '../../interfaces/McqForm'; +import { MtfForm } from '../../interfaces/MtfForm'; import { ServerResponse } from '../../interfaces/serverResponse'; import { QuestionService } from '../../services/question/question.service'; import { PlayerService } from '../../services/player/player.service'; @@ -243,6 +244,31 @@ export class QuestionComponent implements OnInit, AfterViewInit, OnDestroy { this.editorState.responseDeclaration = _.get(this.questionMetaData, 'responseDeclaration'); } } + + if (this.questionInteractionType === 'match') { + const responseDeclaration = this.questionMetaData.responseDeclaration; + this.scoreMapping = _.get(responseDeclaration, 'response1.mapping'); + const templateId = this.questionMetaData.templateId; + const numberOfOptions = this.questionMetaData?.editorState?.options?.length || 0; + const maximumOptions = _.get(this.questionInput, 'config.maximumOptions'); + this.editorService.optionsLength = numberOfOptions; + // converting the options to the format required by the editor + const options = _.map(this.questionMetaData?.editorState?.options?.leftOption, (leftOption, index) => ({ + leftOption, + rightOption:this.questionMetaData?.editorState?.options?.rightOption?.[index] + })); + const question = this.questionMetaData?.editorState?.question; + const interactions = this.questionMetaData?.interactions; + this.editorState = new MtfForm({ + question, options, answer: _.get(responseDeclaration, 'response1.correctResponse.value') + }, { templateId, numberOfOptions, maximumOptions }); + this.editorState.solutions = this.questionMetaData?.editorState?.solutions; + this.editorState.interactions = interactions; + if (_.has(this.questionMetaData, 'responseDeclaration')) { + this.editorState.responseDeclaration = _.get(this.questionMetaData, 'responseDeclaration'); + } + } + if (_.has(this.questionMetaData, 'primaryCategory')) { this.editorState.primaryCategory = _.get(this.questionMetaData, 'primaryCategory'); } @@ -299,6 +325,9 @@ export class QuestionComponent implements OnInit, AfterViewInit, OnDestroy { else if (this.questionInteractionType === 'choice') { this.editorState = new McqForm({ question: '', options: [] }, { numberOfOptions: _.get(this.questionInput, 'config.numberOfOptions'), maximumOptions: _.get(this.questionInput, 'config.maximumOptions') }); } + else if (this.questionInteractionType === 'match') { + this.editorState = new MtfForm({ question: '', options: [] }, { numberOfOptions: _.get(this.questionInput, 'config.numberOfOptions'), maximumOptions: _.get(this.questionInput, 'config.maximumOptions') }); + } /** for observation and survey to show hint,tip,dependent question option. */ if(!_.isUndefined(this.editorService?.editorConfig?.config?.renderTaxonomy)){ this.subMenuConfig(); From ea38850c4a78abb752c2bb1519846157ec4ee73c Mon Sep 17 00:00:00 2001 From: Arpan Gupta Date: Mon, 24 Jul 2023 14:51:54 +0530 Subject: [PATCH 09/37] Issue #42 feat: Modifications in metadata of Match The Following Type Questions --- .../components/options/options.component.ts | 103 +++++++++++++----- 1 file changed, 78 insertions(+), 25 deletions(-) diff --git a/projects/questionset-editor-library/src/lib/components/options/options.component.ts b/projects/questionset-editor-library/src/lib/components/options/options.component.ts index c2a058124..60dd1b29e 100644 --- a/projects/questionset-editor-library/src/lib/components/options/options.component.ts +++ b/projects/questionset-editor-library/src/lib/components/options/options.component.ts @@ -191,36 +191,74 @@ export class OptionsComponent implements OnInit, OnChanges { } setMapping() { - if(!_.isEmpty(this.selectedOptions)) { - this.mapping = []; - const scoreForEachOption = _.round((this.maxScore/this.selectedOptions.length), 2); - _.forEach(this.selectedOptions, (value) => { - const optionMapping = { - value: value, - score: scoreForEachOption, - } - this.mapping.push(optionMapping) - }) + if (this.questionInteractionType === 'choice') { + if (!_.isEmpty(this.selectedOptions)) { + this.mapping = []; + const scoreForEachOption = _.round((this.maxScore/this.selectedOptions.length), 2); + _.forEach(this.selectedOptions, (value) => { + const optionMapping = { + value: value, + score: scoreForEachOption, + }; + this.mapping.push(optionMapping); + }); + } + } else if(this.questionInteractionType === 'match') { + if (!_.isEmpty(this.editorState.answer)) { + this.mapping = []; + const scoreForEachMatch = _.round( + this.maxScore / this.editorState.answer.length, + 2 + ); + _.forEach(this.editorState.answer, (value) => { + const optionMapping = { + value: value, + score: scoreForEachMatch, + }; + this.mapping.push(optionMapping); + }) + } } else { this.mapping = []; } } getInteractions(options) { - let index; - const interactOptions = _.map(options, (opt, key) => { - index = Number(key); - const hints = _.get(this.editorState, `interactions.response1.options[${index}].hints`) - return { label: opt.body, value: index, hints }; - }); - this.subMenuConfig(options); - const interactions = { - response1: { - type: 'choice', - options: interactOptions, - }, - }; - return interactions; + if (this.questionInteractionType === 'choice') { + let index; + const interactOptions = _.map(options, (opt, key) => { + index = Number(key); + const hints = _.get(this.editorState, `interactions.response1.options[${index}].hints`) + return { label: opt.body, value: index, hints }; + }); + this.subMenuConfig(options); + const interactions = { + response1: { + type: 'choice', + options: interactOptions, + }, + }; + return interactions; + } + else if (this.questionInteractionType === 'match') { + const optionSet = { + left: options.map((option) => ({ + label: option.leftOption, + value: option.leftOption.replace(/<\/?[^>]+(>|$)/g, ""), + })), + right: options.map((option) => ({ + label: option.rightOption, + value: option.rightOption.replace(/<\/?[^>]+(>|$)/g, ""), + })), + } + const interactions = { + response1: { + type: 'match', + optionSet: optionSet, + } + }; + return interactions; + } } setTemplete(template) { @@ -269,7 +307,22 @@ export class OptionsComponent implements OnInit, OnChanges { this.setMapping(); this.editorDataHandler(); } - + + onMatchCheck(event) { + if (event.target.checked) { + this.editorState.answer = this.editorState.options.map((option) => { + const obj = {}; + let leftOption = option.leftOption.replace(/<\/?[^>]+(>|$)/g, ""); + let rightOption = option.rightOption.replace(/<\/?[^>]+(>|$)/g, ""); + obj[leftOption] = rightOption; + return obj; + }); + } else { + this.editorState.answer = undefined; + } + this.setMapping(); + this.editorDataHandler(); + } setScore(value, scoreIndex) { const obj = { response: scoreIndex, From 35aa70ed66b30d14c668c5471b605d24d9224a7a Mon Sep 17 00:00:00 2001 From: Arpan Gupta Date: Mon, 24 Jul 2023 17:27:20 +0530 Subject: [PATCH 10/37] Issue #42 feat: body and answer body in the form of HTML --- .../components/question/question.component.ts | 70 ++++++++++++++++++- 1 file changed, 68 insertions(+), 2 deletions(-) diff --git a/projects/questionset-editor-library/src/lib/components/question/question.component.ts b/projects/questionset-editor-library/src/lib/components/question/question.component.ts index 56103a3c5..145d80e85 100644 --- a/projects/questionset-editor-library/src/lib/components/question/question.component.ts +++ b/projects/questionset-editor-library/src/lib/components/question/question.component.ts @@ -615,6 +615,29 @@ export class QuestionComponent implements OnInit, AfterViewInit, OnDestroy { } } + //to handle when question type is match + if (this.questionInteractionType === 'match') { + const data = _.get(this.treeNodeData, 'data.metadata'); + if (_.get(this.editorState, 'interactionTypes[0]') === 'match' && + _.isEmpty(this.editorState?.responseDeclaration?.response1?.mapping) && + !_.isUndefined(this.editorService?.editorConfig?.config?.renderTaxonomy) && + _.get(data,'allowScoring') === 'Yes') { + this.toasterService.error(_.get(this.configService, 'labelConfig.messages.error.005')); + this.showFormError = true; + return; + } else { + this.showFormError = false; + } + const rightOptionValid = _.find(this.editorState.options, option => (option.rightOption === undefined || option.rightOption === '' || option.rightOption.length > this.setCharacterLimit)); + const leftOptionValid = _.find(this.editorState.options, option => (option.leftOption === undefined || option.leftOption === '' || option.leftOption.length > this.setCharacterLimit)); + if (rightOptionValid || leftOptionValid || (_.isUndefined(this.editorState.answer) && this.sourcingSettings?.enforceCorrectAnswer)) { + this.showFormError = true; + return; + } else { + this.showFormError = false; + } + } + if (this.questionInteractionType === 'slider') { const min = _.get(this.sliderDatas, 'validation.range.min'); const max = _.get(this.sliderDatas, 'validation.range.max'); @@ -800,7 +823,7 @@ export class QuestionComponent implements OnInit, AfterViewInit, OnDestroy { _.get(treeNodeData,'allowScoring') === 'Yes' ? '' : _.set(metadata,'responseDeclaration.response1.mapping',[]); } - if (this.questionInteractionType != 'choice') { + if (this.questionInteractionType != 'choice' && this.questionInteractionType != 'match') { if (!_.isUndefined(metadata.answer)) { const answerHtml = this.getAnswerHtml(metadata.answer); const finalAnswer = this.getAnswerWrapperHtml(answerHtml); @@ -823,10 +846,17 @@ export class QuestionComponent implements OnInit, AfterViewInit, OnDestroy { }) const finalAnswer = this.getAnswerWrapperHtml(concatenatedAnswers); metadata.answer = finalAnswer; - } else if (this.questionInteractionType != 'default' && this.questionInteractionType != 'choice') { + } else if (this.questionInteractionType != 'default' && this.questionInteractionType != 'choice' && this.questionInteractionType != 'match') { metadata.responseDeclaration = this.getResponseDeclaration(this.questionInteractionType); } + if (this.questionInteractionType === 'match') { + metadata.body = this.getMatchQuestionHtmlBody(this.editorState.question); + const leftOptions = metadata.interactions.response1.optionSet.left; + const rightOptions = metadata.interactions.response1.optionSet.right; + metadata.answer = this.getMatchAnswerContainerHtml(leftOptions, rightOptions); + } + if (!_.isUndefined(this.selectedSolutionType) && !_.isEmpty(this.selectedSolutionType)) { const solutionObj = this.getSolutionObj(this.solutionUUID, this.selectedSolutionType, this.editorState.solutions); metadata.editorState.solutions = [solutionObj]; @@ -856,6 +886,33 @@ export class QuestionComponent implements OnInit, AfterViewInit, OnDestroy { return optionHtml; } + + getMatchAnswerContainerHtml(leftOptions, rightOptions) { + const matchContainerTemplate = '
{leftOptions}{rightOptions}
'; + const leftOptionsHtml = this.getOptionWrapperHtml(leftOptions, 'left'); + const rightOptionsHtml = this.getOptionWrapperHtml(rightOptions, 'right'); + const matchContainer = matchContainerTemplate + .replace('{leftOptions}', leftOptionsHtml) + .replace('{rightOptions}', rightOptionsHtml); + return matchContainer; + } + + getOptionWrapperHtml(options, type) { + const wrapperTemplate = `
{options}
`; + let optionsHtml = ''; + options.forEach((option) => { + const optionHtml = this.getMatchAnswerHtml(option.label, option.value); + optionsHtml = optionsHtml.concat(optionHtml); + }); + const wrapper = wrapperTemplate.replace('{options}', optionsHtml); + return wrapper; + } + getMatchAnswerHtml(label, value) { + const answerHtml = '
{label}
'; + const optionHtml = answerHtml.replace('{label}', label).replace('{value}', value); + return optionHtml; + } + getAnswerWrapperHtml(concatenatedAnswers) { const answerTemplate = '
{answers}
'; const answer = answerTemplate.replace('{answers}', concatenatedAnswers); @@ -899,6 +956,15 @@ export class QuestionComponent implements OnInit, AfterViewInit, OnDestroy { return videoSolutionValue; } + getMatchQuestionHtmlBody(question) { + const matchTemplateConfig = { + // tslint:disable-next-line:max-line-length + matchBody: '
{question}
' + }; + const { matchBody } = matchTemplateConfig; + const questionBody = matchBody.replace('{question}', question); + return questionBody; + } getMcqQuestionHtmlBody(question, templateId) { const mcqTemplateConfig = { From 862b86d692c7770afc08474d2d99328c4b0991d6 Mon Sep 17 00:00:00 2001 From: Arpan Gupta Date: Tue, 25 Jul 2023 15:22:32 +0530 Subject: [PATCH 11/37] test foreditorDataHandler() --- .../components/options/options.component.spec.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/projects/questionset-editor-library/src/lib/components/options/options.component.spec.ts b/projects/questionset-editor-library/src/lib/components/options/options.component.spec.ts index 83113a3bc..8527b418c 100644 --- a/projects/questionset-editor-library/src/lib/components/options/options.component.spec.ts +++ b/projects/questionset-editor-library/src/lib/components/options/options.component.spec.ts @@ -139,6 +139,7 @@ describe('OptionsComponent', () => { it('#editorDataHandler() should emit option data when answer is single value', () => { spyOn(component, 'prepareMcqBody').and.callThrough(); spyOn(component.editorDataOutput, 'emit').and.callThrough(); + component.questionInteractionType = 'choice'; component.editorState = mockOptionData.editorOptionData; component.editorDataHandler(); component.questionPrimaryCategory='Multiselect Multiple Choice Question'; @@ -149,6 +150,7 @@ describe('OptionsComponent', () => { it('#editorDataHandler() should emit option data when answer is multiple value', () => { spyOn(component, 'prepareMcqBody').and.callThrough(); spyOn(component.editorDataOutput, 'emit').and.callThrough(); + component.questionInteractionType = 'choice'; component.editorState = mockOptionData.editorOptionData; component.editorState.answer = [0,1]; component.editorDataHandler(); @@ -157,6 +159,18 @@ describe('OptionsComponent', () => { expect(component.editorDataOutput.emit).toHaveBeenCalled(); }); + it('when questionInteractionType is match prepareMtfBody will be called', () => { + spyOn(component, 'prepareMtfBody').and.callFake(()=>{}); + spyOn(component.editorDataOutput, 'emit').and.callFake(()=>{}); + component.questionInteractionType = 'match'; + component.editorDataHandler(); + component.questionPrimaryCategory='Match The Following Question'; + expect(component.prepareMtfBody).toHaveBeenCalled(); + expect(component.editorDataOutput.emit).toHaveBeenCalled(); + }); + + + it('#prepareMcqBody() should return expected mcq option data for single select MCQ', () => { component.maxScore = 1; component.selectedOptions = [0]; From 296a02529a28f5babbcebbaf4cc3e5e4a9bb45f8 Mon Sep 17 00:00:00 2001 From: Arpan Gupta Date: Tue, 25 Jul 2023 15:51:15 +0530 Subject: [PATCH 12/37] modifiedtest for setMapping when interaction type is choice --- .../src/lib/components/options/options.component.spec.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/projects/questionset-editor-library/src/lib/components/options/options.component.spec.ts b/projects/questionset-editor-library/src/lib/components/options/options.component.spec.ts index 8527b418c..2927fc75f 100644 --- a/projects/questionset-editor-library/src/lib/components/options/options.component.spec.ts +++ b/projects/questionset-editor-library/src/lib/components/options/options.component.spec.ts @@ -169,8 +169,6 @@ describe('OptionsComponent', () => { expect(component.editorDataOutput.emit).toHaveBeenCalled(); }); - - it('#prepareMcqBody() should return expected mcq option data for single select MCQ', () => { component.maxScore = 1; component.selectedOptions = [0]; @@ -192,6 +190,7 @@ describe('OptionsComponent', () => { }); it('#getResponseDeclaration() should return expected response declaration', () => { + component.questionInteractionType = 'choice'; component.mapping = [{ "value": 0, "score": 1 @@ -228,6 +227,7 @@ describe('OptionsComponent', () => { }); it('setMapping should set the mapping for single select MCQ', () => { + component.questionInteractionType = 'choice'; component.mapping = []; component.selectedOptions = [0]; component.maxScore = 1; @@ -237,6 +237,7 @@ describe('OptionsComponent', () => { }); it('setMapping should set the mapping for single select MCQ', () => { + component.questionInteractionType = 'choice'; component.mapping = []; component.selectedOptions = [0,1]; component.maxScore = 1; @@ -248,6 +249,7 @@ describe('OptionsComponent', () => { it('#getInteractions() should return expected response declaration', () => { spyOn(component,"getInteractions").and.callThrough(); + component.questionInteractionType = 'choice'; component.getInteractions(mockOptionData.editorOptionData.options); expect(component.getInteractions).toHaveBeenCalled(); // expect(mockOptionData.prepareMcqBody.interactions).toEqual(result); From 8b8deadae02446be3d457348c19677be44277ae1 Mon Sep 17 00:00:00 2001 From: Arpan Gupta Date: Tue, 1 Aug 2023 13:34:27 +0530 Subject: [PATCH 13/37] Issue #42 feat: new match component added --- .../lib/components/match/match.component.ts | 200 ++++++++++++++++++ 1 file changed, 200 insertions(+) create mode 100644 projects/questionset-editor-library/src/lib/components/match/match.component.ts diff --git a/projects/questionset-editor-library/src/lib/components/match/match.component.ts b/projects/questionset-editor-library/src/lib/components/match/match.component.ts new file mode 100644 index 000000000..920e556db --- /dev/null +++ b/projects/questionset-editor-library/src/lib/components/match/match.component.ts @@ -0,0 +1,200 @@ +import { Component, Input, OnInit, OnChanges, Output, EventEmitter, SimpleChanges } from '@angular/core'; +import * as _ from 'lodash-es'; +import { EditorTelemetryService } from '../../services/telemetry/telemetry.service'; +import { ConfigService } from '../../services/config/config.service'; +import { SubMenu } from '../question-option-sub-menu/question-option-sub-menu.component'; +import { TreeService } from '../../services/tree/tree.service'; +import { EditorService } from '../../services/editor/editor.service'; + +@Component({ + selector: 'lib-match', + templateUrl: './match.component.html', + styleUrls: ['./match.component.scss'] +}) +export class MatchComponent implements OnInit, OnChanges { + @Input() editorState: any; + @Input() showFormError; + @Input() sourcingSettings; + @Input() questionPrimaryCategory; + @Input() mapping = []; + @Input() isReadOnlyMode; + @Input() maxScore; + @Output() editorDataOutput: EventEmitter = new EventEmitter(); + public setCharacterLimit = 160; + public setImageLimit = 1; + public templateType = 'default'; + subMenus: SubMenu[][]; + hints = []; + showSubMenu: boolean = false; + parentMeta: any; + selectedOptions = []; + constructor( + public telemetryService: EditorTelemetryService, + public configService: ConfigService, + public treeService: TreeService, + private editorService: EditorService + ) {} + + ngOnInit() { + if (!_.isUndefined(this.editorState.answer)) { + this.addSelectedOptions(); + } + if (!_.isUndefined(this.editorState.templateId)) { + this.templateType = this.editorState.templateId; + } + this.mapping = _.get(this.editorState, 'responseDeclaration.response1.mapping') || []; + this.editorDataHandler(); + if (!_.isUndefined(this.editorService.editorConfig.config.renderTaxonomy)) { + this.parentMeta = this.treeService.getFirstChild().data.metadata; + this.showSubMenu = true; + } + } + ngOnChanges(changes: SimpleChanges){ + if (!_.isUndefined(changes.maxScore.previousValue) && !_.isNaN(changes.maxScore.currentValue)) { + this.setMapping(); + this.editorDataHandler(); + } + } + addSelectedOptions() { + this.selectedOptions = this.editorState.answer; + } + + editorDataHandler(event?) { + const body = this.prepareMtfBody(this.editorState); + this.editorDataOutput.emit({ + body, + mediaobj: event ? event.mediaobj : undefined, + }); + } + prepareMtfBody(editorState) { + let metadata: any; + const correctAnswer = editorState.answer; + let options: any; + if (!_.isEmpty(correctAnswer)) { + options = { + leftOptions: editorState.options.map((option, index) => { + return { + value: { + body: option.leftOption, + value: index, + } + } + }), + rightOptions: editorState.options.map((option, index) => { + return { + value: { + body: option.rightOption, + value: index, + } + } + }) + } + } + metadata = { + templateId: this.templateType, + name: this.questionPrimaryCategory || "Match The Following Question", + responseDeclaration: this.getResponseDeclaration(editorState), + outcomeDeclaration: this.getOutcomeDeclaration(), + interactionTypes: ["match"], + interactions: this.getInteractions(editorState.options), + editorState: { + options, + }, + qType: "MTF", + primaryCategory: + this.questionPrimaryCategory || "Match The Following Question", + }; + return metadata; + } + getResponseDeclaration(editorState) { + const responseDeclaration = { + response1: { + cardinality: 'multiple', + type: 'map', + correctResponse: { + value: editorState.answer, + }, + mapping: this.mapping, + }, + }; + return responseDeclaration; + } + + getOutcomeDeclaration() { + const outcomeDeclaration = { + maxScore: { + cardinality: 'multiple', + type: 'integer', + defaultValue: this.maxScore + } + }; + return outcomeDeclaration; + } + + setMapping() { + if (!_.isEmpty(this.editorState.answer)) { + this.mapping = []; + const scoreForEachMatch = _.round( + this.maxScore / this.editorState.answer.length, + 2 + ); + _.forEach(this.editorState.answer, (value) => { + const optionMapping = { + value: value, + score: scoreForEachMatch, + }; + this.mapping.push(optionMapping); + }) + } else { + this.mapping = []; + } + } + + getInteractions(options) { + const optionSet = { + leftOptions: options.map((option,index) => ({ + label: option.leftOption, + value: index, + })), + rightOptions: options.map((option,index) => ({ + label: option.rightOption, + value: index, + })), + } + const interactions = { + response1: { + type: 'match', + options: optionSet, + } + }; + return interactions; + } + + onOptionChange(event,index) { + if(event.body!=='' && !_.includes(this.selectedOptions, index)) { + this.selectedOptions.push(index); + } else if(event.body === '') { + _.remove(this.selectedOptions, (n) => { + return n === index; + }); + } + if(this.selectedOptions === undefined){ + this.editorState.answer = undefined; + } + else { + this.editorState.answer = this.selectedOptions.map((value) => ({ [value]: value })); + } + this.setMapping(); + this.editorDataHandler(); + } + setScore(value, scoreIndex) { + const obj = { + response: scoreIndex, + outcomes: { + score: value, + }, + }; + this.mapping[scoreIndex] = obj; + this.editorDataHandler(); + } +} From b14cda94cca4123405d55a92c3545bc7718324ae Mon Sep 17 00:00:00 2001 From: Arpan Gupta Date: Tue, 1 Aug 2023 13:37:52 +0530 Subject: [PATCH 14/37] Adding match component in question template --- .../src/lib/components/question/question.component.html | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/projects/questionset-editor-library/src/lib/components/question/question.component.html b/projects/questionset-editor-library/src/lib/components/question/question.component.html index 5a26f2329..6bd2590d6 100644 --- a/projects/questionset-editor-library/src/lib/components/question/question.component.html +++ b/projects/questionset-editor-library/src/lib/components/question/question.component.html @@ -64,6 +64,13 @@ (editorDataOutput)="editorDataHandler($event)" [sourcingSettings]="sourcingSettings" [mapping]="scoreMapping" [maxScore]="maxScore" [isReadOnlyMode]="isReadOnlyMode"> + + From 2c982fd390eab8a69c03f005fba673390efd47b3 Mon Sep 17 00:00:00 2001 From: Arpan Gupta Date: Tue, 1 Aug 2023 13:50:31 +0530 Subject: [PATCH 15/37] Adding match component in module --- .../src/lib/questionset-editor-library.module.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/projects/questionset-editor-library/src/lib/questionset-editor-library.module.ts b/projects/questionset-editor-library/src/lib/questionset-editor-library.module.ts index e2f5303eb..ccf245b11 100644 --- a/projects/questionset-editor-library/src/lib/questionset-editor-library.module.ts +++ b/projects/questionset-editor-library/src/lib/questionset-editor-library.module.ts @@ -36,6 +36,7 @@ import { PlainTreeComponent } from './components/plain-tree/plain-tree.component import { A11yModule } from '@angular/cdk/a11y'; import { ProgressStatusComponent } from './components/progress-status/progress-status.component'; import {TermAndConditionComponent} from './components/term-and-condition/term-and-condition.component'; +import { MatchComponent } from './components/match/match.component'; import { QualityParamsModalComponent } from './components/quality-params-modal/quality-params-modal.component'; @NgModule({ @@ -68,7 +69,8 @@ import { QualityParamsModalComponent } from './components/quality-params-modal/q PlainTreeComponent, ProgressStatusComponent, TermAndConditionComponent, - QualityParamsModalComponent + QualityParamsModalComponent, + MatchComponent ], imports: [CommonModule, FormsModule, ReactiveFormsModule.withConfig({callSetDisabledState: 'whenDisabledForLegacyCode'}), RouterModule.forChild([]), SuiModule, CommonFormElementsModule, InfiniteScrollModule, HttpClientModule, ResourceLibraryModule, A11yModule], From 1ea5d7ccaf68cc977789ed69c9f294eb62304f6e Mon Sep 17 00:00:00 2001 From: Arpan Gupta Date: Tue, 1 Aug 2023 16:18:17 +0530 Subject: [PATCH 16/37] Revert "test foreditorDataHandler()" This reverts commit 862b86d692c7770afc08474d2d99328c4b0991d6. --- .../lib/components/options/options.component.spec.ts | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/projects/questionset-editor-library/src/lib/components/options/options.component.spec.ts b/projects/questionset-editor-library/src/lib/components/options/options.component.spec.ts index 2927fc75f..7ba3e7510 100644 --- a/projects/questionset-editor-library/src/lib/components/options/options.component.spec.ts +++ b/projects/questionset-editor-library/src/lib/components/options/options.component.spec.ts @@ -139,7 +139,6 @@ describe('OptionsComponent', () => { it('#editorDataHandler() should emit option data when answer is single value', () => { spyOn(component, 'prepareMcqBody').and.callThrough(); spyOn(component.editorDataOutput, 'emit').and.callThrough(); - component.questionInteractionType = 'choice'; component.editorState = mockOptionData.editorOptionData; component.editorDataHandler(); component.questionPrimaryCategory='Multiselect Multiple Choice Question'; @@ -150,7 +149,6 @@ describe('OptionsComponent', () => { it('#editorDataHandler() should emit option data when answer is multiple value', () => { spyOn(component, 'prepareMcqBody').and.callThrough(); spyOn(component.editorDataOutput, 'emit').and.callThrough(); - component.questionInteractionType = 'choice'; component.editorState = mockOptionData.editorOptionData; component.editorState.answer = [0,1]; component.editorDataHandler(); @@ -159,16 +157,6 @@ describe('OptionsComponent', () => { expect(component.editorDataOutput.emit).toHaveBeenCalled(); }); - it('when questionInteractionType is match prepareMtfBody will be called', () => { - spyOn(component, 'prepareMtfBody').and.callFake(()=>{}); - spyOn(component.editorDataOutput, 'emit').and.callFake(()=>{}); - component.questionInteractionType = 'match'; - component.editorDataHandler(); - component.questionPrimaryCategory='Match The Following Question'; - expect(component.prepareMtfBody).toHaveBeenCalled(); - expect(component.editorDataOutput.emit).toHaveBeenCalled(); - }); - it('#prepareMcqBody() should return expected mcq option data for single select MCQ', () => { component.maxScore = 1; component.selectedOptions = [0]; From c233eae4b16dc0ce1179b8fc9d10f869c8c03b37 Mon Sep 17 00:00:00 2001 From: Arpan Gupta Date: Tue, 1 Aug 2023 16:32:12 +0530 Subject: [PATCH 17/37] Revert "modifiedtest for setMapping when interaction type is choice" This reverts commit 296a02529a28f5babbcebbaf4cc3e5e4a9bb45f8. --- .../src/lib/components/options/options.component.spec.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/projects/questionset-editor-library/src/lib/components/options/options.component.spec.ts b/projects/questionset-editor-library/src/lib/components/options/options.component.spec.ts index 7ba3e7510..83113a3bc 100644 --- a/projects/questionset-editor-library/src/lib/components/options/options.component.spec.ts +++ b/projects/questionset-editor-library/src/lib/components/options/options.component.spec.ts @@ -178,7 +178,6 @@ describe('OptionsComponent', () => { }); it('#getResponseDeclaration() should return expected response declaration', () => { - component.questionInteractionType = 'choice'; component.mapping = [{ "value": 0, "score": 1 @@ -215,7 +214,6 @@ describe('OptionsComponent', () => { }); it('setMapping should set the mapping for single select MCQ', () => { - component.questionInteractionType = 'choice'; component.mapping = []; component.selectedOptions = [0]; component.maxScore = 1; @@ -225,7 +223,6 @@ describe('OptionsComponent', () => { }); it('setMapping should set the mapping for single select MCQ', () => { - component.questionInteractionType = 'choice'; component.mapping = []; component.selectedOptions = [0,1]; component.maxScore = 1; @@ -237,7 +234,6 @@ describe('OptionsComponent', () => { it('#getInteractions() should return expected response declaration', () => { spyOn(component,"getInteractions").and.callThrough(); - component.questionInteractionType = 'choice'; component.getInteractions(mockOptionData.editorOptionData.options); expect(component.getInteractions).toHaveBeenCalled(); // expect(mockOptionData.prepareMcqBody.interactions).toEqual(result); From 23ddf3a0c24efce5403f66b7306fdfe1fb08c905 Mon Sep 17 00:00:00 2001 From: Arpan Gupta Date: Tue, 1 Aug 2023 16:35:18 +0530 Subject: [PATCH 18/37] Reverting option component file --- .../components/options/options.component.ts | 147 ++++-------------- 1 file changed, 28 insertions(+), 119 deletions(-) diff --git a/projects/questionset-editor-library/src/lib/components/options/options.component.ts b/projects/questionset-editor-library/src/lib/components/options/options.component.ts index 60dd1b29e..d317eccf9 100644 --- a/projects/questionset-editor-library/src/lib/components/options/options.component.ts +++ b/projects/questionset-editor-library/src/lib/components/options/options.component.ts @@ -15,7 +15,6 @@ export class OptionsComponent implements OnInit, OnChanges { @Input() showFormError; @Input() sourcingSettings; @Input() questionPrimaryCategory; - @Input() questionInteractionType; @Input() mapping = []; @Input() isReadOnlyMode; @Input() maxScore; @@ -76,16 +75,8 @@ export class OptionsComponent implements OnInit, OnChanges { } editorDataHandler(event?) { - let body: any; - if (this.questionInteractionType === 'choice') { - body = this.prepareMcqBody(this.editorState); - } else if (this.questionInteractionType === 'match') { - body = this.prepareMtfBody(this.editorState); - } - this.editorDataOutput.emit({ - body, - mediaobj: event ? event.mediaobj : undefined, - }); + const body = this.prepareMcqBody(this.editorState); + this.editorDataOutput.emit({ body, mediaobj: event ? event.mediaobj : undefined }); } prepareMcqBody(editorState) { @@ -128,40 +119,11 @@ export class OptionsComponent implements OnInit, OnChanges { return metadata; } - prepareMtfBody(editorState) { - let metadata: any; - const correctAnswer = editorState.answer; - let options: any; - if (!_.isEmpty(correctAnswer)) { - options = _.reduce(this.editorState.options,function (acc, obj) { - acc.leftOption.push(obj.leftOption); - acc.rightOption.push(obj.rightOption); - return acc; - },{ leftOption: [], rightOption: [] } - ); - } - console.log(options); - console.log(editorState.answer); - metadata = { - templateId: this.templateType, - name: this.questionPrimaryCategory || 'Match The Following Question', - responseDeclaration: this.getResponseDeclaration(editorState), - outcomeDeclaration: this.getOutcomeDeclaration(), - interactionTypes: ['match'], - interactions: this.getInteractions(editorState.options), - editorState: { - options, - }, - qType: 'MTF', - primaryCategory: this.questionPrimaryCategory || "Match The Following Question", - }; - return metadata; - } getResponseDeclaration(editorState) { const responseDeclaration = { response1: { cardinality: this.getCardinality(), - type: this.questionInteractionType === 'choice' ? 'integer' : 'map', + type: 'integer', correctResponse: { value: editorState.answer, }, @@ -191,74 +153,36 @@ export class OptionsComponent implements OnInit, OnChanges { } setMapping() { - if (this.questionInteractionType === 'choice') { - if (!_.isEmpty(this.selectedOptions)) { - this.mapping = []; - const scoreForEachOption = _.round((this.maxScore/this.selectedOptions.length), 2); - _.forEach(this.selectedOptions, (value) => { - const optionMapping = { - value: value, - score: scoreForEachOption, - }; - this.mapping.push(optionMapping); - }); - } - } else if(this.questionInteractionType === 'match') { - if (!_.isEmpty(this.editorState.answer)) { - this.mapping = []; - const scoreForEachMatch = _.round( - this.maxScore / this.editorState.answer.length, - 2 - ); - _.forEach(this.editorState.answer, (value) => { - const optionMapping = { - value: value, - score: scoreForEachMatch, - }; - this.mapping.push(optionMapping); - }) - } + if(!_.isEmpty(this.selectedOptions)) { + this.mapping = []; + const scoreForEachOption = _.round((this.maxScore/this.selectedOptions.length), 2); + _.forEach(this.selectedOptions, (value) => { + const optionMapping = { + value: value, + score: scoreForEachOption, + } + this.mapping.push(optionMapping) + }) } else { this.mapping = []; } } getInteractions(options) { - if (this.questionInteractionType === 'choice') { - let index; - const interactOptions = _.map(options, (opt, key) => { - index = Number(key); - const hints = _.get(this.editorState, `interactions.response1.options[${index}].hints`) - return { label: opt.body, value: index, hints }; - }); - this.subMenuConfig(options); - const interactions = { - response1: { - type: 'choice', - options: interactOptions, - }, - }; - return interactions; - } - else if (this.questionInteractionType === 'match') { - const optionSet = { - left: options.map((option) => ({ - label: option.leftOption, - value: option.leftOption.replace(/<\/?[^>]+(>|$)/g, ""), - })), - right: options.map((option) => ({ - label: option.rightOption, - value: option.rightOption.replace(/<\/?[^>]+(>|$)/g, ""), - })), - } - const interactions = { - response1: { - type: 'match', - optionSet: optionSet, - } - }; - return interactions; - } + let index; + const interactOptions = _.map(options, (opt, key) => { + index = Number(key); + const hints = _.get(this.editorState, `interactions.response1.options[${index}].hints`) + return { label: opt.body, value: index, hints }; + }); + this.subMenuConfig(options); + const interactions = { + response1: { + type: 'choice', + options: interactOptions, + }, + }; + return interactions; } setTemplete(template) { @@ -307,22 +231,7 @@ export class OptionsComponent implements OnInit, OnChanges { this.setMapping(); this.editorDataHandler(); } - - onMatchCheck(event) { - if (event.target.checked) { - this.editorState.answer = this.editorState.options.map((option) => { - const obj = {}; - let leftOption = option.leftOption.replace(/<\/?[^>]+(>|$)/g, ""); - let rightOption = option.rightOption.replace(/<\/?[^>]+(>|$)/g, ""); - obj[leftOption] = rightOption; - return obj; - }); - } else { - this.editorState.answer = undefined; - } - this.setMapping(); - this.editorDataHandler(); - } + setScore(value, scoreIndex) { const obj = { response: scoreIndex, From 000c5c3bf83927dd7697244e6202baa00f38df06 Mon Sep 17 00:00:00 2001 From: Arpan Gupta Date: Tue, 1 Aug 2023 20:26:14 +0530 Subject: [PATCH 19/37] Refactoring of code in match component and question component --- .../src/styles.scss | 1 + .../lib/components/match/match.component.html | 52 ++++++++++++++++++ .../lib/components/match/match.component.scss | 0 .../components/match/match.component.spec.ts | 23 ++++++++ .../lib/components/match/match.component.ts | 55 +++++-------------- .../components/question/question.component.ts | 13 ++--- .../src/lib/interfaces/MtfForm.ts | 9 ++- 7 files changed, 101 insertions(+), 52 deletions(-) create mode 100644 projects/questionset-editor-library/src/lib/components/match/match.component.html create mode 100644 projects/questionset-editor-library/src/lib/components/match/match.component.scss create mode 100644 projects/questionset-editor-library/src/lib/components/match/match.component.spec.ts diff --git a/projects/questionset-editor-library-wc/src/styles.scss b/projects/questionset-editor-library-wc/src/styles.scss index 73bf969da..13239b7e9 100644 --- a/projects/questionset-editor-library-wc/src/styles.scss +++ b/projects/questionset-editor-library-wc/src/styles.scss @@ -22,4 +22,5 @@ @import "./../../questionset-editor-library/src/lib/components/template/template.component.scss"; @import "./../../questionset-editor-library/src/lib/components/term-and-condition/term-and-condition.component.scss"; @import "./../../questionset-editor-library/src/lib/components/translations/translations.component.scss"; +@import "./../../questionset-editor-library/src/lib/components/match/match.component.scss"; diff --git a/projects/questionset-editor-library/src/lib/components/match/match.component.html b/projects/questionset-editor-library/src/lib/components/match/match.component.html new file mode 100644 index 000000000..a59dafdd5 --- /dev/null +++ b/projects/questionset-editor-library/src/lib/components/match/match.component.html @@ -0,0 +1,52 @@ + +
+ + + +
+
+
+
+
+ + + + +
+
+ + + + +
+
+
+ +
+
+
+
+ +
+
diff --git a/projects/questionset-editor-library/src/lib/components/match/match.component.scss b/projects/questionset-editor-library/src/lib/components/match/match.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/projects/questionset-editor-library/src/lib/components/match/match.component.spec.ts b/projects/questionset-editor-library/src/lib/components/match/match.component.spec.ts new file mode 100644 index 000000000..932baeae4 --- /dev/null +++ b/projects/questionset-editor-library/src/lib/components/match/match.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { MatchComponent } from './match.component'; + +describe('MatchComponent', () => { + let component: MatchComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ MatchComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(MatchComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/projects/questionset-editor-library/src/lib/components/match/match.component.ts b/projects/questionset-editor-library/src/lib/components/match/match.component.ts index 920e556db..2c479610c 100644 --- a/projects/questionset-editor-library/src/lib/components/match/match.component.ts +++ b/projects/questionset-editor-library/src/lib/components/match/match.component.ts @@ -2,7 +2,6 @@ import { Component, Input, OnInit, OnChanges, Output, EventEmitter, SimpleChange import * as _ from 'lodash-es'; import { EditorTelemetryService } from '../../services/telemetry/telemetry.service'; import { ConfigService } from '../../services/config/config.service'; -import { SubMenu } from '../question-option-sub-menu/question-option-sub-menu.component'; import { TreeService } from '../../services/tree/tree.service'; import { EditorService } from '../../services/editor/editor.service'; @@ -23,11 +22,7 @@ export class MatchComponent implements OnInit, OnChanges { public setCharacterLimit = 160; public setImageLimit = 1; public templateType = 'default'; - subMenus: SubMenu[][]; - hints = []; - showSubMenu: boolean = false; parentMeta: any; - selectedOptions = []; constructor( public telemetryService: EditorTelemetryService, public configService: ConfigService, @@ -36,18 +31,11 @@ export class MatchComponent implements OnInit, OnChanges { ) {} ngOnInit() { - if (!_.isUndefined(this.editorState.answer)) { - this.addSelectedOptions(); - } if (!_.isUndefined(this.editorState.templateId)) { this.templateType = this.editorState.templateId; } this.mapping = _.get(this.editorState, 'responseDeclaration.response1.mapping') || []; this.editorDataHandler(); - if (!_.isUndefined(this.editorService.editorConfig.config.renderTaxonomy)) { - this.parentMeta = this.treeService.getFirstChild().data.metadata; - this.showSubMenu = true; - } } ngOnChanges(changes: SimpleChanges){ if (!_.isUndefined(changes.maxScore.previousValue) && !_.isNaN(changes.maxScore.currentValue)) { @@ -55,9 +43,6 @@ export class MatchComponent implements OnInit, OnChanges { this.editorDataHandler(); } } - addSelectedOptions() { - this.selectedOptions = this.editorState.answer; - } editorDataHandler(event?) { const body = this.prepareMtfBody(this.editorState); @@ -68,9 +53,16 @@ export class MatchComponent implements OnInit, OnChanges { } prepareMtfBody(editorState) { let metadata: any; - const correctAnswer = editorState.answer; + if (_.isEmpty(editorState.correctMatchPair) && !_.isEmpty(editorState.options)) { + editorState.correctMatchPair = editorState.options.map((option, index) => { + const correctMatchPair = {}; + correctMatchPair[index.toString()] = index; + return correctMatchPair; + }); + } + this.setMapping(); let options: any; - if (!_.isEmpty(correctAnswer)) { + if (!_.isEmpty(editorState.correctMatchPair)) { options = { leftOptions: editorState.options.map((option, index) => { return { @@ -112,7 +104,7 @@ export class MatchComponent implements OnInit, OnChanges { cardinality: 'multiple', type: 'map', correctResponse: { - value: editorState.answer, + value: editorState.correctMatchPair, }, mapping: this.mapping, }, @@ -130,15 +122,15 @@ export class MatchComponent implements OnInit, OnChanges { }; return outcomeDeclaration; } - + setMapping() { - if (!_.isEmpty(this.editorState.answer)) { + if (!_.isEmpty(this.editorState.correctMatchPair)) { this.mapping = []; const scoreForEachMatch = _.round( - this.maxScore / this.editorState.answer.length, + this.maxScore / this.editorState.correctMatchPair.length, 2 ); - _.forEach(this.editorState.answer, (value) => { + _.forEach(this.editorState.correctMatchPair, (value) => { const optionMapping = { value: value, score: scoreForEachMatch, @@ -168,25 +160,8 @@ export class MatchComponent implements OnInit, OnChanges { } }; return interactions; - } - - onOptionChange(event,index) { - if(event.body!=='' && !_.includes(this.selectedOptions, index)) { - this.selectedOptions.push(index); - } else if(event.body === '') { - _.remove(this.selectedOptions, (n) => { - return n === index; - }); - } - if(this.selectedOptions === undefined){ - this.editorState.answer = undefined; - } - else { - this.editorState.answer = this.selectedOptions.map((value) => ({ [value]: value })); - } - this.setMapping(); - this.editorDataHandler(); } + setScore(value, scoreIndex) { const obj = { response: scoreIndex, diff --git a/projects/questionset-editor-library/src/lib/components/question/question.component.ts b/projects/questionset-editor-library/src/lib/components/question/question.component.ts index 145d80e85..6ab00be78 100644 --- a/projects/questionset-editor-library/src/lib/components/question/question.component.ts +++ b/projects/questionset-editor-library/src/lib/components/question/question.component.ts @@ -260,7 +260,7 @@ export class QuestionComponent implements OnInit, AfterViewInit, OnDestroy { const question = this.questionMetaData?.editorState?.question; const interactions = this.questionMetaData?.interactions; this.editorState = new MtfForm({ - question, options, answer: _.get(responseDeclaration, 'response1.correctResponse.value') + question, options, correctMatchPair: _.get(responseDeclaration, 'response1.correctResponse.value') }, { templateId, numberOfOptions, maximumOptions }); this.editorState.solutions = this.questionMetaData?.editorState?.solutions; this.editorState.interactions = interactions; @@ -630,7 +630,7 @@ export class QuestionComponent implements OnInit, AfterViewInit, OnDestroy { } const rightOptionValid = _.find(this.editorState.options, option => (option.rightOption === undefined || option.rightOption === '' || option.rightOption.length > this.setCharacterLimit)); const leftOptionValid = _.find(this.editorState.options, option => (option.leftOption === undefined || option.leftOption === '' || option.leftOption.length > this.setCharacterLimit)); - if (rightOptionValid || leftOptionValid || (_.isUndefined(this.editorState.answer) && this.sourcingSettings?.enforceCorrectAnswer)) { + if (rightOptionValid || leftOptionValid || (_.isUndefined(this.editorState.correctMatchPair) && this.sourcingSettings?.enforceCorrectAnswer)) { this.showFormError = true; return; } else { @@ -852,9 +852,9 @@ export class QuestionComponent implements OnInit, AfterViewInit, OnDestroy { if (this.questionInteractionType === 'match') { metadata.body = this.getMatchQuestionHtmlBody(this.editorState.question); - const leftOptions = metadata.interactions.response1.optionSet.left; - const rightOptions = metadata.interactions.response1.optionSet.right; - metadata.answer = this.getMatchAnswerContainerHtml(leftOptions, rightOptions); + const leftOptions = metadata.interactions.response1.options.leftOptions; + const rightOptions = metadata.interactions.response1.options.rightOptions; + metadata.correctMatchPair = this.getMatchAnswerContainerHtml(leftOptions, rightOptions); } if (!_.isUndefined(this.selectedSolutionType) && !_.isEmpty(this.selectedSolutionType)) { @@ -885,7 +885,6 @@ export class QuestionComponent implements OnInit, AfterViewInit, OnDestroy { const optionHtml = answerHtml.replace('{answer}', optionLabel); return optionHtml; } - getMatchAnswerContainerHtml(leftOptions, rightOptions) { const matchContainerTemplate = '
{leftOptions}{rightOptions}
'; @@ -895,7 +894,7 @@ export class QuestionComponent implements OnInit, AfterViewInit, OnDestroy { .replace('{leftOptions}', leftOptionsHtml) .replace('{rightOptions}', rightOptionsHtml); return matchContainer; - } + } getOptionWrapperHtml(options, type) { const wrapperTemplate = `
{options}
`; diff --git a/projects/questionset-editor-library/src/lib/interfaces/MtfForm.ts b/projects/questionset-editor-library/src/lib/interfaces/MtfForm.ts index 9634569bc..26077c4e4 100644 --- a/projects/questionset-editor-library/src/lib/interfaces/MtfForm.ts +++ b/projects/questionset-editor-library/src/lib/interfaces/MtfForm.ts @@ -7,7 +7,7 @@ export class MtfOption { export interface MtfData { question: string; options: Array; - answer?: string; + correctMatchPair?: string; learningOutcome?: string; complexityLevel?: string; maxScore?: number; @@ -17,25 +17,24 @@ export interface MtfConfig { templateId?: string; numberOfOptions?: number; maximumOptions?: number; - } export class MtfForm { public question: string; public options: Array; public templateId: string; - public answer: string; + public correctMatchPair: string; public learningOutcome?: string; public complexityLevel?: string; public maxScore?: number; public maximumOptions; public numberOfOptions; - constructor({question, options, answer, learningOutcome, complexityLevel, maxScore,}: MtfData,{ templateId, numberOfOptions, maximumOptions }: MtfConfig) { + constructor({question, options, correctMatchPair, learningOutcome, complexityLevel, maxScore,}: MtfData,{ templateId, numberOfOptions, maximumOptions }: MtfConfig) { this.question = question; this.options = options || []; this.templateId = templateId; - this.answer = answer; + this.correctMatchPair = correctMatchPair; this.learningOutcome = learningOutcome; this.complexityLevel = complexityLevel; this.numberOfOptions = numberOfOptions || 2; From c19a96e59b7806000432ff2bf71910d429ef8020 Mon Sep 17 00:00:00 2001 From: Arpan Gupta Date: Wed, 2 Aug 2023 00:47:00 +0530 Subject: [PATCH 20/37] Tests for match component --- .../components/match/match.component.spec.ts | 32 +++++++++++++------ 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/projects/questionset-editor-library/src/lib/components/match/match.component.spec.ts b/projects/questionset-editor-library/src/lib/components/match/match.component.spec.ts index 932baeae4..294e61b1c 100644 --- a/projects/questionset-editor-library/src/lib/components/match/match.component.spec.ts +++ b/projects/questionset-editor-library/src/lib/components/match/match.component.spec.ts @@ -1,23 +1,35 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; - +import { TelemetryInteractDirective } from '../../directives/telemetry-interact/telemetry-interact.directive'; +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { FormsModule } from '@angular/forms'; import { MatchComponent } from './match.component'; +import { CUSTOM_ELEMENTS_SCHEMA} from '@angular/core'; +import { ConfigService } from '../../services/config/config.service'; +import { SuiModule } from 'ng2-semantic-ui-v9'; +import { TreeService } from '../../services/tree/tree.service'; +import { EditorTelemetryService } from '../../services/telemetry/telemetry.service'; + -describe('MatchComponent', () => { +describe('OptionsComponent', () => { let component: MatchComponent; let fixture: ComponentFixture; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - declarations: [ MatchComponent ] + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, FormsModule, SuiModule], + declarations: [MatchComponent, TelemetryInteractDirective], + providers: [ConfigService, TreeService, EditorTelemetryService,], + schemas: [CUSTOM_ELEMENTS_SCHEMA] }) - .compileComponents(); + .compileComponents(); + })); + beforeEach(() => { fixture = TestBed.createComponent(MatchComponent); component = fixture.componentInstance; - fixture.detectChanges(); + // fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); -}); +}); \ No newline at end of file From f82c88418e08b43e5381f91560183c8ba820b8a6 Mon Sep 17 00:00:00 2001 From: Arpan Gupta Date: Mon, 7 Aug 2023 15:03:03 +0530 Subject: [PATCH 21/37] Issue #42 feat: New Match Type Question Implementation --- .../lib/components/match/match.component.html | 34 +- .../lib/components/match/match.component.scss | 25 ++ .../match/match.component.spec.data.ts | 114 ++++++ .../components/match/match.component.spec.ts | 119 +++++- .../lib/components/match/match.component.ts | 37 +- .../components/options/options.component.ts | 10 +- .../components/question/question.component.ts | 36 +- .../src/lib/interfaces/MtfForm.ts | 2 +- .../src/lib/services/config/label.config.json | 382 +++++++++--------- 9 files changed, 495 insertions(+), 264 deletions(-) create mode 100644 projects/questionset-editor-library/src/lib/components/match/match.component.spec.data.ts diff --git a/projects/questionset-editor-library/src/lib/components/match/match.component.html b/projects/questionset-editor-library/src/lib/components/match/match.component.html index a59dafdd5..ea938a44b 100644 --- a/projects/questionset-editor-library/src/lib/components/match/match.component.html +++ b/projects/questionset-editor-library/src/lib/components/match/match.component.html @@ -5,37 +5,39 @@
-
-
-
+
+
+
+ [setCharacterLimit]="setCharacterLimit" (editorDataOutput)="option.left= $event.body; editorDataHandler($event);" + [editorDataInput]="option.left" class="ckeditor-tool__option mb-10" + [class.mb-5]="showFormError && option.left.length > setCharacterLimit"> -
-
+
-
-
-
diff --git a/projects/questionset-editor-library/src/lib/components/match/match.component.scss b/projects/questionset-editor-library/src/lib/components/match/match.component.scss index e69de29bb..b6b99bbbe 100644 --- a/projects/questionset-editor-library/src/lib/components/match/match.component.scss +++ b/projects/questionset-editor-library/src/lib/components/match/match.component.scss @@ -0,0 +1,25 @@ +.b-0{ + border: 0 !important; +} + +.bg-none{ + background-color: transparent !important; + + &:hover, + &:focus { + background-color: transparent !important; + } +} + +.w-49{ + width: 49%; + max-width: 49%; +} + +.sb-line-height-24 { + line-height: 24px; +} + +.right{ + float: right; +} \ No newline at end of file diff --git a/projects/questionset-editor-library/src/lib/components/match/match.component.spec.data.ts b/projects/questionset-editor-library/src/lib/components/match/match.component.spec.data.ts new file mode 100644 index 000000000..2552ed8fd --- /dev/null +++ b/projects/questionset-editor-library/src/lib/components/match/match.component.spec.data.ts @@ -0,0 +1,114 @@ +export const mockOptionData = { + editorOptionData: { + question: "

Match the following with appropriate answer?

", + options: [ + { + left: "

Lotus

", + right: "

Flower

", + }, + { + left: "

Mango

", + right: "

Fruit

", + }, + ], + templateId: "mtf-vertical", + correctMatchPair: [{ "0": 0 }, { "1": 1 }], + numberOfOptions: 4, + }, + prepareMtfBody: { + templateId: "default", + name: "Match The Following Question", + responseDeclaration: { + response1: { + cardinality: "multiple", + type: "map", + correctResponse: { + value: [ + { + "0": 0, + }, + { + "1": 1, + }, + ], + }, + mapping: [ + { + value: { + "0": 0, + }, + score: 2, + }, + { + value: { + "1": 1, + }, + score: 2, + }, + ], + }, + }, + interactionTypes: ["match"], + interactions: { + response1: { + type: "match", + options: { + left: [ + { + label: "

Lotus

", + value: 0, + }, + { + label: "

Mango

", + value: 1, + }, + ], + right: [ + { + label: "

Flower

", + value: 0, + }, + { + label: "

Fruit

", + value: 1, + }, + ], + }, + }, + }, + editorState: { + options: { + left: [ + { + value: { + body: "

Lotus

", + value: 0, + }, + }, + { + value: { + body: "

Mango

", + value: 1, + }, + }, + ], + right: [ + { + value: { + body: "

Flower

", + value: 0, + }, + }, + { + value: { + body: "

Fruit

", + value: 1, + }, + }, + ], + }, + }, + qType: "MTF", + primaryCategory: "Match The Following Question", + }, +}; diff --git a/projects/questionset-editor-library/src/lib/components/match/match.component.spec.ts b/projects/questionset-editor-library/src/lib/components/match/match.component.spec.ts index 294e61b1c..79e1a79b7 100644 --- a/projects/questionset-editor-library/src/lib/components/match/match.component.spec.ts +++ b/projects/questionset-editor-library/src/lib/components/match/match.component.spec.ts @@ -1,23 +1,21 @@ import { TelemetryInteractDirective } from '../../directives/telemetry-interact/telemetry-interact.directive'; import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { FormsModule } from '@angular/forms'; import { MatchComponent } from './match.component'; -import { CUSTOM_ELEMENTS_SCHEMA} from '@angular/core'; +import { mockOptionData } from './match.component.spec.data'; +import { CUSTOM_ELEMENTS_SCHEMA, SimpleChange} from '@angular/core'; import { ConfigService } from '../../services/config/config.service'; -import { SuiModule } from 'ng2-semantic-ui-v9'; -import { TreeService } from '../../services/tree/tree.service'; import { EditorTelemetryService } from '../../services/telemetry/telemetry.service'; -describe('OptionsComponent', () => { +describe('MatchComponent', () => { let component: MatchComponent; let fixture: ComponentFixture; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [HttpClientTestingModule, FormsModule, SuiModule], + imports: [HttpClientTestingModule], declarations: [MatchComponent, TelemetryInteractDirective], - providers: [ConfigService, TreeService, EditorTelemetryService,], + providers: [ConfigService, EditorTelemetryService,], schemas: [CUSTOM_ELEMENTS_SCHEMA] }) .compileComponents(); @@ -26,10 +24,117 @@ describe('OptionsComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(MatchComponent); component = fixture.componentInstance; + component.editorState = mockOptionData.editorOptionData; // fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); + + it("#ngOnInit() should call editorDataHandler on ngOnInit", () => { + component.editorState = mockOptionData.editorOptionData; + spyOn(component, "editorDataHandler"); + component.ngOnInit(); + expect(component.editorDataHandler).toHaveBeenCalled(); + }); + + it("should not set #templateType when creating new question", () => { + component.editorState = {}; + spyOn(component, "editorDataHandler"); + component.ngOnInit(); + expect(component.templateType).toEqual("mtf-horizontal"); + }); + + it("should set #templateType when updating an existing question", () => { + component.editorState = mockOptionData.editorOptionData; + spyOn(component, "editorDataHandler"); + component.ngOnInit(); + expect(component.templateType).toEqual("mtf-vertical"); + }); + + it("ngOnChanges should not call editorDataHandler", () => { + spyOn(component, "editorDataHandler").and.callFake(() => {}); + spyOn(component, "ngOnChanges").and.callThrough(); + component.ngOnChanges({ + maxScore: new SimpleChange(undefined, 4, true), + }); + expect(component.editorDataHandler).not.toHaveBeenCalled(); + }); + + it("ngOnChanges should call editorDataHandler", () => { + spyOn(component, "editorDataHandler").and.callFake(() => {}); + spyOn(component, "ngOnChanges").and.callThrough(); + component.ngOnChanges({ + maxScore: new SimpleChange(1, 2, false), + }); + expect(component.editorDataHandler).toHaveBeenCalled(); + }); + + it('#editorDataHandler() should emit option data', () => { + spyOn(component, 'prepareMtfBody').and.callThrough(); + spyOn(component.editorDataOutput, 'emit').and.callThrough(); + component.editorState = mockOptionData.editorOptionData; + component.editorState.correctMatchPair = [{ "0": 0 }, { "1": 1 }]; + component.editorDataHandler(); + expect(component.prepareMtfBody).toHaveBeenCalledWith(mockOptionData.editorOptionData); + expect(component.editorDataOutput.emit).toHaveBeenCalled(); + }); + + it("#prepareMtfBody() should return expected mtf option data for MTF", () => { + component.maxScore = 4; + spyOn(component, 'setMapping').and.callThrough(); + spyOn(component, "getResponseDeclaration").and.callThrough(); + spyOn(component, "getInteractions").and.callThrough(); + const result = component.prepareMtfBody(mockOptionData.editorOptionData); + expect(component.getResponseDeclaration).toHaveBeenCalledWith( + mockOptionData.editorOptionData + ); + expect(component.getInteractions).toHaveBeenCalledWith( + mockOptionData.editorOptionData.options + ); + }); + + it('#getInteractions should return expected interactions', () => { + spyOn(component, 'getInteractions').and.callThrough(); + const result = component.getInteractions(mockOptionData.editorOptionData.options); + expect(result).toEqual(mockOptionData.prepareMtfBody.interactions); + }) + + it('#setMapping should set mapping', () => { + spyOn(component, 'setMapping').and.callThrough(); + component.editorState = mockOptionData.editorOptionData; + component.editorState.correctMatchPair = mockOptionData.editorOptionData.correctMatchPair; + component.maxScore = 4; + component.setMapping(); + expect(component.mapping).toEqual(mockOptionData.prepareMtfBody.responseDeclaration.response1.mapping); + }) + + it('#getOutcomeDeclaration should return expected outcomeDeclaration', () => { + component.maxScore = 4; + spyOn(component, 'getOutcomeDeclaration').and.callThrough(); + const outcomeDeclaration = component.getOutcomeDeclaration(); + expect(outcomeDeclaration.maxScore.cardinality).toEqual('multiple'); + expect(outcomeDeclaration.maxScore.defaultValue).toEqual(4); + }) + + it('#getResponseDeclaration should return expected responseDeclaration', () => { + component.mapping = [ + { + value: { + "0": 0, + }, + score: 2, + }, + { + value: { + "1": 1, + }, + score: 2, + }, + ]; + spyOn(component, "getResponseDeclaration").and.callThrough(); + const responseDeclaration = component.getResponseDeclaration(mockOptionData.editorOptionData); + expect(responseDeclaration.response1.cardinality).toEqual('multiple'); + }) }); \ No newline at end of file diff --git a/projects/questionset-editor-library/src/lib/components/match/match.component.ts b/projects/questionset-editor-library/src/lib/components/match/match.component.ts index 2c479610c..1336531bc 100644 --- a/projects/questionset-editor-library/src/lib/components/match/match.component.ts +++ b/projects/questionset-editor-library/src/lib/components/match/match.component.ts @@ -2,8 +2,6 @@ import { Component, Input, OnInit, OnChanges, Output, EventEmitter, SimpleChange import * as _ from 'lodash-es'; import { EditorTelemetryService } from '../../services/telemetry/telemetry.service'; import { ConfigService } from '../../services/config/config.service'; -import { TreeService } from '../../services/tree/tree.service'; -import { EditorService } from '../../services/editor/editor.service'; @Component({ selector: 'lib-match', @@ -21,13 +19,11 @@ export class MatchComponent implements OnInit, OnChanges { @Output() editorDataOutput: EventEmitter = new EventEmitter(); public setCharacterLimit = 160; public setImageLimit = 1; - public templateType = 'default'; - parentMeta: any; + public templateType = 'mtf-horizontal'; + constructor( public telemetryService: EditorTelemetryService, public configService: ConfigService, - public treeService: TreeService, - private editorService: EditorService ) {} ngOnInit() { @@ -53,7 +49,7 @@ export class MatchComponent implements OnInit, OnChanges { } prepareMtfBody(editorState) { let metadata: any; - if (_.isEmpty(editorState.correctMatchPair) && !_.isEmpty(editorState.options)) { + if (!_.isEmpty(editorState.options)) { editorState.correctMatchPair = editorState.options.map((option, index) => { const correctMatchPair = {}; correctMatchPair[index.toString()] = index; @@ -64,18 +60,18 @@ export class MatchComponent implements OnInit, OnChanges { let options: any; if (!_.isEmpty(editorState.correctMatchPair)) { options = { - leftOptions: editorState.options.map((option, index) => { + left: editorState.options.map((option, index) => { return { value: { - body: option.leftOption, + body: option.left, value: index, } } }), - rightOptions: editorState.options.map((option, index) => { + right: editorState.options.map((option, index) => { return { value: { - body: option.rightOption, + body: option.right, value: index, } } @@ -144,12 +140,12 @@ export class MatchComponent implements OnInit, OnChanges { getInteractions(options) { const optionSet = { - leftOptions: options.map((option,index) => ({ - label: option.leftOption, + left: options.map((option,index) => ({ + label: option.left, value: index, })), - rightOptions: options.map((option,index) => ({ - label: option.rightOption, + right: options.map((option,index) => ({ + label: option.right, value: index, })), } @@ -161,15 +157,4 @@ export class MatchComponent implements OnInit, OnChanges { }; return interactions; } - - setScore(value, scoreIndex) { - const obj = { - response: scoreIndex, - outcomes: { - score: value, - }, - }; - this.mapping[scoreIndex] = obj; - this.editorDataHandler(); - } } diff --git a/projects/questionset-editor-library/src/lib/components/options/options.component.ts b/projects/questionset-editor-library/src/lib/components/options/options.component.ts index d317eccf9..daf368fc5 100644 --- a/projects/questionset-editor-library/src/lib/components/options/options.component.ts +++ b/projects/questionset-editor-library/src/lib/components/options/options.component.ts @@ -162,7 +162,7 @@ export class OptionsComponent implements OnInit, OnChanges { score: scoreForEachOption, } this.mapping.push(optionMapping) - }) + }) } else { this.mapping = []; } @@ -171,9 +171,9 @@ export class OptionsComponent implements OnInit, OnChanges { getInteractions(options) { let index; const interactOptions = _.map(options, (opt, key) => { - index = Number(key); - const hints = _.get(this.editorState, `interactions.response1.options[${index}].hints`) - return { label: opt.body, value: index, hints }; + index = Number(key); + const hints = _.get(this.editorState, `interactions.response1.options[${index}].hints`) + return { label: opt.body, value: index, hints }; }); this.subMenuConfig(options); const interactions = { @@ -181,7 +181,7 @@ export class OptionsComponent implements OnInit, OnChanges { type: 'choice', options: interactOptions, }, - }; + }; return interactions; } diff --git a/projects/questionset-editor-library/src/lib/components/question/question.component.ts b/projects/questionset-editor-library/src/lib/components/question/question.component.ts index 6ab00be78..07ab2b238 100644 --- a/projects/questionset-editor-library/src/lib/components/question/question.component.ts +++ b/projects/questionset-editor-library/src/lib/components/question/question.component.ts @@ -253,9 +253,9 @@ export class QuestionComponent implements OnInit, AfterViewInit, OnDestroy { const maximumOptions = _.get(this.questionInput, 'config.maximumOptions'); this.editorService.optionsLength = numberOfOptions; // converting the options to the format required by the editor - const options = _.map(this.questionMetaData?.editorState?.options?.leftOption, (leftOption, index) => ({ - leftOption, - rightOption:this.questionMetaData?.editorState?.options?.rightOption?.[index] + const options = _.map(this.questionMetaData?.editorState?.options?.left, (left, index) => ({ + left, + right:this.questionMetaData?.editorState?.options?.right?.[index] })); const question = this.questionMetaData?.editorState?.question; const interactions = this.questionMetaData?.interactions; @@ -628,8 +628,8 @@ export class QuestionComponent implements OnInit, AfterViewInit, OnDestroy { } else { this.showFormError = false; } - const rightOptionValid = _.find(this.editorState.options, option => (option.rightOption === undefined || option.rightOption === '' || option.rightOption.length > this.setCharacterLimit)); - const leftOptionValid = _.find(this.editorState.options, option => (option.leftOption === undefined || option.leftOption === '' || option.leftOption.length > this.setCharacterLimit)); + const rightOptionValid = _.find(this.editorState.options, option => (option.right === undefined || option.right === '' || option.right.length > this.setCharacterLimit)); + const leftOptionValid = _.find(this.editorState.options, option => (option.left === undefined || option.left === '' || option.left.length > this.setCharacterLimit)); if (rightOptionValid || leftOptionValid || (_.isUndefined(this.editorState.correctMatchPair) && this.sourcingSettings?.enforceCorrectAnswer)) { this.showFormError = true; return; @@ -851,10 +851,10 @@ export class QuestionComponent implements OnInit, AfterViewInit, OnDestroy { } if (this.questionInteractionType === 'match') { - metadata.body = this.getMatchQuestionHtmlBody(this.editorState.question); - const leftOptions = metadata.interactions.response1.options.leftOptions; - const rightOptions = metadata.interactions.response1.options.rightOptions; - metadata.correctMatchPair = this.getMatchAnswerContainerHtml(leftOptions, rightOptions); + metadata.body = this.getMtfQuestionHtmlBody(this.editorState.question, this.editorState.templateId); + const leftOptions = metadata.interactions.response1.options.left; + const rightOptions = metadata.interactions.response1.options.right; + metadata.correctMatchPair = this.getMtfAnswerContainerHtml(leftOptions, rightOptions); } if (!_.isUndefined(this.selectedSolutionType) && !_.isEmpty(this.selectedSolutionType)) { @@ -886,7 +886,7 @@ export class QuestionComponent implements OnInit, AfterViewInit, OnDestroy { return optionHtml; } - getMatchAnswerContainerHtml(leftOptions, rightOptions) { + getMtfAnswerContainerHtml(leftOptions, rightOptions) { const matchContainerTemplate = '
{leftOptions}{rightOptions}
'; const leftOptionsHtml = this.getOptionWrapperHtml(leftOptions, 'left'); const rightOptionsHtml = this.getOptionWrapperHtml(rightOptions, 'right'); @@ -897,18 +897,18 @@ export class QuestionComponent implements OnInit, AfterViewInit, OnDestroy { } getOptionWrapperHtml(options, type) { - const wrapperTemplate = `
{options}
`; + const wrapperTemplate = `
{options}
`; let optionsHtml = ''; options.forEach((option) => { - const optionHtml = this.getMatchAnswerHtml(option.label, option.value); + const optionHtml = this.getMtfAnswerHtml(option.label, type); optionsHtml = optionsHtml.concat(optionHtml); }); const wrapper = wrapperTemplate.replace('{options}', optionsHtml); return wrapper; } - getMatchAnswerHtml(label, value) { - const answerHtml = '
{label}
'; - const optionHtml = answerHtml.replace('{label}', label).replace('{value}', value); + getMtfAnswerHtml(label, type) { + const answerHtml = `
{label}
`; + const optionHtml = answerHtml.replace('{label}', label); return optionHtml; } @@ -955,13 +955,13 @@ export class QuestionComponent implements OnInit, AfterViewInit, OnDestroy { return videoSolutionValue; } - getMatchQuestionHtmlBody(question) { + getMtfQuestionHtmlBody(question, templateId) { const matchTemplateConfig = { // tslint:disable-next-line:max-line-length - matchBody: '
{question}
' + matchBody: '
{question}
' }; const { matchBody } = matchTemplateConfig; - const questionBody = matchBody.replace('{question}', question); + const questionBody = matchBody.replace('{templateClass}', templateId).replace('{question}', question); return questionBody; } diff --git a/projects/questionset-editor-library/src/lib/interfaces/MtfForm.ts b/projects/questionset-editor-library/src/lib/interfaces/MtfForm.ts index 26077c4e4..67713877e 100644 --- a/projects/questionset-editor-library/src/lib/interfaces/MtfForm.ts +++ b/projects/questionset-editor-library/src/lib/interfaces/MtfForm.ts @@ -1,7 +1,7 @@ import * as _ from "lodash-es"; export class MtfOption { - constructor(public leftOption: string, public rightOption: string) {} + constructor(public left: string, public right: string) {} } export interface MtfData { diff --git a/projects/questionset-editor-library/src/lib/services/config/label.config.json b/projects/questionset-editor-library/src/lib/services/config/label.config.json index 6ecd46581..ef7a67d23 100644 --- a/projects/questionset-editor-library/src/lib/services/config/label.config.json +++ b/projects/questionset-editor-library/src/lib/services/config/label.config.json @@ -47,199 +47,199 @@ "remove_btn_label":"Remove", "done_btn_label":"Done", "add_translation_label":"Add Translation", - "add_page_numbers_to_questions_btn_label": "Pagination" + "add_page_numbers_to_questions_btn_label": "Pagination", + "delete_pair_btn_label":"Delete Pair" }, "lbl":{ - "search": "Search", - "subject": "Subject", - "medium": "Medium", - "gradeLevel": "Class", - "contentType": "Content Type", - "reset": "Reset", - "apply": "apply", - "filterText": "Change Filters", - "Questiondetails": "Question details", - "selectContent": "Select content", - "noMatchingContent": "Sorry there is no matching content", - "changeFilterMessage": "Changing filter helps you find more content", - "changeFilter": "Change filters", - "whereDoYouWantToAddThisContent": "Where do you want to add this content?", - "selectContentToAdd": "Use search and filters above to find more content", - "addedToCollection": "Added to collection", - "changingFilters": "Changing filters make you find more content", - "ChangeFilters": "Change filters", - "addFromLibrary": "Add from Library", - "showContentAddedToCollection": "Show content added to collection", - "addContent": "Add content", - "sortBy": "Sort By", - "sortlabel": "A - Z", - "viewOnOrigin": "View Content on consumption", - "answers": "Answers", - "answersRequired": "Answer is required", - "answersPopupText": "Please provide an answer for the question. Check preview to understand how it would look.", - "selectImage": "Select Image", - "myImages": "My Images", - "allImage": "All Image", - "uploadAndUse": "Upload and Use", - "chooseOrDragImage": "Choose or drag and drop your image here", - "chooseOrDragVideo": "Choose or drag and drop your video here", - "uploadFromComputer": "Upload from Computer", - "upload": "Upload", - "maxFileSize": "Max File size:", - "allowedFileTypes": "Allowed file types are:", - "maximumAllowedFileSize": "Maximum allowed file size:", - "copyRightsAndLicense": "Copyright & License", - "dropChooseFile": "Drop or choose file to upload before entering the details", - "charactersLeft": "Characters left:", - "myVideos": "My Video(s)", - "allVideos": "All Video(s)", - "selectVideo": "Select Video", - "searchPlaceholder": "Search...", - "addAnImage": "Add an image", - "name": "Name", - "copyRightsAndYear": "Copyright & Yrar", - "license": "License", - "author": "Author", - "grade": "Grade", - "board": "Board", - "audience": "Audience", - "copyRight": "Copyright", - "licensedBy": "Licensed by", - "attributions": "Attributions", - "requestForQrCode": "Request for QR Codes", - "confirmDeleteContent": "Confirm Delete Content", - "confirmDeleteNode": "Are you sure want to delete the selected Node?", - "comments": "Comments", - "reviewComments": "Review Comments", - "questionSetPreview": "Question Set Preview", - "numberToolarge": "This number is too large for the request", - "acceptTerms": "Accepting Terms & Conditions", - "iAgreeSubmit": "I agree that by submitting / publishing this Content,", - "iconfirmContent": "I confirm that this Content complies with prescribed guidelines, including the Terms of Use and Content Policy and that I consent to publish it under the", - "createCommonFramework": "Creative Commons Framework in", - "accordance": "accordance with the", - "contentPolicy": "Content Policy.", - "privacyRights": "I have made sure that I do not violate others’ copyright or privacy rights.", - "viewComments": "View Comments", - "addReviewComments": "Add Review Comments", - "enterYourComments": "Enter your comments", - "publishCollection": "Publish ${objectType}", - "confirmPublishCollection": "Are you sure you want to publish this ${objectType}?", - "fillComments": "Fill comments", - "searchLibrary": "Search Library", - "options": "Options", - "optionsPopupText": "Please Preview to check how layout looks. Layout is responsive to the resolution of your device", - "selectOneAns": "Select one correct answer", - "fillThisOption": "Fill this option", - "reduceSize": "Please reduce the size", - "correctAns": "Correct answer", - "correctMatch": "Check if all match pairs are correct", - "addOption": "Add option", - "addPair": "Add pair", - "question": "Question", - "pageNumber": "Page No", - "setAnswers": "Set your answers", - "addQuestionAnswerPairText": "Add question-answer pairs to your question. Answers will be shuffled automatically", - "confirmQuestionNotSaved": "This question will not be saved, are you sure you want to go back to questionset?", - "video": "Video", - "textImage": "Text+Image", - "chooseType": "Choose type", - "solution": "Solution", - "optional": "(Optional)", - "questionRequired": "Question is required", - "addingTo": "Adding To", - "selectTemplate": "Select a template to get started", - "createNew": "Create new", - "selectLayout": "Select Layout", - "vertical": "Vertical", - "grid": "Grid", - "horizontal": "Horizontal", - "folders": "Folders", - "createHierarchyCsv": "Create folders using csv file", - "downloadFoldersInCSV": "Download folders as csv file", - "uploadUpdateCSV": "Update folder metadata using csv file", - "downloadSampleHierarchyCSv": "Upload csv file as per the given sample to create folders", - "makeSureFile": "Make sure that:", - "allColumnsAreAvailable": "The file has all the required columns", - "hasAllMandatoryColumn": "The file has all the required values filled in as per the format", - "noDuplicateRow": "There are no duplicate rows (with exactly same folder levels) in the file", - "downloadSampleCSV": "Download sample csv file", - "dragAndDropCSV": "Drag and Drop files to upload", - "selectFileToUpload": "Select file to upload", - "uploadEntries": "File accepted - csv", - "Or": "Or", - "pleaseWait": "Please wait", - "validateCsvFile": "Validating CSV file", - "hierarchyValidationError": "Error in processing the file", - "followingErrors": "Following errors are found in the file. Please correct and upload again", - "reUploadCSV": "Upload file again", - "hierarchyValidation": "Hierarchy Validation", - "hierarchyAdded": "Folders have been successfully created. Please close the dialog", - "hierarchyUpdated": "Folder metadata has been successfully updated. Please close the dialog", - "successful": "Successful !", - "csvDownloadInstruction": "Please make sure that this is the file downloaded using the “Download folders as csv file” option and changes are made to it", - "collaborators": "Collaborators", - "addCollaborators": "Add Collaborators", - "manageCollaborators": "Manage Collaborators", - "sliderValue": "Slider Value", - "left": "Left", - "stepSize": "Step size", - "right": "Right", - "translation": "Translation", - "publishchecklistTitle": "Please confirm that ALL the following items are verified (by ticking the check-boxes) before you can publish:", - "bulkUploadErrorMessage": "The metadata file has following errors. Please check and upload again", - "bulkUploadQuestion": "Bulk Upload Question", - "lastUploaded": "Last uploaded", - "bulkInProgress": "Bulk upload in progress", - "viewDetails": "View Details", - "downloadSampleMetadataCsvFileAndCreate": "Download sample metadata CSV file and create your own metadata file using the format", - "makeSureYourFile": "Make sure your file", - "allColumnsAreAvailableShownFormat": "All columns are available as shown in format.", - "hasAllMandatoryColumnsFilledAsMarkedInFormat": "Has all mandatory columns filled, as marked in the format", - "hasNoDuplicateUrlsFilepathColumn": "Has no duplicate URLs in the filepath column", - "downloadSampleMetadataCsv": "Download sample metadata CSV", - "processingDroppedFiles": "Processing dropped files...", - "retry": "retry", - "dragAndDrop": "Drag and Drop file", - "or": "or", - "selectFile": "Select file", - "uploadCSVXlEntries": "Upload csv upto 300 entries", - "no": "No", - "yes": "Yes", - "cancel": "Cancel", - "ok": "OK", - "validatingCSVFile": "Validating CSV file", - "metadataFileValidationFailed": "Metadata file validation failed.", - "metadataFollowingError": "The metadata file has following errors. Please check and upload again", - "uploadingYourContentFromCSV": "Uploading your question from CSV", - "uploadFail": "Upload failed", - "uploadSuccessful": "Upload successful", - "uploadRemaining": "Upload remaining", - "bulkUploadComplete": "Bulk upload complete!", - "contentUploaded": "Question uploaded", - "downloadReport": "Download report", - "next": "Next", - "back": "Back", - "close": "Close", - "bulkUploadNoticeLine1": "Uploading in bulk may take some time, click on", - "bulkUploadNoticeLine2": "to continue using Vidyadaan.", - "bulkUploadNoticeLine3": "Your upload will keep running in the background.", - "alreadyContentPresent": "Already present in this folder", - "loaderHeading": "Please wait", - "loaderMessage": "We are fetching details.", - "slidervalue": "Set the minimum and maximum values with which the slider would start and end respectively.", - "stepSizeInfo": "The step size would define the gap/jump between two values on the slider.", - "minSizeInfo": "The minimum slider value", - "maxSizeInfo": "The maximum slider value", - "questionsetAddFromLibraryItemLabel": "question", - "questionsetAddFromLibraryCollectionLabel": "question set", - "marks": "Marks", - "shuffleOnMessage": "Each question will carry equal weightage of 1 mark when using Shuffle. To provide different weightage to individual questions please turn off Shuffle.", - "editingConsentNote": "I confirm that I am allowing my reviewer to make edits to the content and metadata of the content. I consent my reviewer make changes, if any and publish the contain thereafter.", - "acceptBothConsentNote": "Agree to both conditions", - "totalScore": "Total Score", - "qualityReview": "If the score is less than or equal to 15 please re-evaluate the question and send back for corrections." - }, + "search":"Search", + "subject":"Subject", + "medium":"Medium", + "gradeLevel":"Class", + "contentType":"Content Type", + "reset":"Reset", + "apply":"apply", + "filterText":"Change Filters", + "Questiondetails":"Question details", + "selectContent":"Select content", + "noMatchingContent":"Sorry there is no matching content", + "changeFilterMessage":"Changing filter helps you find more content", + "changeFilter":"Change filters", + "whereDoYouWantToAddThisContent":"Where do you want to add this content?", + "selectContentToAdd":"Use search and filters above to find more content", + "addedToCollection":"Added to collection", + "changingFilters":"Changing filters make you find more content", + "ChangeFilters":"Change filters", + "addFromLibrary":"Add from Library", + "showContentAddedToCollection":"Show content added to collection", + "addContent":"Add content", + "sortBy":"Sort By", + "sortlabel":"A - Z", + "viewOnOrigin":"View Content on consumption", + "answers":"Answers", + "answersRequired":"Answer is required", + "answersPopupText":"Please provide an answer for the question. Check preview to understand how it would look.", + "selectImage":"Select Image", + "myImages":"My Images", + "allImage":"All Image", + "uploadAndUse":"Upload and Use", + "chooseOrDragImage":"Choose or drag and drop your image here", + "chooseOrDragVideo":"Choose or drag and drop your video here", + "uploadFromComputer":"Upload from Computer", + "upload":"Upload", + "maxFileSize":"Max File size:", + "allowedFileTypes":"Allowed file types are:", + "maximumAllowedFileSize":"Maximum allowed file size:", + "copyRightsAndLicense":"Copyright & License", + "dropChooseFile":"Drop or choose file to upload before entering the details", + "charactersLeft":"Characters left:", + "myVideos":"My Video(s)", + "allVideos":"All Video(s)", + "selectVideo":"Select Video", + "searchPlaceholder":"Search...", + "addAnImage":"Add an image", + "name":"Name", + "copyRightsAndYear":"Copyright & Yrar", + "license":"License", + "author":"Author", + "grade":"Grade", + "board":"Board", + "audience":"Audience", + "copyRight":"Copyright", + "licensedBy":"Licensed by", + "attributions":"Attributions", + "requestForQrCode":"Request for QR Codes", + "confirmDeleteContent":"Confirm Delete Content", + "confirmDeleteNode":"Are you sure want to delete the selected Node?", + "comments":"Comments", + "reviewComments":"Review Comments", + "questionSetPreview":"Question Set Preview", + "numberToolarge": "This number is too large for the request", + "acceptTerms":"Accepting Terms & Conditions", + "iAgreeSubmit":"I agree that by submitting / publishing this Content,", + "iconfirmContent":"I confirm that this Content complies with prescribed guidelines, including the Terms of Use and Content Policy and that I consent to publish it under the", + "createCommonFramework":"Creative Commons Framework in", + "accordance":"accordance with the", + "contentPolicy":"Content Policy.", + "privacyRights":"I have made sure that I do not violate others’ copyright or privacy rights." , + "viewComments":"View Comments", + "addReviewComments":"Add Review Comments", + "enterYourComments":"Enter your comments", + "publishCollection":"Publish ${objectType}", + "confirmPublishCollection":"Are you sure you want to publish this ${objectType}?", + "fillComments":"Fill comments", + "searchLibrary":"Search Library", + "options":"Options", + "optionsPopupText":"Please Preview to check how layout looks. Layout is responsive to the resolution of your device", + "selectOneAns":"Select one correct answer", + "fillThisOption":"Fill this option", + "reduceSize":"Please reduce the size", + "correctAns":"Correct answer", + "addOption":"Add option", + "question":"Question", + "pageNumber":"Page No", + "confirmQuestionNotSaved":"This question will not be saved, are you sure you want to go back to questionset?", + "video":"Video", + "textImage":"Text+Image", + "chooseType":"Choose type", + "solution":"Solution", + "optional":"(Optional)", + "questionRequired":"Question is required", + "addingTo":"Adding To", + "selectTemplate":"Select a template to get started", + "createNew":"Create new", + "selectLayout":"Select Layout", + "vertical":"Vertical", + "grid":"Grid", + "horizontal":"Horizontal", + "folders":"Folders", + "createHierarchyCsv":"Create folders using csv file", + "downloadFoldersInCSV":"Download folders as csv file", + "uploadUpdateCSV":"Update folder metadata using csv file", + "downloadSampleHierarchyCSv":"Upload csv file as per the given sample to create folders", + "makeSureFile":"Make sure that:", + "allColumnsAreAvailable":"The file has all the required columns", + "hasAllMandatoryColumn":"The file has all the required values filled in as per the format", + "noDuplicateRow":"There are no duplicate rows (with exactly same folder levels) in the file", + "downloadSampleCSV":"Download sample csv file", + "dragAndDropCSV":"Drag and Drop files to upload", + "selectFileToUpload":"Select file to upload", + "uploadEntries":"File accepted - csv", + "Or":"Or", + "pleaseWait":"Please wait", + "validateCsvFile":"Validating CSV file", + "hierarchyValidationError":"Error in processing the file", + "followingErrors":"Following errors are found in the file. Please correct and upload again", + "reUploadCSV":"Upload file again", + "hierarchyValidation":"Hierarchy Validation", + "hierarchyAdded":"Folders have been successfully created. Please close the dialog", + "hierarchyUpdated":"Folder metadata has been successfully updated. Please close the dialog", + "successful":"Successful !", + "csvDownloadInstruction":"Please make sure that this is the file downloaded using the “Download folders as csv file” option and changes are made to it", + "collaborators": "Collaborators", + "addCollaborators": "Add Collaborators", + "manageCollaborators": "Manage Collaborators", + "sliderValue":"Slider Value", + "left":"Left", + "stepSize":"Step size", + "right":"Right", + "translation":"Translation", + "publishchecklistTitle": "Please confirm that ALL the following items are verified (by ticking the check-boxes) before you can publish:", + "bulkUploadErrorMessage" : "The metadata file has following errors. Please check and upload again", + "bulkUploadQuestion": "Bulk Upload Question", + "lastUploaded": "Last uploaded", + "bulkInProgress": "Bulk upload in progress", + "viewDetails": "View Details", + "downloadSampleMetadataCsvFileAndCreate": "Download sample metadata CSV file and create your own metadata file using the format", + "makeSureYourFile": "Make sure your file", + "allColumnsAreAvailableShownFormat": "All columns are available as shown in format.", + "hasAllMandatoryColumnsFilledAsMarkedInFormat": "Has all mandatory columns filled, as marked in the format", + "hasNoDuplicateUrlsFilepathColumn": "Has no duplicate URLs in the filepath column", + "downloadSampleMetadataCsv": "Download sample metadata CSV", + "processingDroppedFiles" : "Processing dropped files...", + "retry": "retry", + "dragAndDrop": "Drag and Drop file", + "or" : "or", + "selectFile": "Select file", + "uploadCSVXlEntries": "Upload csv upto 300 entries", + "no": "No", + "yes": "Yes", + "cancel": "Cancel", + "ok": "OK", + "validatingCSVFile": "Validating CSV file", + "metadataFileValidationFailed": "Metadata file validation failed.", + "metadataFollowingError": "The metadata file has following errors. Please check and upload again", + "uploadingYourContentFromCSV": "Uploading your question from CSV", + "uploadFail" : "Upload failed", + "uploadSuccessful" : "Upload successful", + "uploadRemaining" : "Upload remaining", + "bulkUploadComplete" : "Bulk upload complete!", + "contentUploaded" : "Question uploaded", + "downloadReport": "Download report", + "next": "Next", + "back": "Back", + "close": "Close", + "bulkUploadNoticeLine1": "Uploading in bulk may take some time, click on", + "bulkUploadNoticeLine2": "to continue using Vidyadaan.", + "bulkUploadNoticeLine3": "Your upload will keep running in the background.", + "alreadyContentPresent": "Already present in this folder", + "loaderHeading": "Please wait", + "loaderMessage": "We are fetching details.", + "slidervalue": "Set the minimum and maximum values with which the slider would start and end respectively.", + "stepSizeInfo": "The step size would define the gap/jump between two values on the slider.", + "minSizeInfo": "The minimum slider value", + "maxSizeInfo": "The maximum slider value", + "questionsetAddFromLibraryItemLabel": "question", + "questionsetAddFromLibraryCollectionLabel": "question set", + "marks": "Marks", + "shuffleOnMessage": "Each question will carry equal weightage of 1 mark when using Shuffle. To provide different weightage to individual questions please turn off Shuffle.", + "editingConsentNote": "I confirm that I am allowing my reviewer to make edits to the content and metadata of the content. I consent my reviewer make changes, if any and publish the contain thereafter.", + "acceptBothConsentNote": "Agree to both conditions", + "totalScore": "Total Score", + "qualityReview": "If the score is less than or equal to 15 please re-evaluate the question and send back for corrections.", + "addPair": "Add pair", + "setAnswers": "Set your answers", + "addQuestionAnswerPairText": "Add question-answer pairs to your question. Answers will be shuffled automatically" + }, "err":{ "somethingWentWrong":"Something went wrong", "contentNotFoundonOrigin": "The content not found in consumption", From 1cda982b9ca5a1c31a37b13dd616cdd1a707bf4a Mon Sep 17 00:00:00 2001 From: Arpan Gupta Date: Tue, 8 Aug 2023 17:21:25 +0530 Subject: [PATCH 22/37] Added interfaces in sonar exclusion --- sonar-project.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sonar-project.properties b/sonar-project.properties index 9f8441c8f..0c80de60d 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -1,7 +1,7 @@ sonar.projectName=sunbird-questionset-editor sonar.language=ts sonar.sources=projects/questionset-editor-library/src -sonar.exclusions=projects/questionset-editor-library/src/lib/assets/**,projects/questionset-editor-library/src/lib/**/*.spec.ts,projects/questionset-editor-library/src/lib/**/*.spec.data.ts,projects/questionset-editor-library/src/lib/**/*.module.ts +sonar.exclusions=projects/questionset-editor-library/src/lib/assets/**,projects/questionset-editor-library/src/lib/**/*.spec.ts,projects/questionset-editor-library/src/lib/**/*.spec.data.ts,projects/questionset-editor-library/src/lib/**/*.module.ts,projects/questionset-editor-library/src/lib/interfaces/* sonar.javascript.lcov.reportPaths=projects/questionset-editor-library/coverage/lcov.info sonar.projectKey=Sunbird-inQuiry_editor sonar.host.url=https://sonarcloud.io From d068f4f277bc2b86a39ff113718f22f018bb3757 Mon Sep 17 00:00:00 2001 From: Arpan Gupta Date: Wed, 9 Aug 2023 16:00:00 +0530 Subject: [PATCH 23/37] Added tests for new functions in question component --- .../question/question.component.spec.ts | 182 +++++++++++++++++- 1 file changed, 180 insertions(+), 2 deletions(-) diff --git a/projects/questionset-editor-library/src/lib/components/question/question.component.spec.ts b/projects/questionset-editor-library/src/lib/components/question/question.component.spec.ts index 84b477943..592f7855d 100644 --- a/projects/questionset-editor-library/src/lib/components/question/question.component.spec.ts +++ b/projects/questionset-editor-library/src/lib/components/question/question.component.spec.ts @@ -25,6 +25,7 @@ import { BranchingLogic, mockEditorCursor, interactionChoiceEditorState, + interactionMatchEditorState, RubricData, videoSolutionObject, mediaVideoArray @@ -292,7 +293,7 @@ describe("QuestionComponent", () => { component.previewFormData(true); expect(component.initialize).toHaveBeenCalled(); }); - + it("#initialize should call when question page for question mcq api fail", () => { spyOn(component, "initialize").and.callThrough(); component.questionId = "do_11330103476396851218"; @@ -313,7 +314,44 @@ describe("QuestionComponent", () => { component.initialize(); expect(component.initialize).toHaveBeenCalled(); }); - + + it("#initialize should call when question page for question mtf", () => { + component.initialLeafFormConfig = leafFormConfigMock; + component.leafFormConfig = leafFormConfigMock; + component.questionFormConfig=leafFormConfigMock; + spyOn(component, "initialize").and.callThrough(); + component.questionId = "do_11330103476396851218"; + editorService.parentIdentifier = undefined; + component.questionPrimaryCategory = undefined; + spyOn(editorService, "getToolbarConfig").and.returnValue({ + title: "abcd", + showDialcode: "No", + showPreview: "false", + }); + component.toolbarConfig.showPreview = false; + spyOn(editorService, "fetchCollectionHierarchy").and.callFake(() => { + return of(collectionHierarchyMock); + }); + component.questionId = "do_127"; + component.questionSetHierarchy = collectionHierarchyMock.result.questionset; + spyOn(questionService, "readQuestion").and.returnValue( + of(mockData.mtfQuestionMetaData) + ); + component.questionMetaData = mockData.mtfQuestionMetaData.result.question; + component.questionInteractionType = "match"; + component.scoreMapping = + mockData.mcqQuestionMetaData.result.question.responseDeclaration.response1.mapping; + component.sourcingSettings = sourcingSettingsMock; + component.questionInput.setChildQuestion = false; + component.editorState.solutions = [{ + id: '1', + type: 'vedio' + }] + component.initialize(); + component.previewFormData(true); + expect(component.initialize).toHaveBeenCalled(); + }); + it("#initialize should call when question page for question slider", () => { spyOn(component, "initialize").and.callThrough(); component.questionId = "do_11330103476396851218"; @@ -479,6 +517,54 @@ describe("QuestionComponent", () => { expect(component.setQuestionTitle).toHaveBeenCalled(); }); + it("#initialize should call when question page for question mtf with interactionTypes", () => { + component.questionSetId = "do_1278"; + spyOn(editorService, "fetchCollectionHierarchy").and.callFake(() => { + return of(collectionHierarchyMock); + }); + editorService.parentIdentifier = undefined; + component.questionId = "do_11330103476396851218"; + component.leafFormConfig = leafFormConfigMock; + spyOn(questionService, "readQuestion").and.returnValue( + of(mockData.mtfQuestionMetaData) + ); + spyOn(component, 'setQuestionTitle').and.callFake(() => {}); + spyOn(component, 'populateFormData').and.callFake(() => {}); + component.leafFormConfig = leafFormConfigMock; + spyOn(component, "initialize").and.callThrough(); + component.initialize(); + expect(component.initialize).toHaveBeenCalled(); + expect(component.questionPrimaryCategory).toBeDefined(); + expect(component.questionInteractionType).toBeDefined(); + expect(component.populateFormData).toHaveBeenCalled(); + expect(component.setQuestionTitle).toHaveBeenCalled(); + }); + + it("#initialize should call when question page for question mtf without interactionTypes", () => { + let questionMetadata = mockData.mtfQuestionMetaData.result.question; + questionMetadata = _.omit(questionMetadata, ['interactionTypes', 'primaryCategory']) + component.questionSetId = "do_1278"; + spyOn(editorService, "fetchCollectionHierarchy").and.callFake(() => { + return of(collectionHierarchyMock); + }); + editorService.parentIdentifier = undefined; + component.questionId = "do_11330103476396851218"; + component.leafFormConfig = leafFormConfigMock; + spyOn(questionService, "readQuestion").and.returnValue( + of({result: {question: {questionMetadata}}}) + ); + spyOn(component, 'setQuestionTitle').and.callFake(() => {}); + spyOn(component, 'populateFormData').and.callFake(() => {}); + component.leafFormConfig = leafFormConfigMock; + spyOn(component, "initialize").and.callThrough(); + component.initialize(); + expect(component.initialize).toHaveBeenCalled(); + expect(component.questionPrimaryCategory).toBeUndefined(); + expect(component.questionInteractionType).toEqual("default"); + expect(component.populateFormData).toHaveBeenCalled(); + expect(component.setQuestionTitle).toHaveBeenCalled(); + }); + it("#initialize should call when question page for question slider", () => { spyOn(component, "initialize").and.callThrough(); component.initialLeafFormConfig = leafFormConfigMock; @@ -781,6 +867,12 @@ describe("QuestionComponent", () => { const templateId = "mcq-vertical"; component.getMcqQuestionHtmlBody(question, templateId); }); + + it("call #getMtfQuestionHtmlBody() to verify questionBody", () => { + const question = '
{question}
'; + const templateId = "mtf-horizontal"; + component.getMtfQuestionHtmlBody(question, templateId); + }); it("Unit test for #sendForReview", () => { spyOn(component, "upsertQuestion"); @@ -1169,6 +1261,30 @@ describe("QuestionComponent", () => { expect(metadata['outcomeDeclaration'].maxScore.defaultValue).toEqual(1); }); + it('#getQuestionMetadata() should return question metata when interactionType is match', () => { + component.mediaArr = []; + component.editorState = interactionMatchEditorState; + component.selectedSolutionType = 'video'; + component.creationContext = undefined; + component.questionInteractionType = 'match'; + component.childFormData = { + name: 'MTF', + bloomsLevel: null, + board: 'CBSE', + maxScore: 1 + }; + component.maxScore = 4; + spyOn(component, 'getDefaultSessionContext').and.returnValue({ + creator: 'Vaibahv Bhuva', + createdBy: '5a587cc1-e018-4859-a0a8-e842650b9d64' + } + ); + spyOn(component, 'getQuestionSolution').and.returnValue({}); + spyOn(component, 'getQuestionMetadata').and.callThrough(); + const metadata = component.getQuestionMetadata(); + expect(metadata['outcomeDeclaration'].maxScore.defaultValue).toEqual(4); + }); + it('#getAnswerHtml() should return answer html', () => { spyOn(component, 'getAnswerHtml').and.callThrough(); const answerHtml = component.getAnswerHtml('

Sample Answer

'); @@ -1181,6 +1297,54 @@ describe("QuestionComponent", () => { expect(answerWrappedHtml).toBe('

Sample Answer

'); }); + it('#getMtfAnswerContainerHtml() should return answer html', () => { + spyOn(component, 'getMtfAnswerContainerHtml').and.callThrough(); + const leftOptions = [ + { + label: "

a

", + value: "0", + }, + { + label: "

b

", + value: "1", + }, + ]; + const rightOptions = [ + { + label: "

c

", + value: "0", + }, + { + label: "

d

", + value: "1", + }, + ]; + const matchContainer = component.getMtfAnswerContainerHtml(leftOptions, rightOptions); + expect(matchContainer).toBe('

a

b

c

d

') + }) + + it('#getOptionWrapperHtml() should return wrapper html', () => { + spyOn(component, 'getOptionWrapperHtml').and.callThrough(); + const leftOptions = [ + { + label: "

a

", + value: "0", + }, + { + label: "

b

", + value: "1", + }, + ]; + const wrapperHtml = component.getOptionWrapperHtml(leftOptions, 'left'); + expect(wrapperHtml).toBe('

a

b

'); + }) + + it('#getMtfAnswerHtml() should return answer html', () => { + spyOn(component, 'getMtfAnswerHtml').and.callThrough(); + const answerHtml = component.getMtfAnswerHtml('

Sample Answer

', 'left'); + expect(answerHtml).toBe('

Sample Answer

'); + }) + it('#getInteractionValues() should return correct answer object', () => { spyOn(component, 'getInteractionValues').and.callThrough(); const correctAnswersData = component.getInteractionValues([0], interactionChoiceEditorState.interactions); @@ -1400,6 +1564,20 @@ describe("QuestionComponent", () => { expect(component.showFormError).toBeFalsy(); }); + it("#validateQuestionData() should call validateQuestionData and questionInteractionType is mtf", () => { + component.sourcingSettings = sourcingSettingsMock; + spyOn(treeService, "getFirstChild").and.callFake(() => { + return { data: { metadata: { identifier: "0123",allowScoring:'Yes' } } }; + }); + component.editorState = mockData.mtfQuestionMetaData.result.question; + editorService = TestBed.inject(EditorService); + editorService.editorConfig.renderTaxonomy=false; + component.editorState.question = "

Hi how are you

"; + component.editorState.correctMatchPair = ""; + component.questionInteractionType = "match"; + component.validateQuestionData(); + }); + it("#validateQuestionData() should call validateQuestionData and questionInteractionType is text", () => { component.sourcingSettings = sourcingSettingsMock; component.editorState = mockData.textQuestionNetaData.result.question; From 01707b2b7c90cdc966638d51aca70bc47aa1de49 Mon Sep 17 00:00:00 2001 From: Arpan Gupta Date: Wed, 9 Aug 2023 16:01:29 +0530 Subject: [PATCH 24/37] Mock data for tests --- .../question/question.component.spec.data.ts | 307 ++++++++++++++++++ 1 file changed, 307 insertions(+) diff --git a/projects/questionset-editor-library/src/lib/components/question/question.component.spec.data.ts b/projects/questionset-editor-library/src/lib/components/question/question.component.spec.data.ts index bddc88aef..7faf96b97 100644 --- a/projects/questionset-editor-library/src/lib/components/question/question.component.spec.data.ts +++ b/projects/questionset-editor-library/src/lib/components/question/question.component.spec.data.ts @@ -233,6 +233,206 @@ export const mockData = { }, }, }, + mtfQuestionMetaData: { + id: "api.question.read", + ver: "3.0", + ts: "2022-01-31T04:38:30ZZ", + params: { + resmsgid: "597b1b63-7007-435a-8b9d-68127f3c6fa8", + msgid: null, + err: null, + status: "successful", + errmsg: null, + }, + responseCode: "OK", + result: { + question: { + mimeType: "application/vnd.sunbird.question", + media: [], + editorState: { + options: { + left: [ + { + value: { + body: "

LeftOption1

", + value: 0, + }, + }, + { + value: { + body: "

LeftOption2

", + value: 1, + }, + }, + { + value: { + body: "

LeftOption3

", + value: 2, + }, + }, + { + value: { + body: "

LeftOption4

", + value: 3, + }, + }, + ], + right: [ + { + value: { + body: "

RightOption1

", + value: 0, + }, + }, + { + value: { + body: "

RightOption2

", + value: 1, + }, + }, + { + value: { + body: "

RightOption3

", + value: 2, + }, + }, + { + value: { + body: "

RightOption4

", + value: 3, + }, + }, + ], + }, + question: "

MTF Question

", + }, + templateId: "mtf-horizontal", + solutions: {}, + interactions: { + response1: { + type: "match", + options: { + left: [ + { + label: "

LeftOption1

", + value: 0, + }, + { + label: "

LeftOption2

", + value: 1, + }, + { + label: "

LeftOption3

", + value: 2, + }, + { + label: "

LeftOption4

", + value: 3, + }, + ], + right: [ + { + label: "

RightOption1

", + value: 0, + }, + { + label: "

RightOption2

", + value: 1, + }, + { + label: "

RightOption3

", + value: 2, + }, + { + label: "

RightOption4

", + value: 3, + }, + ], + }, + validation: { + required: "Yes", + }, + }, + }, + name: "MTF Question", + responseDeclaration: { + response1: { + cardinality: "multiple", + type: "map", + correctResponse: { + value: [ + { + "0": 0, + }, + { + "1": 1, + }, + { + "2": 2, + }, + { + "3": 3, + }, + ], + }, + mapping: [ + { + value: { + "0": 0, + }, + score: 1, + }, + { + value: { + "1": 1, + }, + score: 1, + }, + { + value: { + "2": 2, + }, + score: 1, + }, + { + value: { + "3": 3, + }, + score: 1, + }, + ], + }, + }, + outcomeDeclaration: { + maxScore: { + cardinality: "multiple", + type: "integer", + defaultValue: 4, + }, + }, + remarks: { + maxLength: 100, + }, + interactionTypes: ["match"], + qType: "MTF", + primaryCategory: "Match The Following Question", + body: "

MTF Question

", + creator: "Arpan Gupta", + createdBy: "5a587cc1-e018-4859-a0a8-e842650b9d64", + board: "CBSE", + medium: ["English"], + gradeLevel: ["Grade 1"], + subject: ["English"], + topic: ["Forest"], + author: "check1@yopmail.com", + channel: "01309282781705830427", + framework: "nit_k-12", + license: "CC BY 4.0", + maxScore: "4", + identifier: "", + }, + }, + }, sliderQuestionMetaData: { id: "api.question.read", ver: "3.0", @@ -3124,6 +3324,113 @@ export const interactionChoiceEditorState = { primaryCategory: 'Multiple Choice Question' }; +export const interactionMatchEditorState = { + question: "

q

", + options: [ + { + left: "

a

", + right: "

b

", + }, + { + left: "

c

", + right: "

d

", + }, + ], + templateId: "mtf-horizontal", + corectMatchPair: [{ "0": 0 }, { "1": 1 }], + numberOfOptions: 2, + interactions: { + response1: { + type: "match", + options: { + left: [ + { + label: "

a

", + value: 0, + }, + { + label: "

b

", + value: 1, + }, + ], + right: [ + { + label: "

c

", + value: 0, + }, + { + label: "

d

", + value: 1, + }, + ], + }, + }, + validation: { + required: "Yes", + }, + }, + name: "Match The Following Question", + responseDeclaration: { + response1: { + cardinality: "multiple", + type: "integer", + correctResponse: { + value: [{ "0": 0 }, { "1": 1 }], + }, + mapping: [ + { + value: { + "0": 0, + }, + score: 2, + }, + { + value: { + "1": 1, + }, + score: 2, + }, + ], + }, + }, + interactionTypes: ["match"], + editorState: { + options: { + left: [ + { + value: { + body: "

a

", + value: 0, + }, + }, + { + value: { + body: "

b

", + value: 1, + }, + }, + ], + right: [ + { + value: { + body: "

c

", + value: 0, + }, + }, + { + value: { + body: "

d

", + value: 1, + }, + }, + ], + }, + question: "

q

", + }, + qType: "MTF", + primaryCategory: "Match The Following Question", +}; + export const RubricData = [ { parent: "do_1134357224765685761203", From 52f53ec4baa1a0ccfccb4dc1b56542214564a7b9 Mon Sep 17 00:00:00 2001 From: Arpan Gupta Date: Wed, 9 Aug 2023 16:03:39 +0530 Subject: [PATCH 25/37] Refactored getQuestionMetadata function --- .../components/question/question.component.ts | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/projects/questionset-editor-library/src/lib/components/question/question.component.ts b/projects/questionset-editor-library/src/lib/components/question/question.component.ts index 07ab2b238..e408a32a1 100644 --- a/projects/questionset-editor-library/src/lib/components/question/question.component.ts +++ b/projects/questionset-editor-library/src/lib/components/question/question.component.ts @@ -820,7 +820,10 @@ export class QuestionComponent implements OnInit, AfterViewInit, OnDestroy { metadata.body = metadata.question; if (!_.isUndefined(this.editorService?.editorConfig?.config?.renderTaxonomy)) { const treeNodeData = _.get(this.treeNodeData, 'data.metadata'); - _.get(treeNodeData,'allowScoring') === 'Yes' ? '' : _.set(metadata,'responseDeclaration.response1.mapping',[]); + const allowScoring = _.get(treeNodeData, 'allowScoring'); + if (allowScoring !== 'Yes') { + _.set(metadata, "responseDeclaration.response1.mapping", []); + } } if (this.questionInteractionType != 'choice' && this.questionInteractionType != 'match') { @@ -846,17 +849,15 @@ export class QuestionComponent implements OnInit, AfterViewInit, OnDestroy { }) const finalAnswer = this.getAnswerWrapperHtml(concatenatedAnswers); metadata.answer = finalAnswer; - } else if (this.questionInteractionType != 'default' && this.questionInteractionType != 'choice' && this.questionInteractionType != 'match') { + } else if (this.questionInteractionType === 'match') { + const { question, templateId } = this.editorState; + const { left, right } = this.editorState.interactions.response1.options; + metadata.body = this.getMtfQuestionHtmlBody(question, templateId); + metadata.correctMatchPair = this.getMtfAnswerContainerHtml(left, right); + } else if (this.questionInteractionType !== 'default') { metadata.responseDeclaration = this.getResponseDeclaration(this.questionInteractionType); } - - if (this.questionInteractionType === 'match') { - metadata.body = this.getMtfQuestionHtmlBody(this.editorState.question, this.editorState.templateId); - const leftOptions = metadata.interactions.response1.options.left; - const rightOptions = metadata.interactions.response1.options.right; - metadata.correctMatchPair = this.getMtfAnswerContainerHtml(leftOptions, rightOptions); - } - + if (!_.isUndefined(this.selectedSolutionType) && !_.isEmpty(this.selectedSolutionType)) { const solutionObj = this.getSolutionObj(this.solutionUUID, this.selectedSolutionType, this.editorState.solutions); metadata.editorState.solutions = [solutionObj]; @@ -870,12 +871,10 @@ export class QuestionComponent implements OnInit, AfterViewInit, OnDestroy { metadata.programId = _.get(this.editorService, 'editorConfig.context.programId'); metadata.collectionId = _.get(this.editorService, 'editorConfig.context.collectionIdentifier'); metadata.organisationId = _.get(this.editorService, 'editorConfig.context.contributionOrgId'); + metadata.isReviewModificationAllowed = !!_.get(this.questionMetaData, 'isReviewModificationAllowed'); } metadata['outcomeDeclaration'] = this.getOutcomeDeclaration(metadata); metadata = _.merge(metadata, _.pickBy(this.childFormData, _.identity)); - if (_.get(this.creationContext, 'objectType') === 'question') { - metadata.isReviewModificationAllowed = !!_.get(this.questionMetaData, 'isReviewModificationAllowed'); - } // tslint:disable-next-line:max-line-length return _.omit(metadata, ['question', 'numberOfOptions', 'options', 'allowMultiSelect', 'showEvidence', 'evidenceMimeType', 'showRemarks', 'markAsNotMandatory', 'leftAnchor', 'rightAnchor', 'step', 'numberOnly', 'characterLimit', 'dateFormat', 'autoCapture', 'remarksLimit', 'maximumOptions']); } From dba6334d912c8774ea88f70ea680a42a73205628 Mon Sep 17 00:00:00 2001 From: Arpan Gupta Date: Thu, 10 Aug 2023 20:39:53 +0530 Subject: [PATCH 26/37] UI refactored and tests added --- .../lib/components/match/match.component.html | 9 ++++-- .../lib/components/match/match.component.scss | 31 +++++++++++++++++++ .../components/match/match.component.spec.ts | 19 ++++++++++-- .../lib/components/match/match.component.ts | 5 +++ 4 files changed, 60 insertions(+), 4 deletions(-) diff --git a/projects/questionset-editor-library/src/lib/components/match/match.component.html b/projects/questionset-editor-library/src/lib/components/match/match.component.html index ea938a44b..6530a556f 100644 --- a/projects/questionset-editor-library/src/lib/components/match/match.component.html +++ b/projects/questionset-editor-library/src/lib/components/match/match.component.html @@ -1,9 +1,13 @@
- + + + +
+
@@ -49,6 +53,7 @@ libTelemetryInteract [telemetryInteractEdata]="telemetryService.getTelemetryInteractEdata('add_option','click','submit',telemetryService.telemetryPageId)" > {{configService.labelConfig?.lbl?.addPair}} - + +
diff --git a/projects/questionset-editor-library/src/lib/components/match/match.component.scss b/projects/questionset-editor-library/src/lib/components/match/match.component.scss index b6b99bbbe..f4233a529 100644 --- a/projects/questionset-editor-library/src/lib/components/match/match.component.scss +++ b/projects/questionset-editor-library/src/lib/components/match/match.component.scss @@ -1,3 +1,34 @@ +.q-sb-layout-single{ + &:before{ + content: url("/assets/images/layoutoneicon.svg"); + } + &.active, + &:hover + { + border-color: var(--primary-400); + background-color: #ffffff; + color: var(--primary-400); + &:before{ + content: url("/assets/images/layoutoneicon_blue.svg"); + } + } +} +.q-sb-layout-two{ + &:before{ + content: url("/assets/images/layouttwoicon.svg"); + } + &.active, + &:hover + { + border-color: var(--primary-400); + background-color: #ffffff; + color: var(--primary-400); + &:before{ + content: url("/assets/images/layouttwoicon_blue.svg"); + } + } +} + .b-0{ border: 0 !important; } diff --git a/projects/questionset-editor-library/src/lib/components/match/match.component.spec.ts b/projects/questionset-editor-library/src/lib/components/match/match.component.spec.ts index 79e1a79b7..39a321684 100644 --- a/projects/questionset-editor-library/src/lib/components/match/match.component.spec.ts +++ b/projects/questionset-editor-library/src/lib/components/match/match.component.spec.ts @@ -25,7 +25,6 @@ describe('MatchComponent', () => { fixture = TestBed.createComponent(MatchComponent); component = fixture.componentInstance; component.editorState = mockOptionData.editorOptionData; - // fixture.detectChanges(); }); it('should create', () => { @@ -86,7 +85,7 @@ describe('MatchComponent', () => { spyOn(component, 'setMapping').and.callThrough(); spyOn(component, "getResponseDeclaration").and.callThrough(); spyOn(component, "getInteractions").and.callThrough(); - const result = component.prepareMtfBody(mockOptionData.editorOptionData); + component.prepareMtfBody(mockOptionData.editorOptionData); expect(component.getResponseDeclaration).toHaveBeenCalledWith( mockOptionData.editorOptionData ); @@ -110,6 +109,13 @@ describe('MatchComponent', () => { expect(component.mapping).toEqual(mockOptionData.prepareMtfBody.responseDeclaration.response1.mapping); }) + it('#setMapping should set mapping with empty array when correctMatchPair is empty', () => { + spyOn(component, 'setMapping').and.callThrough(); + component.editorState.correctMatchPair = []; + component.setMapping(); + expect(component.mapping).toEqual([]); + }); + it('#getOutcomeDeclaration should return expected outcomeDeclaration', () => { component.maxScore = 4; spyOn(component, 'getOutcomeDeclaration').and.callThrough(); @@ -137,4 +143,13 @@ describe('MatchComponent', () => { const responseDeclaration = component.getResponseDeclaration(mockOptionData.editorOptionData); expect(responseDeclaration.response1.cardinality).toEqual('multiple'); }) + + it('#setTemplate() should set #templateType to "mtf-vertical"', () => { + spyOn(component, "editorDataHandler").and.callThrough(); + const templateType = "mtf-vertical"; + component.editorState = mockOptionData.editorOptionData; + component.setTemplate(templateType); + expect(component.templateType).toEqual(templateType); + expect(component.editorDataHandler).toHaveBeenCalled(); + }); }); \ No newline at end of file diff --git a/projects/questionset-editor-library/src/lib/components/match/match.component.ts b/projects/questionset-editor-library/src/lib/components/match/match.component.ts index 1336531bc..a8d043e36 100644 --- a/projects/questionset-editor-library/src/lib/components/match/match.component.ts +++ b/projects/questionset-editor-library/src/lib/components/match/match.component.ts @@ -157,4 +157,9 @@ export class MatchComponent implements OnInit, OnChanges { }; return interactions; } + + setTemplate(template) { + this.templateType = template; + this.editorDataHandler(); + } } From 957cb3dd873f2e819bea4667f5efd49d71b83f2a Mon Sep 17 00:00:00 2001 From: Arpan Gupta Date: Thu, 10 Aug 2023 20:47:25 +0530 Subject: [PATCH 27/37] Bug resolved --- .../src/lib/components/match/match.component.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/projects/questionset-editor-library/src/lib/components/match/match.component.html b/projects/questionset-editor-library/src/lib/components/match/match.component.html index 6530a556f..b12923f0a 100644 --- a/projects/questionset-editor-library/src/lib/components/match/match.component.html +++ b/projects/questionset-editor-library/src/lib/components/match/match.component.html @@ -4,8 +4,8 @@ - - + +
From 73eb19d410a7964f0bc85cd6bb7bd0b069051998 Mon Sep 17 00:00:00 2001 From: Arpan Gupta Date: Fri, 11 Aug 2023 21:58:32 +0530 Subject: [PATCH 28/37] Refactored question component code and tests added --- .../question/question.component.spec.ts | 35 +++++++++++++- .../components/question/question.component.ts | 46 ++++++++++--------- 2 files changed, 59 insertions(+), 22 deletions(-) diff --git a/projects/questionset-editor-library/src/lib/components/question/question.component.spec.ts b/projects/questionset-editor-library/src/lib/components/question/question.component.spec.ts index 4098e6add..9ddb4bd9e 100644 --- a/projects/questionset-editor-library/src/lib/components/question/question.component.spec.ts +++ b/projects/questionset-editor-library/src/lib/components/question/question.component.spec.ts @@ -1244,7 +1244,7 @@ describe("QuestionComponent", () => { expect(metadata['outcomeDeclaration'].maxScore.defaultValue).toEqual(1); }); - it('#getQuestionMetadata() should return question metata when interactionType is match', () => { + it('#getQuestionMetadata() should return question metadata when interactionType is match', () => { component.mediaArr = []; component.editorState = interactionMatchEditorState; component.selectedSolutionType = 'video'; @@ -1567,6 +1567,39 @@ describe("QuestionComponent", () => { expect(component.showFormError).toBeFalsy(); }); + it("#validateMatchQuestionData() should validate and set showFormError to true", () => { + component.sourcingSettings = sourcingSettingsMock; + component.treeNodeData = { data: { metadata: { allowScoring: "Yes" } } }; + component.editorState = mockData.mtfQuestionMetaData.result.question; + component.editorState.responseDeclaration.response1.mapping = []; + editorService = TestBed.inject(EditorService); + editorService.editorConfig.renderTaxonomy = false; + component.editorState.question = "

Match each object with its correct type

"; + component.editorState.correctMatchPair = ""; + component.questionInteractionType = "match"; + const toasterService = TestBed.inject(ToasterService); + spyOn(toasterService, "error").and.callFake(() => {}); + spyOn(component, "validateMatchQuestionData").and.callThrough(); + component.validateMatchQuestionData(); + expect(component.showFormError).toBeTruthy(); + }); + + it("#validateMatchQuestionData() should validate and set showFormError to false when allowScoring is No", () => { + component.sourcingSettings = sourcingSettingsMock; + component.treeNodeData = { data: { metadata: { allowScoring: "No" } } }; + component.editorState = mockData.mtfQuestionMetaData.result.question; + editorService = TestBed.inject(EditorService); + editorService.editorConfig.renderTaxonomy = false; + component.editorState.question = "

Match each object with its correct type

"; + component.editorState.correctMatchPair = ""; + component.questionInteractionType = "match"; + const toasterService = TestBed.inject(ToasterService); + spyOn(toasterService, "error").and.callFake(() => {}); + spyOn(component, "validateMatchQuestionData").and.callThrough(); + component.validateMatchQuestionData(); + expect(component.showFormError).toBeFalsy(); + }); + it("#validateQuestionData() should call validateQuestionData when questionInteractionType is text", () => { component.sourcingSettings = sourcingSettingsMock; component.editorState = mockData.textQuestionNetaData.result.question; diff --git a/projects/questionset-editor-library/src/lib/components/question/question.component.ts b/projects/questionset-editor-library/src/lib/components/question/question.component.ts index f3e1e3ae2..c6f48bcf1 100644 --- a/projects/questionset-editor-library/src/lib/components/question/question.component.ts +++ b/projects/questionset-editor-library/src/lib/components/question/question.component.ts @@ -589,27 +589,9 @@ export class QuestionComponent implements OnInit, AfterViewInit, OnDestroy { this.validateChoiceQuestionData(); } - //to handle when question type is match + //to handle when question type is mtf if (this.questionInteractionType === 'match') { - const data = _.get(this.treeNodeData, 'data.metadata'); - if (_.get(this.editorState, 'interactionTypes[0]') === 'match' && - _.isEmpty(this.editorState?.responseDeclaration?.response1?.mapping) && - !_.isUndefined(this.editorService?.editorConfig?.config?.renderTaxonomy) && - _.get(data,'allowScoring') === 'Yes') { - this.toasterService.error(_.get(this.configService, 'labelConfig.messages.error.005')); - this.showFormError = true; - return; - } else { - this.showFormError = false; - } - const rightOptionValid = _.find(this.editorState.options, option => (option.right === undefined || option.right === '' || option.right.length > this.setCharacterLimit)); - const leftOptionValid = _.find(this.editorState.options, option => (option.left === undefined || option.left === '' || option.left.length > this.setCharacterLimit)); - if (rightOptionValid || leftOptionValid || (_.isUndefined(this.editorState.correctMatchPair) && this.sourcingSettings?.enforceCorrectAnswer)) { - this.showFormError = true; - return; - } else { - this.showFormError = false; - } + this.validateMatchQuestionData(); } if (this.questionInteractionType === 'slider') { @@ -649,6 +631,28 @@ export class QuestionComponent implements OnInit, AfterViewInit, OnDestroy { } } + validateMatchQuestionData() { + const data = _.get(this.treeNodeData, 'data.metadata'); + if (_.get(this.editorState, 'interactionTypes[0]') === 'match' && + _.isEmpty(this.editorState?.responseDeclaration?.response1?.mapping) && + !_.isUndefined(this.editorService?.editorConfig?.config?.renderTaxonomy) && + _.get(data,'allowScoring') === 'Yes') { + this.toasterService.error(_.get(this.configService, 'labelConfig.messages.error.005')); + this.showFormError = true; + return; + } else { + this.showFormError = false; + } + const rightOptionValid = _.find(this.editorState.options, option => (option.right === undefined || option.right === '' || option.right.length > this.setCharacterLimit)); + const leftOptionValid = _.find(this.editorState.options, option => (option.left === undefined || option.left === '' || option.left.length > this.setCharacterLimit)); + if (rightOptionValid || leftOptionValid || (_.isUndefined(this.editorState.correctMatchPair) && this.sourcingSettings?.enforceCorrectAnswer)) { + this.showFormError = true; + return; //NOSONAR + } else { + this.showFormError = false; + } + } + validateSliderQuestionData() { const min = _.get(this.sliderDatas, 'validation.range.min'); const max = _.get(this.sliderDatas, 'validation.range.max'); @@ -817,7 +821,7 @@ export class QuestionComponent implements OnInit, AfterViewInit, OnDestroy { } setQuestionProperties(metadata) { - if (this.questionInteractionType != 'choice') { + if (this.questionInteractionType != 'choice' && this.questionInteractionType != 'match') { if (!_.isUndefined(metadata.answer)) { const answerHtml = this.getAnswerHtml(metadata.answer); const finalAnswer = this.getAnswerWrapperHtml(answerHtml); From 4ff8fcafb15af30584184bbb1fe178e61c591e57 Mon Sep 17 00:00:00 2001 From: Arpan Gupta Date: Wed, 16 Aug 2023 23:19:21 +0530 Subject: [PATCH 29/37] Code refactored --- .../src/lib/components/match/match.component.spec.data.ts | 2 +- .../src/lib/components/match/match.component.ts | 2 -- .../src/lib/components/question/question.component.html | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/projects/questionset-editor-library/src/lib/components/match/match.component.spec.data.ts b/projects/questionset-editor-library/src/lib/components/match/match.component.spec.data.ts index 2552ed8fd..7d1a30bf2 100644 --- a/projects/questionset-editor-library/src/lib/components/match/match.component.spec.data.ts +++ b/projects/questionset-editor-library/src/lib/components/match/match.component.spec.data.ts @@ -16,7 +16,7 @@ export const mockOptionData = { numberOfOptions: 4, }, prepareMtfBody: { - templateId: "default", + templateId: "mtf-horizontal", name: "Match The Following Question", responseDeclaration: { response1: { diff --git a/projects/questionset-editor-library/src/lib/components/match/match.component.ts b/projects/questionset-editor-library/src/lib/components/match/match.component.ts index a8d043e36..0695010b4 100644 --- a/projects/questionset-editor-library/src/lib/components/match/match.component.ts +++ b/projects/questionset-editor-library/src/lib/components/match/match.component.ts @@ -11,14 +11,12 @@ import { ConfigService } from '../../services/config/config.service'; export class MatchComponent implements OnInit, OnChanges { @Input() editorState: any; @Input() showFormError; - @Input() sourcingSettings; @Input() questionPrimaryCategory; @Input() mapping = []; @Input() isReadOnlyMode; @Input() maxScore; @Output() editorDataOutput: EventEmitter = new EventEmitter(); public setCharacterLimit = 160; - public setImageLimit = 1; public templateType = 'mtf-horizontal'; constructor( diff --git a/projects/questionset-editor-library/src/lib/components/question/question.component.html b/projects/questionset-editor-library/src/lib/components/question/question.component.html index 62063550e..e1806aef2 100644 --- a/projects/questionset-editor-library/src/lib/components/question/question.component.html +++ b/projects/questionset-editor-library/src/lib/components/question/question.component.html @@ -69,7 +69,7 @@ [questionPrimaryCategory]="questionPrimaryCategory" [editorState]="editorState" [showFormError]="showFormError" (editorDataOutput)="editorDataHandler($event)" - [sourcingSettings]="sourcingSettings" [mapping]="scoreMapping" [maxScore]="maxScore" + [mapping]="scoreMapping" [maxScore]="maxScore" [isReadOnlyMode]="isReadOnlyMode"> Date: Thu, 17 Aug 2023 15:06:29 +0530 Subject: [PATCH 30/37] Match template refactored --- .../lib/components/match/match.component.html | 70 ++++++++++++------- 1 file changed, 46 insertions(+), 24 deletions(-) diff --git a/projects/questionset-editor-library/src/lib/components/match/match.component.html b/projects/questionset-editor-library/src/lib/components/match/match.component.html index b12923f0a..149b98883 100644 --- a/projects/questionset-editor-library/src/lib/components/match/match.component.html +++ b/projects/questionset-editor-library/src/lib/components/match/match.component.html @@ -1,11 +1,26 @@
- - + + - - + + + + + +
@@ -17,42 +32,49 @@ [editorDataInput]="option.left" class="ckeditor-tool__option mb-10" [class.mb-5]="showFormError && option.left.length > setCharacterLimit"> - - + +
- - + +
From cd046e4e952bbe2ec30d56f605481ef0bcb1c8f5 Mon Sep 17 00:00:00 2001 From: Arpan Gupta Date: Thu, 17 Aug 2023 15:24:49 +0530 Subject: [PATCH 31/37] Question component refactored --- .../components/question/question.component.ts | 45 +++++++------------ 1 file changed, 16 insertions(+), 29 deletions(-) diff --git a/projects/questionset-editor-library/src/lib/components/question/question.component.ts b/projects/questionset-editor-library/src/lib/components/question/question.component.ts index c6f48bcf1..d4e6d5a8d 100644 --- a/projects/questionset-editor-library/src/lib/components/question/question.component.ts +++ b/projects/questionset-editor-library/src/lib/components/question/question.component.ts @@ -225,45 +225,32 @@ export class QuestionComponent implements OnInit, AfterViewInit, OnDestroy { } } - if (this.questionInteractionType === 'choice') { + if (this.questionInteractionType === 'choice' || this.questionInteractionType === 'match') { const responseDeclaration = this.questionMetaData.responseDeclaration; this.scoreMapping = _.get(responseDeclaration, 'response1.mapping'); const templateId = this.questionMetaData.templateId; const numberOfOptions = this.questionMetaData?.editorState?.options?.length || 0; const maximumOptions = _.get(this.questionInput, 'config.maximumOptions'); - this.editorService.optionsLength = numberOfOptions; - const options = _.map(this.questionMetaData?.editorState?.options, option => ({ body: option.value.body })); + this.editorService.optionsLength = numberOfOptions; const question = this.questionMetaData?.editorState?.question; const interactions = this.questionMetaData?.interactions; - this.editorState = new McqForm({ - question, options, answer: _.get(responseDeclaration, 'response1.correctResponse.value') - }, { templateId, numberOfOptions,maximumOptions }); - this.editorState.solutions = this.questionMetaData?.editorState?.solutions; this.editorState.interactions = interactions; - if (_.has(this.questionMetaData, 'responseDeclaration')) { - this.editorState.responseDeclaration = _.get(this.questionMetaData, 'responseDeclaration'); + if (this.questionInteractionType === 'choice') { + const options = _.map(this.questionMetaData?.editorState?.options, option => ({ body: option.value.body })); + this.editorState = new McqForm({ + question, options, answer: _.get(responseDeclaration, 'response1.correctResponse.value') + }, { templateId, numberOfOptions, maximumOptions }); + } + else if (this.questionInteractionType === 'match') { + const options = _.map(this.questionMetaData?.editorState?.options?.left, (left, index) => ({ + left, + right:this.questionMetaData?.editorState?.options?.right?.[index] + })); + this.editorState = new MtfForm({ + question, options, correctMatchPair: _.get(responseDeclaration, 'response1.correctResponse.value') + }, { templateId, numberOfOptions, maximumOptions }); } - } - - if (this.questionInteractionType === 'match') { - const responseDeclaration = this.questionMetaData.responseDeclaration; - this.scoreMapping = _.get(responseDeclaration, 'response1.mapping'); - const templateId = this.questionMetaData.templateId; - const numberOfOptions = this.questionMetaData?.editorState?.options?.length || 0; - const maximumOptions = _.get(this.questionInput, 'config.maximumOptions'); - this.editorService.optionsLength = numberOfOptions; - // converting the options to the format required by the editor - const options = _.map(this.questionMetaData?.editorState?.options?.left, (left, index) => ({ - left, - right:this.questionMetaData?.editorState?.options?.right?.[index] - })); - const question = this.questionMetaData?.editorState?.question; - const interactions = this.questionMetaData?.interactions; - this.editorState = new MtfForm({ - question, options, correctMatchPair: _.get(responseDeclaration, 'response1.correctResponse.value') - }, { templateId, numberOfOptions, maximumOptions }); this.editorState.solutions = this.questionMetaData?.editorState?.solutions; - this.editorState.interactions = interactions; if (_.has(this.questionMetaData, 'responseDeclaration')) { this.editorState.responseDeclaration = _.get(this.questionMetaData, 'responseDeclaration'); } From 5cbf70cf0673105bf370f10b0a0202eb5e36e816 Mon Sep 17 00:00:00 2001 From: Arpan Gupta Date: Fri, 18 Aug 2023 21:48:18 +0530 Subject: [PATCH 32/37] Refactored Code --- .../question/question.component.spec.ts | 54 ++++++++---------- .../components/question/question.component.ts | 57 +++++++++---------- sonar-project.properties | 2 +- 3 files changed, 52 insertions(+), 61 deletions(-) diff --git a/projects/questionset-editor-library/src/lib/components/question/question.component.spec.ts b/projects/questionset-editor-library/src/lib/components/question/question.component.spec.ts index 9ddb4bd9e..918f3a9a0 100644 --- a/projects/questionset-editor-library/src/lib/components/question/question.component.spec.ts +++ b/projects/questionset-editor-library/src/lib/components/question/question.component.spec.ts @@ -1534,41 +1534,38 @@ describe("QuestionComponent", () => { expect(component.validateChoiceQuestionData).toHaveBeenCalled(); }); - it('#validateChoiceQuestionData() should validate and set showFormError to true', () => { + it('#validateChoiceQuestionData() should validate choice question data when all options are valid and set showFormError to false', () => { component.sourcingSettings = sourcingSettingsMock; - component.treeNodeData = {data: {metadata: {allowScoring: 'Yes'}}} - component.editorState = mockData.mcqQuestionMetaData.result.question; - component.editorState.responseDeclaration.response1.mapping = []; - editorService = TestBed.inject(EditorService); - editorService.editorConfig.renderTaxonomy = false; component.editorState.question = "

Hi how are you

"; + component.editorState.options = [ + { body: "

1

" }, + { body: "

2

" }, + ] component.editorState.answer = ""; component.questionInteractionType = "choice"; - const toasterService = TestBed.inject(ToasterService); - spyOn(toasterService, 'error').and.callFake(() => {}); spyOn(component, 'validateChoiceQuestionData').and.callThrough(); component.validateChoiceQuestionData(); - expect(component.showFormError).toBeTruthy(); + expect(component.showFormError).toBeFalsy(); }); - it('#validateChoiceQuestionData() should validate and set showFormError to false when allowScoring is No', () => { + it("#validateMatchQuestionData() should validate match question data when all options have valid left and right values and set showFormError to false", () => { component.sourcingSettings = sourcingSettingsMock; - component.treeNodeData = {data: {metadata: {allowScoring: 'No'}}} - component.editorState = mockData.mcqQuestionMetaData.result.question; - editorService = TestBed.inject(EditorService); - editorService.editorConfig.renderTaxonomy = false; - component.editorState.question = "

Hi how are you

"; - component.editorState.answer = ""; - component.questionInteractionType = "choice"; - const toasterService = TestBed.inject(ToasterService); - spyOn(toasterService, 'error').and.callFake(() => {}); - spyOn(component, 'validateChoiceQuestionData').and.callThrough(); - component.validateChoiceQuestionData(); + component.editorState.question = "

Match each object with its correct type

"; + component.editorState.options = [ + { left: "

1

", right: "

a

" }, + { left: "

2

", right: "

b

"}, + ] + component.editorState.correctMatchPair = [ + { "0": "0" }, + { "1": "1"}, + ]; + component.questionInteractionType = "match"; + spyOn(component, "validateMatchQuestionData").and.callThrough(); + component.validateMatchQuestionData(); expect(component.showFormError).toBeFalsy(); }); - it("#validateMatchQuestionData() should validate and set showFormError to true", () => { - component.sourcingSettings = sourcingSettingsMock; + it("#validateData() should validate and set showFormError to true when allowScoring is Yes", () => { component.treeNodeData = { data: { metadata: { allowScoring: "Yes" } } }; component.editorState = mockData.mtfQuestionMetaData.result.question; component.editorState.responseDeclaration.response1.mapping = []; @@ -1579,13 +1576,12 @@ describe("QuestionComponent", () => { component.questionInteractionType = "match"; const toasterService = TestBed.inject(ToasterService); spyOn(toasterService, "error").and.callFake(() => {}); - spyOn(component, "validateMatchQuestionData").and.callThrough(); - component.validateMatchQuestionData(); + spyOn(component, "validateData").and.callThrough(); + component.validateData(component.questionInteractionType); expect(component.showFormError).toBeTruthy(); }); - it("#validateMatchQuestionData() should validate and set showFormError to false when allowScoring is No", () => { - component.sourcingSettings = sourcingSettingsMock; + it("#validateData() should validate and set showFormError to false when allowScoring is No", () => { component.treeNodeData = { data: { metadata: { allowScoring: "No" } } }; component.editorState = mockData.mtfQuestionMetaData.result.question; editorService = TestBed.inject(EditorService); @@ -1595,8 +1591,8 @@ describe("QuestionComponent", () => { component.questionInteractionType = "match"; const toasterService = TestBed.inject(ToasterService); spyOn(toasterService, "error").and.callFake(() => {}); - spyOn(component, "validateMatchQuestionData").and.callThrough(); - component.validateMatchQuestionData(); + spyOn(component, "validateData").and.callThrough(); + component.validateData(component.questionInteractionType); expect(component.showFormError).toBeFalsy(); }); diff --git a/projects/questionset-editor-library/src/lib/components/question/question.component.ts b/projects/questionset-editor-library/src/lib/components/question/question.component.ts index d4e6d5a8d..673635656 100644 --- a/projects/questionset-editor-library/src/lib/components/question/question.component.ts +++ b/projects/questionset-editor-library/src/lib/components/question/question.component.ts @@ -597,17 +597,7 @@ export class QuestionComponent implements OnInit, AfterViewInit, OnDestroy { } validateChoiceQuestionData() { - const data = _.get(this.treeNodeData, 'data.metadata'); - if (_.get(this.editorState, 'interactionTypes[0]') === 'choice' && - _.isEmpty(this.editorState?.responseDeclaration?.response1?.mapping) && - !_.isUndefined(this.editorService?.editorConfig?.config?.renderTaxonomy) && - _.get(data,'allowScoring') === 'Yes') { - this.toasterService.error(_.get(this.configService, 'labelConfig.messages.error.005')); - this.showFormError = true; - return; - } else { - this.showFormError = false; - } + this.validateData('choice'); const optionValid = _.find(this.editorState.options, option => (option.body === undefined || option.body === '' || option.length > this.setCharacterLimit)); if (optionValid || (_.isUndefined(this.editorState.answer) && this.sourcingSettings?.enforceCorrectAnswer)) { @@ -619,27 +609,18 @@ export class QuestionComponent implements OnInit, AfterViewInit, OnDestroy { } validateMatchQuestionData() { - const data = _.get(this.treeNodeData, 'data.metadata'); - if (_.get(this.editorState, 'interactionTypes[0]') === 'match' && - _.isEmpty(this.editorState?.responseDeclaration?.response1?.mapping) && - !_.isUndefined(this.editorService?.editorConfig?.config?.renderTaxonomy) && - _.get(data,'allowScoring') === 'Yes') { - this.toasterService.error(_.get(this.configService, 'labelConfig.messages.error.005')); - this.showFormError = true; - return; - } else { - this.showFormError = false; - } - const rightOptionValid = _.find(this.editorState.options, option => (option.right === undefined || option.right === '' || option.right.length > this.setCharacterLimit)); - const leftOptionValid = _.find(this.editorState.options, option => (option.left === undefined || option.left === '' || option.left.length > this.setCharacterLimit)); - if (rightOptionValid || leftOptionValid || (_.isUndefined(this.editorState.correctMatchPair) && this.sourcingSettings?.enforceCorrectAnswer)) { - this.showFormError = true; - return; //NOSONAR - } else { - this.showFormError = false; - } + this.validateData('match'); + const rightOptionValid = _.find(this.editorState.options, option => (option.right === undefined || option.right === '' || option.right.length > this.setCharacterLimit)); + const leftOptionValid = _.find(this.editorState.options, option => (option.left === undefined || option.left === '' || option.left.length > this.setCharacterLimit)); + if (rightOptionValid || leftOptionValid || (_.isUndefined(this.editorState.correctMatchPair) && this.sourcingSettings?.enforceCorrectAnswer)) { + this.showFormError = true; + return; //NOSONAR + } else { + this.showFormError = false; + } } + validateSliderQuestionData() { const min = _.get(this.sliderDatas, 'validation.range.min'); const max = _.get(this.sliderDatas, 'validation.range.max'); @@ -651,7 +632,21 @@ export class QuestionComponent implements OnInit, AfterViewInit, OnDestroy { this.showFormError = false; } } - + + validateData(interactionType) { + const data = _.get(this.treeNodeData, 'data.metadata'); + if (_.get(this.editorState, 'interactionTypes[0]') === interactionType && + _.isEmpty(this.editorState?.responseDeclaration?.response1?.mapping) && + !_.isUndefined(this.editorService?.editorConfig?.config?.renderTaxonomy) && + _.get(data,'allowScoring') === 'Yes') { + this.toasterService.error(_.get(this.configService, 'labelConfig.messages.error.005')); + this.showFormError = true; + return; //NOSONAR + } else { + this.showFormError = false; + } + } + redirectToQuestionset() { this.showConfirmPopup = false; this.treeService.clearTreeCache(); diff --git a/sonar-project.properties b/sonar-project.properties index 0c80de60d..10f91e1bf 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -1,7 +1,7 @@ sonar.projectName=sunbird-questionset-editor sonar.language=ts sonar.sources=projects/questionset-editor-library/src -sonar.exclusions=projects/questionset-editor-library/src/lib/assets/**,projects/questionset-editor-library/src/lib/**/*.spec.ts,projects/questionset-editor-library/src/lib/**/*.spec.data.ts,projects/questionset-editor-library/src/lib/**/*.module.ts,projects/questionset-editor-library/src/lib/interfaces/* +sonar.exclusions=projects/questionset-editor-library/src/lib/assets/**,projects/questionset-editor-library/src/lib/**/*.spec.ts,projects/questionset-editor-library/src/lib/**/*.spec.data.ts,projects/questionset-editor-library/src/lib/**/*.module.ts,projects/questionset-editor-library/src/lib/interfaces/*,projects/questionset-editor-library/src/lib/services/config/label.config.json sonar.javascript.lcov.reportPaths=projects/questionset-editor-library/coverage/lcov.info sonar.projectKey=Sunbird-inQuiry_editor sonar.host.url=https://sonarcloud.io From e3a703825331e07afeaf0c6de7fa7be269ebfa7f Mon Sep 17 00:00:00 2001 From: Arpan Gupta Date: Sat, 19 Aug 2023 14:56:18 +0530 Subject: [PATCH 33/37] Canaged correctMatchPair property in metadata --- .../src/lib/components/question/question.component.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/projects/questionset-editor-library/src/lib/components/question/question.component.ts b/projects/questionset-editor-library/src/lib/components/question/question.component.ts index 673635656..3c73f944b 100644 --- a/projects/questionset-editor-library/src/lib/components/question/question.component.ts +++ b/projects/questionset-editor-library/src/lib/components/question/question.component.ts @@ -243,8 +243,8 @@ export class QuestionComponent implements OnInit, AfterViewInit, OnDestroy { } else if (this.questionInteractionType === 'match') { const options = _.map(this.questionMetaData?.editorState?.options?.left, (left, index) => ({ - left, - right:this.questionMetaData?.editorState?.options?.right?.[index] + left: left.value.body, + right:this.questionMetaData?.editorState?.options?.right?.[index].value.body })); this.editorState = new MtfForm({ question, options, correctMatchPair: _.get(responseDeclaration, 'response1.correctResponse.value') @@ -830,7 +830,9 @@ export class QuestionComponent implements OnInit, AfterViewInit, OnDestroy { const { question, templateId } = this.editorState; const { left, right } = this.editorState.interactions.response1.options; metadata.body = this.getMtfQuestionHtmlBody(question, templateId); - metadata.correctMatchPair = this.getMtfAnswerContainerHtml(left, right); + metadata['answer'] = metadata['correctMatchPair']; + delete metadata['correctMatchPair']; + metadata.answer = this.getMtfAnswerContainerHtml(left, right); } else if (this.questionInteractionType !== 'default') { metadata.responseDeclaration = this.getResponseDeclaration(this.questionInteractionType); } From 7eb285a122328c58a123c771bfeb6c6c9eae5638 Mon Sep 17 00:00:00 2001 From: Arpan Gupta Date: Wed, 23 Aug 2023 17:49:57 +0530 Subject: [PATCH 34/37] Resolved fill this option error bug --- .../src/lib/components/match/match.component.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/projects/questionset-editor-library/src/lib/components/match/match.component.html b/projects/questionset-editor-library/src/lib/components/match/match.component.html index 149b98883..ce10ec67b 100644 --- a/projects/questionset-editor-library/src/lib/components/match/match.component.html +++ b/projects/questionset-editor-library/src/lib/components/match/match.component.html @@ -30,7 +30,7 @@ + [class.mb-5]="showFormError && ([undefined, ''].includes(option.left.body) || option.left.length > setCharacterLimit)">