{
+ 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 (
+
+
+
+
+ {__('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..05c86b817
--- /dev/null
+++ b/frontend/src/components/AllIntegrations/SenseiLMS/SenseiLMS.jsx
@@ -0,0 +1,103 @@
+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, isInfo }) {
+ 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..c9da42259
--- /dev/null
+++ b/frontend/src/components/AllIntegrations/SenseiLMS/SenseiLMSAuthorization.jsx
@@ -0,0 +1,38 @@
+import { useCallback } from 'react'
+import { AUTH_TYPES } from '../../../Utils/connectionAuth'
+import { __ } from '../../../Utils/i18nwrap'
+import tutorialLinks from '../../../Utils/StaticData/tutorialLinks'
+import Authorization from '../../Connections/Authorization'
+
+export default function SenseiLMSAuthorization({
+ senseiLMSConf,
+ setSenseiLMSConf,
+ step,
+ nextPage,
+ isInfo
+}) {
+ const setStep = useCallback(value => nextPage?.(value), [nextPage])
+
+ return (
+
+ )
+}
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..45ea7d32f
--- /dev/null
+++ b/frontend/src/components/AllIntegrations/SenseiLMS/SenseiLMSIntegLayout.jsx
@@ -0,0 +1,223 @@
+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,
+ gradeTypeActions,
+ gradeTypeOptions,
+ lessonActions,
+ lessonStatusActions,
+ lessonStatusOptions,
+ markCompleteActions,
+ markCompleteOptions,
+ modules,
+ postStatusOptions,
+ quizActions,
+ senseiLMSStaticData,
+ statusActions
+} 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
+ />
+
+
+ >
+ )
+
+ const staticDropdown = (label, confKey, options) => (
+ <>
+
+
+ {label}
+ 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)}
+
+ {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 && (
+
+ )}
+
+ {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..b0c688c3d
--- /dev/null
+++ b/frontend/src/components/AllIntegrations/SenseiLMS/staticData.js
@@ -0,0 +1,123 @@
+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 }
+]
+
+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,
+ update_lesson_status: userEmailFields,
+ grade_quiz: [
+ ...userEmailFields,
+ { key: 'grade', label: __('Grade', 'bit-integrations'), required: true }
+ ],
+ 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']
+
+// actions whose static choices are shown as Utilities dropdowns (not field-map rows)
+export const statusActions = ['create_course', 'create_lesson']
+export const markCompleteActions = ['start_lesson_for_user']
+export const gradeTypeActions = ['grade_quiz']
+export const lessonStatusActions = ['update_lesson_status']
+
+export const postStatusOptions = [
+ { label: __('Publish', 'bit-integrations'), value: 'publish' },
+ { label: __('Draft', 'bit-integrations'), value: 'draft' },
+ { label: __('Pending', 'bit-integrations'), value: 'pending' },
+ { label: __('Private', 'bit-integrations'), value: 'private' }
+]
+export const markCompleteOptions = [
+ { label: __('No', 'bit-integrations'), value: 'no' },
+ { label: __('Yes', 'bit-integrations'), value: 'yes' }
+]
+export const gradeTypeOptions = [
+ { label: __('Auto', 'bit-integrations'), value: 'auto' },
+ { label: __('Manual', 'bit-integrations'), value: 'manual' }
+]
+export const lessonStatusOptions = [
+ { label: __('In Progress', 'bit-integrations'), value: 'in-progress' },
+ { label: __('Ungraded', 'bit-integrations'), value: 'ungraded' },
+ { label: __('Graded', 'bit-integrations'), value: 'graded' },
+ { label: __('Passed', 'bit-integrations'), value: 'passed' },
+ { label: __('Failed', 'bit-integrations'), value: 'failed' }
+]
diff --git a/frontend/src/components/Flow/New/SelectAction.jsx b/frontend/src/components/Flow/New/SelectAction.jsx
index f2efc8ff1..d8190b32b 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: 'FluentPlayer', is_pro: true },
{ type: 'BitCrm', name: 'Bit CRM', logo: 'bitCrm', is_pro: false },
{ type: 'MoreConvert Wishlist', logo: 'moreConvertWishlist', is_pro: true },
diff --git a/frontend/src/resource/img/integ/senseiLMS.webp b/frontend/src/resource/img/integ/senseiLMS.webp
index d6ec8074c..4261c1b94 100644
Binary files a/frontend/src/resource/img/integ/senseiLMS.webp and b/frontend/src/resource/img/integ/senseiLMS.webp differ