feat: new course modal
This commit is contained in:
@@ -1,38 +1,21 @@
|
||||
<template>
|
||||
<div class="h-full">
|
||||
<div class="grid grid-cols-1 md:grid-cols-[70%,30%] h-full">
|
||||
<div>
|
||||
<header
|
||||
class="sticky top-0 z-10 flex flex-col md:flex-row md:items-center justify-between border-b bg-surface-white px-3 py-2.5 sm:px-5"
|
||||
>
|
||||
<Breadcrumbs class="h-7" :items="breadcrumbs" />
|
||||
<div class="flex items-center mt-3 md:mt-0">
|
||||
<Button v-if="courseResource.data?.name" @click="trashCourse()">
|
||||
<template #icon>
|
||||
<Trash2 class="w-4 h-4 stroke-1.5" />
|
||||
</template>
|
||||
</Button>
|
||||
<Button variant="solid" @click="submitCourse()" class="ml-2">
|
||||
<span>
|
||||
{{ __('Save') }}
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
</header>
|
||||
<div class="mt-5 mb-5">
|
||||
<div class="px-5 md:px-10 pb-5 mb-5 space-y-5 border-b">
|
||||
<div class="pl-5">
|
||||
<div class="grid grid-cols-1 md:grid-cols-[70%,30%] ">
|
||||
<div v-if="courseResource.doc" class="max-h-[88vh] overflow-y-auto">
|
||||
<div class="my-5">
|
||||
<div class="pr-5 md:pr-10 pb-5 mb-5 space-y-5 border-b">
|
||||
<div class="text-lg font-semibold mb-4 text-ink-gray-9">
|
||||
{{ __('Details') }}
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-5">
|
||||
<FormControl
|
||||
v-model="course.title"
|
||||
v-model="courseResource.doc.title"
|
||||
:label="__('Title')"
|
||||
:required="true"
|
||||
/>
|
||||
<Link
|
||||
doctype="LMS Category"
|
||||
v-model="course.category"
|
||||
v-model="courseResource.doc.category"
|
||||
:label="__('Category')"
|
||||
:onCreate="(value, close) => openSettings('Categories', close)"
|
||||
/>
|
||||
@@ -60,8 +43,8 @@
|
||||
<div>
|
||||
<div class="flex items-center flex-wrap gap-2">
|
||||
<div
|
||||
v-if="course.tags"
|
||||
v-for="tag in course.tags?.split(', ')"
|
||||
v-if="courseResource.doc.tags"
|
||||
v-for="tag in courseResource.doc.tags?.split(', ')"
|
||||
class="flex items-center bg-surface-gray-2 text-ink-gray-7 p-2 rounded-md"
|
||||
>
|
||||
{{ tag }}
|
||||
@@ -76,13 +59,13 @@
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-5">
|
||||
<Uploader
|
||||
v-model="course.image"
|
||||
v-model="courseResource.doc.image"
|
||||
:label="__('Course Image')"
|
||||
:required="false"
|
||||
/>
|
||||
|
||||
<ColorSwatches
|
||||
v-model="course.card_gradient"
|
||||
v-model="courseResource.doc.card_gradient"
|
||||
:label="__('Color')"
|
||||
:description="__('Choose a color for the course card')"
|
||||
class="w-full"
|
||||
@@ -90,7 +73,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-5 md:px-10 pb-5 mb-5 space-y-5 border-b">
|
||||
<div class="pr-5 md:pr-10 pb-5 mb-5 space-y-5 border-b">
|
||||
<div class="text-lg font-semibold text-ink-gray-9">
|
||||
{{ __('Settings') }}
|
||||
</div>
|
||||
@@ -101,11 +84,11 @@
|
||||
>
|
||||
<FormControl
|
||||
type="checkbox"
|
||||
v-model="course.published"
|
||||
v-model="courseResource.doc.published"
|
||||
:label="__('Published')"
|
||||
/>
|
||||
<FormControl
|
||||
v-model="course.published_on"
|
||||
v-model="courseResource.doc.published_on"
|
||||
:label="__('Published On')"
|
||||
type="date"
|
||||
/>
|
||||
@@ -113,29 +96,29 @@
|
||||
<div class="flex flex-col space-y-5">
|
||||
<FormControl
|
||||
type="checkbox"
|
||||
v-model="course.upcoming"
|
||||
v-model="courseResource.doc.upcoming"
|
||||
:label="__('Upcoming')"
|
||||
/>
|
||||
<FormControl
|
||||
type="checkbox"
|
||||
v-model="course.featured"
|
||||
v-model="courseResource.doc.featured"
|
||||
:label="__('Featured')"
|
||||
/>
|
||||
<FormControl
|
||||
type="checkbox"
|
||||
v-model="course.disable_self_learning"
|
||||
v-model="courseResource.doc.disable_self_learning"
|
||||
:label="__('Disable Self Enrollment')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-5 md:px-10 pb-5 mb-5 space-y-5 border-b">
|
||||
<div class="pr-5 md:pr-10 pb-5 mb-5 space-y-5 border-b">
|
||||
<div class="text-lg font-semibold text-ink-gray-9">
|
||||
{{ __('About the Course') }}
|
||||
</div>
|
||||
<FormControl
|
||||
v-model="course.short_introduction"
|
||||
v-model="courseResource.doc.short_introduction"
|
||||
type="textarea"
|
||||
:rows="5"
|
||||
:label="__('Short Introduction')"
|
||||
@@ -152,8 +135,8 @@
|
||||
<span class="text-ink-red-3">*</span>
|
||||
</div>
|
||||
<TextEditor
|
||||
:content="course.description"
|
||||
@change="(val) => (course.description = val)"
|
||||
:content="courseResource.doc.description"
|
||||
@change="(val) => (courseResource.doc.description = val)"
|
||||
:editable="true"
|
||||
:fixedMenu="true"
|
||||
editorClass="prose-sm max-w-none border-b border-x bg-surface-gray-2 rounded-b-md py-1 px-2 min-h-[7rem]"
|
||||
@@ -161,7 +144,7 @@
|
||||
</div>
|
||||
|
||||
<FormControl
|
||||
v-model="course.video_link"
|
||||
v-model="courseResource.doc.video_link"
|
||||
:label="__('Preview Video')"
|
||||
:placeholder="
|
||||
__(
|
||||
@@ -174,7 +157,7 @@
|
||||
v-model="related_courses"
|
||||
doctype="LMS Course"
|
||||
:label="__('Related Courses')"
|
||||
:filters="{ name: ['!=', courseResource.data?.name] }"
|
||||
:filters="{ name: ['!=', courseResource.doc?.name] }"
|
||||
:onCreate="
|
||||
(close) => {
|
||||
router.push({
|
||||
@@ -186,41 +169,41 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="px-5 md:px-10 pb-5 space-y-5 border-b">
|
||||
<div class="pr-5 md:pr-10 pb-5 space-y-5 border-b">
|
||||
<div class="text-lg font-semibold mt-5 text-ink-gray-9">
|
||||
{{ __('Pricing and Certification') }}
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-5">
|
||||
<FormControl
|
||||
type="checkbox"
|
||||
v-model="course.paid_course"
|
||||
v-model="courseResource.doc.paid_course"
|
||||
:label="__('Paid Course')"
|
||||
/>
|
||||
<FormControl
|
||||
type="checkbox"
|
||||
v-model="course.enable_certification"
|
||||
v-model="courseResource.doc.enable_certification"
|
||||
:label="__('Completion Certificate')"
|
||||
/>
|
||||
<FormControl
|
||||
type="checkbox"
|
||||
v-model="course.paid_certificate"
|
||||
v-model="courseResource.doc.paid_certificate"
|
||||
:label="__('Paid Certificate')"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-5">
|
||||
<div class="space-y-5">
|
||||
<FormControl
|
||||
v-if="course.paid_course || course.paid_certificate"
|
||||
v-model="course.course_price"
|
||||
v-if="courseResource.doc.paid_course || courseResource.doc.paid_certificate"
|
||||
v-model="courseResource.doc.course_price"
|
||||
:label="__('Amount')"
|
||||
:required="course.paid_course || course.paid_certificate"
|
||||
:required="courseResource.doc.paid_course || courseResource.doc.paid_certificate"
|
||||
/>
|
||||
<Link
|
||||
v-if="course.paid_certificate"
|
||||
v-if="courseResource.doc.paid_certificate"
|
||||
doctype="Course Evaluator"
|
||||
v-model="course.evaluator"
|
||||
v-model="courseResource.doc.evaluator"
|
||||
:label="__('Evaluator')"
|
||||
:required="course.paid_certificate"
|
||||
:required="courseResource.doc.paid_certificate"
|
||||
:onCreate="
|
||||
(value, close) => openSettings('Evaluators', close)
|
||||
"
|
||||
@@ -228,25 +211,25 @@
|
||||
</div>
|
||||
<div class="space-y-5">
|
||||
<Link
|
||||
v-if="course.paid_course || course.paid_certificate"
|
||||
v-if="courseResource.doc.paid_course || courseResource.doc.paid_certificate"
|
||||
doctype="Currency"
|
||||
v-model="course.currency"
|
||||
v-model="courseResource.doc.currency"
|
||||
:filters="{ enabled: 1 }"
|
||||
:label="__('Currency')"
|
||||
:required="course.paid_course || course.paid_certificate"
|
||||
:required="courseResource.doc.paid_course || courseResource.doc.paid_certificate"
|
||||
/>
|
||||
<FormControl
|
||||
v-if="course.paid_certificate"
|
||||
v-model="course.timezone"
|
||||
v-if="courseResource.doc.paid_certificate"
|
||||
v-model="courseResource.doc.timezone"
|
||||
:label="__('Timezone')"
|
||||
:required="course.paid_certificate"
|
||||
:required="courseResource.doc.paid_certificate"
|
||||
:placeholder="__('e.g. IST, UTC, GMT...')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-5 md:px-10 pb-5 space-y-5">
|
||||
<div class="pr-5 md:pr-10 pb-5 space-y-5">
|
||||
<div class="text-lg font-semibold mt-5 text-ink-gray-9">
|
||||
{{ __('Meta Tags') }}
|
||||
</div>
|
||||
@@ -270,9 +253,9 @@
|
||||
</div>
|
||||
<div class="border-l">
|
||||
<CourseOutline
|
||||
v-if="courseResource.data"
|
||||
:courseName="courseResource.data.name"
|
||||
:title="__('Course Outline')"
|
||||
v-if="courseResource.doc"
|
||||
:courseName="courseResource.doc.name"
|
||||
:title="__('Chapters')"
|
||||
:allowEdit="true"
|
||||
/>
|
||||
</div>
|
||||
@@ -281,10 +264,10 @@
|
||||
</template>
|
||||
<script setup>
|
||||
import {
|
||||
Breadcrumbs,
|
||||
TextEditor,
|
||||
Button,
|
||||
createResource,
|
||||
createDocumentResource,
|
||||
FormControl,
|
||||
usePageMeta,
|
||||
toast,
|
||||
@@ -308,7 +291,6 @@ import {
|
||||
} from '@/utils'
|
||||
import { Trash2, X } from 'lucide-vue-next'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useOnboarding, useTelemetry } from 'frappe-ui/frappe'
|
||||
import { sessionStore } from '../../stores/session'
|
||||
import Link from '@/components/Controls/Link.vue'
|
||||
import CourseOutline from '@/components/CourseOutline.vue'
|
||||
@@ -323,39 +305,14 @@ const router = useRouter()
|
||||
const instructors = ref([])
|
||||
const related_courses = ref([])
|
||||
const app = getCurrentInstance()
|
||||
const { capture } = useTelemetry()
|
||||
const { updateOnboardingStep } = useOnboarding('learning')
|
||||
const { $dialog } = app.appContext.config.globalProperties
|
||||
|
||||
const props = defineProps({
|
||||
courseName: {
|
||||
type: String,
|
||||
course: {
|
||||
type: Object,
|
||||
},
|
||||
})
|
||||
|
||||
const course = reactive({
|
||||
title: '',
|
||||
short_introduction: '',
|
||||
description: '',
|
||||
video_link: '',
|
||||
image: null,
|
||||
card_gradient: '',
|
||||
tags: '',
|
||||
category: '',
|
||||
published: false,
|
||||
published_on: '',
|
||||
featured: false,
|
||||
upcoming: false,
|
||||
disable_self_learning: false,
|
||||
enable_certification: false,
|
||||
paid_course: false,
|
||||
paid_certificate: false,
|
||||
course_price: '',
|
||||
currency: '',
|
||||
evaluator: '',
|
||||
timezone: '',
|
||||
})
|
||||
|
||||
const meta = reactive({
|
||||
description: '',
|
||||
keywords: '',
|
||||
@@ -365,18 +322,86 @@ onMounted(() => {
|
||||
if (!user.data?.is_moderator && !user.data?.is_instructor) {
|
||||
router.push({ name: 'Courses' })
|
||||
}
|
||||
|
||||
if (props.courseName !== 'new') {
|
||||
fetchCourseInfo()
|
||||
} else {
|
||||
capture('course_form_opened')
|
||||
}
|
||||
window.addEventListener('keydown', keyboardShortcut)
|
||||
})
|
||||
|
||||
const fetchCourseInfo = () => {
|
||||
courseResource.reload()
|
||||
getMetaInfo('courses', props.courseName, meta)
|
||||
const courseResource = createDocumentResource({
|
||||
doctype: 'LMS Course',
|
||||
name: props.course.data?.name,
|
||||
auto: true,
|
||||
transform(data) {
|
||||
updateCourseData(data)
|
||||
},
|
||||
onSuccess(data) {
|
||||
check_permission()
|
||||
getMetaInfo('courses', data.name, meta)
|
||||
},
|
||||
})
|
||||
|
||||
const updateCourseData = (data) => {
|
||||
Object.keys(data).forEach((key) => {
|
||||
if (key == 'instructors') {
|
||||
instructors.value = []
|
||||
data.instructors.forEach((instructor) => {
|
||||
instructors.value.push(instructor.instructor)
|
||||
})
|
||||
} else if (key == 'related_courses') {
|
||||
related_courses.value = []
|
||||
data.related_courses.forEach((course) => {
|
||||
related_courses.value.push(course.course)
|
||||
})
|
||||
}
|
||||
})
|
||||
let checkboxes = [
|
||||
'published',
|
||||
'upcoming',
|
||||
'disable_self_learning',
|
||||
'paid_course',
|
||||
'featured',
|
||||
'enable_certification',
|
||||
'paid_certificate',
|
||||
]
|
||||
for (let idx in checkboxes) {
|
||||
let key = checkboxes[idx]
|
||||
data[key] = data[key] ? true : false
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const submitCourse = () => {
|
||||
validateFields()
|
||||
updateCourse()
|
||||
}
|
||||
|
||||
const validateFields = () => {
|
||||
courseResource.doc.description = sanitizeHTML(courseResource.doc.description)
|
||||
|
||||
Object.keys(courseResource.doc).forEach((key) => {
|
||||
if (key != 'description' && typeof courseResource.doc[key] === 'string') {
|
||||
courseResource.doc[key] = escapeHTML(courseResource.doc[key])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const updateCourse = () => {
|
||||
courseResource.setValue.submit({
|
||||
...courseResource.doc,
|
||||
instructors: instructors.value.map((instructor) => ({
|
||||
instructor: instructor,
|
||||
})),
|
||||
related_courses: related_courses.value.map((course) => ({
|
||||
course: course,
|
||||
})),
|
||||
}, {
|
||||
onSuccess() {
|
||||
updateMetaInfo('courses', courseResource.doc?.name, meta)
|
||||
toast.success(__('Course updated successfully'))
|
||||
},
|
||||
onError(err) {
|
||||
toast.error(err.messages?.[0] || err)
|
||||
console.error(err)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const keyboardShortcut = (e) => {
|
||||
@@ -394,151 +419,11 @@ onBeforeUnmount(() => {
|
||||
window.removeEventListener('keydown', keyboardShortcut)
|
||||
})
|
||||
|
||||
const courseCreationResource = createResource({
|
||||
url: 'frappe.client.insert',
|
||||
makeParams(values) {
|
||||
return {
|
||||
doc: {
|
||||
doctype: 'LMS Course',
|
||||
image: course.image,
|
||||
instructors: instructors.value.map((instructor) => ({
|
||||
instructor: instructor,
|
||||
})),
|
||||
related_courses: related_courses.value.map((course) => ({
|
||||
course: course,
|
||||
})),
|
||||
...values,
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const courseEditResource = createResource({
|
||||
url: 'frappe.client.set_value',
|
||||
auto: false,
|
||||
makeParams(values) {
|
||||
return {
|
||||
doctype: 'LMS Course',
|
||||
name: values.course,
|
||||
fieldname: {
|
||||
image: course.image,
|
||||
instructors: instructors.value.map((instructor) => ({
|
||||
instructor: instructor,
|
||||
})),
|
||||
related_courses: related_courses.value.map((course) => ({
|
||||
course: course,
|
||||
})),
|
||||
...course,
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const courseResource = createResource({
|
||||
url: 'frappe.client.get',
|
||||
makeParams(values) {
|
||||
return {
|
||||
doctype: 'LMS Course',
|
||||
name: props.courseName,
|
||||
}
|
||||
},
|
||||
auto: false,
|
||||
onSuccess(data) {
|
||||
Object.keys(data).forEach((key) => {
|
||||
if (key == 'instructors') {
|
||||
instructors.value = []
|
||||
data.instructors.forEach((instructor) => {
|
||||
instructors.value.push(instructor.instructor)
|
||||
})
|
||||
} else if (key == 'related_courses') {
|
||||
related_courses.value = []
|
||||
data.related_courses.forEach((course) => {
|
||||
related_courses.value.push(course.course)
|
||||
})
|
||||
} else if (Object.hasOwn(course, key)) course[key] = data[key]
|
||||
})
|
||||
let checkboxes = [
|
||||
'published',
|
||||
'upcoming',
|
||||
'disable_self_learning',
|
||||
'paid_course',
|
||||
'featured',
|
||||
'enable_certification',
|
||||
'paid_certificate',
|
||||
]
|
||||
for (let idx in checkboxes) {
|
||||
let key = checkboxes[idx]
|
||||
course[key] = course[key] ? true : false
|
||||
}
|
||||
|
||||
check_permission()
|
||||
},
|
||||
})
|
||||
|
||||
const validateFields = () => {
|
||||
course.description = sanitizeHTML(course.description)
|
||||
|
||||
Object.keys(course).forEach((key) => {
|
||||
if (key != 'description' && typeof course[key] === 'string') {
|
||||
course[key] = escapeHTML(course[key])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const submitCourse = () => {
|
||||
validateFields()
|
||||
if (courseResource.data) {
|
||||
editCourse()
|
||||
} else {
|
||||
createCourse()
|
||||
}
|
||||
}
|
||||
|
||||
const createCourse = () => {
|
||||
courseCreationResource.submit(course, {
|
||||
onSuccess(data) {
|
||||
updateMetaInfo('courses', data.name, meta)
|
||||
if (user.data?.is_system_manager) {
|
||||
updateOnboardingStep('create_first_course', true, false, () => {
|
||||
localStorage.setItem('firstCourse', data.name)
|
||||
})
|
||||
}
|
||||
|
||||
capture('course_created')
|
||||
toast.success(__('Course created successfully'))
|
||||
router.push({
|
||||
name: 'CourseForm',
|
||||
params: { courseName: data.name },
|
||||
})
|
||||
},
|
||||
onError(err) {
|
||||
toast.error(err.messages?.[0] || err)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const editCourse = () => {
|
||||
courseEditResource.submit(
|
||||
{
|
||||
course: courseResource.data.name,
|
||||
},
|
||||
{
|
||||
onSuccess() {
|
||||
updateMetaInfo('courses', props.courseName, meta)
|
||||
toast.success(__('Course updated successfully'))
|
||||
},
|
||||
onError(err) {
|
||||
toast.error(err.messages?.[0] || err)
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const deleteCourse = createResource({
|
||||
url: 'lms.lms.api.delete_course',
|
||||
makeParams(values) {
|
||||
return {
|
||||
course: props.courseName,
|
||||
course: courseResource.doc?.name,
|
||||
}
|
||||
},
|
||||
onSuccess() {
|
||||
@@ -567,24 +452,17 @@ const trashCourse = () => {
|
||||
})
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.courseName !== 'new',
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
fetchCourseInfo()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const updateTags = () => {
|
||||
if (newTag.value) {
|
||||
course.tags = course.tags ? `${course.tags}, ${newTag.value}` : newTag.value
|
||||
courseResource.doc.tags = courseResource.doc.tags
|
||||
? `${courseResource.doc.tags}, ${newTag.value}`
|
||||
: newTag.value
|
||||
newTag.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
const removeTag = (tag) => {
|
||||
course.tags = course.tags
|
||||
courseResource.doc.tags = courseResource.doc.tags
|
||||
?.split(', ')
|
||||
.filter((t) => t !== tag)
|
||||
.join(', ')
|
||||
@@ -606,29 +484,9 @@ const check_permission = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const breadcrumbs = computed(() => {
|
||||
let crumbs = [
|
||||
{
|
||||
label: 'Courses',
|
||||
route: { name: 'Courses' },
|
||||
},
|
||||
]
|
||||
if (courseResource.data) {
|
||||
crumbs.push({
|
||||
label: course.title,
|
||||
route: { name: 'CourseDetail', params: { courseName: props.courseName } },
|
||||
})
|
||||
}
|
||||
crumbs.push({
|
||||
label: props.courseName == 'new' ? 'New Course' : 'Edit Course',
|
||||
route: { name: 'CourseForm', params: { courseName: props.courseName } },
|
||||
})
|
||||
return crumbs
|
||||
})
|
||||
|
||||
usePageMeta(() => {
|
||||
return {
|
||||
title: courseResource.data?.title || __('New Course'),
|
||||
title: courseResource.doc?.title,
|
||||
icon: brand.favicon,
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user