From 4b6a7349c0966ae9aef3371d46293627c8147bcd Mon Sep 17 00:00:00 2001 From: Rishad Alam Date: Tue, 30 Jun 2026 15:46:47 +0600 Subject: [PATCH 1/6] feat(sensei-lms): add Sensei LMS action integration Add the Sensei LMS action integration (Free side): controller with plugin authorize check and course/lesson/quiz refresh endpoints, RecordApiHelper that dispatches each of the 12 write actions to the Pro plugin via bit_integrations_sensei_lms_* hooks, routes, and the React UI (authorization, action select, course/lesson/quiz dropdowns, field map, wizard + edit). Register the integration in NewInteg, EditInteg, IntegInfo and SelectAction. --- backend/Actions/SenseiLMS/RecordApiHelper.php | 96 ++++++++++ backend/Actions/SenseiLMS/Routes.php | 13 ++ .../Actions/SenseiLMS/SenseiLMSController.php | 93 ++++++++++ .../components/AllIntegrations/EditInteg.jsx | 3 + .../components/AllIntegrations/IntegInfo.jsx | 3 + .../components/AllIntegrations/NewInteg.jsx | 10 ++ .../SenseiLMS/EditSenseiLMS.jsx | 76 ++++++++ .../AllIntegrations/SenseiLMS/SenseiLMS.jsx | 105 +++++++++++ .../SenseiLMS/SenseiLMSAuthorization.jsx | 112 ++++++++++++ .../SenseiLMS/SenseiLMSCommonFunc.js | 68 +++++++ .../SenseiLMS/SenseiLMSFieldMap.jsx | 104 +++++++++++ .../SenseiLMS/SenseiLMSIntegLayout.jsx | 166 ++++++++++++++++++ .../AllIntegrations/SenseiLMS/staticData.js | 71 ++++++++ .../src/components/Flow/New/SelectAction.jsx | 1 + 14 files changed, 921 insertions(+) create mode 100644 backend/Actions/SenseiLMS/RecordApiHelper.php create mode 100644 backend/Actions/SenseiLMS/Routes.php create mode 100644 backend/Actions/SenseiLMS/SenseiLMSController.php create mode 100644 frontend/src/components/AllIntegrations/SenseiLMS/EditSenseiLMS.jsx create mode 100644 frontend/src/components/AllIntegrations/SenseiLMS/SenseiLMS.jsx create mode 100644 frontend/src/components/AllIntegrations/SenseiLMS/SenseiLMSAuthorization.jsx create mode 100644 frontend/src/components/AllIntegrations/SenseiLMS/SenseiLMSCommonFunc.js create mode 100644 frontend/src/components/AllIntegrations/SenseiLMS/SenseiLMSFieldMap.jsx create mode 100644 frontend/src/components/AllIntegrations/SenseiLMS/SenseiLMSIntegLayout.jsx create mode 100644 frontend/src/components/AllIntegrations/SenseiLMS/staticData.js diff --git a/backend/Actions/SenseiLMS/RecordApiHelper.php b/backend/Actions/SenseiLMS/RecordApiHelper.php new file mode 100644 index 000000000..924cc91e7 --- /dev/null +++ b/backend/Actions/SenseiLMS/RecordApiHelper.php @@ -0,0 +1,96 @@ +_integrationDetails = $integrationDetails; + $this->_integrationID = $integId; + } + + public function execute($fieldValues, $fieldMap, $utilities) + { + if (!class_exists('Sensei_Main')) { + return [ + 'success' => false, + 'message' => __('Sensei LMS is not installed or activated', 'bit-integrations') + ]; + } + + $fieldData = static::generateReqDataFromFieldMap($fieldMap, $fieldValues); + + $mainAction = $this->_integrationDetails->mainAction ?? 'enroll_user_in_course'; + + $defaultResponse = [ + 'success' => false, + // translators: %s: Plugin name + 'message' => wp_sprintf(__('%s plugin is not installed or activate', 'bit-integrations'), 'Bit Integrations Pro') + ]; + + $actionMap = [ + 'enroll_user_in_course' => 'sensei_lms_enroll_user_in_course', + 'withdraw_user_from_course'=> 'sensei_lms_withdraw_user_from_course', + 'start_course_for_user' => 'sensei_lms_start_course_for_user', + 'complete_course_for_user' => 'sensei_lms_complete_course_for_user', + 'reset_course_for_user' => 'sensei_lms_reset_course_for_user', + 'start_lesson_for_user' => 'sensei_lms_start_lesson_for_user', + 'update_lesson_status' => 'sensei_lms_update_lesson_status', + 'reset_lesson_for_user' => 'sensei_lms_reset_lesson_for_user', + 'grade_quiz' => 'sensei_lms_grade_quiz', + 'create_course' => 'sensei_lms_create_course', + 'create_lesson' => 'sensei_lms_create_lesson', + 'create_certificate' => 'sensei_lms_create_certificate', + ]; + + if (!isset($actionMap[$mainAction])) { + $response = ['success' => false, 'message' => __('Invalid action', 'bit-integrations')]; + LogHandler::save($this->_integrationID, ['type' => 'SenseiLMS', 'type_name' => 'unknown'], 'error', $response); + + return $response; + } + + $response = Hooks::apply(Config::withPrefix($actionMap[$mainAction]), $defaultResponse, $fieldData, $utilities, $this->_integrationDetails); + + $responseType = isset($response['success']) && $response['success'] ? 'success' : 'error'; + LogHandler::save($this->_integrationID, ['type' => 'SenseiLMS', 'type_name' => $mainAction], $responseType, $response); + + return $response; + } + + private function generateReqDataFromFieldMap($fieldMap, $fieldValues) + { + $dataFinal = []; + foreach ($fieldMap as $item) { + if (empty($item->senseiLMSField)) { + continue; + } + + $triggerValue = $item->formField; + $actionValue = $item->senseiLMSField; + + $dataFinal[$actionValue] = $triggerValue === 'custom' && isset($item->customValue) + ? Common::replaceFieldWithValue($item->customValue, $fieldValues) + : $fieldValues[$triggerValue] ?? ''; + } + + return $dataFinal; + } +} diff --git a/backend/Actions/SenseiLMS/Routes.php b/backend/Actions/SenseiLMS/Routes.php new file mode 100644 index 000000000..daefc34a2 --- /dev/null +++ b/backend/Actions/SenseiLMS/Routes.php @@ -0,0 +1,13 @@ +flow_details; + $integId = $integrationData->id; + $fieldMap = $integrationDetails->field_map; + $utilities = isset($integrationDetails->utilities) ? $integrationDetails->utilities : []; + + if (empty($fieldMap)) { + return new WP_Error('field_map_empty', __('Field map is empty', 'bit-integrations')); + } + + $recordApiHelper = new RecordApiHelper($integrationDetails, $integId); + + return $recordApiHelper->execute($fieldValues, $fieldMap, $utilities); + } + + private static function postOptions($postType) + { + $options = []; + + $posts = get_posts( + [ + 'post_type' => $postType, + 'post_status' => 'publish', + 'numberposts' => -1, + ] + ); + + foreach ($posts as $post) { + $options[] = (object) [ + 'value' => $post->ID, + 'label' => $post->post_title, + ]; + } + + return $options; + } +} diff --git a/frontend/src/components/AllIntegrations/EditInteg.jsx b/frontend/src/components/AllIntegrations/EditInteg.jsx index e498edfe5..4d7d637db 100644 --- a/frontend/src/components/AllIntegrations/EditInteg.jsx +++ b/frontend/src/components/AllIntegrations/EditInteg.jsx @@ -180,6 +180,7 @@ const EditCreatorLms = lazy(() => import('./CreatorLms/EditCreatorLms')) const EditUltimateAffiliatePro = lazy(() => import('./UltimateAffiliatePro/EditUltimateAffiliatePro')) const EditBookly = lazy(() => import('./Bookly/EditBookly')) const EditFluentCart = lazy(() => import('./FluentCart/EditFluentCart')) +const EditSenseiLMS = lazy(() => import('./SenseiLMS/EditSenseiLMS')) const EditWsms = lazy(() => import('./Wsms/EditWsms')) const EditWebbaBooking = lazy(() => import('./WebbaBooking/EditWebbaBooking')) const EditMoreConvertWishlist = lazy(() => import('./MoreConvertWishlist/EditMoreConvertWishlist')) @@ -629,6 +630,8 @@ const IntegType = memo(({ allIntegURL, flow }) => { return case 'FluentCart': return + case 'SenseiLMS': + return case 'Wsms': return case 'WebbaBooking': diff --git a/frontend/src/components/AllIntegrations/IntegInfo.jsx b/frontend/src/components/AllIntegrations/IntegInfo.jsx index f6e27f286..f0e748570 100644 --- a/frontend/src/components/AllIntegrations/IntegInfo.jsx +++ b/frontend/src/components/AllIntegrations/IntegInfo.jsx @@ -182,6 +182,7 @@ const UltimateAffiliateProAuthorization = lazy( ) const BooklyAuthorization = lazy(() => import('./Bookly/BooklyAuthorization')) const FluentCartAuthorization = lazy(() => import('./FluentCart/FluentCartAuthorization')) +const SenseiLMSAuthorization = lazy(() => import('./SenseiLMS/SenseiLMSAuthorization')) const WsmsAuthorization = lazy(() => import('./Wsms/WsmsAuthorization')) const MoreConvertWishlistAuthorization = lazy( () => import('./MoreConvertWishlist/MoreConvertWishlistAuthorization') @@ -628,6 +629,8 @@ const IntegrationInfo = memo(({ integrationConf, location }) => { return case 'FluentCart': return + case 'SenseiLMS': + return case 'Wsms': return case 'WebbaBooking': diff --git a/frontend/src/components/AllIntegrations/NewInteg.jsx b/frontend/src/components/AllIntegrations/NewInteg.jsx index 11840feeb..22de089d4 100644 --- a/frontend/src/components/AllIntegrations/NewInteg.jsx +++ b/frontend/src/components/AllIntegrations/NewInteg.jsx @@ -179,6 +179,7 @@ const CreatorLms = lazy(() => import('./CreatorLms/CreatorLms')) const UltimateAffiliatePro = lazy(() => import('./UltimateAffiliatePro/UltimateAffiliatePro')) const Bookly = lazy(() => import('./Bookly/Bookly')) const FluentCart = lazy(() => import('./FluentCart/FluentCart')) +const SenseiLMS = lazy(() => import('./SenseiLMS/SenseiLMS')) const Wsms = lazy(() => import('./Wsms/Wsms')) const WebbaBooking = lazy(() => import('./WebbaBooking/WebbaBooking')) const MoreConvertWishlist = lazy(() => import('./MoreConvertWishlist/MoreConvertWishlist')) @@ -1748,6 +1749,15 @@ const NewIntegs = memo(({ integUrlName, allIntegURL, flow, setFlow }) => { setFlow={setFlow} /> ) + case 'SenseiLMS': + return ( + + ) case 'Wsms': return ( + + +
+ {__('Integration Name:', 'bit-integrations')} + handleInput(e, senseiLMSConf, setSenseiLMSConf)} + name="name" + value={senseiLMSConf.name} + type="text" + placeholder={__('Integration Name...', 'bit-integrations')} + /> +
+
+ + + + + + + saveActionConf({ + flow, + setFlow, + allIntegURL, + conf: senseiLMSConf, + navigate, + id, + edit: 1, + setIsLoading, + setSnackbar + }) + } + disabled={!checkMappedFields(senseiLMSConf)} + isLoading={isLoading} + dataConf={senseiLMSConf} + setDataConf={setSenseiLMSConf} + formFields={formFields} + /> +
+ + ) +} diff --git a/frontend/src/components/AllIntegrations/SenseiLMS/SenseiLMS.jsx b/frontend/src/components/AllIntegrations/SenseiLMS/SenseiLMS.jsx new file mode 100644 index 000000000..c25b778c8 --- /dev/null +++ b/frontend/src/components/AllIntegrations/SenseiLMS/SenseiLMS.jsx @@ -0,0 +1,105 @@ +import { useState } from 'react' +import 'react-multiple-select-dropdown-lite/dist/index.css' +import { useNavigate, useParams } from 'react-router' +import BackIcn from '../../../Icons/BackIcn' +import { __ } from '../../../Utils/i18nwrap' +import SnackMsg from '../../Utilities/SnackMsg' +import { saveIntegConfig } from '../IntegrationHelpers/IntegrationHelpers' +import IntegrationStepThree from '../IntegrationHelpers/IntegrationStepThree' +import SenseiLMSAuthorization from './SenseiLMSAuthorization' +import { checkMappedFields } from './SenseiLMSCommonFunc' +import SenseiLMSIntegLayout from './SenseiLMSIntegLayout' + +export default function SenseiLMS({ formFields, setFlow, flow, allIntegURL }) { + const navigate = useNavigate() + const { formID } = useParams() + const [isLoading, setIsLoading] = useState(false) + const [step, setStep] = useState(1) + const [snack, setSnackbar] = useState({ show: false }) + const [senseiLMSConf, setSenseiLMSConf] = useState({ + name: 'Sensei LMS', + type: 'SenseiLMS', + field_map: [{ formField: '', senseiLMSField: '' }], + actions: {}, + mainAction: '' + }) + + const nextPage = val => { + setTimeout(() => { + document.getElementById('btcd-settings-wrp').scrollTop = 0 + }, 300) + + if (val === 3) { + if (!checkMappedFields(senseiLMSConf)) { + setSnackbar({ + show: true, + msg: __('Please map all required fields to continue.', 'bit-integrations') + }) + return + } + + if (senseiLMSConf.name !== '' && senseiLMSConf.field_map.length > 0) { + setStep(val) + } + } else { + setStep(val) + } + } + + return ( +
+ +
+ + + +
+ +
+
+
+ +
+ + + saveIntegConfig(flow, setFlow, allIntegURL, senseiLMSConf, navigate, '', '', setIsLoading) + } + isLoading={isLoading} + dataConf={senseiLMSConf} + setDataConf={setSenseiLMSConf} + formFields={formFields} + /> +
+ ) +} diff --git a/frontend/src/components/AllIntegrations/SenseiLMS/SenseiLMSAuthorization.jsx b/frontend/src/components/AllIntegrations/SenseiLMS/SenseiLMSAuthorization.jsx new file mode 100644 index 000000000..81be9a685 --- /dev/null +++ b/frontend/src/components/AllIntegrations/SenseiLMS/SenseiLMSAuthorization.jsx @@ -0,0 +1,112 @@ +import { useState } from 'react' +import BackIcn from '../../../Icons/BackIcn' +import bitsFetch from '../../../Utils/bitsFetch' +import { __ } from '../../../Utils/i18nwrap' +import LoaderSm from '../../Loaders/LoaderSm' +import TutorialLink from '../../Utilities/TutorialLink' + +export default function SenseiLMSAuthorization({ + senseiLMSConf, + setSenseiLMSConf, + step, + nextPage, + isLoading, + setIsLoading, + setSnackbar +}) { + const [isAuthorized, setIsAuthorized] = useState(false) + const [showAuthMsg, setShowAuthMsg] = useState(false) + + const authorizeHandler = () => { + setIsLoading('auth') + bitsFetch({}, 'sensei_lms_authorize').then(result => { + if (result?.success) { + setIsAuthorized(true) + setSnackbar({ + show: true, + msg: __('Connected with Sensei LMS Successfully', 'bit-integrations') + }) + } + setIsLoading(false) + setShowAuthMsg(true) + }) + } + + const handleInput = e => { + const newConf = { ...senseiLMSConf } + newConf[e.target.name] = e.target.value + setSenseiLMSConf(newConf) + } + + return ( +
+ + +
+ {__('Integration Name:', 'bit-integrations')} +
+ + + {isLoading === 'auth' && ( +
+ + {__('Checking if Sensei LMS is authorized!!!', 'bit-integrations')} +
+ )} + + {showAuthMsg && !isAuthorized && !isLoading && ( +
+
+
+ +
+
+ {__('Sensei LMS is not activated or not installed', 'bit-integrations')} +
+
+
+ )} + + {showAuthMsg && isAuthorized && !isLoading && ( +
+
+ +
+
{__('Sensei LMS is activated', 'bit-integrations')}
+
+ )} + + +
+ +
+ ) +} diff --git a/frontend/src/components/AllIntegrations/SenseiLMS/SenseiLMSCommonFunc.js b/frontend/src/components/AllIntegrations/SenseiLMS/SenseiLMSCommonFunc.js new file mode 100644 index 000000000..90400caa4 --- /dev/null +++ b/frontend/src/components/AllIntegrations/SenseiLMS/SenseiLMSCommonFunc.js @@ -0,0 +1,68 @@ +import { create } from 'mutative' +import toast from 'react-hot-toast' +import bitsFetch from '../../../Utils/bitsFetch' +import { __ } from '../../../Utils/i18nwrap' + +export const handleInput = (e, senseiLMSConf, setSenseiLMSConf) => { + const { name, value } = e.target + + setSenseiLMSConf(prevConf => + create(prevConf, draftConf => { + draftConf[name] = value + }) + ) +} + +const refreshResource = (route, dataKey, confKey, setSenseiLMSConf, setIsLoading) => { + setIsLoading(true) + bitsFetch(null, route) + .then(result => { + if (result && result?.success && result?.data?.[dataKey]) { + setSenseiLMSConf(prevConf => + create(prevConf, draftConf => { + draftConf[confKey] = result.data[dataKey] + }) + ) + setIsLoading(false) + toast.success(__('Fetched successfully', 'bit-integrations')) + return + } + setIsLoading(false) + toast.error(__('Sensei LMS fetch failed. Please try again', 'bit-integrations')) + }) + .catch(() => setIsLoading(false)) +} + +export const refreshCourses = (setSenseiLMSConf, setIsLoading) => + refreshResource('refresh_sensei_lms_courses', 'courses', 'allCourses', setSenseiLMSConf, setIsLoading) + +export const refreshLessons = (setSenseiLMSConf, setIsLoading) => + refreshResource('refresh_sensei_lms_lessons', 'lessons', 'allLessons', setSenseiLMSConf, setIsLoading) + +export const refreshQuizzes = (setSenseiLMSConf, setIsLoading) => + refreshResource('refresh_sensei_lms_quizzes', 'quizzes', 'allQuizzes', setSenseiLMSConf, setIsLoading) + +export const checkMappedFields = senseiLMSConf => { + const mappedFields = senseiLMSConf?.field_map + ? senseiLMSConf.field_map.filter( + mappedField => + !mappedField.formField || + !mappedField.senseiLMSField || + (mappedField.formField === 'custom' && !mappedField.customValue) + ) + : [] + + if (!senseiLMSConf?.mainAction || mappedFields.length > 0) { + return false + } + + return true +} + +export const generateMappedField = fields => { + const requiredFlds = fields.filter(fld => fld.required === true) + + return requiredFlds.length > 0 + ? requiredFlds.map(field => ({ formField: '', senseiLMSField: field.key })) + : [{ formField: '', senseiLMSField: '' }] +} diff --git a/frontend/src/components/AllIntegrations/SenseiLMS/SenseiLMSFieldMap.jsx b/frontend/src/components/AllIntegrations/SenseiLMS/SenseiLMSFieldMap.jsx new file mode 100644 index 000000000..de90c3ac6 --- /dev/null +++ b/frontend/src/components/AllIntegrations/SenseiLMS/SenseiLMSFieldMap.jsx @@ -0,0 +1,104 @@ +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 SenseiLMSFieldMap({ i, formFields, field, senseiLMSConf, setSenseiLMSConf }) { + const btcbi = useRecoilValue($appConfigState) + const { isPro } = btcbi + + const requiredFlds = senseiLMSConf?.senseiLMSFields?.filter(fld => fld.required === true) || [] + const nonRequiredFlds = senseiLMSConf?.senseiLMSFields?.filter(fld => fld.required === false) || [] + + return ( +
+
+
+ + + {field.formField === 'custom' && ( + handleCustomValue(e, i, senseiLMSConf, setSenseiLMSConf)} + 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/SenseiLMS/SenseiLMSIntegLayout.jsx b/frontend/src/components/AllIntegrations/SenseiLMS/SenseiLMSIntegLayout.jsx new file mode 100644 index 000000000..983808c32 --- /dev/null +++ b/frontend/src/components/AllIntegrations/SenseiLMS/SenseiLMSIntegLayout.jsx @@ -0,0 +1,166 @@ +import { create } from 'mutative' +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 { checkIsPro, getProLabel } from '../../Utilities/ProUtilHelpers' +import { addFieldMap } from '../IntegrationHelpers/IntegrationHelpers' +import { + courseActions, + lessonActions, + modules, + quizActions, + senseiLMSStaticData +} from './staticData' +import { generateMappedField, refreshCourses, refreshLessons, refreshQuizzes } from './SenseiLMSCommonFunc' +import SenseiLMSFieldMap from './SenseiLMSFieldMap' + +export default function SenseiLMSIntegLayout({ + formFields, + senseiLMSConf, + setSenseiLMSConf, + isLoading, + setIsLoading +}) { + const btcbi = useRecoilValue($appConfigState) + const { isPro } = btcbi + + const setVal = (key, val) => + setSenseiLMSConf(prevConf => + create(prevConf, draftConf => { + draftConf[key] = val + }) + ) + + const handleMainAction = value => { + setSenseiLMSConf(prevConf => + create(prevConf, draftConf => { + draftConf.mainAction = value + draftConf.senseiLMSFields = senseiLMSStaticData[value] || [] + draftConf.field_map = generateMappedField(draftConf.senseiLMSFields) + }) + ) + + if (courseActions.includes(value)) { + refreshCourses(setSenseiLMSConf, setIsLoading) + } else if (lessonActions.includes(value)) { + refreshLessons(setSenseiLMSConf, setIsLoading) + } else if (quizActions.includes(value)) { + refreshQuizzes(setSenseiLMSConf, setIsLoading) + } + } + + const resourceDropdown = (label, confKey, listKey, refreshFn) => ( + <> +
+
+ {label} + ({ + label: item.label, + value: item.value?.toString() + })) + : [] + } + onChange={val => setVal(confKey, val)} + singleSelect + closeOnSelect + /> + +
+ + ) + + return ( + <> +
+
+ {__('Action:', 'bit-integrations')} + handleMainAction(value)} + options={modules?.map(action => ({ + label: checkIsPro(isPro, action.is_pro) ? action.label : getProLabel(action.label), + value: action.name, + disabled: !checkIsPro(isPro, action.is_pro) + }))} + singleSelect + closeOnSelect + /> +
+ + {courseActions.includes(senseiLMSConf?.mainAction) && + resourceDropdown(__('Course:', 'bit-integrations'), 'selectedCourse', 'allCourses', refreshCourses)} + + {lessonActions.includes(senseiLMSConf?.mainAction) && + resourceDropdown(__('Lesson:', 'bit-integrations'), 'selectedLesson', 'allLessons', refreshLessons)} + + {quizActions.includes(senseiLMSConf?.mainAction) && + resourceDropdown(__('Quiz:', 'bit-integrations'), 'selectedQuiz', 'allQuizzes', refreshQuizzes)} + + {isLoading && ( + + )} + + {senseiLMSConf?.mainAction && senseiLMSConf.senseiLMSFields && ( +
+ {__('Map Fields', 'bit-integrations')} +
+
+
+ {__('Form Fields', 'bit-integrations')} +
+
+ {__('Sensei LMS Fields', 'bit-integrations')} +
+
+ + {senseiLMSConf?.field_map?.map((itm, i) => ( + + ))} +
+ +
+
+
+ )} + + ) +} diff --git a/frontend/src/components/AllIntegrations/SenseiLMS/staticData.js b/frontend/src/components/AllIntegrations/SenseiLMS/staticData.js new file mode 100644 index 000000000..ea653872f --- /dev/null +++ b/frontend/src/components/AllIntegrations/SenseiLMS/staticData.js @@ -0,0 +1,71 @@ +import { __ } from '../../../Utils/i18nwrap' + +export const modules = [ + { name: 'enroll_user_in_course', label: __('Enroll User in Course', 'bit-integrations'), is_pro: true }, + { name: 'withdraw_user_from_course', label: __('Withdraw User from Course', 'bit-integrations'), is_pro: true }, + { name: 'start_course_for_user', label: __('Start Course for User', 'bit-integrations'), is_pro: true }, + { name: 'complete_course_for_user', label: __('Complete Course for User', 'bit-integrations'), is_pro: true }, + { name: 'reset_course_for_user', label: __('Reset Course Progress', 'bit-integrations'), is_pro: true }, + { name: 'start_lesson_for_user', label: __('Start Lesson for User', 'bit-integrations'), is_pro: true }, + { name: 'update_lesson_status', label: __('Update Lesson Status', 'bit-integrations'), is_pro: true }, + { name: 'reset_lesson_for_user', label: __('Reset Lesson Progress', 'bit-integrations'), is_pro: true }, + { name: 'grade_quiz', label: __('Grade Quiz for User', 'bit-integrations'), is_pro: true }, + { name: 'create_course', label: __('Create Course', 'bit-integrations'), is_pro: true }, + { name: 'create_lesson', label: __('Create Lesson', 'bit-integrations'), is_pro: true }, + { name: 'create_certificate', label: __('Create Certificate for User', 'bit-integrations'), is_pro: true } +] + +const userEmailFields = [ + { key: 'user_email', label: __('User Email', 'bit-integrations'), required: true } +] + +const postContentFields = [ + { key: 'post_content', label: __('Content', 'bit-integrations'), required: false }, + { key: 'post_excerpt', label: __('Excerpt', 'bit-integrations'), required: false }, + { key: 'post_status', label: __('Status (publish, draft, pending, private)', 'bit-integrations'), required: false } +] + +export const senseiLMSStaticData = { + enroll_user_in_course: userEmailFields, + withdraw_user_from_course: userEmailFields, + start_course_for_user: userEmailFields, + complete_course_for_user: userEmailFields, + reset_course_for_user: userEmailFields, + reset_lesson_for_user: userEmailFields, + create_certificate: userEmailFields, + start_lesson_for_user: [ + ...userEmailFields, + { key: 'mark_complete', label: __('Mark Complete (yes/no)', 'bit-integrations'), required: false } + ], + update_lesson_status: [ + ...userEmailFields, + { key: 'status', label: __('Status (in-progress, ungraded, graded, passed, failed)', 'bit-integrations'), required: true } + ], + grade_quiz: [ + ...userEmailFields, + { key: 'grade', label: __('Grade', 'bit-integrations'), required: true }, + { key: 'grade_type', label: __('Grade Type (auto, manual)', 'bit-integrations'), required: false } + ], + create_course: [ + { key: 'post_title', label: __('Title', 'bit-integrations'), required: true }, + ...postContentFields, + { key: 'author_email', label: __('Author Email', 'bit-integrations'), required: false } + ], + create_lesson: [ + { key: 'post_title', label: __('Title', 'bit-integrations'), required: true }, + ...postContentFields + ] +} + +// action -> which resource dropdown it needs +export const courseActions = [ + 'enroll_user_in_course', + 'withdraw_user_from_course', + 'start_course_for_user', + 'complete_course_for_user', + 'reset_course_for_user', + 'create_lesson', + 'create_certificate' +] +export const lessonActions = ['start_lesson_for_user', 'update_lesson_status', 'reset_lesson_for_user'] +export const quizActions = ['grade_quiz'] diff --git a/frontend/src/components/Flow/New/SelectAction.jsx b/frontend/src/components/Flow/New/SelectAction.jsx index 5c134ef4e..a676b85a9 100644 --- a/frontend/src/components/Flow/New/SelectAction.jsx +++ b/frontend/src/components/Flow/New/SelectAction.jsx @@ -185,6 +185,7 @@ export default function SelectAction() { { type: 'CreatorLms', is_pro: true }, { type: 'Bookly', is_pro: true }, { type: 'FluentCart', is_pro: true }, + { type: 'SenseiLMS', logo: 'senseiLMS', is_pro: true }, { type: 'MoreConvert Wishlist', logo: 'moreConvertWishlist', is_pro: true }, { type: 'Heffl CRM', is_pro: true }, { type: 'Secure Custom Fields', is_pro: true }, From a5729d93580880d9c4fd0bfefccbf38b17a4767d Mon Sep 17 00:00:00 2001 From: Rishad Alam Date: Tue, 30 Jun 2026 16:41:58 +0600 Subject: [PATCH 2/6] refactor(sensei-lms): move static choices to dropdowns, switch-case dispatch Move the fixed-choice fields (post status, mark complete, grade type, lesson status) out of the field map into Utilities dropdowns stored on the config (selectedStatus / selectedMarkComplete / selectedGradeType / selectedLessonStatus), so the field map carries only free-form inputs. Replace the action-map lookup in RecordApiHelper::execute with an explicit switch-case per action, matching the FluentCart convention. --- backend/Actions/SenseiLMS/RecordApiHelper.php | 71 +++++++++++++----- .../SenseiLMS/SenseiLMSIntegLayout.jsx | 40 +++++++++- .../AllIntegrations/SenseiLMS/staticData.js | 44 ++++++++--- .../src/resource/img/integ/senseiLMS.webp | Bin 26770 -> 4624 bytes 4 files changed, 122 insertions(+), 33 deletions(-) diff --git a/backend/Actions/SenseiLMS/RecordApiHelper.php b/backend/Actions/SenseiLMS/RecordApiHelper.php index 924cc91e7..dca7e2ed1 100644 --- a/backend/Actions/SenseiLMS/RecordApiHelper.php +++ b/backend/Actions/SenseiLMS/RecordApiHelper.php @@ -45,29 +45,60 @@ public function execute($fieldValues, $fieldMap, $utilities) 'message' => wp_sprintf(__('%s plugin is not installed or activate', 'bit-integrations'), 'Bit Integrations Pro') ]; - $actionMap = [ - 'enroll_user_in_course' => 'sensei_lms_enroll_user_in_course', - 'withdraw_user_from_course'=> 'sensei_lms_withdraw_user_from_course', - 'start_course_for_user' => 'sensei_lms_start_course_for_user', - 'complete_course_for_user' => 'sensei_lms_complete_course_for_user', - 'reset_course_for_user' => 'sensei_lms_reset_course_for_user', - 'start_lesson_for_user' => 'sensei_lms_start_lesson_for_user', - 'update_lesson_status' => 'sensei_lms_update_lesson_status', - 'reset_lesson_for_user' => 'sensei_lms_reset_lesson_for_user', - 'grade_quiz' => 'sensei_lms_grade_quiz', - 'create_course' => 'sensei_lms_create_course', - 'create_lesson' => 'sensei_lms_create_lesson', - 'create_certificate' => 'sensei_lms_create_certificate', - ]; + switch ($mainAction) { + case 'enroll_user_in_course': + $response = Hooks::apply(Config::withPrefix('sensei_lms_enroll_user_in_course'), $defaultResponse, $fieldData, $utilities, $this->_integrationDetails); - if (!isset($actionMap[$mainAction])) { - $response = ['success' => false, 'message' => __('Invalid action', 'bit-integrations')]; - LogHandler::save($this->_integrationID, ['type' => 'SenseiLMS', 'type_name' => 'unknown'], 'error', $response); + break; + case 'withdraw_user_from_course': + $response = Hooks::apply(Config::withPrefix('sensei_lms_withdraw_user_from_course'), $defaultResponse, $fieldData, $utilities, $this->_integrationDetails); - return $response; - } + break; + case 'start_course_for_user': + $response = Hooks::apply(Config::withPrefix('sensei_lms_start_course_for_user'), $defaultResponse, $fieldData, $utilities, $this->_integrationDetails); + + break; + case 'complete_course_for_user': + $response = Hooks::apply(Config::withPrefix('sensei_lms_complete_course_for_user'), $defaultResponse, $fieldData, $utilities, $this->_integrationDetails); + + break; + case 'reset_course_for_user': + $response = Hooks::apply(Config::withPrefix('sensei_lms_reset_course_for_user'), $defaultResponse, $fieldData, $utilities, $this->_integrationDetails); + + break; + case 'start_lesson_for_user': + $response = Hooks::apply(Config::withPrefix('sensei_lms_start_lesson_for_user'), $defaultResponse, $fieldData, $utilities, $this->_integrationDetails); + + break; + case 'update_lesson_status': + $response = Hooks::apply(Config::withPrefix('sensei_lms_update_lesson_status'), $defaultResponse, $fieldData, $utilities, $this->_integrationDetails); + + break; + case 'reset_lesson_for_user': + $response = Hooks::apply(Config::withPrefix('sensei_lms_reset_lesson_for_user'), $defaultResponse, $fieldData, $utilities, $this->_integrationDetails); - $response = Hooks::apply(Config::withPrefix($actionMap[$mainAction]), $defaultResponse, $fieldData, $utilities, $this->_integrationDetails); + break; + case 'grade_quiz': + $response = Hooks::apply(Config::withPrefix('sensei_lms_grade_quiz'), $defaultResponse, $fieldData, $utilities, $this->_integrationDetails); + + break; + case 'create_course': + $response = Hooks::apply(Config::withPrefix('sensei_lms_create_course'), $defaultResponse, $fieldData, $utilities, $this->_integrationDetails); + + break; + case 'create_lesson': + $response = Hooks::apply(Config::withPrefix('sensei_lms_create_lesson'), $defaultResponse, $fieldData, $utilities, $this->_integrationDetails); + + break; + case 'create_certificate': + $response = Hooks::apply(Config::withPrefix('sensei_lms_create_certificate'), $defaultResponse, $fieldData, $utilities, $this->_integrationDetails); + + break; + default: + $response = ['success' => false, 'message' => __('Invalid action', 'bit-integrations')]; + + break; + } $responseType = isset($response['success']) && $response['success'] ? 'success' : 'error'; LogHandler::save($this->_integrationID, ['type' => 'SenseiLMS', 'type_name' => $mainAction], $responseType, $response); diff --git a/frontend/src/components/AllIntegrations/SenseiLMS/SenseiLMSIntegLayout.jsx b/frontend/src/components/AllIntegrations/SenseiLMS/SenseiLMSIntegLayout.jsx index 983808c32..356fe3a53 100644 --- a/frontend/src/components/AllIntegrations/SenseiLMS/SenseiLMSIntegLayout.jsx +++ b/frontend/src/components/AllIntegrations/SenseiLMS/SenseiLMSIntegLayout.jsx @@ -8,10 +8,18 @@ import { checkIsPro, getProLabel } from '../../Utilities/ProUtilHelpers' import { addFieldMap } from '../IntegrationHelpers/IntegrationHelpers' import { courseActions, + gradeTypeActions, + gradeTypeOptions, lessonActions, + lessonStatusActions, + lessonStatusOptions, + markCompleteActions, + markCompleteOptions, modules, + postStatusOptions, quizActions, - senseiLMSStaticData + senseiLMSStaticData, + statusActions } from './staticData' import { generateMappedField, refreshCourses, refreshLessons, refreshQuizzes } from './SenseiLMSCommonFunc' import SenseiLMSFieldMap from './SenseiLMSFieldMap' @@ -84,6 +92,24 @@ export default function SenseiLMSIntegLayout({ ) + const staticDropdown = (label, confKey, options) => ( + <> +
+
+ {label} + setVal(confKey, val)} + singleSelect + closeOnSelect + /> +
+ + ) + return ( <>
@@ -113,6 +139,18 @@ export default function SenseiLMSIntegLayout({ {quizActions.includes(senseiLMSConf?.mainAction) && resourceDropdown(__('Quiz:', 'bit-integrations'), 'selectedQuiz', 'allQuizzes', refreshQuizzes)} + {statusActions.includes(senseiLMSConf?.mainAction) && + staticDropdown(__('Status:', 'bit-integrations'), 'selectedStatus', postStatusOptions)} + + {lessonStatusActions.includes(senseiLMSConf?.mainAction) && + staticDropdown(__('Status:', 'bit-integrations'), 'selectedLessonStatus', lessonStatusOptions)} + + {markCompleteActions.includes(senseiLMSConf?.mainAction) && + staticDropdown(__('Mark Complete:', 'bit-integrations'), 'selectedMarkComplete', markCompleteOptions)} + + {gradeTypeActions.includes(senseiLMSConf?.mainAction) && + staticDropdown(__('Grade Type:', 'bit-integrations'), 'selectedGradeType', gradeTypeOptions)} + {isLoading && ( r&%0M5 zK_sR!#g%Qn4 znWjwFbaJ@wlR9sXBv97uZ8d)wB$TBrCF&n)wq;wzM_N~UvU`*n;%N8DQja7sN@2~7 z5sewBHquqSMHUg+>1!G!%z(+@g#%7mEZh}WhnRssx?Yhj3^w1Dh{g<_L@t%t1i+qu zP9@9~iCSN1<2q@b|LYht(O7q-o8LA!Mkq5`I?$TWWe#swjG1z*^Y9AZavcdV6aW6T zwLFzs`+zc2r~SuWoFusd!<3mm9SXQ54)VM|#5i%FHiviYZw*k!jWm4B!85j3Zc)aS z^!{^pUQu>;F%G@C%fcm_u1+#8ow}UIA1mK|%(!*zy{Gs>+1bmuws)Vx5zgi*#<@=# zT)d!^_fy8b^kKQe1)g@oxR_jA#{s@ul<{)7%@3oN&DqA4s)$ z(U?$bEfxc_HWDV7q|s_v_Ey4#leF3m%GyepfRa|LAz2y;6H?M>F`)Do!UUDHq#KTW zMq|RNos^BHYMu$~T$QoN{umS5m|r#$ZH zT60HJsCb+S_jqy2tp1h>`1h+)rJf1-*%T?85wlreN~{YrLBHim0p%hSb=t1zubc=Q zBhTv2^^ggCzf)&Q0~I;itmtds6DIUiJ9YI`FBAOUm3n%Q3IDq~${Q2E$D4Fxxy%so zs-l-&Qy4J0TPOCL3K~9*V;eaP;QZ2;;S}P^X zJ-`rg$d#-BL&ZRnTtm1poSB^C6c=c1Qr7k}WPIRC#%mNC=#eC>_@1F-$dS0i3?aRV z>%@qzgyqLDBDOhEA25_`loI0}z=_DZ#8gt8p#Fr|x*1Y_ZBImD3@c*!iKu62c`gAq z6)#lV^2!Ka5a04HF~q#Gtn3J8MBK}%Aef<5EJMD|Q1hxBpYsuJ3_0UFz>w1y-v#Vw zic22EjzJ}!Ed)DAVLa6gJ!|5Sy71$Mw3KHAKStIqRjT2KR=U(lhM*^xuy$iew{@}X zQ4AT)UThmf(QS*>;7HA4f54GHEF!5mGAJ#`Avi)j3%nFZ=>7$D3`y%2e1auk0Dz?r zOZpT5_b`@>cmRqfv1Gagpn_rvJp@qA(DVdAJDzla(upUXiX((4AxD;qCy}h&89bTI zEn$dSRE{YJ{g~npU`pUTrZil`l2z=f?-U#r(%jafhiMFOo?ikqOt$G zs$q&2#gym-rc9`qqTa)la0pX^J($w-4@~*%bxgU|fGOt#m=f?~ivJ*{low%2N$xD3 z%;aW8@I=jWgzzNfP&)CXQ-KaVX$LrgC)EIlD3;I)fRbq}nJfbEj9|&I8$jv9l0FLn ze1RpO007isN!^0|6i29cfrsD-$yp!`;)p6OfIr~KA7IfM9I07s8;)#SEPE6~Mza^Q zc4J7lbul=JAtzzUQVl<}(xujo;Kzt(Dd`4&bV*A=4SrNZoP`8C$d-7NLF^cm9P2Y<$-?R6~FgD$18h9uh*d`*f}M3PxgVXm1Zh(v_a7qIQvEJp$qi> zED<+S4!T#`sjyvC^aq|>q845Uspy*(vzanT&9h=w4=K9pdlCC?b*ez|xOj~hL+T!# z6Rr75Na3`LqIF40CCCVhRWJimJAYWDB3mKVpPLq^splc(IDOpUyX@P%r6@WK-HYc%vXWI%BM7O z(ay+51L-Y9R7guY7_X&~hzV)5fDv0;iHML^D;TrAm52vvwS!Sx8;NL;Mk^S%r9~49 zt;GUHo_=~>B<4<~gR#q1F>#1h$zb^Ys3`pWDwv?`9TS5wpKJyIh4(}tTmoj9*F$-a zew7F2nfW2_edq);P5aFh&rSa_4a`+~KEiLq&&goATZ6n7%m*{hxHQjWa~CtfoR!LP z{u)1;2Buy79dCVK3}(K%euk%J&#nTKFAH-Mz2yV*Uw4u45oz}T4=UvkDgV&^as@ci z)$oLOrkb6=kIKGYzUkem09QKR8{?P9@2&*y%)30rCzDq<0gqaC-Qs4Y7L_L>p z2mZH}U5XH+(;`=VHW0<^itCSzZ0Kr*8-!AJ9=I?_jED?2`J6HcsJx}RYeX{=ZRAEx zp#tJ6c}h=qj~c^h_sPO+x=;FsC#k;Z^hn6Y9NS z&p)m=1pEh!aY~FrXwnJ=+p2c2~F8- z8UT7T6C)=7C1Iuf0Q^-&`EF~i>vuf~czM0Q{P<6ye$x6}Fh&IqH9c3(VjG@M|KBuU zesAZ41bNeE z`>e#eHttBM$FPHF3I7>fVvnoF7<**zDP;UN!zmYqXHD)~)}z4jJKcpDHPH`70BFlz zxK;^mu;#Pt3>rveni$MN?VZP~HvqCw*0aKIAt)x6TKA;J21J0>)h;AL3*emFZ1JX? z$S|{t0xeS%ZiOi-@a5Xg%_u+!?6*w%P-KV|BJ)YdlE12R8w|o0VN+$5WhCP{P{o3s zcRMVdsjXH@KQ5;)bVrbUJf|{-!zwBiTICEHFn{JaJ~ab5cYt+7Zj0+KC$RVw<|~~4 zPQ06~22@}r*m-V{u!i@79XMVc7=0z7S)xEs|Gnu`>S(-OdW5SSR%LRQ_jde&dBd$# zOt;*OfFbeS;1Kga6IUL7Ik+T02~Zbs7C+v_=9aDOdDnh8nX6LBcoDa- z0HBre5&pvDr>5l_x$G<<#2y(busB-tWU!?e@({Qx`XA6x07G-_9ADH7Ksf=0XZJhg zJs<*%eKFFc#CB^9D)AO>UJAsdEt}t#gyZ)b9^?=xJ&svn?l1SGrvGVyO~%+D(%bqw z5QX_SehH?S?4U9;2xWf*ffnM4>-XpeFj#t>pUNZlp2_@DfG%mhLDi`7Ill*3)4d=P z3nfUJzAe_#+-3=n*syKS0qArg8x_X#yh$_8;P|W2vO}>Hf`6GYV%#g|U9>nv@7e_Z zE`uigLO^Pd2rJOtI(Xr69uDqNobYM!Bg*81{SqM7`QPUQD~$?KN{rszs-YE>&>G^< z5H)C3Mb(gTQ-MSA(9xC;S+72y{oy^A5B=Sz657qb-xf^KFp6Mp_wY)<8wgiOlfvx* z9C^^FlPVO$f5e!E4&n?yk7D{&9M?Bhhj%fK$fK*yX; zqqv+BQ?aqml1tq*2It~qFQ#(7cD%&#;dS!ORMH(0a59^R>IW;JDaKo-ae+=q#Ln(= zHp>pYse&n7IKcSXwwFjW_ejUu(At=Q@yt7>QEcqit&V!Hb3*v_ZuIBpnfjmx9sBRb z2=?$cA7aLwE_0v;pMJI@u3>7p5L7J%b~&6awYkhz)>#)tGwa%@IiIG`my<`x0X^=M zg?Z%y*(xuiRu=2FUoUyTHAnwfuWjbZGDw&+F&EFv{DAkrlZLb+jN_LR_TR|FDY*fX z6Uf3c^I|=&Q8RiWUv}z6F#JHGi(^gK^}6Uzy(hUB-RAo9oInY|VFwN1I$fPr0^0Jb z)sFC5LF|@1tBBfeMf2t(7gWLK0=x_8rB$@JS`mfn(Goz(uK)Z3b)VI*bBC24zvm*@}tR7Qg31$3$6%%e~0HMKhh}ho4m&jN3!%t z3EIO%&cE+)qF#bKf2jVd`b(6sIvwmt`_|NXVMne1)Zty8KdPA?PQci<@LMU&7z5Ty zhKhS)+6`m7kyk$|a??Ny1L&3*+#C~taJjGm;1nk!7gjZ#4Vl2c)^6Fr9e0pej@DDS zlxQXJrl)DhYf2w6((eH@C`nG^d|2qj0DU~51|%tWk&_0>G~R$zO&fk5Ryr10S?&WO z@*1J{JUVxP0`Zg1KmY-Ip}Mu>8ifJE!7O5{=wh5p{iWHsM)b7l-q=z^M8{-7}QYyPA zSnJ8`v0x+mUXM~NzRD`JPR}!~*i`n`#vz@t|T|-j6kLD{ZITtlg GBH#eYK)AU8 literal 26770 zcmeFYWmFwav?kn#g9UfD0Kwgz6I>FU;BLX)?Ia;U5(o~#-5~@IPJ+8ba0u?sLC+c9 zd)K$_{FrZk&D3Nmq4iDJv*^ehUD4@-iB4G(_|<007{5ioZa> zXAqz)E345DdU^yv{r{xMPeslyULIPCGIaU|hID9a0O;xZk6>Zt>Gq%Kf0;f>UC;if z>=ft!QsRG~`OMnJ)9Oj&^eHlWJXQYGCZZ=yWcQz#z+<|vVk@J+*)_zyZC!fv^BD0r%Wp> zD)8*F8@?o<>iecL6`y=wayBXszrWJiXeQZz^wHfnWjq%J&ZIQ3B>0hJNhJr9ZTwFz zkOEtQEi>@eIL~~(%eZ&*Wd^i9lq~lJue2%RkO9bltQ(vc8$uSVo>J$Pp6%X|}n@S0z^c5C}LHcw38KA{TatI0=5Uj@ZXU0aN2X=r3Nsif2@wyMWVM z^6;Rx^czF6HWP-o=-399(ABv5k-g#vtTMiiU`O^Nq_UhA-#GU*R4y%$dA|E&ZO2S) zYisLMJ@wlyBj~FUz6cc-${G*tWx|j zj5XKeTPk@gYKQQ|eQNM9P5|)-TLUl&F~6rx82m)eChU^EW+IX78|O2{_qdY&)x8C8QRD2 z7lpohwPpWZ9=IHCj|2UGeeD14eeDKqtQ;T_H$ndCv3Ml_ov8~Hg>(mD(gi%=#Nro& z6VrI1>Y>M()c;NG`M+!JzuNzs7)VYp?0|h33V?RaJYoVi9Qcrt2_y}L7(m0H7cfDN z)I!7CQik5Ei)LbIG`>P*=^}#8bl`HMirm#7dYy>~t4U^|n>TTxeT#152I^5Dcmj;6 zmVl;7i<)j%J5YT{3q&$PY&lR*2Vs+dqS@We?}2sY`TPm30xm;GW?G@^D>&6~B?%IKtKhpK!9ytCO}RO7jN8BBaEM`uhoOhM|>KQPj1e zP+ip1o;f4_(#6|J=@+1DJ>Lkhb@yoTt7Hl*E2wV<%=Bvd*qOF@w}607eq=AfTAnEh$Lm(+42-CaIr!Pp6;&Vl)nhXwm|IzL2vYT z3`qjWEI&XSvUd9dSgv`h3s>}^&1@%4?qi&fcs&RjcaIddKnf{Y=_j0d2JII^TiZwM z$N~mjpva6CP-4?HxL>F!vwq-56a^s4joMF!3A+B?_D$;3T?7eCn3}F^b>CAGTXs+b zRm0&Jj5h&8Le2(P2xUBZC6EA06)@{XPmhE?gNipaf)C8WU!b@h>GEk8O>8tG7(hH` zW8nJ<>Uou!Bj^@=mP`&}qz=|6|0n$=#v*F=_x+m82O-;9`P;t|T0j)!FJ3 zCtI*kJ<%2^bZO-_=>4X{)#0A1GxU##I8@7@RZ?@{4G^g4VutK@T9=Ry$}&4_Lp@E% z_HXqd5HR%f65>o?3BVc3wwpXC;YX^Z&_;TYHkZH(i2y4Wxf0VV}Oj$h6#iS}P0)`iqVH7?BWSCjnbW;erOgH6o#3Q4)M}D%j`C zM)q5m(&LVf@7(ka@mj=iJ6m&rG3DFqI3vg6JA&}oL6$n?P#ug|dNd6M#jcvF>HY}- z?P4T%#H=v;_UDqdt(_xWAC(R}P0FMM1SEah=BV7qgihW&LMMfbukyQ2cWFU57uy8T zYXsYgELz1|h*}QpTY4iw zqEv9ZhCXuxD$}e0En=EkFNSM+LJ{XpB{=BrB4QP3DRkG%2rAaY8*M^wg9GtFqASzJ zj+uN$cjo~xq=6y{pj(!Y?44t@#UiQxMkS{1@b^6$$R*_d&=PWRKUvTAf^^wufOY43 zFa6HP3)M5+0i{2RGySTR%##(OAmsXbyfF*A{xXEnTyv=BX_C6xU1hGU-Z`ngqJ>Vj zVbI+tI`hcTLYRj@4z?%|O>7{rd}I&6r4kdRUhBx-e+lJohrb;`wqzG}%i-@+_~zw zJAi@e6lp^rGnuyFhuasTfPDQ|ZhI?1P8%a=lph)0)DtILFV&RQ(l9MjJmqiUVzud`MJ5FjJbGMvpSo z@mHbmK~hvAI7lL=a2uA+lGp2i`^Cz~Tby}e9Y3_3l8xRtUpGU0n_y^je;|-|hYO9M zxlB3$eeFOv(L!(Iw#=Z25SKBsvogf&L_U3|qu!1%n8^FLTWj=?QEkX;a55#%Od zfnY)U>r1haK*WZevYM%cVI!2!H~oMT*ha(U2IQ`q#cO8zsUVsNwB3a#kBM!k0F4fI zf}Jz&M%X`$zb}BU-{6XaBmXoD3kqaae?yIp+o_P91Ou>a|7%f5gCAy&(RVWxk-Oc5 z`41?fD+$22<)9F8NU4}La>@^E9Fy0F5UfDZA#8CG6tXjHmjp;(Hwia0EA6RwoK`b? z0xncy^BCrDZgp0Vy0}31tggsEMNcHLPB+Y!j9R02NZ~BcPOpd5dx7aVG?3hNE}$}n z>gYyDLp+fVPL!%b!#ktwl(h>P$2o1xzTX`K=2);|3slUlMdWK{%=iOSk|HN0hgCgDi&! z2`98_ffNfc^v4pG+vELM1QJ#7!axiV(4(T!$?Kv)OqN&p zMn8vcR9%&hg`9T)X7B@L^3E16vl9R>I12;v2ApVE0|+XpYcswW90i6leB zX@l-x(S7NlX)jFZ=K;W*0*0IOd4e2D+v#ZgvY*z|Wt}w4hV13OLfw>Em!vw-^j*eETJHimSB3G?H~1 zI$nMAP07YRM0$L37&}?{NXlZZ6YZk#a-^cAnq0gGF-$Cx=D(m-4`K;}&jY0w8j%U068a%2=d6`Z%*=$QnVKEBce z_h*1(bvXe|YP5bbT2LenCZ0_50Cnc6} z%4}4SQ-iqCP161~bylhZZ2oaq zH$1Y)6Xx)N$Aw`bPjvgIj6K_u((Xd(V=?atnvc+7A1{9Q^Vz((Pd z``ZcFE(8=@F!A;@fSK2ewhcBq#1Qf}F!VD$w1fltIRrogYFlA{q#r-CanvRE5JT-^ zR=q9g+&kA58 z0Lb&;3KV0p-#>RES^uZUbp8cpV)b0Wq>-UZwNF;F@sce2q9QWfR`Xda1cb#hMkjm z?ee|Y$yrqw0~+c2v6y%T)cr=AU-`WGZQfaWb9JDPrWu+w``FK!3;tRZ^EVf7p;5e; zboo0-)_Xt+Ct_;amn*KcSGoT3o(bwQDA4J2>;uH5f6Y&5x@oqh>=F5$-HOXIn1TEQ z4jUtb9;YR_K|ObHE%zCv#=dNgf&tAdDTu-`53-YNz)tUg%GE2VSmoqv$@Y>GN|JkW z-3?)L0Zlc7@_SHLVCNM!C1JdT+uhj!FzNgl6_5xGb^=ElfPhQ`6V$i)lYl9;=pB~Z z&5W}$bfsp-1*n$C>%61&7LUj-%ZS`U^!L$LA&bD_wP!%!!N?TA%Iu6;u8sllB_rWw zgUK!|pt?`*k&mY0py_@FP+npTy^^gE(zHA+#?#Q_s~|j9y@{Fa*Vgi>Zjcp$8an~b zQACdABoTOVn%C@*7t2}z3Og1D@-@y}T+2E@WNks2fZK78?xbAlSK}6}(xT7Z_A<*c zQfIxiK%aD*3OLaUrU(ja5{K;Z;?rq^AXikt%n86zv^)n2+0{lvfNt_P5GE+6zaMeF zqq1B`wBu&eY9>!Ql0oP1fby*XqE6Q%(^;Xmjdm{toP;hK4dl?14G7czXvK0nTbXG9 zChpOpcxAvi7#NmB9Tz}DmlOiJ`)z?rQ?yR~z>CR+^68d*TuH-~&ERHuLumKY>ii&N z+}wBpt^-;#8%;5D4c9EolI6SKIV(Jl z*eeUc;?f7CbbUwO@6IL7PQiLO$8b?p{ne%nX2UeWW!L^Ip2GG|Nl`SyPAu3YUyfu3l~6LMmqes4kj3S3zoF zm3B(!OeFdM-jPv-?zT1hhm4L~5GYkh1effi^+$C8iKRgIBG7oU)3^6`7hG&yfri&~ z`0R)NHZ?%rHua;TxDXkr-LD3CVfVnY01ims))@QL>cl@kFi#|4H5Ydg*soc2`TXwq zDhr9uvhA1CZvp7{9xh;cJD|3`qwmwoF3X4vk$=GIozm*p5KlYU1PbS50GNP0&P-jN zSe9b#`tMm;JK2=iMEL3KPz)}TlK)%W@tceC73v%{t6l}h06e;BS3t10z&5~`Uluw9 z3&KmMp|O)hC@+8a1l-G03)g7f=k7?$;j zZ7Kr;YArrr=L+V9YbbIm4xFIPI-t{OD_WQU@%72!k`CC|WVsuFZ|v2UyVAH0`esyR zJY)+&Z59_va=3cA0L^;m>`+&Z=~rk#*d2M!>>V|<(Zt;Y2THR6DtHF)A_FEDxSdyB z+&*Z2m!GQILTf7Hb<~cg4BGcRD*@6Iu}ZX(Aw_qnlgVW4Fw9jIsEHm3eBpBGA0+3Jm@_%7BL? zAN)9b3^vPB1*pF3ErKF#e4*d0kRc5vPWyqqk#aH9OQn{dH@By*lqHcX+ubf=6YpQo z!D`cBNai$s4!^yA8{Jbiow?n*h}R3U`~(LKrdbIgIUa&LBp zyK=<%79SIjLHcQT<_xs9m(ZvQeDF60|8#SgHlHoLdYyWG-&ZUJlvN@tm7+sf$Psz` zJ`OZ^a@zUpBDv^%sB1l4DV~7r6cLo46ecw-Mi|)FR|K#YZHY3%2^zdC_|Axpui|&H z#-}B?(aZ-@Z^|;B3F;2aL~zpXXxJ)4msjDomEJ04>e_CgEi4u{);WswM zmHhZdZ~(n6WaqmS2jLFS@OJ-awDW~M1QMWSjSk^S<09PA2+%n*2dia?!*Ndku3un5 zb$GcEdqS%4XIQ~ITXY%mA* zi6O?j+`wcuf)+TGLTKG`Wb&8ub_2(V6=e$Z5Lht9+9+cH10Mrz4yybrW9BQ^{fJmf=5tR(3&$ML3adY4hd?Tuy@P?d-FvLQ=URfQ>SL!z9Arx z$0#d0&Wr9Ol7flPGBYvrO_9|M7g|L+s<}Ax2va08FtMDkyIk<~93;d46y+3VFtYLK zm0jzpgEULFzoy@$!HHyje(x``w3?<%8ihm``BL6+Iq&vqC#fY7mo)$W?BWI@8%+Siav zB-d;tJByB36R2W^vTiW8tj6Cp$uPpRNF&q14mXpYHHSIO+)t3z84&?<*B#RvIm7yDm zO6Td#*v{WN`1`+fu@FutS8btMl~EWD*V4_diTWDcSDCl7n&Y7*(JvsI@h~Tfo>@ibjmq!7*A)*EzN_Ft?o7GpS{WzXid0--o$gHIVP;Iy$hg zj7s;$>TH4k9P^oNc>S%&Lz- zC-aoLsQp0|y6>m8_sCqqF9p(ojTjt!z`FVE6bcsNq5!IW?mN(-9+EPz>l&CDoOGi~ z9ISR3MwS(b*CiLbUbD^fpiU}*x@v<(n%b(|zVo~vty?{kPEsdqz0LE^Gn=c6ANTl| zbEprqlS%De2ZF4vvVqGE6p5X z7Hc+eM)7JHhWA_VlZz+@_s^Ks_AwVKoW3DE(}e|-i?&9y%U|B8iqGY5iWAi`IJ&lm zt_Wol>UO3V=IiM%4-8SEA=4b9uORzC@51P-F)L|5_l}jYKB_#w=sw>iS2({g>HU|f z+1sYdmql+ZU`r`7d^FT}IbXY!$~rY1re`jHUmPWLE^C_=g~nk`JyM>&W(zIw-qpC6 z{fV0A4>4kEdT&gAg(h|O${(YRh9gM;8*J*91>5A!-zWzw;w|~u?qkj}(OK(3*Yn^kZIIHO6nQxR0sYF+`-s!P1`mv$MeaD{;RcHUzhhc zLgap)CjuEuFc_n#;OO_+QK|?&_BgT^Wd+1?0YOYtPPTM=;F(_~mId`FeuBj>rT5R_ zyeXmg%cG%>SMw{cVP&iNm+yB^uQB&qOe=q#{*gix^%UTk&iyWkR!X8{t%Do~q&;IK zC!LzCEiow(QN2%c>T`#q)oskGuSKqek{xy7RDFHk`nazjs8ZumvS~m3d%vy&H+;zd zgQ%G&Q8iL^vkFoH@h4k5Ul*o_aaqc~?FW%Qwl=gVf59AuFE-r;SoD#}gLxwbQz~4L zil*1$#C8k%aAg0sJFwr#FYJJp{llIg*@&|fq2{@y=FHI~f55OYf?jZP+JQc~F4qgC z8r>dJieyPSb1C4+MlMtDD&y1g{95JcYokATyML(1Ug-&?yW?Q~{0N13=mXEqVnjB=%Z+NN`BB-yST-UZf%~Ph5~M0D^87qK>D9L>px7l?U%OZoMd(>RE3x zIcYf7Tjm5d6>o|Beiv_?ZkL-Du6H_XHXv~>nvcmpxY30@MeW%ZNqC1%-k!YO4*m8-33@@l!B&HS|H zh&RckQ>SoF8yHpXi8dcD&$VIQF~m$O$wPh#EWC=Bx0~%1dFle(3z|bBTKtZkx#;Y_ zslBhQ_3uZYi%TRMHoG#_g5m;|iLWXY(lUCsEJV^k7ITd3)n~E%&(tA7t3xY$H zfQWO?zrF^;Tbnp!qg{4TF;Y(~g9)TkkF;2(y{YHvZeh=I)zKgrWrn574&g79_Kgdf zMy|VC#tz0*VCu(1n5WJsPH;>n;bQ3y#G8kN)dvg8@VKV1m*Pe*kjjJR1UDt$G zM!&zm9D*A-X(cifXb}%HCT?z$l?NQro{(N1}Q!VOSqPmIx>Tz+Q}I91auZ@)6;gJ7S+@W3$S!BZ80b0)GNpq;f6OHM49vpn@}CaGwQ-$vMNLQ=(2b|=e}SN4v+df$dC)`b?{jpPYge}eaX{1@|Gmp9A8PS>R4SV zr>#B^X_tRug~B=NH%YIpXZ9yX`p7byUJdUlkkk+$I7 zjbNgq)~gvB18GC)sORDcsTu0xj+W4syOq(c!|j4K`@sV@_{bM=qjROSyHk+|suB)n zIqzy_hIBYC+m(H%T!P+yCSMSG<9&YmdRvwPqNkJVFo@T$wnN~TlaQo#W=YXFAsvGd zh=Fa7$g(oUn4CEC3nzzkd;5-*$l}l{g@1!ieo@NzNqXU2IGq% zrLoo4IK<++HIE(B(!dv<^A9g~73XRQU5VtDQ07}R)l}5_csoF2DY#4S`5&GYwasKt z1dr9l8B%Vro)KZcx1^n7Cn%7XSoXa5MKK1A6Mm_@)K(#N9R7Bd;aU~m(0;UnGuIXr zavRB55GmEZA|h=4$f|J_kz4e1vd%7-izWD2{<6(v$`v+!eY&@D5`mdbwkpS)(~)yM zaOXQe{VcM3(HdpRXsXHW8+Qk^jCS8JXnAYbe{=0Td;MYpcH`-+_6S%< zMQhDYRFD4>9q$%BpXuF#9lu$hzTyshwai|xBPQuHod+bTGU168Vw7gt#uhZi( zlG)yUi_j@jW+N^hfUvy;@Ic;?5(cG?e$NJyaNB46WwDkA65=Y%zGksJ>K9OyLKZ(2 zNzZNSybKU*LlfNzgwS533%AW>7wnxn^}l$&pe%Wv zEZ!HQ4o9n@3E4co^SXh(oQoIHJd~%D=eVBt@mMeoNLzN>^-zqKz+lX}=BT~38=3g( zLloehp^p2jG1%zm(UnwRYi+W?hAiUpYB|uM@oqp8_s=bBtWxe#w;oX+ZVi1pFBART z0?NdvRQr(%)^DXCU9L<86=LEaWg6P{Y3R?9(ebAz9DkB7o^2vMbbc7r%eoX2UUl}} z?n_w~k#59J%?3Gpsz(QuNln?8-_hO1HfQd?IRNtr;>jy>zLtzn4!|q;YQtRaMf9oa z9)o-yIj+2t&(g=H*XYDo1H9V0Gqa!TuvbxQ@}=rITg>3@D{|sbN?)5L8Q|MBZ*si@ z(N(JSn*F2miaztaQ=wI`6}xxn`1Yb9SBn*i)utR(i!!IIU+#TS+5Ts~x74J8BG-dx zt0B`9%u@o>xBVZ2*~)Z$sKeOgUPh}Ny}wo+*QV8T==j<#T7=xyMx&y=?=JP)j8WUW zGQOaIA^=RbQKrAl{@7*mJ`~@}@JHI>(T7p}qb&z@#!NXuUruLp{RCs|A>{rUw*GIb z-@{00T|NgMr@VKqKpfsj-Ku8?Qs} z{KH;$aezJP)wcyTeh)08BRAymM$)9CkvTIT$sL!KQA44a4}T!`IC2hHhI8y!nlHqU zF6pQ1#WnrC(+Mx^_tm5^Yo-+WpG~I{b~hjl*{A4r%FJlytv#!ufz%U54lIb; zl0^$mC^U46a{k+VXX8C6{Q_Czp)HS1Gk=SJ~w&Py))_vlQyHxRY zFaL|ZJ2jVEesrB-+VQzpZvf&^pJ^2JrDySZoCI>DZ~nfVDgo5ud9vy7FybUX!Xb4t z1>Ui=SG#tzB|)|7{C2M{YJ_KlE6bMfrRki3c5Zyr+YSEo*aQhL4Cg33%Ml**`|~EH zopnl>aM`Mt=3P;T;t(4xttN_1**|u7^fCQ~xl`irI%h*L&_hwuQxz zG&|D_>X)&F&2dF*q08kaqu5Cl=tRiVABY6t6N;F z50qvP9@Lw|k`&$ARm%y?E7M~k_brdmmc8)Ioqn7n?^fJ@Q|dVXAlxrH);ICP-}i5h1h)T6Qb7p!gA@+`=v`9@+O%`3lKy6OPV*e}5G>hYRTTGSIAC{ic=pL!t zTe`>caRs3Zoj*D_8x>WWCw7g9j=+pZy_=}TInHhMjPE-&;_(dZ5sK2-Z*}4iHI!uv z=r%qkz1mATUnqBsve-dS9D<9Y9bDX~Mu(V9Qq2aLpnb*2&j*~3X=1sQXO*ad_sYCI zCE+xixyX58b(n_r9{xxou0bnDGm6Jm+~r$R?)OC2kJE17%RUY2hzg>P$J}vOite_0 z?%ia{2AuCsO|g5D913lH;4bO9{X&Wh9GD!X)_*2Gx8LonS@0n!fGk{oSZk_7&P9fwkVP-6@^n>-vSdAc_QvyHi7^Jw3a= zM84^dl#jUB*3jo_s0u1}Wp3Y}y0u7XRdjja@eD-lBVzAPtivOF&6k?)8WEj)aXViJ z4PZ70#m08A61YjX#T`wVQqtDa19Nuir^JsRby^T-fzqLmCy(>_=|#CQX`7?IWn>jO#4 z8^S3m>4t@CM9|i0G-lt?$IlFS^`C!JMJcgeP@rF2tGwB}7P%lc4bS-fcU$Ztkhoueyv!U^pGt9^ zJ#3RQ1CKpac%>yt&82xheU!xev(@X$?BN_!8*$sT*KqECvV0GdJ)>%(w7BltEZNpg z4aHnt;)c@(b^9r-#_ayuYJW)CD)8|+8XG!~c$t}DH4+(4QH?pg9WkO3LtO4MJJ*&` zulX4}R(%4qcEYt{;ceR3PUS+CRD3)}%BO9g{sy7sMY_tF8lPS0*=K zvVxOX*#z}8xqE-?b!4SZwiIuZk$Ik3wq|kc<}Sy}>t+LQKZkJe?`~k1j2xMT!bUqg zLYWk+)e_Z%?-PsHnOAQcXWdSczi(y1X~%s6i!7Df=Cc;P60kxZdx%R&6?cA{ID6I{ zSwm9SIg@1PR^Lt1hL2Vip<(j1p1w%vMF4@DKu?a4{QGc2LZg_aTJ#ROBgd7yhh_Vl z^GfF()f9358%jCJ_f7UhARS|Jc4&t#g}D5a5~NH zPqCBVjgpiXEM{|I5`&>*#mtCx<`Y)glXD5yt#2(A;_UR(<|1Y>LVNcO;?e5?-c)Sf zI3@mJ=Dm)GQ+iXEcN3)L-p@2RGDdJy>2&se=3F6S(ssw(eI;~dS~(6;yQ71`A5aWj zHvbxGnljkhbiYDpifX`aG1FidQZ3*Gm3Xa;I-@295ccM~+{4DO^fEmYfeB!?^1AzGEQ3l8eFula&*t_)Bn#nS!Q1VgbhP4z4oBObkBCS)-i>@m`TvnKEF zC9|`_9&oJ>Rd95@2^fOsBvlzZ;96h3ViLS|?=jqm_Z8~jr zzo8e}!6k})l(An&t{ve$-LSchOEb0_T%T)W!`wP_If0Or3=14H3wj3&&%RB_KdPaw z^eOz>RXaZ8m(U)OGUH&N{y_w{ux36s{%6`k-fGLk*|MslV;v6`PRlCWV{bL<_fTvk zeAfP5-j}ZAlTRE{9da0mp>wM5aC)1t>&Z_dg9`Wl|YZN(xqX?2>XVvdt& zoRP$X4~NXPm~52AhVWC(k%N=*CDLsgUNx#Qby8Wi9>h0_@z?NU@jD6G{P9YI-BML? zpZonxPH_hW*w5c8zh_MpQi$yndkMM6E01W}{uS%(^(iRrBv0?0`@Rs>!R#pF`g71! z9S=AY$+FbAKM@5UPurt^Y6$#y z)I%;o8f-WtThmTJ$X`_E<72k9H$g$2{!E!QZ{vj( z&+#MG`;o$(usuOn-}aL>5-Qh}j>teZWhDzkR>6`x(YnQlZC&B9(KB~m5LwLG#KhzJ zuiJ%#?gL9ftU-Q(YTp-iEyoQ_klau;tIIvzF$4A?{K&+r`3V8{HxI)pisjRiJ6DpY zhrP267qw_JxQNTf1(VU*dtC@y?Ls^a=r&}rb!zO<8v#%Kg&_sA@%?GmfwAspF-Tld zg@v&Zas*c|+xG3?z#{s%3X|oqRA{)Rkn<+tzi1Cw7JUJ!%!xLz27wk_}ehLvt~=4EbX+Z$&|@ zvo{7~Olk_R?uNw(;kcB3xFl4s)eC*ze{$bUW-3AW8_H#*+8MZGIgnV53hpaEZwXOYXwoyK?dweN8S!f0ywVb4ORnu?un@$m$S~b8WuG%5tRVH~eM!YmDy zlyUks;9-x&na|!t$2yXm7=~{D1JWi-xKm^MJkz4YtMrH{{&vtqIF@8etWh`QAbJxV zvoSXV3zwWeC~0aiQk~ury_=A?Mv~leKij_2p z+*tp3^yv&M@*&crJcqdEH`a$Tz}xQcx`S^sT%c|!2zq9b{5W)yad<3cJaP{Az~%Zz z*RdW|w%E=Xc((6>Gme@x`rg`3VWb~=SS6o$DcdqH`fVN0;)P&;hUGD;@v|{RiT?PP z(O*)}Re!_d-F|Xdgy$Z~1&fUpxlo~{Tq!%^H6>Zs7Be3;A@D*juHW9@US3`{r%S*O z=ka*j_FmPT{o!A`8guSW7$~FR-whh(3-9-OnWq8L%u#LDA`Td_ z|5K`?M6gXJgXxgWyr7nQH*bvAcP%DZHr=vOxLSVs7I74HK7H*VkYMtx*c0A780v*%Z7@g2JD?>B;a8)#XSnJv&RcM9t{9Ebbi z@+K|0fXM9v7x7Oz`O{#Hz13Kzs_Ie~`@8vD662L*J*s)i(aL-L7k+YxqFUvPVTB#I zRItZ$W8Hu^d$_qSs%RU!LB&}ky^V`_zmZFVND3P8lQnzLiE|^Yt;d$%;gO`P&2a6F zXw_vR%awl>!?byP0p!^J0i6Z?sT}RATr_g?n^=8!_P7NHqY5Vx!dF^qle)ev@8oJ> zLh=j}r*pKdGLLFw#zG}t?_SrUkuTk5Gh^=_zeyix&M%qgi=4wI=pBmR(yyWCvn34A z7cMWdwn8ANg>gC0p-mgQ)tuL1+N^iv<8O$^Eyp<~!&D@{jY7pL{=E0%lFbfa9&3I+U$tW#7TE6l zg?7we--$ZjkIc`Nj9XPwypN86(-^N%;l-;wuFJ&{GtBu{bn!*;^m~12GUAPV(;No) z+RvF#b(wOD5)S3iow;5_+9((X@!z;N3$iQ_Omk73d=GO(rR8j+ZJr8Uoo@GT6SJK9 zFk8nwl&{)O71r#IJa}FU+28OIbEsmP%dtbC__;4TXNLrKTy!3hpHRA{tYrl%nzvS5 zr7e;QtOF*IAqi))t6%{K$=#<(@|oUDPepU z`<6e}fAlJ)KqHK{o~Uc?tAFOo^#p^xf<|f3GM!E-!8T;yRH;}ox}U2Fqs9nrr*eUx znYDNTTS;U1-J5##l$vL%uB^Nx&sZMi(v%D1RcxR_bU4q&pA*YQ>!XB)aA=A>XMY#s zmBD09$oMQ#g*11Mt~W|0d)=)iv$Tut?x}IBrMw51a7)(|qs0+?eg`WqON0Hc_qb zmK<0JpeoAbBZD&I2W-c*JlBOwbT?{E}ur`UGLr(cK;>Kkt)rAm)G(@t4Oj?i1 zYdK>!kk&h$Fla0j68<|3=SHkax$)6Y5CRY#TG)$^b5uQP`F<2$yH?I$Qyi{`N4_0n zO?fObe3&%|J7r1SSN#4V6Op|6!zknirX-c~bLf&79eL@ zd}S6hGo6x?88V_!r$lVmc}e^3uk1)f(gHaR9Njp;-?=MlTOEBYazRwr(FXs;h~nO* zG#bQp+X}~c@7RPgyb_o zX#?} z$cRFfkX+zaRB(BWpS!P>YH{sC`X-wDopi{zDyGFuBl5bx^6^rVl*K~1(mP$vwy>hG z4$;cr9)g6{aa{h{hmPE(k|XaYNw#hQn&f$zsv&m!Qw1C|VU=dGC_B#|CIwmy zRv*I1-jx6N`a;GYKa8!EEvfx=p1BEw;u^f_4$aqN9L#V=9Kf{k6Cx~ zJYG^Lg@1`*Z0&f`6$Ug8neMNpVYlfmW4|9=&4Pldr?PK8{3V}oJ63d{l?w#PTk9Ci zv_C!uI^S%ijKNkN?ykqO$zCG-p8ikChch02f27RyzRwWRX2Zm!ypn7=Vu^j}` zrc>Nv!1A?nPvl(NXf9%t^TR2Y(=}*2@g|p|)Lfn}0N<4Y|2AE7iv33* z9FBFOIb1|3ov2h)&r|KUq2Y_X0(JBk>~=hA9*=<9h$NNDN~LQzt50fbq*({Ib@kob z?Uj@JbiQzLYaLP@=|Gw>iluhSijrA^ok`zF1wQn+4|ga(bF&5K*Y#&XGoKG8gH|ohf?Dh60nU$~zD3x8d>K$Jwj!^~;mB%VTK|&9U~V zUj+1%nhL6ws%?R5JF%&CNP0ELx?1O9b0v~=2dK#+i6l-e!T!ZAdqY)t;j4=eJZV5S zUC7#1w<@aGfiO=(pCxJM7GIT&S?$v!g%9jw_a&h3+F zYCY@Eg5lg+l}gwA@w7%x&73xKe~-6TBDKyfFb0cf2~}=q9){i4!mDkn{}-op?~YD+ zb?aS{XKvJCysm$;2<_Z2hZQSJ>Ivx9)2K&k#nRQ9^Ky8g(u*+cz5!>hvo*kX$>A_3 zd$r4(P!w?Ggv>wa9$8KE6#NMp{70t9--gpTOyKnWvx{)~`px?3`}6Alwkz$Db;#By zd#f21O5F)b&H`Gcsw6Diaa(J4Ik|gm%PX1trx))+&eDn2=$@6Mh>CPew zB2cw-7U3Du)xCtg6({1z;d2fE0RhpS@87G#@b2pVYR~C%47WfGV3p}@JLO2NAG>rV z^9wpq(-SIR`b8v2l_AoIX*QF+QHosEj}dY^8r072u##Tx015!)b zsn%4gRlX_=IMe=V0+0YDVcj98!-_4y7+!UP`E6a5Is%8+Z!SQ8|7`Ce{kLX31qJfU zSEplmejMG{?N)DVsb~2CwAttk(OTN^QXQ9iAE*IDstffj`t)4ZSvb32p8(IfJPnDx9sl{O zOxfeMzOUUr369yQ!+fPrqmS-WG8YvZKsM1MMYR^d)X|L6dfqvMm)Q{7wExB8j z${<_kWO=GjzvyQVO0XhZ<^ReAX&2b$kd=~g<8RAXuv`t%si za+pHCQ^h*a|Nj2%ty`2K<{IEAH8+cLtm-nVKMvLTr@Qm~dtvw~owuvL{{<8H+{;T- zg4?g}JPgCrPm5egE#ismJQW?yPQN%@P1+vYOSS$>>pYU=m(G^$JbIBapGJZ2|L*_4 z`04Q3`RYIYUz9%n)7LlF+xsaECcV5JmEfP;&)?&sHfm#;ye{!yx( z=1iJ1)4SxU)*d`h_u%ngf{uf!|m>`7|#8>=jW~O zz<>1rhi@T${|VWzU%v4eJpb5N-mxF-osPDZ+3Pd!gY9q0tXKMM+wFHkqB8v_k==gO z?TXH7zGts*TswWdTI`4SDs=-=Ctsn=PT;5lr^ng-0{Hm#-3Q?G{fn!gvh`+YaQFVu zI9%68I^}kB9Uc*uz!r-?Jm&EJ{oQvVn-8f4a&T?mXWI{9LjFg6RPQyF4g|h6PxGHTUd-(2r8-B-)wOCqptNW$K zpedxH2}Pe}nCMCkSM$C6{>kp{+l#8$Y@NO32^@)~7ScQjyXW-@7@bt#5Do5cM_HuQ zZ@cF1%xSoPyZs4hcYELRa{piW@q)jct&brRf1*-IDkCK-nKD!=mm0u0J%%%8t7WLT z4VBtW=OC4!wSYnbl&(?>ZMU^k=V#&8kKSy+w?BI}{*=kOy97HcYrju;mjM3cm;uN9 zBiDX;e5i}z$m!Coh~a9$-+uSx+wlH3``;+uQti||3n-BY(%dDA;+Ezz44K~tA_L`7;D)bE6f zFQoOBZkzl2#n%8a@~d3)b>H6nzuzR`zo<-NZzb_f`tK6_iZgp-cUxU|YnB*Te;t%u z!?X5UZ`SJ2C|C9dbw-4?-zhC}z$mTOtvnWp~q^@3Gmj4cq z=xvbU$?yK;Cj97c{`O4U>$P3Bds`2G5>S_l7UF*>YI-H9N1<>1N2v6x#*I?D?aUi+ zxSYP-XB(Y;nC|C(v3vbSvyi|~+&j~3%ZJY8nwv$a_GY6+W;44sE7>{SZ@mw$adCk$ zBCIK@nQ)S#k}%RZ+T?WJ7en(<@S)KA)G`e>pl110I`fjA%^_5l{{e?%eQW69=D4=c z9}ixKPkng2rhndfb^j4)_gBA2>i(XtQ|XmeI^GRJjkh7^>r39BUFi%r$Z+}T?D~`2 z$^U^d>;Hmgdq{J9ab?=p614S;F)Y>1Zg0Uj%MYac?YVta=kigh@%>+(egOXSa|0CD zUAGq;l*69c3O2GWD{9~Qn;)J$o9850<*H-WE7Oh9@ii|xhKrJRW7P zYn$?o-li2dfA(kwnX6u<_i|=$KHcHkwH8nYu>xe-)&kkBRtp za~uf|%y>L)<1))2ZKF`qE@P&enIbsb#Qj}@20_>jq zX0@wzbnRdt)`97i0S(PUP1O*tM|>(Ze!=81;|(G_VDj6ydLVkBCR3XET7#+zq$^fH z)0XO&s>PH_jh$PV)7CShYCnKbNyxcNGgL%XWk%LR=~YH#ii+>xQZh`tM#WOc1`+<7s5N)gb9zZObe6?NtI+QeT9&^J!A8>1?T3?D0KfN}nJHs&D3JLQ8#AOf`dU9mo`6_I6_$l3KI{lBzb) z^&qZvt7V*z)xDs%iI&o>-?u3PEm{hp@pKX>32TUJ&N8guU!y+6HkX7HqY85%6@@lZ zY(uh0p@VhO&g~+gL&gX-oj`UjKuvuH`<3prb1uTr!-vJRCI|c5@j+7g@XR6XKfazu z6#y{2oD6E$YU`xq(ao%7XsypcO<$<=BC^RWKusha^=%ku-FnP;i?@T^|2!E()mNaI zpPSBoQswNdfmF0egsjr*(0Htvrnb^-71NKuc=g*sG6*xK$eIB_O_FvBD5Wfg%&3Q; zK1D_7C`Ok-ZLw%Jmafp9JBU*Hl9ZzcqB?-hu~-=qN3M)cT^v`bRYZOu;-hcBdynVQ z?b`>QlWYP2)o445MN!i;Nap~wuB6%OQwMd_n{e`BE|!{bJhd1>ky`U0auj``s%WkFUS@*a5;FH&(K~1Tud_%|&W8 zU0>vpDnaA~6ge4#RIk!SI@8j#bY^sa8%4$(XyY1*qC%t)ZGxaBKW$@diq2F)V!k@? z1u8&_q(Fn|T`4q*in6l;>)spnA*RHVQkzR*4y48?N(MDYyhx!Jzc`slE&=KE>2cdh z+a>{AfHadz&m=?Kg0_bjq(-H#j+fe4VBC}$aW^&AWO_PXJMG7)92}p*Hl71`%trpG4!}4(1xq~QCm7I-P3nBuD>exmH zjVOtX$jeUdSCc@|fCl%@+s4hSIx*#rm?kgQZSO~aZ4wv{@TF^6G}ZRliw z98V!xl3fo*OJ=45Y1&ZJHDr~JU)3o+{+t>?P4>v1-3AxdVskKTZ<$1TkewaOReAvv zD3Ue#N+CchZk;F8rjB8=4l+885<&R*0(tsE?HVF*TtGQfO1AuDBg z6Q!hQ;jN$j@Xm`>K^5juM1mwVevi0gSoCznRWhW8m6{d>07!*M(8Nfg&(f3(RlBXl z&McW_sQZ50k97tBC-ybywJb|T)2>tvA`wW1E8QLSErw}2S{J9-^biB=qh5Cjk!U5t`JHzc`A?iPg|M!V%I zqLZM+K!lAb84-pKqeMuQ>O>DYED6&)pImJB`Zv(8{vAZB$Pgkis5uDtO$xC98DT7n zY^G3Ci3&B9w4Ks)nQHnCqnNiL=lqKt+r@Uj-s2;q&BNN4UhaN;zWhJ%#qq}HN~)Rd zQ;AO@RVP*ug@`Hv+Dj;BUD46`J2KS%@O0mQ?bI?^aQzvT_U`^EsAO(7IaN{~*7^h- z-)_$JOa}eRkMGvonqlqhrLlUeh~`jo&Gm|Dtaa@lpFOrPyxgALdAhN=rB7cJ?ZdhI zAvpU5aaTONlVR@L3z>8alzdrlA&-dDUS{N+FTS?s^obTT@NA-HJO^v7#iq&Xa%{$||9Ba<8 z2Yu#f_nDh7L7Kk?lF#A3N~NjtOqD8#l#!KhTY8Od>={XbNF zq-EKSy+>|%bchmxq4z_QQ|1EnC;Vf8!sU zX=pR6lzsWB&l9bh%Y7BIvreL@gN}PX|FFJN^j=@~pMu-|>?7;d=e`$Rc=2lakL}bN z);(>vpAS-xT-_Pvj9}T6VY2X35kqU@>mp#R4xp$0OY64Zr(db`Yjx~~H*!A~4}49R z7twcqFy(q&{XJ-x-A`{j)tWvl&Z@W{>&dH3->rVS(gm5miJE_9#DcQ!_u<()|J`lx z>EKYTFA&0CZ0>I!Bhpd5ZVVf(!EC6j0HM~=`fw4e-)65FuDDq-tnkA_j|aRJfypI6 zx2C3IHf#M6Az6_tR?$ywtG(*sUGV;L_uta#+zcD@DQ{o(!QIjwe2Tk0mL zKE~OZttFgV!vtd)`>+~^|HSR?F4Aux;4H1d`ivA&5LG9M1TM%obVTpn}2j#2UE z>tM#qpAtc~mxvZ-D@kVo841vKcD+RHtvncQDm~ruU<%3M>dP0$rT4#i{b*`uia%46 za9Q1&f{Y;MQp~>22OoWUac!}5!Rle~>n}dmz3=)84BcNWUB$=fB5OOvcEs8;2N|(2 zGiRu|_?2##>@ey09|9;N>O?lx978gqzM8ih2XIb)W9H<;&)*pz zzI&&dU3LzZmtf~`p{C|^=E4S4tV7$Bq+(56DqYO~EXkoprnx$XN;{B8&sB0*#$HXo zgY{fDzwmPEK68J!_c!3T?p=6opM<;Dt+#P~ja0QEV2D1M%AqU8!57~8_ii}4 z{P(X{^uEOC-*kIZ?&XW)(c#U_{^2;j(bL}r@-kpKO|7QWR0T>(2-oAO*REX>nE`rG zu}6m3R~I3h_r~d7JGR|_L5||)(tMn`Hd@oFA64q>>6FTH9#SVrNu*R2P=O)|(TdfH zJ~e8aS*cNmZV3G!h+ZAbjqy(z-Tl`TzO9UbvwlRc^=9(f>0lZp6H0Yc8P$j^pnoe- zj4ot|XLdz48$s9C|EadS(2*0RIg~)Y1@i5R@pos z40qCE8Ikm22YhH#dZT8L$NA;ZoWGL$>2x^lcfa`fvuB6!dt}`kW#})qUk&7;tm8FR z$(f_jr{>c{g$2(potb0=)(xn`JCd8^X6yco(C^omFPI=-5$)e#PCm1L1%BW`HM%;u zE}b0L4??qjUaa)PJ(2{;@%(M-~hrA@U=qSmw^D>+Vjvwa9-+^(a$NqXsF2lk&%--3f@dtcFi4eGwS zM*lQV^bz%byS)f4a{{u^AAXtiQP+VAd@`dnR>r&;<9?jsFcAM}+ z?^F*?+`TpzMUVr88%KqLEZ~L=*{&Q}wE_(7eF~N}cjh}j_vl_|g aAL(q@0y2-#_m~ftf49&382j&k9b*7b1_(s} From 3713cf1ebd08705c1c7717e3da9cd12902549d46 Mon Sep 17 00:00:00 2001 From: Rishad Alam Date: Tue, 30 Jun 2026 16:44:14 +0600 Subject: [PATCH 3/6] refactor(sensei-lms): drop unused utilities arg from action filters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Sensei action filters never use the utilities argument, so stop passing it through Hooks::apply — each filter now receives only the default response, the field data and the integration details. --- backend/Actions/SenseiLMS/RecordApiHelper.php | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/backend/Actions/SenseiLMS/RecordApiHelper.php b/backend/Actions/SenseiLMS/RecordApiHelper.php index dca7e2ed1..5c8f15405 100644 --- a/backend/Actions/SenseiLMS/RecordApiHelper.php +++ b/backend/Actions/SenseiLMS/RecordApiHelper.php @@ -37,61 +37,61 @@ public function execute($fieldValues, $fieldMap, $utilities) $fieldData = static::generateReqDataFromFieldMap($fieldMap, $fieldValues); - $mainAction = $this->_integrationDetails->mainAction ?? 'enroll_user_in_course'; + $mainAction = $this->_integrationDetails->mainAction ?? null; $defaultResponse = [ 'success' => false, // translators: %s: Plugin name - 'message' => wp_sprintf(__('%s plugin is not installed or activate', 'bit-integrations'), 'Bit Integrations Pro') + 'message' => wp_sprintf(__('%s plugin is not installed or activated', 'bit-integrations'), 'Bit Integrations Pro') ]; switch ($mainAction) { case 'enroll_user_in_course': - $response = Hooks::apply(Config::withPrefix('sensei_lms_enroll_user_in_course'), $defaultResponse, $fieldData, $utilities, $this->_integrationDetails); + $response = Hooks::apply(Config::withPrefix('sensei_lms_enroll_user_in_course'), $defaultResponse, $fieldData, $this->_integrationDetails); break; case 'withdraw_user_from_course': - $response = Hooks::apply(Config::withPrefix('sensei_lms_withdraw_user_from_course'), $defaultResponse, $fieldData, $utilities, $this->_integrationDetails); + $response = Hooks::apply(Config::withPrefix('sensei_lms_withdraw_user_from_course'), $defaultResponse, $fieldData, $this->_integrationDetails); break; case 'start_course_for_user': - $response = Hooks::apply(Config::withPrefix('sensei_lms_start_course_for_user'), $defaultResponse, $fieldData, $utilities, $this->_integrationDetails); + $response = Hooks::apply(Config::withPrefix('sensei_lms_start_course_for_user'), $defaultResponse, $fieldData, $this->_integrationDetails); break; case 'complete_course_for_user': - $response = Hooks::apply(Config::withPrefix('sensei_lms_complete_course_for_user'), $defaultResponse, $fieldData, $utilities, $this->_integrationDetails); + $response = Hooks::apply(Config::withPrefix('sensei_lms_complete_course_for_user'), $defaultResponse, $fieldData, $this->_integrationDetails); break; case 'reset_course_for_user': - $response = Hooks::apply(Config::withPrefix('sensei_lms_reset_course_for_user'), $defaultResponse, $fieldData, $utilities, $this->_integrationDetails); + $response = Hooks::apply(Config::withPrefix('sensei_lms_reset_course_for_user'), $defaultResponse, $fieldData, $this->_integrationDetails); break; case 'start_lesson_for_user': - $response = Hooks::apply(Config::withPrefix('sensei_lms_start_lesson_for_user'), $defaultResponse, $fieldData, $utilities, $this->_integrationDetails); + $response = Hooks::apply(Config::withPrefix('sensei_lms_start_lesson_for_user'), $defaultResponse, $fieldData, $this->_integrationDetails); break; case 'update_lesson_status': - $response = Hooks::apply(Config::withPrefix('sensei_lms_update_lesson_status'), $defaultResponse, $fieldData, $utilities, $this->_integrationDetails); + $response = Hooks::apply(Config::withPrefix('sensei_lms_update_lesson_status'), $defaultResponse, $fieldData, $this->_integrationDetails); break; case 'reset_lesson_for_user': - $response = Hooks::apply(Config::withPrefix('sensei_lms_reset_lesson_for_user'), $defaultResponse, $fieldData, $utilities, $this->_integrationDetails); + $response = Hooks::apply(Config::withPrefix('sensei_lms_reset_lesson_for_user'), $defaultResponse, $fieldData, $this->_integrationDetails); break; case 'grade_quiz': - $response = Hooks::apply(Config::withPrefix('sensei_lms_grade_quiz'), $defaultResponse, $fieldData, $utilities, $this->_integrationDetails); + $response = Hooks::apply(Config::withPrefix('sensei_lms_grade_quiz'), $defaultResponse, $fieldData, $this->_integrationDetails); break; case 'create_course': - $response = Hooks::apply(Config::withPrefix('sensei_lms_create_course'), $defaultResponse, $fieldData, $utilities, $this->_integrationDetails); + $response = Hooks::apply(Config::withPrefix('sensei_lms_create_course'), $defaultResponse, $fieldData, $this->_integrationDetails); break; case 'create_lesson': - $response = Hooks::apply(Config::withPrefix('sensei_lms_create_lesson'), $defaultResponse, $fieldData, $utilities, $this->_integrationDetails); + $response = Hooks::apply(Config::withPrefix('sensei_lms_create_lesson'), $defaultResponse, $fieldData, $this->_integrationDetails); break; case 'create_certificate': - $response = Hooks::apply(Config::withPrefix('sensei_lms_create_certificate'), $defaultResponse, $fieldData, $utilities, $this->_integrationDetails); + $response = Hooks::apply(Config::withPrefix('sensei_lms_create_certificate'), $defaultResponse, $fieldData, $this->_integrationDetails); break; default: From 4791022abc32f39d76ce2bd10327339c683116e4 Mon Sep 17 00:00:00 2001 From: Rishad Alam Date: Tue, 30 Jun 2026 17:18:26 +0600 Subject: [PATCH 4/6] refactor: front end prettier format --- .../SenseiLMS/SenseiLMSIntegLayout.jsx | 27 +++++++++-- .../AllIntegrations/SenseiLMS/staticData.js | 48 +++++++++++++++---- 2 files changed, 63 insertions(+), 12 deletions(-) diff --git a/frontend/src/components/AllIntegrations/SenseiLMS/SenseiLMSIntegLayout.jsx b/frontend/src/components/AllIntegrations/SenseiLMS/SenseiLMSIntegLayout.jsx index 356fe3a53..45ea7d32f 100644 --- a/frontend/src/components/AllIntegrations/SenseiLMS/SenseiLMSIntegLayout.jsx +++ b/frontend/src/components/AllIntegrations/SenseiLMS/SenseiLMSIntegLayout.jsx @@ -21,7 +21,12 @@ import { senseiLMSStaticData, statusActions } from './staticData' -import { generateMappedField, refreshCourses, refreshLessons, refreshQuizzes } from './SenseiLMSCommonFunc' +import { + generateMappedField, + refreshCourses, + refreshLessons, + refreshQuizzes +} from './SenseiLMSCommonFunc' import SenseiLMSFieldMap from './SenseiLMSFieldMap' export default function SenseiLMSIntegLayout({ @@ -131,10 +136,20 @@ export default function SenseiLMSIntegLayout({
{courseActions.includes(senseiLMSConf?.mainAction) && - resourceDropdown(__('Course:', 'bit-integrations'), 'selectedCourse', 'allCourses', refreshCourses)} + resourceDropdown( + __('Course:', 'bit-integrations'), + 'selectedCourse', + 'allCourses', + refreshCourses + )} {lessonActions.includes(senseiLMSConf?.mainAction) && - resourceDropdown(__('Lesson:', 'bit-integrations'), 'selectedLesson', 'allLessons', refreshLessons)} + resourceDropdown( + __('Lesson:', 'bit-integrations'), + 'selectedLesson', + 'allLessons', + refreshLessons + )} {quizActions.includes(senseiLMSConf?.mainAction) && resourceDropdown(__('Quiz:', 'bit-integrations'), 'selectedQuiz', 'allQuizzes', refreshQuizzes)} @@ -146,7 +161,11 @@ export default function SenseiLMSIntegLayout({ staticDropdown(__('Status:', 'bit-integrations'), 'selectedLessonStatus', lessonStatusOptions)} {markCompleteActions.includes(senseiLMSConf?.mainAction) && - staticDropdown(__('Mark Complete:', 'bit-integrations'), 'selectedMarkComplete', markCompleteOptions)} + staticDropdown( + __('Mark Complete:', 'bit-integrations'), + 'selectedMarkComplete', + markCompleteOptions + )} {gradeTypeActions.includes(senseiLMSConf?.mainAction) && staticDropdown(__('Grade Type:', 'bit-integrations'), 'selectedGradeType', gradeTypeOptions)} diff --git a/frontend/src/components/AllIntegrations/SenseiLMS/staticData.js b/frontend/src/components/AllIntegrations/SenseiLMS/staticData.js index babf646c1..b0c688c3d 100644 --- a/frontend/src/components/AllIntegrations/SenseiLMS/staticData.js +++ b/frontend/src/components/AllIntegrations/SenseiLMS/staticData.js @@ -1,18 +1,50 @@ import { __ } from '../../../Utils/i18nwrap' export const modules = [ - { name: 'enroll_user_in_course', label: __('Enroll User in Course', 'bit-integrations'), is_pro: true }, - { name: 'withdraw_user_from_course', label: __('Withdraw User from Course', 'bit-integrations'), is_pro: true }, - { name: 'start_course_for_user', label: __('Start Course for User', 'bit-integrations'), is_pro: true }, - { name: 'complete_course_for_user', label: __('Complete Course for User', 'bit-integrations'), is_pro: true }, - { name: 'reset_course_for_user', label: __('Reset Course Progress', 'bit-integrations'), is_pro: true }, - { name: 'start_lesson_for_user', label: __('Start Lesson for User', 'bit-integrations'), is_pro: true }, + { + name: 'enroll_user_in_course', + label: __('Enroll User in Course', 'bit-integrations'), + is_pro: true + }, + { + name: 'withdraw_user_from_course', + label: __('Withdraw User from Course', 'bit-integrations'), + is_pro: true + }, + { + name: 'start_course_for_user', + label: __('Start Course for User', 'bit-integrations'), + is_pro: true + }, + { + name: 'complete_course_for_user', + label: __('Complete Course for User', 'bit-integrations'), + is_pro: true + }, + { + name: 'reset_course_for_user', + label: __('Reset Course Progress', 'bit-integrations'), + is_pro: true + }, + { + name: 'start_lesson_for_user', + label: __('Start Lesson for User', 'bit-integrations'), + is_pro: true + }, { name: 'update_lesson_status', label: __('Update Lesson Status', 'bit-integrations'), is_pro: true }, - { name: 'reset_lesson_for_user', label: __('Reset Lesson Progress', 'bit-integrations'), is_pro: true }, + { + name: 'reset_lesson_for_user', + label: __('Reset Lesson Progress', 'bit-integrations'), + is_pro: true + }, { name: 'grade_quiz', label: __('Grade Quiz for User', 'bit-integrations'), is_pro: true }, { name: 'create_course', label: __('Create Course', 'bit-integrations'), is_pro: true }, { name: 'create_lesson', label: __('Create Lesson', 'bit-integrations'), is_pro: true }, - { name: 'create_certificate', label: __('Create Certificate for User', 'bit-integrations'), is_pro: true } + { + name: 'create_certificate', + label: __('Create Certificate for User', 'bit-integrations'), + is_pro: true + } ] const userEmailFields = [ From a68bd3acc7446a5d1972810d4235499aeb939031 Mon Sep 17 00:00:00 2001 From: Rishad Alam Date: Tue, 30 Jun 2026 17:30:49 +0600 Subject: [PATCH 5/6] fix(sensei-lms): rebuild edit-mode config, harden auth and static callbacks EditSenseiLMS now rebuilds the non-persisted senseiLMSFields from the saved action and repopulates the course/lesson/quiz dropdowns on mount, leaving the saved field_map untouched. Add a .catch to the authorize request so the loading state resets and an error is shown on failure. Declare the refresh* route callbacks and generateReqDataFromFieldMap static to match how they are invoked. --- backend/Actions/SenseiLMS/RecordApiHelper.php | 2 +- .../Actions/SenseiLMS/SenseiLMSController.php | 6 ++-- .../SenseiLMS/EditSenseiLMS.jsx | 36 +++++++++++++++++-- .../SenseiLMS/SenseiLMSAuthorization.jsx | 25 ++++++++----- 4 files changed, 55 insertions(+), 14 deletions(-) diff --git a/backend/Actions/SenseiLMS/RecordApiHelper.php b/backend/Actions/SenseiLMS/RecordApiHelper.php index 5c8f15405..96d5ab0dc 100644 --- a/backend/Actions/SenseiLMS/RecordApiHelper.php +++ b/backend/Actions/SenseiLMS/RecordApiHelper.php @@ -106,7 +106,7 @@ public function execute($fieldValues, $fieldMap, $utilities) return $response; } - private function generateReqDataFromFieldMap($fieldMap, $fieldValues) + private static function generateReqDataFromFieldMap($fieldMap, $fieldValues) { $dataFinal = []; foreach ($fieldMap as $item) { diff --git a/backend/Actions/SenseiLMS/SenseiLMSController.php b/backend/Actions/SenseiLMS/SenseiLMSController.php index 51deb4dec..164704f0f 100644 --- a/backend/Actions/SenseiLMS/SenseiLMSController.php +++ b/backend/Actions/SenseiLMS/SenseiLMSController.php @@ -29,7 +29,7 @@ public static function senseiLMSAuthorize() wp_send_json_success(true); } - public function refreshCourses() + public static function refreshCourses() { self::isExists(); @@ -37,7 +37,7 @@ public function refreshCourses() wp_send_json_success($response, 200); } - public function refreshLessons() + public static function refreshLessons() { self::isExists(); @@ -45,7 +45,7 @@ public function refreshLessons() wp_send_json_success($response, 200); } - public function refreshQuizzes() + public static function refreshQuizzes() { self::isExists(); diff --git a/frontend/src/components/AllIntegrations/SenseiLMS/EditSenseiLMS.jsx b/frontend/src/components/AllIntegrations/SenseiLMS/EditSenseiLMS.jsx index 28107e267..cfb6749df 100644 --- a/frontend/src/components/AllIntegrations/SenseiLMS/EditSenseiLMS.jsx +++ b/frontend/src/components/AllIntegrations/SenseiLMS/EditSenseiLMS.jsx @@ -1,4 +1,5 @@ -import { useState } from 'react' +import { create } from 'mutative' +import { useEffect, useState } from 'react' import { useNavigate, useParams } from 'react-router' import { useRecoilState, useRecoilValue } from 'recoil' import { $actionConf, $formFields, $newFlow } from '../../../GlobalStates' @@ -7,8 +8,15 @@ import SnackMsg from '../../Utilities/SnackMsg' import { saveActionConf } from '../IntegrationHelpers/IntegrationHelpers' import IntegrationStepThree from '../IntegrationHelpers/IntegrationStepThree' import SetEditIntegComponents from '../IntegrationHelpers/SetEditIntegComponents' -import { checkMappedFields, handleInput } from './SenseiLMSCommonFunc' +import { + checkMappedFields, + handleInput, + refreshCourses, + refreshLessons, + refreshQuizzes +} from './SenseiLMSCommonFunc' import SenseiLMSIntegLayout from './SenseiLMSIntegLayout' +import { courseActions, lessonActions, quizActions, senseiLMSStaticData } from './staticData' export default function EditSenseiLMS({ allIntegURL }) { const navigate = useNavigate() @@ -20,6 +28,30 @@ export default function EditSenseiLMS({ allIntegURL }) { const [isLoading, setIsLoading] = useState(false) const [snack, setSnackbar] = useState({ show: false }) + // On edit, rebuild the non-persisted field definitions from the saved action and + // repopulate the resource dropdown options. The saved field_map mapping is untouched. + useEffect(() => { + const action = senseiLMSConf?.mainAction + if (!action) { + return + } + + setSenseiLMSConf(prevConf => + create(prevConf, draftConf => { + draftConf.senseiLMSFields = senseiLMSStaticData[action] || [] + }) + ) + + if (courseActions.includes(action)) { + refreshCourses(setSenseiLMSConf, setIsLoading) + } else if (lessonActions.includes(action)) { + refreshLessons(setSenseiLMSConf, setIsLoading) + } else if (quizActions.includes(action)) { + refreshQuizzes(setSenseiLMSConf, setIsLoading) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + return (
diff --git a/frontend/src/components/AllIntegrations/SenseiLMS/SenseiLMSAuthorization.jsx b/frontend/src/components/AllIntegrations/SenseiLMS/SenseiLMSAuthorization.jsx index 81be9a685..631c13ab2 100644 --- a/frontend/src/components/AllIntegrations/SenseiLMS/SenseiLMSAuthorization.jsx +++ b/frontend/src/components/AllIntegrations/SenseiLMS/SenseiLMSAuthorization.jsx @@ -19,17 +19,26 @@ export default function SenseiLMSAuthorization({ const authorizeHandler = () => { setIsLoading('auth') - bitsFetch({}, 'sensei_lms_authorize').then(result => { - if (result?.success) { - setIsAuthorized(true) + bitsFetch({}, 'sensei_lms_authorize') + .then(result => { + if (result?.success) { + setIsAuthorized(true) + setSnackbar({ + show: true, + msg: __('Connected with Sensei LMS Successfully', 'bit-integrations') + }) + } + setIsLoading(false) + setShowAuthMsg(true) + }) + .catch(() => { + setIsLoading(false) + setShowAuthMsg(true) setSnackbar({ show: true, - msg: __('Connected with Sensei LMS Successfully', 'bit-integrations') + msg: __('Sensei LMS authorization failed', 'bit-integrations') }) - } - setIsLoading(false) - setShowAuthMsg(true) - }) + }) } const handleInput = e => { From 82430723e95d896c5ad0e7fdd98310d9635318db Mon Sep 17 00:00:00 2001 From: Rishad Alam Date: Sat, 1 Aug 2026 12:03:17 +0600 Subject: [PATCH 6/6] refactor(SenseiLMS): remove authorization logic and integrate Authorization component --- backend/Actions/SenseiLMS/Routes.php | 1 - .../Actions/SenseiLMS/SenseiLMSController.php | 6 - .../AllIntegrations/SenseiLMS/SenseiLMS.jsx | 6 +- .../SenseiLMS/SenseiLMSAuthorization.jsx | 135 ++++-------------- 4 files changed, 28 insertions(+), 120 deletions(-) diff --git a/backend/Actions/SenseiLMS/Routes.php b/backend/Actions/SenseiLMS/Routes.php index daefc34a2..d63c7f576 100644 --- a/backend/Actions/SenseiLMS/Routes.php +++ b/backend/Actions/SenseiLMS/Routes.php @@ -7,7 +7,6 @@ use BitApps\Integrations\Actions\SenseiLMS\SenseiLMSController; use BitApps\Integrations\Core\Util\Route; -Route::post('sensei_lms_authorize', [SenseiLMSController::class, 'senseiLMSAuthorize']); Route::post('refresh_sensei_lms_courses', [SenseiLMSController::class, 'refreshCourses']); Route::post('refresh_sensei_lms_lessons', [SenseiLMSController::class, 'refreshLessons']); Route::post('refresh_sensei_lms_quizzes', [SenseiLMSController::class, 'refreshQuizzes']); diff --git a/backend/Actions/SenseiLMS/SenseiLMSController.php b/backend/Actions/SenseiLMS/SenseiLMSController.php index 164704f0f..30e8d0d6e 100644 --- a/backend/Actions/SenseiLMS/SenseiLMSController.php +++ b/backend/Actions/SenseiLMS/SenseiLMSController.php @@ -23,12 +23,6 @@ public static function isExists() } } - public static function senseiLMSAuthorize() - { - self::isExists(); - wp_send_json_success(true); - } - public static function refreshCourses() { self::isExists(); diff --git a/frontend/src/components/AllIntegrations/SenseiLMS/SenseiLMS.jsx b/frontend/src/components/AllIntegrations/SenseiLMS/SenseiLMS.jsx index c25b778c8..05c86b817 100644 --- a/frontend/src/components/AllIntegrations/SenseiLMS/SenseiLMS.jsx +++ b/frontend/src/components/AllIntegrations/SenseiLMS/SenseiLMS.jsx @@ -10,7 +10,7 @@ import SenseiLMSAuthorization from './SenseiLMSAuthorization' import { checkMappedFields } from './SenseiLMSCommonFunc' import SenseiLMSIntegLayout from './SenseiLMSIntegLayout' -export default function SenseiLMS({ formFields, setFlow, flow, allIntegURL }) { +export default function SenseiLMS({ formFields, setFlow, flow, allIntegURL, isInfo }) { const navigate = useNavigate() const { formID } = useParams() const [isLoading, setIsLoading] = useState(false) @@ -56,9 +56,7 @@ export default function SenseiLMS({ formFields, setFlow, flow, allIntegURL }) { setSenseiLMSConf={setSenseiLMSConf} step={step} nextPage={nextPage} - isLoading={isLoading} - setIsLoading={setIsLoading} - setSnackbar={setSnackbar} + isInfo={isInfo} />
{ - setIsLoading('auth') - bitsFetch({}, 'sensei_lms_authorize') - .then(result => { - if (result?.success) { - setIsAuthorized(true) - setSnackbar({ - show: true, - msg: __('Connected with Sensei LMS Successfully', 'bit-integrations') - }) - } - setIsLoading(false) - setShowAuthMsg(true) - }) - .catch(() => { - setIsLoading(false) - setShowAuthMsg(true) - setSnackbar({ - show: true, - msg: __('Sensei LMS authorization failed', 'bit-integrations') - }) - }) - } - - const handleInput = e => { - const newConf = { ...senseiLMSConf } - newConf[e.target.name] = e.target.value - setSenseiLMSConf(newConf) - } + const setStep = useCallback(value => nextPage?.(value), [nextPage]) return ( -
- - -
- {__('Integration Name:', 'bit-integrations')} -
- - - {isLoading === 'auth' && ( -
- - {__('Checking if Sensei LMS is authorized!!!', 'bit-integrations')} -
- )} - - {showAuthMsg && !isAuthorized && !isLoading && ( -
-
-
- -
-
- {__('Sensei LMS is not activated or not installed', 'bit-integrations')} -
-
-
- )} - - {showAuthMsg && isAuthorized && !isLoading && ( -
-
- -
-
{__('Sensei LMS is activated', 'bit-integrations')}
-
- )} - - -
- -
+ ) }