feat: onboarding
This commit is contained in:
@@ -62,20 +62,41 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="m-2 flex flex-col gap-1">
|
||||
<TrialBanner
|
||||
v-if="
|
||||
userResource.data?.is_system_manager && userResource.data?.is_fc_site
|
||||
"
|
||||
: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
|
||||
:link="{
|
||||
label: sidebarStore.isSidebarCollapsed ? 'Expand' : 'Collapse',
|
||||
}"
|
||||
:isCollapsed="sidebarStore.isSidebarCollapsed"
|
||||
@click="toggleSidebar()"
|
||||
class="m-2"
|
||||
>
|
||||
<template #icon>
|
||||
<span class="grid h-5 w-6 flex-shrink-0 place-items-center">
|
||||
@@ -95,6 +116,23 @@
|
||||
v-model:reloadSidebar="sidebarSettings"
|
||||
: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>
|
||||
|
||||
<script setup>
|
||||
@@ -102,16 +140,36 @@ import UserDropdown from '@/components/UserDropdown.vue'
|
||||
import CollapseSidebar from '@/components/Icons/CollapseSidebar.vue'
|
||||
import SidebarLink from '@/components/SidebarLink.vue'
|
||||
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 { usersStore } from '@/stores/user'
|
||||
import { sessionStore } from '@/stores/session'
|
||||
import { useSidebar } from '@/stores/sidebar'
|
||||
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 { 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 { capture } from '@/telemetry'
|
||||
import LMSLogo from '@/components/Icons/LMSLogo.vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const { user, sidebarSettings } = sessionStore()
|
||||
const { userResource } = usersStore()
|
||||
@@ -124,12 +182,22 @@ const isModerator = ref(false)
|
||||
const isInstructor = ref(false)
|
||||
const pageToEdit = ref(null)
|
||||
const settingsStore = useSettings()
|
||||
const showOnboarding = ref(false)
|
||||
const showIntermediateModal = ref(false)
|
||||
const currentStep = ref({})
|
||||
const router = useRouter()
|
||||
let onboardingDetails
|
||||
let isOnboardingStepsCompleted = false
|
||||
|
||||
onMounted(() => {
|
||||
addNotifications()
|
||||
setSidebarLinks()
|
||||
socket.on('publish_lms_notifications', (data) => {
|
||||
unreadNotifications.reload()
|
||||
})
|
||||
addNotifications()
|
||||
})
|
||||
|
||||
const setSidebarLinks = () => {
|
||||
sidebarSettings.reload(
|
||||
{},
|
||||
{
|
||||
@@ -144,7 +212,7 @@ onMounted(() => {
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
const unreadNotifications = createResource({
|
||||
cache: 'Unread Notifications Count',
|
||||
@@ -272,16 +340,6 @@ const getSidebarFromStorage = () => {
|
||||
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 = () => {
|
||||
sidebarStore.isSidebarCollapsed = !sidebarStore.isSidebarCollapsed
|
||||
localStorage.setItem(
|
||||
@@ -297,4 +355,221 @@ const toggleWebPages = () => {
|
||||
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>
|
||||
|
||||
@@ -63,6 +63,9 @@
|
||||
</ListSelectBanner>
|
||||
</ListView>
|
||||
</div>
|
||||
<div v-else class="text-sm italic text-ink-gray-5">
|
||||
{{ __('No courses added') }}
|
||||
</div>
|
||||
<BatchCourseModal
|
||||
v-model="showCourseModal"
|
||||
:batch="batch"
|
||||
|
||||
@@ -264,7 +264,8 @@ const students = createResource({
|
||||
auto: true,
|
||||
onSuccess(data) {
|
||||
chartData.value = getChartData()
|
||||
showProgressChart.value = data.length && true
|
||||
showProgressChart.value =
|
||||
data.length && (props.batch?.courses?.length || assessmentCount.value)
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -116,6 +116,7 @@ import { createResource, Avatar, Button, FormControl, Badge } from 'frappe-ui'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ref, watch, reactive, inject } from 'vue'
|
||||
import { RefreshCw, Plus, X } from 'lucide-vue-next'
|
||||
import { useOnboarding } from 'frappe-ui/frappe'
|
||||
|
||||
const router = useRouter()
|
||||
const show = defineModel('show')
|
||||
@@ -125,6 +126,7 @@ const memberList = ref([])
|
||||
const hasNextPage = ref(false)
|
||||
const showForm = ref(false)
|
||||
const dayjs = inject('$dayjs')
|
||||
const { updateOnboardingStep } = useOnboarding('learning')
|
||||
|
||||
const member = reactive({
|
||||
email: '',
|
||||
@@ -185,6 +187,7 @@ const newMember = createResource({
|
||||
auto: false,
|
||||
onSuccess(data) {
|
||||
show.value = false
|
||||
updateOnboardingStep('invite_students')
|
||||
router.push({
|
||||
name: 'Profile',
|
||||
params: {
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
doctype="Course Evaluator"
|
||||
v-model="evaluator"
|
||||
:label="__('Evaluator')"
|
||||
:onCreate="(value, close) => openSettings(close)"
|
||||
class="mt-4"
|
||||
/>
|
||||
</template>
|
||||
@@ -34,11 +35,15 @@ import { Dialog, createResource } from 'frappe-ui'
|
||||
import { ref } from 'vue'
|
||||
import Link from '@/components/Controls/Link.vue'
|
||||
import { showToast } from '@/utils'
|
||||
import { useOnboarding } from 'frappe-ui/frappe'
|
||||
import { useSettings } from '@/stores/settings'
|
||||
|
||||
const show = defineModel()
|
||||
const course = ref(null)
|
||||
const evaluator = ref(null)
|
||||
const courses = defineModel('courses')
|
||||
const { updateOnboardingStep } = useOnboarding('learning')
|
||||
const settingsStore = useSettings()
|
||||
|
||||
const props = defineProps({
|
||||
batch: {
|
||||
@@ -69,6 +74,7 @@ const addCourse = (close) => {
|
||||
{
|
||||
onSuccess() {
|
||||
courses.value.reload()
|
||||
updateOnboardingStep('add_batch_course')
|
||||
close()
|
||||
course.value = null
|
||||
evaluator.value = null
|
||||
@@ -79,4 +85,10 @@ const addCourse = (close) => {
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const openSettings = (close) => {
|
||||
close()
|
||||
settingsStore.activeTab = 'Categories'
|
||||
settingsStore.isSettingsOpen = true
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -81,11 +81,11 @@ import { reactive, watch } from 'vue'
|
||||
import { showToast, getFileSize } from '@/utils/'
|
||||
import { capture } from '@/telemetry'
|
||||
import { FileText, X } from 'lucide-vue-next'
|
||||
import { useSettings } from '@/stores/settings'
|
||||
import { useOnboarding } from 'frappe-ui/frappe'
|
||||
|
||||
const show = defineModel()
|
||||
const outline = defineModel('outline')
|
||||
const settingsStore = useSettings()
|
||||
const { updateOnboardingStep } = useOnboarding('learning')
|
||||
|
||||
const props = defineProps({
|
||||
course: {
|
||||
@@ -140,14 +140,12 @@ const addChapter = async (close) => {
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
capture('chapter_created')
|
||||
updateOnboardingStep('create_first_chapter')
|
||||
chapterReference.submit(
|
||||
{ name: data.name },
|
||||
{
|
||||
onSuccess(data) {
|
||||
cleanChapter()
|
||||
/* if (!settingsStore.onboardingDetails.data?.is_onboarded) {
|
||||
settingsStore.onboardingDetails.reload()
|
||||
} */
|
||||
outline.value.reload()
|
||||
showToast(
|
||||
__('Success'),
|
||||
|
||||
@@ -29,9 +29,11 @@ import { Dialog, createResource } from 'frappe-ui'
|
||||
import { ref } from 'vue'
|
||||
import Link from '@/components/Controls/Link.vue'
|
||||
import { showToast } from '@/utils'
|
||||
import { useOnboarding } from 'frappe-ui/frappe'
|
||||
|
||||
const students = defineModel('reloadStudents')
|
||||
const student = ref()
|
||||
const { updateOnboardingStep } = useOnboarding('learning')
|
||||
const show = defineModel()
|
||||
|
||||
const props = defineProps({
|
||||
@@ -61,6 +63,7 @@ const addStudent = (close) => {
|
||||
onSuccess() {
|
||||
students.value.reload()
|
||||
student.value = null
|
||||
updateOnboardingStep('add_batch_student')
|
||||
close()
|
||||
},
|
||||
onError(err) {
|
||||
|
||||
Reference in New Issue
Block a user