feat: onboarding

This commit is contained in:
Jannat Patel
2025-03-26 13:08:06 +05:30
parent f9b2471b32
commit aa979b96f2
23 changed files with 4602 additions and 2882 deletions

View File

@@ -26,7 +26,7 @@
"codemirror-editor-vue3": "^2.8.0", "codemirror-editor-vue3": "^2.8.0",
"dayjs": "^1.11.6", "dayjs": "^1.11.6",
"feather-icons": "^4.28.0", "feather-icons": "^4.28.0",
"frappe-ui": "^0.1.118", "frappe-ui": "^0.1.121",
"highlight.js": "^11.11.1", "highlight.js": "^11.11.1",
"lucide-vue-next": "^0.383.0", "lucide-vue-next": "^0.383.0",
"markdown-it": "^14.0.0", "markdown-it": "^14.0.0",

View File

@@ -62,20 +62,41 @@
</div> </div>
</div> </div>
</div> </div>
<div> <div class="m-2 flex flex-col gap-1">
<TrialBanner <TrialBanner
v-if=" v-if="
userResource.data?.is_system_manager && userResource.data?.is_fc_site userResource.data?.is_system_manager && userResource.data?.is_fc_site
" "
:isSidebarCollapsed="sidebarStore.isSidebarCollapsed" :isSidebarCollapsed="sidebarStore.isSidebarCollapsed"
/> />
<GettingStartedBanner
v-if="showOnboarding && !isOnboardingStepsCompleted"
:isSidebarCollapsed="sidebarStore.isSidebarCollapsed"
appName="learning"
/>
<SidebarLink
v-if="isOnboardingStepsCompleted"
:link="{
label: __('Help'),
}"
:isCollapsed="sidebarStore.isSidebarCollapsed"
@click="
() => {
showHelpModal = minimize ? true : !showHelpModal
minimize = !showHelpModal
}
"
>
<template #icon>
<CircleHelp class="h-4 w-4" />
</template>
</SidebarLink>
<SidebarLink <SidebarLink
:link="{ :link="{
label: sidebarStore.isSidebarCollapsed ? 'Expand' : 'Collapse', label: sidebarStore.isSidebarCollapsed ? 'Expand' : 'Collapse',
}" }"
:isCollapsed="sidebarStore.isSidebarCollapsed" :isCollapsed="sidebarStore.isSidebarCollapsed"
@click="toggleSidebar()" @click="toggleSidebar()"
class="m-2"
> >
<template #icon> <template #icon>
<span class="grid h-5 w-6 flex-shrink-0 place-items-center"> <span class="grid h-5 w-6 flex-shrink-0 place-items-center">
@@ -95,6 +116,23 @@
v-model:reloadSidebar="sidebarSettings" v-model:reloadSidebar="sidebarSettings"
:page="pageToEdit" :page="pageToEdit"
/> />
<HelpModal
v-if="showOnboarding && showHelpModal"
v-model="showHelpModal"
v-model:articles="articles"
appName="learning"
title="Frappe Learning"
:logo="LMSLogo"
:afterSkip="(step) => capture('onboarding_step_skipped_' + step)"
:afterSkipAll="() => capture('onboarding_steps_skipped')"
:afterReset="(step) => capture('onboarding_step_reset_' + step)"
:afterResetAll="() => capture('onboarding_steps_reset')"
docsLink="https://docs.frappe.io/learning"
/>
<IntermediateStepModal
v-model="showIntermediateModal"
:currentStep="currentStep"
/>
</template> </template>
<script setup> <script setup>
@@ -102,16 +140,36 @@ import UserDropdown from '@/components/UserDropdown.vue'
import CollapseSidebar from '@/components/Icons/CollapseSidebar.vue' import CollapseSidebar from '@/components/Icons/CollapseSidebar.vue'
import SidebarLink from '@/components/SidebarLink.vue' import SidebarLink from '@/components/SidebarLink.vue'
import { useStorage } from '@vueuse/core' import { useStorage } from '@vueuse/core'
import { ref, onMounted, inject, watch } from 'vue' import { ref, onMounted, inject, watch, reactive, markRaw } from 'vue'
import { getSidebarLinks } from '../utils' import { getSidebarLinks } from '../utils'
import { usersStore } from '@/stores/user' import { usersStore } from '@/stores/user'
import { sessionStore } from '@/stores/session' import { sessionStore } from '@/stores/session'
import { useSidebar } from '@/stores/sidebar' import { useSidebar } from '@/stores/sidebar'
import { useSettings } from '@/stores/settings' import { useSettings } from '@/stores/settings'
import { ChevronRight, Plus } from 'lucide-vue-next' import {
BookOpen,
ChevronRight,
Plus,
CircleHelp,
FolderTree,
FileText,
UserPlus,
Users,
} from 'lucide-vue-next'
import { Button, createResource } from 'frappe-ui' import { Button, createResource } from 'frappe-ui'
import { TrialBanner } from 'frappe-ui/frappe' import {
TrialBanner,
HelpModal,
GettingStartedBanner,
useOnboarding,
showHelpModal,
minimize,
IntermediateStepModal,
} from 'frappe-ui/frappe'
import PageModal from '@/components/Modals/PageModal.vue' import PageModal from '@/components/Modals/PageModal.vue'
import { capture } from '@/telemetry'
import LMSLogo from '@/components/Icons/LMSLogo.vue'
import { useRouter } from 'vue-router'
const { user, sidebarSettings } = sessionStore() const { user, sidebarSettings } = sessionStore()
const { userResource } = usersStore() const { userResource } = usersStore()
@@ -124,12 +182,22 @@ const isModerator = ref(false)
const isInstructor = ref(false) const isInstructor = ref(false)
const pageToEdit = ref(null) const pageToEdit = ref(null)
const settingsStore = useSettings() const settingsStore = useSettings()
const showOnboarding = ref(false)
const showIntermediateModal = ref(false)
const currentStep = ref({})
const router = useRouter()
let onboardingDetails
let isOnboardingStepsCompleted = false
onMounted(() => { onMounted(() => {
addNotifications()
setSidebarLinks()
socket.on('publish_lms_notifications', (data) => { socket.on('publish_lms_notifications', (data) => {
unreadNotifications.reload() unreadNotifications.reload()
}) })
addNotifications() })
const setSidebarLinks = () => {
sidebarSettings.reload( sidebarSettings.reload(
{}, {},
{ {
@@ -144,7 +212,7 @@ onMounted(() => {
}, },
} }
) )
}) }
const unreadNotifications = createResource({ const unreadNotifications = createResource({
cache: 'Unread Notifications Count', cache: 'Unread Notifications Count',
@@ -272,16 +340,6 @@ const getSidebarFromStorage = () => {
return useStorage('sidebar_is_collapsed', false) return useStorage('sidebar_is_collapsed', false)
} }
watch(userResource, () => {
if (userResource.data) {
isModerator.value = userResource.data.is_moderator
isInstructor.value = userResource.data.is_instructor
addPrograms()
addQuizzes()
addAssignments()
}
})
const toggleSidebar = () => { const toggleSidebar = () => {
sidebarStore.isSidebarCollapsed = !sidebarStore.isSidebarCollapsed sidebarStore.isSidebarCollapsed = !sidebarStore.isSidebarCollapsed
localStorage.setItem( localStorage.setItem(
@@ -297,4 +355,221 @@ const toggleWebPages = () => {
JSON.stringify(sidebarStore.isWebpagesCollapsed) JSON.stringify(sidebarStore.isWebpagesCollapsed)
) )
} }
const getFirstCourse = async () => {
let firstCourse = localStorage.getItem('firstCourse')
if (firstCourse) return firstCourse
return await call('lms.lms.onboarding.get_first_course')
}
const getFirstBatch = async () => {
let firstBatch = localStorage.getItem('firstBatch')
if (firstBatch) return firstBatch
return await call('lms.lms.onboarding.get_first_batch')
}
const steps = reactive([
{
name: 'create_first_course',
title: __('Create your first course'),
icon: markRaw(BookOpen),
completed: false,
onClick: () => {
minimize.value = true
router.push({
name: 'CourseForm',
params: {
courseName: 'new',
},
})
},
},
{
name: 'create_first_chapter',
title: __('Add your first chapter'),
icon: markRaw(FolderTree),
completed: false,
onClick: async () => {
minimize.value = true
let course = await getFirstCourse()
if (course) {
router.push({ name: 'CourseForm', params: { courseName: course } })
} else {
router.push({ name: 'CourseForm' })
}
},
},
{
name: 'create_first_lesson',
title: __('Add your first lesson'),
icon: markRaw(FileText),
completed: false,
onClick: async () => {
minimize.value = true
let course = await getFirstCourse()
if (course) {
router.push({
name: 'LessonForm',
params: { courseName: course, chapterNumber: 1, lessonNumber: 1 },
})
} else {
router.push({ name: 'CourseForm' })
}
},
},
{
name: 'create_first_quiz',
title: __('Create your first quiz'),
icon: markRaw(CircleHelp),
completed: false,
onClick: () => {
minimize.value = true
router.push({ name: 'QuizForm', params: { quizID: 'new' } })
},
},
{
name: 'invite_students',
title: __('Invite your team and students'),
icon: markRaw(UserPlus),
completed: false,
onClick: () => {
minimize.value = true
settingsStore.activeTab = 'Members'
settingsStore.isSettingsOpen = true
},
},
{
name: 'create_first_batch',
title: __('Create your first batch'),
icon: markRaw(Users),
completed: false,
onClick: () => {
minimize.value = true
router.push({ name: 'BatchForm', params: { batchName: 'new' } })
},
},
{
name: 'add_batch_student',
title: __('Add students to your batch'),
icon: markRaw(UserPlus),
completed: false,
onClick: async () => {
minimize.value = true
let batch = await getFirstBatch()
if (batch) {
router.push({
name: 'Batch',
params: {
batchName: batch,
},
})
} else {
router.push({ name: 'Batch' })
}
},
},
{
name: 'add_batch_course',
title: __('Add courses to your batch'),
icon: markRaw(BookOpen),
completed: false,
onClick: async () => {
minimize.value = true
let batch = await getFirstBatch()
if (batch) {
router.push({
name: 'Batch',
params: {
batchName: batch,
},
hash: 'courses',
})
} else {
router.push({ name: 'Batch' })
}
},
},
])
const articles = ref([
{
title: __('Introduction'),
opened: false,
subArticles: [
{ name: 'introduction', title: __('Introduction') },
{ name: 'setting-up', title: __('Setting up') },
],
},
{
title: __('Creating a course'),
opened: false,
subArticles: [
{ name: 'create-a-course', title: __('Create a course') },
{ name: 'add-a-chapter', title: __('Add a chapter') },
{ name: 'add-a-lesson', title: __('Add a lesson') },
],
},
{
title: __('Creating a batch'),
opened: false,
subArticles: [
{ name: 'create-a-batch', title: __('Create a batch') },
{ name: 'create-a-live-class', title: __('Create a live class') },
],
},
{
title: __('Assessments'),
opened: false,
subArticles: [
{ name: 'quizzes', title: __('Quizzes') },
{ name: 'assignments', title: __('Assignments') },
],
},
{
title: __('Certification'),
opened: false,
subArticles: [
{ name: 'issue-a-certificate', title: __('Issue a Certificate') },
{
name: 'custom-certificate-templates',
title: __('Custom Certificate Templates'),
},
],
},
{
title: __('Monetization'),
opened: false,
subArticles: [
{
name: 'setting-up-payment-gateway',
title: __('Setting up payment gateway'),
},
],
},
{
title: __('Settings'),
opened: false,
subArticles: [{ name: 'roles', title: __('Roles') }],
},
])
const setUpOnboarding = () => {
if (userResource.data?.is_system_manager) {
onboardingDetails = useOnboarding('learning')
onboardingDetails.setUp(steps)
isOnboardingStepsCompleted = onboardingDetails.isOnboardingStepsCompleted
showOnboarding.value = true
}
}
watch(userResource, () => {
if (userResource.data) {
isModerator.value = userResource.data.is_moderator
isInstructor.value = userResource.data.is_instructor
addPrograms()
addQuizzes()
addAssignments()
setUpOnboarding()
}
})
</script> </script>

View File

@@ -63,6 +63,9 @@
</ListSelectBanner> </ListSelectBanner>
</ListView> </ListView>
</div> </div>
<div v-else class="text-sm italic text-ink-gray-5">
{{ __('No courses added') }}
</div>
<BatchCourseModal <BatchCourseModal
v-model="showCourseModal" v-model="showCourseModal"
:batch="batch" :batch="batch"

View File

@@ -264,7 +264,8 @@ const students = createResource({
auto: true, auto: true,
onSuccess(data) { onSuccess(data) {
chartData.value = getChartData() chartData.value = getChartData()
showProgressChart.value = data.length && true showProgressChart.value =
data.length && (props.batch?.courses?.length || assessmentCount.value)
}, },
}) })

View File

@@ -116,6 +116,7 @@ import { createResource, Avatar, Button, FormControl, Badge } from 'frappe-ui'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { ref, watch, reactive, inject } from 'vue' import { ref, watch, reactive, inject } from 'vue'
import { RefreshCw, Plus, X } from 'lucide-vue-next' import { RefreshCw, Plus, X } from 'lucide-vue-next'
import { useOnboarding } from 'frappe-ui/frappe'
const router = useRouter() const router = useRouter()
const show = defineModel('show') const show = defineModel('show')
@@ -125,6 +126,7 @@ const memberList = ref([])
const hasNextPage = ref(false) const hasNextPage = ref(false)
const showForm = ref(false) const showForm = ref(false)
const dayjs = inject('$dayjs') const dayjs = inject('$dayjs')
const { updateOnboardingStep } = useOnboarding('learning')
const member = reactive({ const member = reactive({
email: '', email: '',
@@ -185,6 +187,7 @@ const newMember = createResource({
auto: false, auto: false,
onSuccess(data) { onSuccess(data) {
show.value = false show.value = false
updateOnboardingStep('invite_students')
router.push({ router.push({
name: 'Profile', name: 'Profile',
params: { params: {

View File

@@ -24,6 +24,7 @@
doctype="Course Evaluator" doctype="Course Evaluator"
v-model="evaluator" v-model="evaluator"
:label="__('Evaluator')" :label="__('Evaluator')"
:onCreate="(value, close) => openSettings(close)"
class="mt-4" class="mt-4"
/> />
</template> </template>
@@ -34,11 +35,15 @@ import { Dialog, createResource } from 'frappe-ui'
import { ref } from 'vue' import { ref } from 'vue'
import Link from '@/components/Controls/Link.vue' import Link from '@/components/Controls/Link.vue'
import { showToast } from '@/utils' import { showToast } from '@/utils'
import { useOnboarding } from 'frappe-ui/frappe'
import { useSettings } from '@/stores/settings'
const show = defineModel() const show = defineModel()
const course = ref(null) const course = ref(null)
const evaluator = ref(null) const evaluator = ref(null)
const courses = defineModel('courses') const courses = defineModel('courses')
const { updateOnboardingStep } = useOnboarding('learning')
const settingsStore = useSettings()
const props = defineProps({ const props = defineProps({
batch: { batch: {
@@ -69,6 +74,7 @@ const addCourse = (close) => {
{ {
onSuccess() { onSuccess() {
courses.value.reload() courses.value.reload()
updateOnboardingStep('add_batch_course')
close() close()
course.value = null course.value = null
evaluator.value = null evaluator.value = null
@@ -79,4 +85,10 @@ const addCourse = (close) => {
} }
) )
} }
const openSettings = (close) => {
close()
settingsStore.activeTab = 'Categories'
settingsStore.isSettingsOpen = true
}
</script> </script>

View File

@@ -81,11 +81,11 @@ import { reactive, watch } from 'vue'
import { showToast, getFileSize } from '@/utils/' import { showToast, getFileSize } from '@/utils/'
import { capture } from '@/telemetry' import { capture } from '@/telemetry'
import { FileText, X } from 'lucide-vue-next' import { FileText, X } from 'lucide-vue-next'
import { useSettings } from '@/stores/settings' import { useOnboarding } from 'frappe-ui/frappe'
const show = defineModel() const show = defineModel()
const outline = defineModel('outline') const outline = defineModel('outline')
const settingsStore = useSettings() const { updateOnboardingStep } = useOnboarding('learning')
const props = defineProps({ const props = defineProps({
course: { course: {
@@ -140,14 +140,12 @@ const addChapter = async (close) => {
}, },
onSuccess: (data) => { onSuccess: (data) => {
capture('chapter_created') capture('chapter_created')
updateOnboardingStep('create_first_chapter')
chapterReference.submit( chapterReference.submit(
{ name: data.name }, { name: data.name },
{ {
onSuccess(data) { onSuccess(data) {
cleanChapter() cleanChapter()
/* if (!settingsStore.onboardingDetails.data?.is_onboarded) {
settingsStore.onboardingDetails.reload()
} */
outline.value.reload() outline.value.reload()
showToast( showToast(
__('Success'), __('Success'),

View File

@@ -29,9 +29,11 @@ import { Dialog, createResource } from 'frappe-ui'
import { ref } from 'vue' import { ref } from 'vue'
import Link from '@/components/Controls/Link.vue' import Link from '@/components/Controls/Link.vue'
import { showToast } from '@/utils' import { showToast } from '@/utils'
import { useOnboarding } from 'frappe-ui/frappe'
const students = defineModel('reloadStudents') const students = defineModel('reloadStudents')
const student = ref() const student = ref()
const { updateOnboardingStep } = useOnboarding('learning')
const show = defineModel() const show = defineModel()
const props = defineProps({ const props = defineProps({
@@ -61,6 +63,7 @@ const addStudent = (close) => {
onSuccess() { onSuccess() {
students.value.reload() students.value.reload()
student.value = null student.value = null
updateOnboardingStep('add_batch_student')
close() close()
}, },
onError(err) { onError(err) {

View File

@@ -190,11 +190,15 @@
</div> </div>
</div> </div>
</div> </div>
<BulkCertificates v-model="openCertificateDialog" :batch="batch.data" /> <BulkCertificates
v-if="batch.data"
v-model="openCertificateDialog"
:batch="batch.data"
/>
</template> </template>
<script setup> <script setup>
import { computed, inject, ref } from 'vue' import { computed, inject, ref, onMounted, watch } from 'vue'
import { useRouteQuery } from '@vueuse/router' import { useRoute, useRouter } from 'vue-router'
import { Breadcrumbs, Button, createResource, Tabs, Badge } from 'frappe-ui' import { Breadcrumbs, Button, createResource, Tabs, Badge } from 'frappe-ui'
import CourseInstructors from '@/components/CourseInstructors.vue' import CourseInstructors from '@/components/CourseInstructors.vue'
import UserAvatar from '@/components/UserAvatar.vue' import UserAvatar from '@/components/UserAvatar.vue'
@@ -226,52 +230,10 @@ import BatchFeedback from '@/components/BatchFeedback.vue'
const user = inject('$user') const user = inject('$user')
const showAnnouncementModal = ref(false) const showAnnouncementModal = ref(false)
const openCertificateDialog = ref(false) const openCertificateDialog = ref(false)
const route = useRoute()
const router = useRouter()
const tabIndex = ref(0)
const props = defineProps({
batchName: {
type: String,
required: true,
},
})
const batch = createResource({
url: 'lms.lms.utils.get_batch_details',
cache: ['batch', props.batchName],
params: {
batch: props.batchName,
},
auto: true,
})
const breadcrumbs = computed(() => {
let crumbs = [{ label: 'Batches', route: { name: 'Batches' } }]
if (!isStudent.value) {
crumbs.push({
label: 'Details',
route: {
name: 'BatchDetail',
params: {
batchName: batch.data?.name,
},
},
})
}
crumbs.push({
label: batch?.data?.title,
route: { name: 'Batch', params: { batchName: props.batchName } },
})
return crumbs
})
const isStudent = computed(() => {
return (
user?.data &&
batch.data?.students?.length &&
batch.data?.students.includes(user.data.name)
)
})
const tabIndex = useRouteQuery('tab', 0, { transform: Number })
const tabs = computed(() => { const tabs = computed(() => {
let batchTabs = [] let batchTabs = []
batchTabs.push({ batchTabs.push({
@@ -313,6 +275,61 @@ const tabs = computed(() => {
return batchTabs return batchTabs
}) })
const props = defineProps({
batchName: {
type: String,
required: true,
},
})
onMounted(() => {
const hash = route.hash
if (hash) {
tabs.value.forEach((tab, index) => {
if (tab.label?.toLowerCase() === hash.replace('#', '')) {
tabIndex.value = index
}
})
}
})
const batch = createResource({
url: 'lms.lms.utils.get_batch_details',
cache: ['batch', props.batchName],
params: {
batch: props.batchName,
},
auto: true,
})
const breadcrumbs = computed(() => {
let crumbs = [{ label: 'Batches', route: { name: 'Batches' } }]
if (!isStudent.value) {
crumbs.push({
label: 'Details',
route: {
name: 'BatchDetail',
params: {
batchName: batch.data?.name,
},
},
})
}
crumbs.push({
label: batch?.data?.title,
route: { name: 'Batch', params: { batchName: props.batchName } },
})
return crumbs
})
const isStudent = computed(() => {
return (
user?.data &&
batch.data?.students?.length &&
batch.data?.students.includes(user.data.name)
)
})
const redirectToLogin = () => { const redirectToLogin = () => {
window.location.href = `/login?redirect-to=/lms/batches/${props.batchName}` window.location.href = `/login?redirect-to=/lms/batches/${props.batchName}`
} }
@@ -321,6 +338,13 @@ const openAnnouncementModal = () => {
showAnnouncementModal.value = true showAnnouncementModal.value = true
} }
watch(tabIndex, () => {
const tab = tabs.value[tabIndex.value]
if (tab.label != route.hash.replace('#', '')) {
router.push({ ...route, hash: `#${tab.label.toLowerCase()}` })
}
})
const pageMeta = computed(() => { const pageMeta = computed(() => {
return { return {
title: batch.data?.title, title: batch.data?.title,

View File

@@ -271,9 +271,11 @@ import { showToast } from '@/utils'
import { Image } from 'lucide-vue-next' import { Image } from 'lucide-vue-next'
import { capture } from '@/telemetry' import { capture } from '@/telemetry'
import MultiSelect from '@/components/Controls/MultiSelect.vue' import MultiSelect from '@/components/Controls/MultiSelect.vue'
import { useOnboarding } from 'frappe-ui/frappe'
const router = useRouter() const router = useRouter()
const user = inject('$user') const user = inject('$user')
const { updateOnboardingStep } = useOnboarding('learning')
const props = defineProps({ const props = defineProps({
batchName: { batchName: {
@@ -426,6 +428,9 @@ const createNewBatch = () => {
{ {
onSuccess(data) { onSuccess(data) {
capture('batch_created') capture('batch_created')
updateOnboardingStep('create_first_batch', true, false, () => {
localStorage.setItem('firstBatch', data.name)
})
router.push({ router.push({
name: 'BatchDetail', name: 'BatchDetail',
params: { params: {

View File

@@ -105,7 +105,6 @@ import {
Select, Select,
TabButtons, TabButtons,
} from 'frappe-ui' } from 'frappe-ui'
import { useRouteQuery } from '@vueuse/router'
import { computed, inject, onMounted, ref, watch } from 'vue' import { computed, inject, onMounted, ref, watch } from 'vue'
import { BookOpen, Plus } from 'lucide-vue-next' import { BookOpen, Plus } from 'lucide-vue-next'
import { updateDocumentTitle } from '@/utils' import { updateDocumentTitle } from '@/utils'
@@ -120,10 +119,8 @@ const currentCategory = ref(null)
const title = ref('') const title = ref('')
const certification = ref(false) const certification = ref(false)
const filters = ref({}) const filters = ref({})
const currentTab = useRouteQuery( const is_student = computed(() => user.data?.is_student)
'tab', const currentTab = ref(is_student.value ? 'All' : 'Upcoming')
user.data?.is_student ? 'All' : 'Upcoming'
)
const orderBy = ref('start_date') const orderBy = ref('start_date')
onMounted(() => { onMounted(() => {
@@ -208,12 +205,12 @@ const updateTabFilter = () => {
if (!user.data) { if (!user.data) {
return return
} }
if (currentTab.value == 'Enrolled' && user.data?.is_student) { if (currentTab.value == 'Enrolled' && is_student.value) {
filters.value['enrolled'] = 1 filters.value['enrolled'] = 1
delete filters.value['start_date'] delete filters.value['start_date']
delete filters.value['published'] delete filters.value['published']
orderBy.value = 'start_date desc' orderBy.value = 'start_date desc'
} else if (user.data?.is_student) { } else if (is_student.value) {
delete filters.value['enrolled'] delete filters.value['enrolled']
} else { } else {
delete filters.value['start_date'] delete filters.value['start_date']
@@ -232,7 +229,7 @@ const updateTabFilter = () => {
} }
const updateStudentFilter = () => { const updateStudentFilter = () => {
if (!user.data || (user.data?.is_student && currentTab.value != 'Enrolled')) { if (!user.data || (is_student.value && currentTab.value != 'Enrolled')) {
filters.value['start_date'] = ['>=', dayjs().format('YYYY-MM-DD')] filters.value['start_date'] = ['>=', dayjs().format('YYYY-MM-DD')]
filters.value['published'] = 1 filters.value['published'] = 1
} }
@@ -285,12 +282,17 @@ const batchTabs = computed(() => {
label: __('All'), label: __('All'),
}, },
] ]
if (user.data?.is_student) {
tabs.push({ label: __('Enrolled') }) if (
} else { user.data?.is_moderator ||
user.data?.is_instructor ||
user.data?.is_evaluator
) {
tabs.push({ label: __('Upcoming') }) tabs.push({ label: __('Upcoming') })
tabs.push({ label: __('Archived') }) tabs.push({ label: __('Archived') })
tabs.push({ label: __('Unpublished') }) tabs.push({ label: __('Unpublished') })
} else if (user.data) {
tabs.push({ label: __('Enrolled') })
} }
return tabs return tabs
}) })

View File

@@ -270,6 +270,7 @@ import { useRouter } from 'vue-router'
import CourseOutline from '@/components/CourseOutline.vue' import CourseOutline from '@/components/CourseOutline.vue'
import MultiSelect from '@/components/Controls/MultiSelect.vue' import MultiSelect from '@/components/Controls/MultiSelect.vue'
import { capture } from '@/telemetry' import { capture } from '@/telemetry'
import { useOnboarding } from 'frappe-ui/frappe'
import { useSettings } from '@/stores/settings' import { useSettings } from '@/stores/settings'
const user = inject('$user') const user = inject('$user')
@@ -278,6 +279,7 @@ const router = useRouter()
const instructors = ref([]) const instructors = ref([])
const settingsStore = useSettings() const settingsStore = useSettings()
const app = getCurrentInstance() const app = getCurrentInstance()
const { updateOnboardingStep } = useOnboarding('learning')
const { $dialog } = app.appContext.config.globalProperties const { $dialog } = app.appContext.config.globalProperties
const props = defineProps({ const props = defineProps({
@@ -443,9 +445,9 @@ const submitCourse = () => {
onSuccess(data) { onSuccess(data) {
capture('course_created') capture('course_created')
showToast('Success', 'Course created successfully', 'check') showToast('Success', 'Course created successfully', 'check')
/* if (!settingsStore.onboardingDetails.data?.is_onboarded) { updateOnboardingStep('create_first_course', true, false, () => {
settingsStore.onboardingDetails.reload() localStorage.setItem('firstCourse', data.name)
} */ })
router.push({ router.push({
name: 'CourseForm', name: 'CourseForm',
params: { courseName: data.name }, params: { courseName: data.name },

View File

@@ -101,7 +101,6 @@ import {
Select, Select,
TabButtons, TabButtons,
} from 'frappe-ui' } from 'frappe-ui'
import { useRouteQuery } from '@vueuse/router'
import { computed, inject, onMounted, ref, watch } from 'vue' import { computed, inject, onMounted, ref, watch } from 'vue'
import { BookOpen, Plus } from 'lucide-vue-next' import { BookOpen, Plus } from 'lucide-vue-next'
import { updateDocumentTitle } from '@/utils' import { updateDocumentTitle } from '@/utils'
@@ -116,7 +115,7 @@ const currentCategory = ref(null)
const title = ref('') const title = ref('')
const certification = ref(false) const certification = ref(false)
const filters = ref({}) const filters = ref({})
const currentTab = useRouteQuery('tab', 'Live') const currentTab = ref('Live')
onMounted(() => { onMounted(() => {
setFiltersFromQuery() setFiltersFromQuery()
@@ -285,10 +284,14 @@ const courseTabs = computed(() => {
label: __('Upcoming'), label: __('Upcoming'),
}, },
] ]
if (user.data?.is_student) { if (
tabs.push({ label: __('Enrolled') }) user.data?.is_moderator ||
} else if (user.data) { user.data?.is_instructor ||
user.data?.is_evaluator
) {
tabs.push({ label: __('Created') }) tabs.push({ label: __('Created') })
} else if (user.data) {
tabs.push({ label: __('Enrolled') })
} }
return tabs return tabs
}) })

View File

@@ -92,13 +92,13 @@ import LessonHelp from '@/components/LessonHelp.vue'
import { ChevronRight } from 'lucide-vue-next' import { ChevronRight } from 'lucide-vue-next'
import { updateDocumentTitle, createToast, getEditorTools } from '@/utils' import { updateDocumentTitle, createToast, getEditorTools } from '@/utils'
import { capture } from '@/telemetry' import { capture } from '@/telemetry'
import { useSettings } from '@/stores/settings' import { useOnboarding } from 'frappe-ui/frappe'
const editor = ref(null) const editor = ref(null)
const instructorEditor = ref(null) const instructorEditor = ref(null)
const user = inject('$user') const user = inject('$user')
const openInstructorEditor = ref(false) const openInstructorEditor = ref(false)
const settingsStore = useSettings() const { updateOnboardingStep } = useOnboarding('learning')
let autoSaveInterval let autoSaveInterval
let showSuccessMessage = false let showSuccessMessage = false
@@ -395,10 +395,8 @@ const createNewLesson = () => {
{ {
onSuccess() { onSuccess() {
capture('lesson_created') capture('lesson_created')
updateOnboardingStep('create_first_lesson')
showToast('Success', 'Lesson created successfully', 'check') showToast('Success', 'Lesson created successfully', 'check')
/* if (!settingsStore.onboardingDetails.data?.is_onboarded) {
settingsStore.onboardingDetails.reload()
} */
lessonDetails.reload() lessonDetails.reload()
}, },
} }

View File

@@ -211,6 +211,7 @@ import { Plus, Trash2 } from 'lucide-vue-next'
import Question from '@/components/Modals/Question.vue' import Question from '@/components/Modals/Question.vue'
import { showToast, updateDocumentTitle } from '@/utils' import { showToast, updateDocumentTitle } from '@/utils'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { useOnboarding } from 'frappe-ui/frappe'
const showQuestionModal = ref(false) const showQuestionModal = ref(false)
const currentQuestion = reactive({ const currentQuestion = reactive({
@@ -220,6 +221,7 @@ const currentQuestion = reactive({
}) })
const user = inject('$user') const user = inject('$user')
const router = useRouter() const router = useRouter()
const { updateOnboardingStep } = useOnboarding('learning')
const props = defineProps({ const props = defineProps({
quizID: { quizID: {
@@ -337,6 +339,7 @@ const createQuiz = () => {
{ {
onSuccess(data) { onSuccess(data) {
showToast(__('Success'), __('Quiz created successfully'), 'check') showToast(__('Success'), __('Quiz created successfully'), 'check')
updateOnboardingStep('create_first_quiz')
router.push({ router.push({
name: 'QuizForm', name: 'QuizForm',
params: { quizID: data.name }, params: { quizID: data.name },

View File

@@ -25,7 +25,7 @@ export default defineConfig({
}), }),
], ],
server: { server: {
allowedHosts: ['fs', 'bs'], allowedHosts: ['fs', 'onb'],
}, },
resolve: { resolve: {
alias: { alias: {

File diff suppressed because it is too large Load Diff

View File

@@ -177,7 +177,9 @@ def get_user_info():
user.is_instructor = "Course Creator" in user.roles user.is_instructor = "Course Creator" in user.roles
user.is_moderator = "Moderator" in user.roles user.is_moderator = "Moderator" in user.roles
user.is_evaluator = "Batch Evaluator" in user.roles user.is_evaluator = "Batch Evaluator" in user.roles
user.is_student = "LMS Student" in user.roles user.is_student = (
not user.is_instructor and not user.is_moderator and not user.is_evaluator
)
user.is_fc_site = is_fc_site() user.is_fc_site = is_fc_site()
user.is_system_manager = "System Manager" in user.roles user.is_system_manager = "System Manager" in user.roles
if user.is_fc_site and user.is_system_manager: if user.is_fc_site and user.is_system_manager:

19
lms/lms/onboarding.py Normal file
View File

@@ -0,0 +1,19 @@
import frappe
def get_first_course():
course = frappe.get_all(
"LMS Course",
fields=["name"],
order_by="creation",
limit=1,
)
return course[0].name if course else None
def get_first_batch():
batch = frappe.get_all(
"LMS Batch",
fields=["name"],
order_by="creation",
limit=1,
)
return batch[0].name if batch else None

View File

@@ -533,10 +533,11 @@ def has_course_evaluator_role(member=None):
def has_student_role(member=None): def has_student_role(member=None):
return frappe.db.get_value( roles = frappe.get_roles(member or frappe.session.user)
"Has Role", return (
{"parent": member or frappe.session.user, "role": "LMS Student"}, "Moderator" not in roles
"name", and "Course Creator" not in roles
and "Batch Evaluator" not in roles
) )

View File

@@ -2,7 +2,7 @@
"name": "frappe_lms", "name": "frappe_lms",
"version": "1.0.0", "version": "1.0.0",
"description": "Easy to use, open-source, Learning Management System", "description": "Easy to use, open-source, Learning Management System",
"workspaces1": [ "workspaces": [
"frappe-ui", "frappe-ui",
"frontend" "frontend"
], ],

4174
yarn.lock

File diff suppressed because it is too large Load Diff