feat: new course form slow
This commit is contained in:
@@ -19,12 +19,14 @@
|
||||
showOptions = true
|
||||
}
|
||||
"
|
||||
@click="(e) => {
|
||||
showOptions = true
|
||||
nextTick(() => {
|
||||
setFocus()
|
||||
})
|
||||
}"
|
||||
@click="
|
||||
(e) => {
|
||||
showOptions = true
|
||||
nextTick(() => {
|
||||
setFocus()
|
||||
})
|
||||
}
|
||||
"
|
||||
@focus="
|
||||
() => {
|
||||
if (!filterOptions.data || filterOptions.data.length === 0) {
|
||||
@@ -60,7 +62,11 @@
|
||||
>
|
||||
<div class="flex flex-col gap-1 p-1">
|
||||
<div class="text-base font-medium text-ink-gray-8">
|
||||
{{ option.description }}
|
||||
{{
|
||||
option.value == option.label
|
||||
? option.description
|
||||
: option.label
|
||||
}}
|
||||
</div>
|
||||
<div class="text-sm text-ink-gray-5">
|
||||
{{ option.value }}
|
||||
@@ -159,6 +165,7 @@ const error = ref(null)
|
||||
const query = ref('')
|
||||
const text = ref('')
|
||||
const showOptions = ref(false)
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const selectedValue = computed({
|
||||
get: () => query.value || '',
|
||||
@@ -166,6 +173,7 @@ const selectedValue = computed({
|
||||
query.value = ''
|
||||
val?.value && addValue(val.value)
|
||||
showOptions.value = false
|
||||
emit('update:modelValue', values.value)
|
||||
},
|
||||
})
|
||||
|
||||
@@ -237,6 +245,7 @@ const addValue = (value) => {
|
||||
|
||||
const removeValue = (value) => {
|
||||
values.value = values.value.filter((v) => v !== value)
|
||||
emit('update:modelValue', values.value)
|
||||
}
|
||||
|
||||
function setFocus() {
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
<CertificationLinks :courseName="course.data.name" class="w-full" />
|
||||
</div>
|
||||
<router-link
|
||||
v-else-if="course.data.paid_course"
|
||||
v-else-if="course.data.paid_course && !isAdmin"
|
||||
:to="{
|
||||
name: 'Billing',
|
||||
params: {
|
||||
|
||||
@@ -15,7 +15,10 @@
|
||||
{{ __(title) }}
|
||||
</div>
|
||||
<Button size="sm" v-if="allowEdit" @click="openChapterModal()">
|
||||
{{ __('Add Chapter') }}
|
||||
<template #prefix>
|
||||
<Plus class="size-4 stroke-1.5" />
|
||||
</template>
|
||||
{{ __('Add') }}
|
||||
</Button>
|
||||
</div>
|
||||
<div
|
||||
@@ -174,6 +177,7 @@ import {
|
||||
FilePenLine,
|
||||
HelpCircle,
|
||||
MonitorPlay,
|
||||
Plus,
|
||||
Trash2,
|
||||
} from 'lucide-vue-next'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
@@ -23,10 +23,8 @@
|
||||
(value, close) => {
|
||||
close()
|
||||
router.push({
|
||||
name: 'CourseForm',
|
||||
params: {
|
||||
courseName: 'new',
|
||||
},
|
||||
name: 'Courses',
|
||||
query: { newCourse: '1' },
|
||||
})
|
||||
}
|
||||
"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="border rounded-md p-3 space-y-2">
|
||||
<div class="border rounded-lg p-3 space-y-2">
|
||||
<div class="text-ink-gray-5">
|
||||
{{ __(title) }}
|
||||
</div>
|
||||
|
||||
@@ -8,22 +8,24 @@
|
||||
<h1 class="mb-3 px-2 pt-2 text-lg font-semibold text-ink-gray-9">
|
||||
{{ __('Settings') }}
|
||||
</h1>
|
||||
<div v-for="tab in tabs" :key="tab.label">
|
||||
<div
|
||||
v-if="!tab.hideLabel"
|
||||
class="mb-2 mt-3 flex cursor-pointer gap-1.5 px-1 text-base text-ink-gray-5 transition-all duration-300 ease-in-out"
|
||||
>
|
||||
<span>{{ __(tab.label) }}</span>
|
||||
</div>
|
||||
<nav class="space-y-1">
|
||||
<div v-for="item in tab.items" @click="activeTab = item">
|
||||
<SidebarLink
|
||||
:link="item"
|
||||
:key="item.label"
|
||||
:activeTab="activeTab?.label"
|
||||
/>
|
||||
<div class="space-y-6">
|
||||
<div v-for="tab in tabs" :key="tab.label">
|
||||
<div
|
||||
v-if="!tab.hideLabel"
|
||||
class="mb-2 mt-3 flex cursor-pointer gap-1.5 px-1 text-base text-ink-gray-5 transition-all duration-300 ease-in-out"
|
||||
>
|
||||
<span>{{ __(tab.label) }}</span>
|
||||
</div>
|
||||
</nav>
|
||||
<nav class="space-y-1">
|
||||
<div v-for="item in tab.items" @click="activeTab = item">
|
||||
<SidebarLink
|
||||
:link="item"
|
||||
:key="item.label"
|
||||
:activeTab="activeTab?.label"
|
||||
/>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
|
||||
@@ -405,9 +405,13 @@ const steps = reactive([
|
||||
minimize.value = true
|
||||
let course = await getFirstCourse()
|
||||
if (course) {
|
||||
router.push({ name: 'CourseForm', params: { courseName: course } })
|
||||
router.push({
|
||||
name: 'CourseDetail',
|
||||
params: { courseName: course },
|
||||
hash: '#settings',
|
||||
})
|
||||
} else {
|
||||
router.push({ name: 'CourseForm' })
|
||||
router.push({ name: 'Courses', query: { newCourse: '1' } })
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -422,11 +426,12 @@ const steps = reactive([
|
||||
let course = await getFirstCourse()
|
||||
if (course) {
|
||||
router.push({
|
||||
name: 'CourseForm',
|
||||
name: 'CourseDetail',
|
||||
params: { courseName: course },
|
||||
hash: '#settings',
|
||||
})
|
||||
} else {
|
||||
router.push({ name: 'Courses' })
|
||||
router.push({ name: 'Courses', query: { newCourse: '1' } })
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
<template>
|
||||
<div class="p-5">
|
||||
<div class="grid grid-cols-3 gap-5 mb-5">
|
||||
<NumberChartGraph :title="__('Enrolled')" :value="memberCount || 0" />
|
||||
<div class="grid grid-cols-4 gap-5 mb-5">
|
||||
<NumberChartGraph
|
||||
:title="__('Enrolled')"
|
||||
:value="formatAmount(course.data?.enrollments)"
|
||||
/>
|
||||
<NumberChartGraph
|
||||
:title="__('Average Completion Rate')"
|
||||
:value="averageCompletionRate"
|
||||
@@ -14,17 +17,18 @@
|
||||
<Star class="size-5 text-transparent fill-amber-500" />
|
||||
</template>
|
||||
</NumberChartGraph>
|
||||
<NumberChartGraph :title="__('Lessons')" :value="course.data?.lessons" />
|
||||
</div>
|
||||
<div class="grid grid-cols-[2fr_1fr] gap-5 items-start">
|
||||
<div class="border rounded-md py-3 px-4">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="border rounded-lg py-3 px-4">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div class="text-lg font-semibold">
|
||||
{{ __('Students') }}
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<FormControl
|
||||
v-model="searchFilter"
|
||||
:placeholder="__('Search by Member')"
|
||||
:placeholder="__('Search by name')"
|
||||
type="text"
|
||||
/>
|
||||
<Button @click="showEnrollmentModal = true">
|
||||
@@ -49,7 +53,7 @@
|
||||
}"
|
||||
>
|
||||
<ListHeader
|
||||
class="mb-2 grid items-center space-x-4 rounded bg-surface-white p-2"
|
||||
class="mb-2 grid items-center space-x-4 rounded bg-surface-white border-b rounded-none p-2"
|
||||
>
|
||||
<ListHeaderItem
|
||||
:item="item"
|
||||
@@ -115,67 +119,82 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="border rounded-md p-4">
|
||||
<div class="text-ink-gray-5 mb-4">
|
||||
{{ __('Progress Summary') }}
|
||||
</div>
|
||||
<div class="grid grid-cols-[2fr_1fr] items-center justify-between">
|
||||
<div class="flex flex-col space-y-3 flex-1 text-xs">
|
||||
<div
|
||||
class="flex items-center"
|
||||
v-for="row in chartDetails.data?.progress_distribution"
|
||||
>
|
||||
<div class="space-y-5">
|
||||
<div class="border rounded-lg p-4">
|
||||
<div class="text-ink-gray-5 mb-4">
|
||||
{{ __('Progress Summary') }}
|
||||
</div>
|
||||
<div class="grid grid-cols-[2fr_1fr] items-center justify-between">
|
||||
<div class="flex flex-col space-y-4 flex-1 text-sm">
|
||||
<div
|
||||
class="size-2 rounded"
|
||||
:style="{
|
||||
backgroundColor:
|
||||
colors[theme][
|
||||
row.name.startsWith('Just')
|
||||
? 'red'
|
||||
: row.name.startsWith('In')
|
||||
? 'amber'
|
||||
: 'green'
|
||||
][400],
|
||||
}"
|
||||
></div>
|
||||
<div class="ml-2">
|
||||
{{ row.name }}
|
||||
</div>
|
||||
<div class="ml-auto">
|
||||
{{ Math.round((row.value / course.data?.enrollments) * 100) }}%
|
||||
class="flex items-center"
|
||||
v-for="row in chartDetails.data?.progress_distribution"
|
||||
>
|
||||
<div
|
||||
class="size-2 rounded"
|
||||
:style="{
|
||||
backgroundColor:
|
||||
colors[theme][
|
||||
row.name.startsWith('Just')
|
||||
? 'red'
|
||||
: row.name.startsWith('In')
|
||||
? 'amber'
|
||||
: 'green'
|
||||
][400],
|
||||
}"
|
||||
></div>
|
||||
<Tooltip :text="row.name.split('(')[1].replace(')', '')">
|
||||
<div class="ml-2">
|
||||
{{ row.name.split('(')[0] }}
|
||||
</div>
|
||||
</Tooltip>
|
||||
<div class="ml-auto">
|
||||
{{
|
||||
Math.round((row.value / course.data?.enrollments) * 100)
|
||||
}}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ECharts
|
||||
class="w-40 h-20"
|
||||
:options="{
|
||||
color: progressColors,
|
||||
series: [
|
||||
{
|
||||
type: 'pie',
|
||||
radius: ['50%', '70%'],
|
||||
center: ['50%', '50%'],
|
||||
label: {
|
||||
show: false,
|
||||
},
|
||||
labelLine: {
|
||||
show: false,
|
||||
},
|
||||
emphasis: {
|
||||
<ECharts
|
||||
class="w-40 h-20"
|
||||
:options="{
|
||||
color: progressColors,
|
||||
series: [
|
||||
{
|
||||
type: 'pie',
|
||||
radius: ['50%', '70%'],
|
||||
center: ['50%', '50%'],
|
||||
label: {
|
||||
show: false,
|
||||
},
|
||||
scale: false,
|
||||
labelLine: {
|
||||
show: false,
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: false,
|
||||
},
|
||||
scale: false,
|
||||
},
|
||||
legend: {
|
||||
show: false,
|
||||
},
|
||||
data: chartDetails.data?.progress_distribution || [],
|
||||
},
|
||||
legend: {
|
||||
show: false,
|
||||
},
|
||||
data: chartDetails.data?.progress_distribution || [],
|
||||
},
|
||||
],
|
||||
showInlineLabels: false,
|
||||
}"
|
||||
/>
|
||||
],
|
||||
showInlineLabels: false,
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{{ lessonProgress.data }}
|
||||
<div v-if="lessonProgress.data?.length" class="border rounded-lg p-4">
|
||||
<div class="text-ink-gray-5 mb-4">
|
||||
{{ __('Lesson Completion') }}
|
||||
</div>
|
||||
<!-- <div v-for="progress in lessonProgress.data">
|
||||
{{ progress }}
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -201,9 +220,11 @@ import {
|
||||
ListRows,
|
||||
ListRow,
|
||||
ListRowItem,
|
||||
Tooltip,
|
||||
} from 'frappe-ui'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { Plus, Star } from 'lucide-vue-next'
|
||||
import { formatAmount } from '@/utils'
|
||||
import colors from '@/utils/frappe-ui-colors.json'
|
||||
import CourseEnrollmentModal from '@/pages/Courses/CourseEnrollmentModal.vue'
|
||||
import NumberChartGraph from '@/components/NumberChartGraph.vue'
|
||||
@@ -215,7 +236,6 @@ const props = defineProps<{
|
||||
|
||||
const showEnrollmentModal = ref(false)
|
||||
const searchFilter = ref<string | null>(null)
|
||||
const memberCount = ref<number>(props.course.data?.enrollments || 0)
|
||||
const theme = ref<'darkMode' | 'lightMode'>(
|
||||
localStorage.getItem('theme') == 'dark' ? 'darkMode' : 'lightMode'
|
||||
)
|
||||
@@ -252,6 +272,25 @@ const progressList = createListResource({
|
||||
auto: true,
|
||||
})
|
||||
|
||||
const lessonProgress = createResource({
|
||||
url: 'lms.lms.api.get_lesson_completion_stats',
|
||||
params: {
|
||||
course: props.course.data?.name,
|
||||
},
|
||||
auto: true,
|
||||
})
|
||||
|
||||
/* const lessonProgress = createListResource({
|
||||
doctype: 'LMS Course Progress',
|
||||
filters: {
|
||||
course: props.course.data?.name,
|
||||
status: 'Complete',
|
||||
},
|
||||
fields: ['lesson', `count(name) as completed_count`],
|
||||
groupBy: 'lesson',
|
||||
auto: true,
|
||||
}) */
|
||||
|
||||
watch([searchFilter], () => {
|
||||
let filterApplied = false
|
||||
let filters: Filters = {
|
||||
@@ -266,16 +305,7 @@ watch([searchFilter], () => {
|
||||
progressList.update({
|
||||
filters: filters,
|
||||
})
|
||||
progressList.reload(
|
||||
{},
|
||||
{
|
||||
onSuccess(data: any[]) {
|
||||
memberCount.value = filterApplied
|
||||
? data.length
|
||||
: props.course.data?.enrollments || 0
|
||||
},
|
||||
}
|
||||
)
|
||||
progressList.reload()
|
||||
})
|
||||
|
||||
const averageCompletionRate = computed(() => {
|
||||
|
||||
@@ -5,13 +5,16 @@
|
||||
>
|
||||
<Breadcrumbs class="h-7" :items="breadcrumbs" />
|
||||
<div v-if="tabIndex == 2" class="flex items-center space-x-2">
|
||||
<Button>
|
||||
<Badge v-if="childRef?.isDirty" theme="orange">
|
||||
{{ __('Not Saved') }}
|
||||
</Badge>
|
||||
<Button @click="childRef.trashCourse()">
|
||||
<template #icon>
|
||||
<Trash2 class="w-4 h-4 stroke-1.5" />
|
||||
</template>
|
||||
</Button>
|
||||
<Button variant="solid">
|
||||
{{ __("Save") }}
|
||||
<Button variant="solid" @click="childRef.submitCourse()">
|
||||
{{ __('Save') }}
|
||||
</Button>
|
||||
</div>
|
||||
</header>
|
||||
@@ -19,14 +22,21 @@
|
||||
<div v-else>
|
||||
<Tabs :tabs="tabs" v-model="tabIndex">
|
||||
<template #tab-panel="{ tab }">
|
||||
<component :is="tab.component" :course="course" />
|
||||
<component :is="tab.component" :course="course" ref="childRef" />
|
||||
</template>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { Button, createResource, Breadcrumbs, Tabs, usePageMeta } from 'frappe-ui'
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
createResource,
|
||||
Breadcrumbs,
|
||||
Tabs,
|
||||
usePageMeta,
|
||||
} from 'frappe-ui'
|
||||
import { computed, inject, markRaw, onMounted, ref, watch } from 'vue'
|
||||
import { sessionStore } from '@/stores/session'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
@@ -40,6 +50,7 @@ const router = useRouter()
|
||||
const route = useRoute()
|
||||
const user = inject('$user')
|
||||
const tabIndex = ref(0)
|
||||
const childRef = ref(null)
|
||||
|
||||
const props = defineProps({
|
||||
courseName: {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<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="grid grid-cols-1 md:grid-cols-[70%,30%] overflow-hidden">
|
||||
<div v-if="courseResource.doc" class="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">
|
||||
@@ -12,12 +12,14 @@
|
||||
v-model="courseResource.doc.title"
|
||||
:label="__('Title')"
|
||||
:required="true"
|
||||
@input="makeFormDirty()"
|
||||
/>
|
||||
<Link
|
||||
doctype="LMS Category"
|
||||
v-model="courseResource.doc.category"
|
||||
:label="__('Category')"
|
||||
:onCreate="(value, close) => openSettings('Categories', close)"
|
||||
@update:modelValue="makeFormDirty()"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-5">
|
||||
@@ -28,6 +30,7 @@
|
||||
:filters="{ ignore_user_type: 1 }"
|
||||
:onCreate="(close) => openSettings('Members', close)"
|
||||
:required="true"
|
||||
@update:modelValue="makeFormDirty()"
|
||||
/>
|
||||
<div>
|
||||
<div class="text-xs text-ink-gray-5">
|
||||
@@ -62,6 +65,7 @@
|
||||
v-model="courseResource.doc.image"
|
||||
:label="__('Course Image')"
|
||||
:required="false"
|
||||
@update:modelValue="makeFormDirty()"
|
||||
/>
|
||||
|
||||
<ColorSwatches
|
||||
@@ -69,6 +73,7 @@
|
||||
:label="__('Color')"
|
||||
:description="__('Choose a color for the course card')"
|
||||
class="w-full"
|
||||
@update:modelValue="makeFormDirty()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -86,11 +91,13 @@
|
||||
type="checkbox"
|
||||
v-model="courseResource.doc.published"
|
||||
:label="__('Published')"
|
||||
@change="makeFormDirty()"
|
||||
/>
|
||||
<FormControl
|
||||
v-model="courseResource.doc.published_on"
|
||||
:label="__('Published On')"
|
||||
type="date"
|
||||
@change="makeFormDirty()"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col space-y-5">
|
||||
@@ -98,16 +105,19 @@
|
||||
type="checkbox"
|
||||
v-model="courseResource.doc.upcoming"
|
||||
:label="__('Upcoming')"
|
||||
@change="makeFormDirty()"
|
||||
/>
|
||||
<FormControl
|
||||
type="checkbox"
|
||||
v-model="courseResource.doc.featured"
|
||||
:label="__('Featured')"
|
||||
@change="makeFormDirty()"
|
||||
/>
|
||||
<FormControl
|
||||
type="checkbox"
|
||||
v-model="courseResource.doc.disable_self_learning"
|
||||
:label="__('Disable Self Enrollment')"
|
||||
@change="makeFormDirty()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -128,6 +138,7 @@
|
||||
)
|
||||
"
|
||||
:required="true"
|
||||
@change="makeFormDirty()"
|
||||
/>
|
||||
<div class="">
|
||||
<div class="mb-1.5 text-sm text-ink-gray-5">
|
||||
@@ -136,7 +147,12 @@
|
||||
</div>
|
||||
<TextEditor
|
||||
:content="courseResource.doc.description"
|
||||
@change="(val) => (courseResource.doc.description = val)"
|
||||
@change="
|
||||
(val) => {
|
||||
courseResource.doc.description = val
|
||||
makeFormDirty()
|
||||
}
|
||||
"
|
||||
: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]"
|
||||
@@ -151,6 +167,7 @@
|
||||
'Paste the youtube link of a short video introducing the course'
|
||||
)
|
||||
"
|
||||
@input="makeFormDirty()"
|
||||
/>
|
||||
|
||||
<MultiSelect
|
||||
@@ -161,11 +178,12 @@
|
||||
:onCreate="
|
||||
(close) => {
|
||||
router.push({
|
||||
name: 'CourseForm',
|
||||
params: { courseName: 'new' },
|
||||
name: 'Courses',
|
||||
query: { newCourse: '1' },
|
||||
})
|
||||
}
|
||||
"
|
||||
@update:modelValue="makeFormDirty()"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -178,25 +196,35 @@
|
||||
type="checkbox"
|
||||
v-model="courseResource.doc.paid_course"
|
||||
:label="__('Paid Course')"
|
||||
@change="makeFormDirty()"
|
||||
/>
|
||||
<FormControl
|
||||
type="checkbox"
|
||||
v-model="courseResource.doc.enable_certification"
|
||||
:label="__('Completion Certificate')"
|
||||
@change="makeFormDirty()"
|
||||
/>
|
||||
<FormControl
|
||||
type="checkbox"
|
||||
v-model="courseResource.doc.paid_certificate"
|
||||
:label="__('Paid Certificate')"
|
||||
@change="makeFormDirty()"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-5">
|
||||
<div class="space-y-5">
|
||||
<FormControl
|
||||
v-if="courseResource.doc.paid_course || courseResource.doc.paid_certificate"
|
||||
v-if="
|
||||
courseResource.doc.paid_course ||
|
||||
courseResource.doc.paid_certificate
|
||||
"
|
||||
v-model="courseResource.doc.course_price"
|
||||
:label="__('Amount')"
|
||||
:required="courseResource.doc.paid_course || courseResource.doc.paid_certificate"
|
||||
:required="
|
||||
courseResource.doc.paid_course ||
|
||||
courseResource.doc.paid_certificate
|
||||
"
|
||||
@input="makeFormDirty()"
|
||||
/>
|
||||
<Link
|
||||
v-if="courseResource.doc.paid_certificate"
|
||||
@@ -207,16 +235,24 @@
|
||||
:onCreate="
|
||||
(value, close) => openSettings('Evaluators', close)
|
||||
"
|
||||
@update:modelValue="makeFormDirty()"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-5">
|
||||
<Link
|
||||
v-if="courseResource.doc.paid_course || courseResource.doc.paid_certificate"
|
||||
v-if="
|
||||
courseResource.doc.paid_course ||
|
||||
courseResource.doc.paid_certificate
|
||||
"
|
||||
doctype="Currency"
|
||||
v-model="courseResource.doc.currency"
|
||||
:filters="{ enabled: 1 }"
|
||||
:label="__('Currency')"
|
||||
:required="courseResource.doc.paid_course || courseResource.doc.paid_certificate"
|
||||
:required="
|
||||
courseResource.doc.paid_course ||
|
||||
courseResource.doc.paid_certificate
|
||||
"
|
||||
@update:modelValue="makeFormDirty()"
|
||||
/>
|
||||
<FormControl
|
||||
v-if="courseResource.doc.paid_certificate"
|
||||
@@ -224,6 +260,7 @@
|
||||
:label="__('Timezone')"
|
||||
:required="courseResource.doc.paid_certificate"
|
||||
:placeholder="__('e.g. IST, UTC, GMT...')"
|
||||
@input="makeFormDirty()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -239,6 +276,7 @@
|
||||
:label="__('Meta Description')"
|
||||
type="textarea"
|
||||
:rows="7"
|
||||
@input="makeFormDirty()"
|
||||
/>
|
||||
<FormControl
|
||||
v-model="meta.keywords"
|
||||
@@ -246,12 +284,13 @@
|
||||
type="textarea"
|
||||
:rows="7"
|
||||
:placeholder="__('Comma separated keywords for SEO')"
|
||||
@input="makeFormDirty()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="border-l">
|
||||
<div class="border-l h-[88vh] overflow-y-auto">
|
||||
<CourseOutline
|
||||
v-if="courseResource.doc"
|
||||
:courseName="courseResource.doc.name"
|
||||
@@ -276,7 +315,6 @@ import {
|
||||
inject,
|
||||
onMounted,
|
||||
onBeforeUnmount,
|
||||
computed,
|
||||
ref,
|
||||
reactive,
|
||||
watch,
|
||||
@@ -306,6 +344,7 @@ const instructors = ref([])
|
||||
const related_courses = ref([])
|
||||
const app = getCurrentInstance()
|
||||
const { $dialog } = app.appContext.config.globalProperties
|
||||
const isDirty = ref(false)
|
||||
|
||||
const props = defineProps({
|
||||
course: {
|
||||
@@ -365,7 +404,6 @@ const updateCourseData = (data) => {
|
||||
let key = checkboxes[idx]
|
||||
data[key] = data[key] ? true : false
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const submitCourse = () => {
|
||||
@@ -384,24 +422,29 @@ const validateFields = () => {
|
||||
}
|
||||
|
||||
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'))
|
||||
courseResource.setValue.submit(
|
||||
{
|
||||
...courseResource.doc,
|
||||
instructors: instructors.value.map((instructor) => ({
|
||||
instructor: instructor,
|
||||
})),
|
||||
related_courses: related_courses.value.map((course) => ({
|
||||
course: course,
|
||||
})),
|
||||
},
|
||||
onError(err) {
|
||||
toast.error(err.messages?.[0] || err)
|
||||
console.error(err)
|
||||
},
|
||||
})
|
||||
{
|
||||
onSuccess() {
|
||||
updateMetaInfo('courses', courseResource.doc?.name, meta)
|
||||
toast.success(__('Course updated successfully'))
|
||||
isDirty.value = false
|
||||
courseResource.reload()
|
||||
},
|
||||
onError(err) {
|
||||
toast.error(err.messages?.[0] || err)
|
||||
console.error(err)
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const keyboardShortcut = (e) => {
|
||||
@@ -458,6 +501,7 @@ const updateTags = () => {
|
||||
? `${courseResource.doc.tags}, ${newTag.value}`
|
||||
: newTag.value
|
||||
newTag.value = ''
|
||||
makeFormDirty()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -467,6 +511,7 @@ const removeTag = (tag) => {
|
||||
.filter((t) => t !== tag)
|
||||
.join(', ')
|
||||
newTag.value = ''
|
||||
makeFormDirty()
|
||||
}
|
||||
|
||||
const check_permission = () => {
|
||||
@@ -484,10 +529,20 @@ const check_permission = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const makeFormDirty = () => {
|
||||
isDirty.value = true
|
||||
}
|
||||
|
||||
usePageMeta(() => {
|
||||
return {
|
||||
title: courseResource.doc?.title,
|
||||
icon: brand.favicon,
|
||||
}
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
submitCourse,
|
||||
trashCourse,
|
||||
isDirty,
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -106,7 +106,11 @@
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<NewCourseModal v-if="showCourseModal" v-model="showCourseModal" :courses="courses" />
|
||||
<NewCourseModal
|
||||
v-if="showCourseModal"
|
||||
v-model="showCourseModal"
|
||||
:courses="courses"
|
||||
/>
|
||||
</template>
|
||||
<script setup>
|
||||
import {
|
||||
@@ -133,7 +137,12 @@ const user = inject('$user')
|
||||
const dayjs = inject('$dayjs')
|
||||
const start = ref(0)
|
||||
const pageLength = ref(30)
|
||||
const categories = ref([])
|
||||
const categories = ref([
|
||||
{
|
||||
label: '',
|
||||
value: null,
|
||||
},
|
||||
])
|
||||
const currentCategory = ref(null)
|
||||
const title = ref('')
|
||||
const certification = ref(false)
|
||||
@@ -148,12 +157,6 @@ onMounted(() => {
|
||||
setFiltersFromQuery()
|
||||
updateCourses()
|
||||
getCourseCount()
|
||||
categories.value = [
|
||||
{
|
||||
label: '',
|
||||
value: null,
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
const setFiltersFromQuery = () => {
|
||||
@@ -161,6 +164,9 @@ const setFiltersFromQuery = () => {
|
||||
title.value = queries.get('title') || ''
|
||||
currentCategory.value = queries.get('category') || null
|
||||
certification.value = queries.get('certification') || false
|
||||
if (queries.get('newCourse') == '1') {
|
||||
showCourseModal.value = true
|
||||
}
|
||||
}
|
||||
|
||||
const courses = createListResource({
|
||||
|
||||
@@ -74,7 +74,7 @@
|
||||
}}
|
||||
</div>
|
||||
<router-link
|
||||
:to="{ name: 'CourseForm', params: { courseName: 'new' } }"
|
||||
:to="{ name: 'Courses', query: { newCourse: '1' } }"
|
||||
class="mt-4"
|
||||
>
|
||||
<Button>
|
||||
|
||||
@@ -471,7 +471,11 @@ const breadcrumbs = computed(() => {
|
||||
},
|
||||
{
|
||||
label: lessonDetails.data?.course_title,
|
||||
route: { name: 'CourseForm', params: { courseName: props.courseName } },
|
||||
route: {
|
||||
name: 'CourseDetail',
|
||||
params: { courseName: props.courseName },
|
||||
hash: '#settings',
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@@ -465,7 +465,6 @@ const getSidebarItems = () => {
|
||||
'Courses',
|
||||
'CourseDetail',
|
||||
'Lesson',
|
||||
'CourseForm',
|
||||
'LessonForm',
|
||||
],
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user