From 53dcd2ffdf7cdab194affbad4e70894870fdd295 Mon Sep 17 00:00:00 2001 From: Rishad Alam Date: Tue, 16 Jun 2026 17:26:29 +0600 Subject: [PATCH 1/5] feat: masterstudy missing actions added --- .github/workflows/plugin-check.yml | 2 +- .../MasterStudyLms/RecordApiHelper.php | 97 ++++++++++++- .../MasterStudyLms/MasterStudyLms.jsx | 64 +++++++-- .../MasterStudyLmsCommonFunc.js | 33 +++++ .../MasterStudyLms/MasterStudyLmsFieldMap.jsx | 110 +++++++++++++++ .../MasterStudyLmsIntegLayout.jsx | 133 ++++++++++++++---- .../WishlistMemberIntegLayout.jsx | 1 + 7 files changed, 394 insertions(+), 46 deletions(-) create mode 100644 frontend/src/components/AllIntegrations/MasterStudyLms/MasterStudyLmsFieldMap.jsx diff --git a/.github/workflows/plugin-check.yml b/.github/workflows/plugin-check.yml index 2c9c4e956..b195c7b11 100644 --- a/.github/workflows/plugin-check.yml +++ b/.github/workflows/plugin-check.yml @@ -75,7 +75,7 @@ jobs: wp-env start - name: WordPress Plugin Check - uses: wordpress/plugin-check-action@v1 + uses: wordpress/plugin-check-action@v1.1.7 with: build-dir: './build/${{ env.PLUGIN_SLUG }}' exclude-directories: | diff --git a/backend/Actions/MasterStudyLms/RecordApiHelper.php b/backend/Actions/MasterStudyLms/RecordApiHelper.php index c94f03751..217858b80 100644 --- a/backend/Actions/MasterStudyLms/RecordApiHelper.php +++ b/backend/Actions/MasterStudyLms/RecordApiHelper.php @@ -2,6 +2,9 @@ namespace BitApps\Integrations\Actions\MasterStudyLms; +use BitApps\Integrations\Config; +use BitApps\Integrations\Core\Util\Common; +use BitApps\Integrations\Core\Util\Hooks; use BitApps\Integrations\Log\LogHandler; use STM_LMS_Course; use STM_LMS_Helpers; @@ -12,6 +15,24 @@ class RecordApiHelper { + private const COMPLETE_COURSE = 1; + + private const COMPLETE_LESSON = 2; + + private const COMPLETE_QUIZ = 3; + + private const RESET_COURSE = 4; + + private const RESET_LESSON = 5; + + private const ENROLL_USER = 6; + + private const UNENROLL_USER = 7; + + private const MARK_COURSE_COMPLETE = 8; + + private const MARK_LESSON_COMPLETE = 9; + private $integrationID; private $_integrationDetails; @@ -305,9 +326,15 @@ public function execute( $integrationData ) { $response = []; - $fieldData = []; + $fieldData = static::generateReqDataFromFieldMap($integrationDetails->field_map ?? [], $fieldValues); + + $defaultResponse = [ + 'success' => false, + // translators: %s: Plugin name + 'message' => wp_sprintf(__('%s plugin is not installed or activate', 'bit-integrations'), 'Bit Integrations Pro'), + ]; - if ($mainAction == 1) { + if ((int) $mainAction === self::COMPLETE_COURSE) { $courseId = $integrationDetails->courseId; $response = self::complete_course($courseId); if ($response) { @@ -315,7 +342,7 @@ public function execute( } else { LogHandler::save($this->integrationID, wp_json_encode(['type' => 'course-complete', 'type_name' => 'user-course-complete']), 'error', __('Failed to completed course', 'bit-integrations')); } - } elseif ($mainAction == 2) { + } elseif ((int) $mainAction === self::COMPLETE_LESSON) { $courseId = $integrationDetails->courseId; $lessonId = $integrationDetails->lessonId; $response = self::complete_lesson($courseId, $lessonId); @@ -324,7 +351,7 @@ public function execute( } else { LogHandler::save($this->integrationID, wp_json_encode(['type' => 'lesson-complete', 'type_name' => 'user-lesson-complete']), 'error', __('Failed to completed lesson', 'bit-integrations')); } - } elseif ($mainAction == 3) { + } elseif ((int) $mainAction === self::COMPLETE_QUIZ) { $courseId = $integrationDetails->courseId; $quizId = $integrationDetails->quizId; $response = self::complete_quiz($courseId, $quizId); @@ -333,7 +360,7 @@ public function execute( } else { LogHandler::save($this->integrationID, wp_json_encode(['type' => 'quiz-complete', 'type_name' => 'user-quiz-complete']), 'error', __('Failed to completed quiz', 'bit-integrations')); } - } elseif ($mainAction == 4) { + } elseif ((int) $mainAction === self::RESET_COURSE) { $courseId = $integrationDetails->courseId; $response = self::reset_course($courseId); if ($response) { @@ -341,7 +368,7 @@ public function execute( } else { LogHandler::save($this->integrationID, wp_json_encode(['type' => 'course-reset', 'type_name' => 'user-course-reset']), 'error', __('Failed to reset course', 'bit-integrations')); } - } elseif ($mainAction == 5) { + } elseif ((int) $mainAction === self::RESET_LESSON) { $course_id = $integrationDetails->courseId; $lesson_id = $integrationDetails->lessonId; $response = self::reset_lesson($course_id, $lesson_id); @@ -350,8 +377,66 @@ public function execute( } else { LogHandler::save($this->integrationID, wp_json_encode(['type' => 'lesson-reset', 'type_name' => 'user-lesson-reset']), 'error', __('Failed to reset lesson', 'bit-integrations')); } + } elseif ((int) $mainAction === self::ENROLL_USER) { + if (empty($fieldData['user_email'])) { + return new WP_Error('REQ_FIELD_EMPTY', __('User email is required', 'bit-integrations')); + } + $response = Hooks::apply(Config::withPrefix('master_study_lms_enroll_user'), $defaultResponse, [ + 'course_id' => $integrationDetails->courseId, + 'email' => $fieldData['user_email'], + ]); + LogHandler::save($this->integrationID, wp_json_encode(['type' => 'enroll-user', 'type_name' => 'enroll-user-to-course']), !empty($response['success']) ? 'success' : 'error', $response['message'] ?? ''); + } elseif ((int) $mainAction === self::UNENROLL_USER) { + if (empty($fieldData['user_email'])) { + return new WP_Error('REQ_FIELD_EMPTY', __('User email is required', 'bit-integrations')); + } + $response = Hooks::apply(Config::withPrefix('master_study_lms_unenroll_user'), $defaultResponse, [ + 'course_id' => $integrationDetails->courseId, + 'email' => $fieldData['user_email'], + ]); + LogHandler::save($this->integrationID, wp_json_encode(['type' => 'unenroll-user', 'type_name' => 'unenroll-user-from-course']), !empty($response['success']) ? 'success' : 'error', $response['message'] ?? ''); + } elseif ((int) $mainAction === self::MARK_COURSE_COMPLETE) { + if (empty($fieldData['user_email'])) { + return new WP_Error('REQ_FIELD_EMPTY', __('User email is required', 'bit-integrations')); + } + $response = Hooks::apply(Config::withPrefix('master_study_lms_mark_course_complete'), $defaultResponse, [ + 'course_id' => $integrationDetails->courseId, + 'email' => $fieldData['user_email'], + ]); + LogHandler::save($this->integrationID, wp_json_encode(['type' => 'mark-course-complete', 'type_name' => 'mark-course-complete-for-user']), !empty($response['success']) ? 'success' : 'error', $response['message'] ?? ''); + } elseif ((int) $mainAction === self::MARK_LESSON_COMPLETE) { + if (empty($fieldData['user_email'])) { + return new WP_Error('REQ_FIELD_EMPTY', __('User email is required', 'bit-integrations')); + } + $response = Hooks::apply(Config::withPrefix('master_study_lms_mark_lesson_complete'), $defaultResponse, [ + 'course_id' => $integrationDetails->courseId, + 'lesson_id' => $integrationDetails->lessonId, + 'email' => $fieldData['user_email'], + ]); + LogHandler::save($this->integrationID, wp_json_encode(['type' => 'mark-lesson-complete', 'type_name' => 'mark-lesson-complete-for-user']), !empty($response['success']) ? 'success' : 'error', $response['message'] ?? ''); } return $response; } + + protected static function generateReqDataFromFieldMap($fieldMap, $fieldValues) + { + $data = []; + + foreach ($fieldMap as $map) { + if (empty($map->msLmsFormField)) { + continue; + } + + $formField = $map->formField ?? ''; + + if ($formField === 'custom' && isset($map->customValue)) { + $data[$map->msLmsFormField] = Common::replaceFieldWithValue($map->customValue, $fieldValues); + } else { + $data[$map->msLmsFormField] = $fieldValues[$formField] ?? ''; + } + } + + return $data; + } } diff --git a/frontend/src/components/AllIntegrations/MasterStudyLms/MasterStudyLms.jsx b/frontend/src/components/AllIntegrations/MasterStudyLms/MasterStudyLms.jsx index fb8967705..d1e3cbd31 100644 --- a/frontend/src/components/AllIntegrations/MasterStudyLms/MasterStudyLms.jsx +++ b/frontend/src/components/AllIntegrations/MasterStudyLms/MasterStudyLms.jsx @@ -8,11 +8,44 @@ import SnackMsg from '../../Utilities/SnackMsg' import Steps from '../../Utilities/Steps' import { saveActionConf } from '../IntegrationHelpers/IntegrationHelpers' import IntegrationStepThree from '../IntegrationHelpers/IntegrationStepThree' -import { handleInput, checkMappedFields } from './MasterStudyLmsCommonFunc' +import { + handleInput, + checkMappedFields, + isUserEmailMapped, + MS_LMS_ACTIONS +} from './MasterStudyLmsCommonFunc' import MasterStudyLmsAuthorization from './MasterStudyLmsAuthorization' import MasterStudyLmsIntegLayout from './MasterStudyLmsIntegLayout' import TutorialLink from '../../Utilities/TutorialLink' +export const allActions = [ + { key: MS_LMS_ACTIONS.COMPLETE_COURSE, label: __('Course complete for the user', 'bit-integrations') }, + { key: MS_LMS_ACTIONS.COMPLETE_LESSON, label: __('Lesson complete for the user', 'bit-integrations') }, + { key: MS_LMS_ACTIONS.COMPLETE_QUIZ, label: __('Quiz complete for the user', 'bit-integrations') }, + { key: MS_LMS_ACTIONS.RESET_COURSE, label: __('Reset user course', 'bit-integrations') }, + { key: MS_LMS_ACTIONS.RESET_LESSON, label: __('Reset user lesson', 'bit-integrations') }, + { + key: MS_LMS_ACTIONS.ENROLL_USER, + label: __('Enroll user in a course', 'bit-integrations'), + is_pro: true + }, + { + key: MS_LMS_ACTIONS.UNENROLL_USER, + label: __('Unenroll user from a course', 'bit-integrations'), + is_pro: true + }, + { + key: MS_LMS_ACTIONS.MARK_COURSE_COMPLETE, + label: __('Mark a course complete for the user', 'bit-integrations'), + is_pro: true + }, + { + key: MS_LMS_ACTIONS.MARK_LESSON_COMPLETE, + label: __('Mark a lesson complete for the user', 'bit-integrations'), + is_pro: true + } +] + function MasterStudyLms({ formFields, setFlow, flow, allIntegURL, isInfo, edit }) { const navigate = useNavigate() const { formID } = useParams() @@ -21,20 +54,11 @@ function MasterStudyLms({ formFields, setFlow, flow, allIntegURL, isInfo, edit } const [step, setStep] = useState(1) const [snack, setSnackbar] = useState({ show: false }) - const allActions = [ - { key: '1', label: __('Course complete for the user', 'bit-integrations') }, - { key: '2', label: __('Lesson complete for the user', 'bit-integrations') }, - { key: '3', label: __('Quiz complete for the user', 'bit-integrations') }, - { key: '4', label: __('Reset user course', 'bit-integrations') }, - { key: '5', label: __('Reset user lesson', 'bit-integrations') } - ] - const [msLmsConf, setMsLmsConf] = useState({ name: 'MasterStudyLms', type: 'MasterStudyLms', mainAction: '', field_map: [{ formField: '', msLmsFormField: '' }], - allActions, actions: {} }) @@ -50,16 +74,26 @@ function MasterStudyLms({ formFields, setFlow, flow, allIntegURL, isInfo, edit } function isDisabled() { switch (msLmsConf.mainAction) { - case '1': + case MS_LMS_ACTIONS.COMPLETE_COURSE: return msLmsConf.courseId === undefined - case '4': + case MS_LMS_ACTIONS.RESET_COURSE: return msLmsConf.courseId === undefined - case '2': + case MS_LMS_ACTIONS.COMPLETE_LESSON: return msLmsConf.lessonId === undefined - case '5': + case MS_LMS_ACTIONS.RESET_LESSON: return msLmsConf.lessonId === undefined - case '3': + case MS_LMS_ACTIONS.COMPLETE_QUIZ: return msLmsConf.quizId === undefined + case MS_LMS_ACTIONS.ENROLL_USER: + case MS_LMS_ACTIONS.UNENROLL_USER: + case MS_LMS_ACTIONS.MARK_COURSE_COMPLETE: + return msLmsConf.courseId === undefined || !isUserEmailMapped(msLmsConf) + case MS_LMS_ACTIONS.MARK_LESSON_COMPLETE: + return ( + msLmsConf.courseId === undefined || + msLmsConf.lessonId === undefined || + !isUserEmailMapped(msLmsConf) + ) default: return false } diff --git a/frontend/src/components/AllIntegrations/MasterStudyLms/MasterStudyLmsCommonFunc.js b/frontend/src/components/AllIntegrations/MasterStudyLms/MasterStudyLmsCommonFunc.js index 60f418f1f..1d695f7bd 100644 --- a/frontend/src/components/AllIntegrations/MasterStudyLms/MasterStudyLmsCommonFunc.js +++ b/frontend/src/components/AllIntegrations/MasterStudyLms/MasterStudyLmsCommonFunc.js @@ -3,6 +3,39 @@ import bitsFetch from '../../../Utils/bitsFetch' import { deepCopy } from '../../../Utils/Helpers' import { sprintf, __ } from '../../../Utils/i18nwrap' +export const MS_LMS_ACTIONS = { + COMPLETE_COURSE: '1', + COMPLETE_LESSON: '2', + COMPLETE_QUIZ: '3', + RESET_COURSE: '4', + RESET_LESSON: '5', + ENROLL_USER: '6', + UNENROLL_USER: '7', + MARK_COURSE_COMPLETE: '8', + MARK_LESSON_COMPLETE: '9' +} + +// Field-map definition for the email-based (Pro) actions. +export const msLmsUserFields = [ + { key: 'user_email', label: __('User Email', 'bit-integrations'), required: true } +] + +export const generateMappedField = (fields = msLmsUserFields) => { + const requiredFlds = fields.filter(fld => fld.required === true) + return requiredFlds.length > 0 + ? requiredFlds.map(field => ({ formField: '', msLmsFormField: field.key })) + : [{ formField: '', msLmsFormField: '' }] +} + +export const isUserEmailMapped = conf => + Boolean( + conf?.field_map?.some( + f => + f.msLmsFormField === 'user_email' && + (f.formField === 'custom' ? f.customValue : f.formField) + ) + ) + export const handleInput = (e, msLmsConf, setMsLmsConf, setIsLoading, setSnackbar, formID) => { const newConf = { ...msLmsConf } const { name } = e.target diff --git a/frontend/src/components/AllIntegrations/MasterStudyLms/MasterStudyLmsFieldMap.jsx b/frontend/src/components/AllIntegrations/MasterStudyLms/MasterStudyLmsFieldMap.jsx new file mode 100644 index 000000000..6499b9e99 --- /dev/null +++ b/frontend/src/components/AllIntegrations/MasterStudyLms/MasterStudyLmsFieldMap.jsx @@ -0,0 +1,110 @@ +import { useRecoilValue } from 'recoil' +import { $appConfigState } from '../../../GlobalStates' +import { __, sprintf } from '../../../Utils/i18nwrap' +import { SmartTagField } from '../../../Utils/StaticData/SmartTagField' +import TagifyInput from '../../Utilities/TagifyInput' +import { + addFieldMap, + delFieldMap, + handleCustomValue, + handleFieldMapping +} from '../GlobalIntegrationHelper' + +export default function MasterStudyLmsFieldMap({ + i, + formFields, + field, + msLmsFields = [], + msLmsConf, + setMsLmsConf +}) { + const { isPro } = useRecoilValue($appConfigState) + + const requiredFlds = msLmsFields.filter(fld => fld.required === true) + const nonRequiredFlds = msLmsFields.filter(fld => fld.required === false) + + return ( +
+
+
+ + + {field.formField === 'custom' && ( + handleCustomValue(e, i, msLmsConf, setMsLmsConf)} + label={__('Custom Value', 'bit-integrations')} + className="mr-2" + type="text" + value={field.customValue} + placeholder={__('Custom Value', 'bit-integrations')} + formFields={formFields} + /> + )} + + +
+ {i >= requiredFlds.length && ( + <> + + + + )} +
+
+ ) +} diff --git a/frontend/src/components/AllIntegrations/MasterStudyLms/MasterStudyLmsIntegLayout.jsx b/frontend/src/components/AllIntegrations/MasterStudyLms/MasterStudyLmsIntegLayout.jsx index ff70f9a38..3f2efc548 100644 --- a/frontend/src/components/AllIntegrations/MasterStudyLms/MasterStudyLmsIntegLayout.jsx +++ b/frontend/src/components/AllIntegrations/MasterStudyLms/MasterStudyLmsIntegLayout.jsx @@ -1,10 +1,21 @@ import { useEffect } from 'react' import MultiSelect from 'react-multiple-select-dropdown-lite' +import { useRecoilValue } from 'recoil' +import { $appConfigState } from '../../../GlobalStates' import { __ } from '../../../Utils/i18nwrap' import Loader from '../../Loaders/Loader' -import { addFieldMap } from '../IntegrationHelpers/IntegrationHelpers' -import { fetchAllLesson, fetchAllMsLmsCourse, fetchAllQuiz } from './MasterStudyLmsCommonFunc' +import { checkIsPro, getProLabel } from '../../Utilities/ProUtilHelpers' +import { + fetchAllLesson, + fetchAllMsLmsCourse, + fetchAllQuiz, + generateMappedField, + MS_LMS_ACTIONS, + msLmsUserFields +} from './MasterStudyLmsCommonFunc' +import MasterStudyLmsFieldMap from './MasterStudyLmsFieldMap' import Note from '../../Utilities/Note' +import { allActions } from './MasterStudyLms' export default function MasterStudyLmsIntegLayout({ formFields, @@ -18,8 +29,37 @@ export default function MasterStudyLmsIntegLayout({ isInfo, edit }) { + const { isPro } = useRecoilValue($appConfigState) + + const { + COMPLETE_COURSE, + COMPLETE_LESSON, + COMPLETE_QUIZ, + RESET_COURSE, + RESET_LESSON, + ENROLL_USER, + UNENROLL_USER, + MARK_COURSE_COMPLETE, + MARK_LESSON_COMPLETE + } = MS_LMS_ACTIONS + + const courseActions = [ + COMPLETE_COURSE, + COMPLETE_LESSON, + COMPLETE_QUIZ, + RESET_COURSE, + RESET_LESSON, + ENROLL_USER, + UNENROLL_USER, + MARK_COURSE_COMPLETE, + MARK_LESSON_COMPLETE + ] + const lessonActions = [COMPLETE_LESSON, RESET_LESSON, MARK_LESSON_COMPLETE] + const emailActions = [ENROLL_USER, UNENROLL_USER, MARK_COURSE_COMPLETE, MARK_LESSON_COMPLETE] + const loggedInActions = [COMPLETE_COURSE, COMPLETE_LESSON, COMPLETE_QUIZ, RESET_COURSE, RESET_LESSON] + useEffect(() => { - if (['1', '2', '3', '4', '5'].includes(msLmsConf.mainAction)) { + if (courseActions.includes(msLmsConf.mainAction)) { fetchAllMsLmsCourse(msLmsConf, setMsLmsConf, setIsLoading, setSnackbar) } }, [msLmsConf.mainAction]) @@ -28,10 +68,10 @@ export default function MasterStudyLmsIntegLayout({ const newConf = { ...msLmsConf } if (val !== '') { newConf[status] = val - if (msLmsConf.mainAction === '2' || msLmsConf.mainAction === '5') { + if (lessonActions.includes(msLmsConf.mainAction)) { fetchAllLesson(newConf, setMsLmsConf, setIsLoading, setSnackbar) } - if (msLmsConf.mainAction === '3') { + if (msLmsConf.mainAction === COMPLETE_QUIZ) { fetchAllQuiz(newConf, setMsLmsConf, setIsLoading, setSnackbar) } } else { @@ -39,26 +79,35 @@ export default function MasterStudyLmsIntegLayout({ } setMsLmsConf({ ...newConf }) } + + const handleMainAction = val => { + const newConf = { ...msLmsConf, mainAction: val } + newConf.field_map = emailActions.includes(val) + ? generateMappedField(msLmsUserFields) + : [{ formField: '', msLmsFormField: '' }] + setMsLmsConf(newConf) + } return ( <>
- {__('Actions:', 'bit-integrations')} - +
+ {__('Actions:', 'bit-integrations')} + ({ + label: checkIsPro(isPro, action.is_pro) ? action.label : getProLabel(action.label), + value: action.key, + disabled: !checkIsPro(isPro, action.is_pro) + }))} + onChange={handleMainAction} + /> +


- {['1', '2', '3', '4', '5'].includes(msLmsConf.mainAction) && ( + {courseActions.includes(msLmsConf.mainAction) && (
{__('Select a Course:', 'bit-integrations')} changeHandler(val, 'courseId')} />
)} - {(msLmsConf.mainAction === '2' || msLmsConf.mainAction === '5') && msLmsConf?.courseId && ( + {lessonActions.includes(msLmsConf.mainAction) && msLmsConf?.courseId && (
{__('Select Lesson:', 'bit-integrations')}
)} - {msLmsConf.mainAction === '3' && msLmsConf?.courseId && ( + {msLmsConf.mainAction === COMPLETE_QUIZ && msLmsConf?.courseId && (
{__('Select Quiz:', 'bit-integrations')} )} + {emailActions.includes(msLmsConf.mainAction) && ( +
+ {__('Map User Email', 'bit-integrations')} +
+
+
+ {__('Form Fields', 'bit-integrations')} +
+
+ {__('MasterStudy LMS Fields', 'bit-integrations')} +
+
+ {msLmsConf?.field_map?.map((itm, idx) => ( + + ))} +
+ )} +

{isLoading && ( @@ -150,7 +225,17 @@ export default function MasterStudyLmsIntegLayout({ }} /> )} - + {loggedInActions.includes(msLmsConf.mainAction) && ( + + )} + {emailActions.includes(msLmsConf.mainAction) && ( + + )} ) } diff --git a/frontend/src/components/AllIntegrations/WishlistMember/WishlistMemberIntegLayout.jsx b/frontend/src/components/AllIntegrations/WishlistMember/WishlistMemberIntegLayout.jsx index 4d8fba4a5..7bf5df77e 100644 --- a/frontend/src/components/AllIntegrations/WishlistMember/WishlistMemberIntegLayout.jsx +++ b/frontend/src/components/AllIntegrations/WishlistMember/WishlistMemberIntegLayout.jsx @@ -39,6 +39,7 @@ export default function WishlistMemberIntegLayout({ } return ( + <>
From 9406c0aaaf7de107eb1c5a0fb352b97eee496ca5 Mon Sep 17 00:00:00 2001 From: Rishad Alam Date: Tue, 16 Jun 2026 17:44:54 +0600 Subject: [PATCH 2/5] fix: harden masterstudy actions per code review - guard non-array hook response and null course/lesson ids - share per-action validation (isActionConfigIncomplete) with edit screen - preserve mapped user email when switching between email actions - move allActions to common helper (break circular import) - drop dead handleInput prop from integ layout --- .../MasterStudyLms/RecordApiHelper.php | 18 +++--- .../MasterStudyLms/EditMasterStudyLms.jsx | 4 +- .../MasterStudyLms/MasterStudyLms.jsx | 61 +------------------ .../MasterStudyLmsCommonFunc.js | 50 +++++++++++++++ .../MasterStudyLmsIntegLayout.jsx | 13 ++-- 5 files changed, 71 insertions(+), 75 deletions(-) diff --git a/backend/Actions/MasterStudyLms/RecordApiHelper.php b/backend/Actions/MasterStudyLms/RecordApiHelper.php index 217858b80..c49cc970a 100644 --- a/backend/Actions/MasterStudyLms/RecordApiHelper.php +++ b/backend/Actions/MasterStudyLms/RecordApiHelper.php @@ -382,38 +382,38 @@ public function execute( return new WP_Error('REQ_FIELD_EMPTY', __('User email is required', 'bit-integrations')); } $response = Hooks::apply(Config::withPrefix('master_study_lms_enroll_user'), $defaultResponse, [ - 'course_id' => $integrationDetails->courseId, + 'course_id' => $integrationDetails->courseId ?? null, 'email' => $fieldData['user_email'], ]); - LogHandler::save($this->integrationID, wp_json_encode(['type' => 'enroll-user', 'type_name' => 'enroll-user-to-course']), !empty($response['success']) ? 'success' : 'error', $response['message'] ?? ''); + LogHandler::save($this->integrationID, wp_json_encode(['type' => 'enroll-user', 'type_name' => 'enroll-user-to-course']), (\is_array($response) && !empty($response['success'])) ? 'success' : 'error', \is_array($response) ? ($response['message'] ?? '') : ''); } elseif ((int) $mainAction === self::UNENROLL_USER) { if (empty($fieldData['user_email'])) { return new WP_Error('REQ_FIELD_EMPTY', __('User email is required', 'bit-integrations')); } $response = Hooks::apply(Config::withPrefix('master_study_lms_unenroll_user'), $defaultResponse, [ - 'course_id' => $integrationDetails->courseId, + 'course_id' => $integrationDetails->courseId ?? null, 'email' => $fieldData['user_email'], ]); - LogHandler::save($this->integrationID, wp_json_encode(['type' => 'unenroll-user', 'type_name' => 'unenroll-user-from-course']), !empty($response['success']) ? 'success' : 'error', $response['message'] ?? ''); + LogHandler::save($this->integrationID, wp_json_encode(['type' => 'unenroll-user', 'type_name' => 'unenroll-user-from-course']), (\is_array($response) && !empty($response['success'])) ? 'success' : 'error', \is_array($response) ? ($response['message'] ?? '') : ''); } elseif ((int) $mainAction === self::MARK_COURSE_COMPLETE) { if (empty($fieldData['user_email'])) { return new WP_Error('REQ_FIELD_EMPTY', __('User email is required', 'bit-integrations')); } $response = Hooks::apply(Config::withPrefix('master_study_lms_mark_course_complete'), $defaultResponse, [ - 'course_id' => $integrationDetails->courseId, + 'course_id' => $integrationDetails->courseId ?? null, 'email' => $fieldData['user_email'], ]); - LogHandler::save($this->integrationID, wp_json_encode(['type' => 'mark-course-complete', 'type_name' => 'mark-course-complete-for-user']), !empty($response['success']) ? 'success' : 'error', $response['message'] ?? ''); + LogHandler::save($this->integrationID, wp_json_encode(['type' => 'mark-course-complete', 'type_name' => 'mark-course-complete-for-user']), (\is_array($response) && !empty($response['success'])) ? 'success' : 'error', \is_array($response) ? ($response['message'] ?? '') : ''); } elseif ((int) $mainAction === self::MARK_LESSON_COMPLETE) { if (empty($fieldData['user_email'])) { return new WP_Error('REQ_FIELD_EMPTY', __('User email is required', 'bit-integrations')); } $response = Hooks::apply(Config::withPrefix('master_study_lms_mark_lesson_complete'), $defaultResponse, [ - 'course_id' => $integrationDetails->courseId, - 'lesson_id' => $integrationDetails->lessonId, + 'course_id' => $integrationDetails->courseId ?? null, + 'lesson_id' => $integrationDetails->lessonId ?? null, 'email' => $fieldData['user_email'], ]); - LogHandler::save($this->integrationID, wp_json_encode(['type' => 'mark-lesson-complete', 'type_name' => 'mark-lesson-complete-for-user']), !empty($response['success']) ? 'success' : 'error', $response['message'] ?? ''); + LogHandler::save($this->integrationID, wp_json_encode(['type' => 'mark-lesson-complete', 'type_name' => 'mark-lesson-complete-for-user']), (\is_array($response) && !empty($response['success'])) ? 'success' : 'error', \is_array($response) ? ($response['message'] ?? '') : ''); } return $response; diff --git a/frontend/src/components/AllIntegrations/MasterStudyLms/EditMasterStudyLms.jsx b/frontend/src/components/AllIntegrations/MasterStudyLms/EditMasterStudyLms.jsx index 0e2972bfe..2c1295b52 100644 --- a/frontend/src/components/AllIntegrations/MasterStudyLms/EditMasterStudyLms.jsx +++ b/frontend/src/components/AllIntegrations/MasterStudyLms/EditMasterStudyLms.jsx @@ -10,7 +10,7 @@ import SetEditIntegComponents from '../IntegrationHelpers/SetEditIntegComponents import EditWebhookInteg from '../EditWebhookInteg' import { saveActionConf } from '../IntegrationHelpers/IntegrationHelpers' import IntegrationStepThree from '../IntegrationHelpers/IntegrationStepThree' -import { handleInput } from './MasterStudyLmsCommonFunc' +import { handleInput, isActionConfigIncomplete } from './MasterStudyLmsCommonFunc' import MasterStudyLmsIntegLayout from './MasterStudyLmsIntegLayout' function EditMasterStudyLms({ allIntegURL }) { @@ -66,7 +66,7 @@ function EditMasterStudyLms({ allIntegURL }) { setSnackbar }) } - disabled={msLmsConf.mainAction === '' || isLoading} + disabled={msLmsConf.mainAction === '' || isLoading || isActionConfigIncomplete(msLmsConf)} isLoading={isLoading} dataConf={msLmsConf} setDataConf={setMsLmsConf} diff --git a/frontend/src/components/AllIntegrations/MasterStudyLms/MasterStudyLms.jsx b/frontend/src/components/AllIntegrations/MasterStudyLms/MasterStudyLms.jsx index d1e3cbd31..5fa5f3af8 100644 --- a/frontend/src/components/AllIntegrations/MasterStudyLms/MasterStudyLms.jsx +++ b/frontend/src/components/AllIntegrations/MasterStudyLms/MasterStudyLms.jsx @@ -8,44 +8,11 @@ import SnackMsg from '../../Utilities/SnackMsg' import Steps from '../../Utilities/Steps' import { saveActionConf } from '../IntegrationHelpers/IntegrationHelpers' import IntegrationStepThree from '../IntegrationHelpers/IntegrationStepThree' -import { - handleInput, - checkMappedFields, - isUserEmailMapped, - MS_LMS_ACTIONS -} from './MasterStudyLmsCommonFunc' +import { isActionConfigIncomplete } from './MasterStudyLmsCommonFunc' import MasterStudyLmsAuthorization from './MasterStudyLmsAuthorization' import MasterStudyLmsIntegLayout from './MasterStudyLmsIntegLayout' import TutorialLink from '../../Utilities/TutorialLink' -export const allActions = [ - { key: MS_LMS_ACTIONS.COMPLETE_COURSE, label: __('Course complete for the user', 'bit-integrations') }, - { key: MS_LMS_ACTIONS.COMPLETE_LESSON, label: __('Lesson complete for the user', 'bit-integrations') }, - { key: MS_LMS_ACTIONS.COMPLETE_QUIZ, label: __('Quiz complete for the user', 'bit-integrations') }, - { key: MS_LMS_ACTIONS.RESET_COURSE, label: __('Reset user course', 'bit-integrations') }, - { key: MS_LMS_ACTIONS.RESET_LESSON, label: __('Reset user lesson', 'bit-integrations') }, - { - key: MS_LMS_ACTIONS.ENROLL_USER, - label: __('Enroll user in a course', 'bit-integrations'), - is_pro: true - }, - { - key: MS_LMS_ACTIONS.UNENROLL_USER, - label: __('Unenroll user from a course', 'bit-integrations'), - is_pro: true - }, - { - key: MS_LMS_ACTIONS.MARK_COURSE_COMPLETE, - label: __('Mark a course complete for the user', 'bit-integrations'), - is_pro: true - }, - { - key: MS_LMS_ACTIONS.MARK_LESSON_COMPLETE, - label: __('Mark a lesson complete for the user', 'bit-integrations'), - is_pro: true - } -] - function MasterStudyLms({ formFields, setFlow, flow, allIntegURL, isInfo, edit }) { const navigate = useNavigate() const { formID } = useParams() @@ -73,30 +40,7 @@ function MasterStudyLms({ formFields, setFlow, flow, allIntegURL, isInfo, edit } } function isDisabled() { - switch (msLmsConf.mainAction) { - case MS_LMS_ACTIONS.COMPLETE_COURSE: - return msLmsConf.courseId === undefined - case MS_LMS_ACTIONS.RESET_COURSE: - return msLmsConf.courseId === undefined - case MS_LMS_ACTIONS.COMPLETE_LESSON: - return msLmsConf.lessonId === undefined - case MS_LMS_ACTIONS.RESET_LESSON: - return msLmsConf.lessonId === undefined - case MS_LMS_ACTIONS.COMPLETE_QUIZ: - return msLmsConf.quizId === undefined - case MS_LMS_ACTIONS.ENROLL_USER: - case MS_LMS_ACTIONS.UNENROLL_USER: - case MS_LMS_ACTIONS.MARK_COURSE_COMPLETE: - return msLmsConf.courseId === undefined || !isUserEmailMapped(msLmsConf) - case MS_LMS_ACTIONS.MARK_LESSON_COMPLETE: - return ( - msLmsConf.courseId === undefined || - msLmsConf.lessonId === undefined || - !isUserEmailMapped(msLmsConf) - ) - default: - return false - } + return isActionConfigIncomplete(msLmsConf) } return ( @@ -124,7 +68,6 @@ function MasterStudyLms({ formFields, setFlow, flow, allIntegURL, isInfo, edit } style={{ ...(step === 2 && { width: 900, height: 'auto', overflow: 'visible' }) }}> handleInput(e, msLmsConf, setMsLmsConf, setIsLoading, setSnackbar, formID)} msLmsConf={msLmsConf} setMsLmsConf={setMsLmsConf} isLoading={isLoading} diff --git a/frontend/src/components/AllIntegrations/MasterStudyLms/MasterStudyLmsCommonFunc.js b/frontend/src/components/AllIntegrations/MasterStudyLms/MasterStudyLmsCommonFunc.js index 1d695f7bd..6021bbd66 100644 --- a/frontend/src/components/AllIntegrations/MasterStudyLms/MasterStudyLmsCommonFunc.js +++ b/frontend/src/components/AllIntegrations/MasterStudyLms/MasterStudyLmsCommonFunc.js @@ -36,6 +36,56 @@ export const isUserEmailMapped = conf => ) ) +export const allActions = [ + { key: MS_LMS_ACTIONS.COMPLETE_COURSE, label: __('Course complete for the user', 'bit-integrations') }, + { key: MS_LMS_ACTIONS.COMPLETE_LESSON, label: __('Lesson complete for the user', 'bit-integrations') }, + { key: MS_LMS_ACTIONS.COMPLETE_QUIZ, label: __('Quiz complete for the user', 'bit-integrations') }, + { key: MS_LMS_ACTIONS.RESET_COURSE, label: __('Reset user course', 'bit-integrations') }, + { key: MS_LMS_ACTIONS.RESET_LESSON, label: __('Reset user lesson', 'bit-integrations') }, + { + key: MS_LMS_ACTIONS.ENROLL_USER, + label: __('Enroll user in a course', 'bit-integrations'), + is_pro: true + }, + { + key: MS_LMS_ACTIONS.UNENROLL_USER, + label: __('Unenroll user from a course', 'bit-integrations'), + is_pro: true + }, + { + key: MS_LMS_ACTIONS.MARK_COURSE_COMPLETE, + label: __('Mark a course complete for the user', 'bit-integrations'), + is_pro: true + }, + { + key: MS_LMS_ACTIONS.MARK_LESSON_COMPLETE, + label: __('Mark a lesson complete for the user', 'bit-integrations'), + is_pro: true + } +] + +// Per-action required-field validation shared by the New wizard and Edit screen. +export const isActionConfigIncomplete = conf => { + switch (conf?.mainAction) { + case MS_LMS_ACTIONS.COMPLETE_COURSE: + case MS_LMS_ACTIONS.RESET_COURSE: + return conf.courseId === undefined + case MS_LMS_ACTIONS.COMPLETE_LESSON: + case MS_LMS_ACTIONS.RESET_LESSON: + return conf.lessonId === undefined + case MS_LMS_ACTIONS.COMPLETE_QUIZ: + return conf.quizId === undefined + case MS_LMS_ACTIONS.ENROLL_USER: + case MS_LMS_ACTIONS.UNENROLL_USER: + case MS_LMS_ACTIONS.MARK_COURSE_COMPLETE: + return conf.courseId === undefined || !isUserEmailMapped(conf) + case MS_LMS_ACTIONS.MARK_LESSON_COMPLETE: + return conf.courseId === undefined || conf.lessonId === undefined || !isUserEmailMapped(conf) + default: + return false + } +} + export const handleInput = (e, msLmsConf, setMsLmsConf, setIsLoading, setSnackbar, formID) => { const newConf = { ...msLmsConf } const { name } = e.target diff --git a/frontend/src/components/AllIntegrations/MasterStudyLms/MasterStudyLmsIntegLayout.jsx b/frontend/src/components/AllIntegrations/MasterStudyLms/MasterStudyLmsIntegLayout.jsx index 3f2efc548..c9b583452 100644 --- a/frontend/src/components/AllIntegrations/MasterStudyLms/MasterStudyLmsIntegLayout.jsx +++ b/frontend/src/components/AllIntegrations/MasterStudyLms/MasterStudyLmsIntegLayout.jsx @@ -6,6 +6,7 @@ import { __ } from '../../../Utils/i18nwrap' import Loader from '../../Loaders/Loader' import { checkIsPro, getProLabel } from '../../Utilities/ProUtilHelpers' import { + allActions, fetchAllLesson, fetchAllMsLmsCourse, fetchAllQuiz, @@ -15,11 +16,9 @@ import { } from './MasterStudyLmsCommonFunc' import MasterStudyLmsFieldMap from './MasterStudyLmsFieldMap' import Note from '../../Utilities/Note' -import { allActions } from './MasterStudyLms' export default function MasterStudyLmsIntegLayout({ formFields, - handleInput, msLmsConf, setMsLmsConf, isLoading, @@ -82,9 +81,13 @@ export default function MasterStudyLmsIntegLayout({ const handleMainAction = val => { const newConf = { ...msLmsConf, mainAction: val } - newConf.field_map = emailActions.includes(val) - ? generateMappedField(msLmsUserFields) - : [{ formField: '', msLmsFormField: '' }] + const hasEmailRow = newConf.field_map?.some(f => f.msLmsFormField === 'user_email') + if (emailActions.includes(val)) { + // Keep an existing user_email mapping when switching between email actions. + if (!hasEmailRow) newConf.field_map = generateMappedField(msLmsUserFields) + } else { + newConf.field_map = [{ formField: '', msLmsFormField: '' }] + } setMsLmsConf(newConf) } return ( From 829381ba6af1c19c80d04a93a1bcda102e86644c Mon Sep 17 00:00:00 2001 From: Rishad Alam Date: Tue, 16 Jun 2026 17:56:51 +0600 Subject: [PATCH 3/5] fix: treat null/empty config values as incomplete in masterstudy validation --- .../MasterStudyLms/MasterStudyLmsCommonFunc.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/frontend/src/components/AllIntegrations/MasterStudyLms/MasterStudyLmsCommonFunc.js b/frontend/src/components/AllIntegrations/MasterStudyLms/MasterStudyLmsCommonFunc.js index 6021bbd66..664f266fe 100644 --- a/frontend/src/components/AllIntegrations/MasterStudyLms/MasterStudyLmsCommonFunc.js +++ b/frontend/src/components/AllIntegrations/MasterStudyLms/MasterStudyLmsCommonFunc.js @@ -69,18 +69,18 @@ export const isActionConfigIncomplete = conf => { switch (conf?.mainAction) { case MS_LMS_ACTIONS.COMPLETE_COURSE: case MS_LMS_ACTIONS.RESET_COURSE: - return conf.courseId === undefined + return !conf.courseId case MS_LMS_ACTIONS.COMPLETE_LESSON: case MS_LMS_ACTIONS.RESET_LESSON: - return conf.lessonId === undefined + return !conf.lessonId case MS_LMS_ACTIONS.COMPLETE_QUIZ: - return conf.quizId === undefined + return !conf.quizId case MS_LMS_ACTIONS.ENROLL_USER: case MS_LMS_ACTIONS.UNENROLL_USER: case MS_LMS_ACTIONS.MARK_COURSE_COMPLETE: - return conf.courseId === undefined || !isUserEmailMapped(conf) + return !conf.courseId || !isUserEmailMapped(conf) case MS_LMS_ACTIONS.MARK_LESSON_COMPLETE: - return conf.courseId === undefined || conf.lessonId === undefined || !isUserEmailMapped(conf) + return !conf.courseId || !conf.lessonId || !isUserEmailMapped(conf) default: return false } From 69e2742aa4c612b1e2e4ed5c5af65fd25f7c2d19 Mon Sep 17 00:00:00 2001 From: Rishad Alam Date: Wed, 17 Jun 2026 14:01:28 +0600 Subject: [PATCH 4/5] fix: masterstudy lms fetch course and quiz --- .../MasterStudyLms/MasterStudyLmsHelper.php | 64 +++++-------------- 1 file changed, 16 insertions(+), 48 deletions(-) diff --git a/backend/Actions/MasterStudyLms/MasterStudyLmsHelper.php b/backend/Actions/MasterStudyLms/MasterStudyLmsHelper.php index fb6e9c0cf..d603981ef 100644 --- a/backend/Actions/MasterStudyLms/MasterStudyLmsHelper.php +++ b/backend/Actions/MasterStudyLms/MasterStudyLmsHelper.php @@ -6,59 +6,27 @@ class MasterStudyLmsHelper { public static function getLessonByCourse($courseId) { - global $wpdb; + $args = [ + 'post_type' => 'stm-lessons', + 'posts_per_page' => 999, + 'orderby' => 'title', + 'order' => 'ASC', + 'post_status' => 'publish', + ]; - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Direct query needed for MasterStudy lessons - $lesson = $wpdb->get_results( - $wpdb->prepare( - "SELECT ID, post_title,post_content - FROM {$wpdb->posts} - WHERE FIND_IN_SET( - ID, - (SELECT meta_value FROM wp_postmeta WHERE post_id = %d AND meta_key = 'curriculum') - ) - AND post_type = 'stm-lessons' - ORDER BY post_title ASC - ", - absint($courseId) - ) - ); - - return $lesson; - - // if ($courseId == 'any') { - // $lesson = $wpdb->get_results( - // $wpdb->prepare( - // "SELECT ID, post_title,post_content - // FROM $wpdb->posts - // WHERE post_type = 'stm-lesson' - // ORDER BY post_title ASC - // " - // ) - // ); - // return $quizzes; - // } + return get_posts($args); } public static function getQuizByCourse($courseId) { - global $wpdb; - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Direct query needed for MasterStudy quizzes - $quizzes = $wpdb->get_results( - $wpdb->prepare( - "SELECT ID, post_title,post_content - FROM {$wpdb->posts} - WHERE FIND_IN_SET( - ID, - (SELECT meta_value FROM wp_postmeta WHERE post_id = %d AND meta_key = 'curriculum') - ) - AND post_type = 'stm-quizzes' - ORDER BY post_title ASC - ", - absint($courseId) - ) - ); + $args = [ + 'post_type' => 'stm-quizzes', + 'posts_per_page' => 999, + 'orderby' => 'title', + 'order' => 'ASC', + 'post_status' => 'publish', + ]; - return $quizzes; + return get_posts($args); } } From f358e20a1dd5fb8d13fd7fdfda79d578eac995ea Mon Sep 17 00:00:00 2001 From: Rishad Alam Date: Sat, 27 Jun 2026 12:03:00 +0600 Subject: [PATCH 5/5] fix: fetch masterstudy lessons/quizzes from course curriculum Replace broad get_posts queries returning all published stm-lessons/ stm-quizzes with CurriculumRepository::get_curriculum lookups so only materials actually attached to the selected course are returned. --- .../MasterStudyLms/MasterStudyLmsHelper.php | 46 +++++++++++-------- 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/backend/Actions/MasterStudyLms/MasterStudyLmsHelper.php b/backend/Actions/MasterStudyLms/MasterStudyLmsHelper.php index d603981ef..28cc69523 100644 --- a/backend/Actions/MasterStudyLms/MasterStudyLmsHelper.php +++ b/backend/Actions/MasterStudyLms/MasterStudyLmsHelper.php @@ -6,27 +6,37 @@ class MasterStudyLmsHelper { public static function getLessonByCourse($courseId) { - $args = [ - 'post_type' => 'stm-lessons', - 'posts_per_page' => 999, - 'orderby' => 'title', - 'order' => 'ASC', - 'post_status' => 'publish', - ]; - - return get_posts($args); + return self::getCourseMaterialsByIdAndType($courseId, 'stm-lessons'); } public static function getQuizByCourse($courseId) { - $args = [ - 'post_type' => 'stm-quizzes', - 'posts_per_page' => 999, - 'orderby' => 'title', - 'order' => 'ASC', - 'post_status' => 'publish', - ]; - - return get_posts($args); + return self::getCourseMaterialsByIdAndType($courseId, 'stm-quizzes'); + } + + public static function getCourseMaterialsByIdAndType($courseId, $postType) + { + if (!class_exists('\MasterStudy\Lms\Repositories\CurriculumRepository')) { + return []; + } + + $CurriculumRepository = new \MasterStudy\Lms\Repositories\CurriculumRepository(); + + $curriculum = $CurriculumRepository->get_curriculum(absint($courseId)); + + if (empty($curriculum) || !isset($curriculum['materials'])) { + return []; + } + + foreach ($curriculum['materials'] as $material) { + if ($material['post_type'] === $postType) { + $posts[] = [ + 'ID' => $material['post_id'], + 'post_title' => $material['title'], + ]; + } + } + + return $posts ?? []; } }