feat: onboarding steps
This commit is contained in:
@@ -8,6 +8,7 @@
|
|||||||
<AppSidebar />
|
<AppSidebar />
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full overflow-auto" id="scrollContainer">
|
<div class="w-full overflow-auto" id="scrollContainer">
|
||||||
|
<OnboardingBanner />
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -16,4 +17,5 @@
|
|||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import AppSidebar from './AppSidebar.vue'
|
import AppSidebar from './AppSidebar.vue'
|
||||||
|
import OnboardingBanner from '@/components/OnboardingBanner.vue'
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -81,9 +81,11 @@ import { defineModel, 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'
|
||||||
|
|
||||||
const show = defineModel()
|
const show = defineModel()
|
||||||
const outline = defineModel('outline')
|
const outline = defineModel('outline')
|
||||||
|
const settingsStore = useSettings()
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
course: {
|
course: {
|
||||||
@@ -143,6 +145,9 @@ const addChapter = async (close) => {
|
|||||||
{
|
{
|
||||||
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'),
|
||||||
|
|||||||
151
frontend/src/components/OnboardingBanner.vue
Normal file
151
frontend/src/components/OnboardingBanner.vue
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="showOnboardingBanner && onboardingDetails.data">
|
||||||
|
<Tooltip :text="__('Skip Onboarding')" placement="left">
|
||||||
|
<X
|
||||||
|
class="w-4 h-4 stroke-1 absolute top-2 right-2 cursor-pointer mr-1"
|
||||||
|
@click="skipOnboarding.reload()"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<div class="flex items-center justify-evenly bg-gray-100 p-10">
|
||||||
|
<div
|
||||||
|
@click="redirectToCourseForm()"
|
||||||
|
class="flex items-center space-x-2"
|
||||||
|
:class="{
|
||||||
|
'cursor-pointer': !onboardingDetails.data.course_created.length,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-if="onboardingDetails.data.course_created.length"
|
||||||
|
class="py-1 px-1 bg-white rounded-full"
|
||||||
|
>
|
||||||
|
<Check class="h-4 w-4 stroke-2 text-green-600" />
|
||||||
|
</span>
|
||||||
|
<span v-else class="font-semibold bg-white px-2 py-1 rounded-full">
|
||||||
|
1
|
||||||
|
</span>
|
||||||
|
<span class="text-lg font-semibold">
|
||||||
|
{{ __('Create a course') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
@click="redirectToChapterForm()"
|
||||||
|
class="flex items-center space-x-2"
|
||||||
|
:class="{
|
||||||
|
'cursor-pointer':
|
||||||
|
onboardingDetails.data.course_created.length &&
|
||||||
|
!onboardingDetails.data.chapter_created.length,
|
||||||
|
'text-gray-400': !onboardingDetails.data.course_created.length,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-if="onboardingDetails.data.chapter_created.length"
|
||||||
|
class="py-1 px-1 bg-white rounded-full"
|
||||||
|
>
|
||||||
|
<Check class="h-4 w-4 stroke-2 text-green-600" />
|
||||||
|
</span>
|
||||||
|
<span v-else class="font-semibold bg-white px-2 py-1 rounded-full">
|
||||||
|
2
|
||||||
|
</span>
|
||||||
|
<span class="text-lg font-semibold">
|
||||||
|
{{ __('Add a chapter') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
@click="redirectToLessonForm()"
|
||||||
|
class="flex items-center space-x-2"
|
||||||
|
:class="{
|
||||||
|
'cursor-pointer':
|
||||||
|
onboardingDetails.data.course_created.length &&
|
||||||
|
onboardingDetails.data.chapter_created.length,
|
||||||
|
'text-gray-400':
|
||||||
|
!onboardingDetails.data.course_created.length ||
|
||||||
|
!onboardingDetails.data.chapter_created.length,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-if="onboardingDetails.data.lesson_created.length"
|
||||||
|
class="py-1 px-1 bg-white rounded-full"
|
||||||
|
>
|
||||||
|
<Check class="h-4 w-4 stroke-2 text-green-600" />
|
||||||
|
</span>
|
||||||
|
<span class="font-semibold bg-white px-2 py-1 rounded-full"> 3 </span>
|
||||||
|
<span class="text-lg font-semibold">
|
||||||
|
{{ __('Add a lesson') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch } from 'vue'
|
||||||
|
import { Check, X } from 'lucide-vue-next'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { useSettings } from '@/stores/settings'
|
||||||
|
import { createResource, Tooltip } from 'frappe-ui'
|
||||||
|
|
||||||
|
const showOnboardingBanner = ref(false)
|
||||||
|
const settings = useSettings()
|
||||||
|
const onboardingDetails = settings.onboardingDetails
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
watch(onboardingDetails, () => {
|
||||||
|
if (!onboardingDetails.data?.is_onboarded) {
|
||||||
|
showOnboardingBanner.value = true
|
||||||
|
} else {
|
||||||
|
showOnboardingBanner.value = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const redirectToCourseForm = () => {
|
||||||
|
if (onboardingDetails.data?.course_created.length) {
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
router.push({ name: 'CourseForm', params: { courseName: 'new' } })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const redirectToChapterForm = () => {
|
||||||
|
if (!onboardingDetails.data?.course_created.length) {
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
router.push({
|
||||||
|
name: 'CourseForm',
|
||||||
|
params: {
|
||||||
|
courseName: onboardingDetails.data?.first_course,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const redirectToLessonForm = () => {
|
||||||
|
if (!onboardingDetails.data?.course_created.length) {
|
||||||
|
return
|
||||||
|
} else if (!onboardingDetails.data?.chapter_created.length) {
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
router.push({
|
||||||
|
name: 'LessonForm',
|
||||||
|
params: {
|
||||||
|
courseName: onboardingDetails.data?.first_course,
|
||||||
|
chapterNumber: 1,
|
||||||
|
lessonNumber: 1,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const skipOnboarding = createResource({
|
||||||
|
url: 'frappe.client.set_value',
|
||||||
|
makeParams() {
|
||||||
|
return {
|
||||||
|
doctype: 'LMS Settings',
|
||||||
|
name: 'LMS Settings',
|
||||||
|
fieldname: 'is_onboarding_complete',
|
||||||
|
value: 1,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSuccess(data) {
|
||||||
|
onboardingDetails.reload()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -434,6 +434,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) {
|
||||||
|
settingsStore.onboardingDetails.reload()
|
||||||
|
}
|
||||||
router.push({
|
router.push({
|
||||||
name: 'CourseForm',
|
name: 'CourseForm',
|
||||||
params: { courseName: data.name },
|
params: { courseName: data.name },
|
||||||
|
|||||||
@@ -92,11 +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'
|
||||||
|
|
||||||
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()
|
||||||
let autoSaveInterval
|
let autoSaveInterval
|
||||||
let showSuccessMessage = false
|
let showSuccessMessage = false
|
||||||
|
|
||||||
@@ -393,6 +395,9 @@ const createNewLesson = () => {
|
|||||||
onSuccess() {
|
onSuccess() {
|
||||||
capture('lesson_created')
|
capture('lesson_created')
|
||||||
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()
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,9 +17,16 @@ export const useSettings = defineStore('settings', () => {
|
|||||||
cache: ['learningPaths'],
|
cache: ['learningPaths'],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const onboardingDetails = createResource({
|
||||||
|
url: 'lms.lms.utils.is_onboarding_complete',
|
||||||
|
auto: true,
|
||||||
|
cache: ['onboardingDetails'],
|
||||||
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isSettingsOpen,
|
isSettingsOpen,
|
||||||
activeTab,
|
activeTab,
|
||||||
learningPaths,
|
learningPaths,
|
||||||
|
onboardingDetails,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -855,7 +855,10 @@ def get_telemetry_boot_info():
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
def is_onboarding_complete():
|
def is_onboarding_complete():
|
||||||
|
if not has_course_moderator_role():
|
||||||
|
return {"is_onboarded": False}
|
||||||
course_created = frappe.db.a_row_exists("LMS Course")
|
course_created = frappe.db.a_row_exists("LMS Course")
|
||||||
chapter_created = frappe.db.a_row_exists("Course Chapter")
|
chapter_created = frappe.db.a_row_exists("Course Chapter")
|
||||||
lesson_created = frappe.db.a_row_exists("Course Lesson")
|
lesson_created = frappe.db.a_row_exists("Course Lesson")
|
||||||
|
|||||||
Reference in New Issue
Block a user