chore: fixed conflicts

This commit is contained in:
Jannat Patel
2025-05-20 19:09:19 +05:30
97 changed files with 8874 additions and 10099 deletions

View File

@@ -105,7 +105,7 @@ jobs:
- name: cypress pre-requisites - name: cypress pre-requisites
run: | run: |
cd ~/frappe-bench/apps/lms cd ~/frappe-bench/apps/lms
yarn add cypress@^10 --no-lockfile yarn add cypress@^10 --no-lockfile -W
- name: UI Tests - name: UI Tests
run: cd ~/frappe-bench/ && bench --site lms.test run-ui-tests lms --headless run: cd ~/frappe-bench/ && bench --site lms.test run-ui-tests lms --headless

View File

@@ -1,4 +1,4 @@
module.exports = { export default {
parserPreset: "conventional-changelog-conventionalcommits", parserPreset: "conventional-changelog-conventionalcommits",
rules: { rules: {
"subject-empty": [2, "never"], "subject-empty": [2, "never"],

View File

@@ -1,6 +1,6 @@
const { defineConfig } = require("cypress"); import { defineConfig } from "cypress";
module.exports = defineConfig({ export default defineConfig({
projectId: "vandxn", projectId: "vandxn",
adminPassword: "admin", adminPassword: "admin",
testUser: "frappe@example.com", testUser: "frappe@example.com",

View File

@@ -16,9 +16,9 @@ cd frappe-bench
# Use containers instead of localhost # Use containers instead of localhost
bench set-mariadb-host mariadb bench set-mariadb-host mariadb
bench set-redis-cache-host redis:6379 bench set-redis-cache-host redis://redis:6379
bench set-redis-queue-host redis:6379 bench set-redis-queue-host redis://redis:6379
bench set-redis-socketio-host redis:6379 bench set-redis-socketio-host redis://redis:6379
# Remove redis, watch from Procfile # Remove redis, watch from Procfile
sed -i '/redis/d' ./Procfile sed -i '/redis/d' ./Procfile

View File

@@ -47,6 +47,7 @@ declare module 'vue' {
Discussions: typeof import('./src/components/Discussions.vue')['default'] Discussions: typeof import('./src/components/Discussions.vue')['default']
EditCoverImage: typeof import('./src/components/Modals/EditCoverImage.vue')['default'] EditCoverImage: typeof import('./src/components/Modals/EditCoverImage.vue')['default']
EditProfile: typeof import('./src/components/Modals/EditProfile.vue')['default'] EditProfile: typeof import('./src/components/Modals/EditProfile.vue')['default']
EmptyState: typeof import('./src/components/EmptyState.vue')['default']
EvaluationModal: typeof import('./src/components/Modals/EvaluationModal.vue')['default'] EvaluationModal: typeof import('./src/components/Modals/EvaluationModal.vue')['default']
Evaluators: typeof import('./src/components/Evaluators.vue')['default'] Evaluators: typeof import('./src/components/Evaluators.vue')['default']
Event: typeof import('./src/components/Modals/Event.vue')['default'] Event: typeof import('./src/components/Modals/Event.vue')['default']

View File

@@ -27,7 +27,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.141", "frappe-ui": "^0.1.143",
"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

@@ -1,12 +1,13 @@
<template> <template>
<Layout> <FrappeUIProvider>
<router-view /> <Layout>
</Layout> <router-view />
<Dialogs /> </Layout>
<Toasts /> <Dialogs />
</FrappeUIProvider>
</template> </template>
<script setup> <script setup>
import { Toasts } from 'frappe-ui' import { FrappeUIProvider } from 'frappe-ui'
import { Dialogs } from '@/utils/dialogs' import { Dialogs } from '@/utils/dialogs'
import { computed, onMounted, onUnmounted, ref } from 'vue' import { computed, onMounted, onUnmounted, ref } from 'vue'
import { useScreenSize } from './utils/composables' import { useScreenSize } from './utils/composables'

View File

@@ -125,7 +125,7 @@
@click="redirectToWebsite()" @click="redirectToWebsite()"
/> />
</Tooltip> </Tooltip>
<Tooltip :text="__('Help')"> <Tooltip v-if="showOnboarding" :text="__('Help')">
<CircleHelp <CircleHelp
class="size-4 stroke-1.5 text-ink-gray-7 cursor-pointer" class="size-4 stroke-1.5 text-ink-gray-7 cursor-pointer"
@click=" @click="

View File

@@ -191,10 +191,11 @@ import {
FileUploader, FileUploader,
FormControl, FormControl,
TextEditor, TextEditor,
toast,
} from 'frappe-ui' } from 'frappe-ui'
import { computed, inject, onMounted, onBeforeUnmount, ref, watch } from 'vue' import { computed, inject, onMounted, onBeforeUnmount, ref, watch } from 'vue'
import { FileText, X } from 'lucide-vue-next' import { FileText, X } from 'lucide-vue-next'
import { showToast, getFileSize } from '@/utils' import { getFileSize } from '@/utils'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
const submissionFile = ref(null) const submissionFile = ref(null)
@@ -284,7 +285,7 @@ const submissionResource = createDocumentResource({
doctype: 'LMS Assignment Submission', doctype: 'LMS Assignment Submission',
name: props.submissionName, name: props.submissionName,
onError(err) { onError(err) {
showToast(__('Error'), __(err.messages?.[0] || err), 'x') toast.error(err.messages?.[0] || err)
}, },
auto: false, auto: false,
cache: [user.data?.name, props.assignmentID], cache: [user.data?.name, props.assignmentID],
@@ -338,7 +339,7 @@ const submitAssignment = () => {
}, },
{ {
onSuccess(data) { onSuccess(data) {
showToast(__('Success'), __('Changes saved successfully'), 'check') toast.success(__('Changes saved successfully'))
}, },
} }
) )
@@ -352,7 +353,7 @@ const addNewSubmission = () => {
{}, {},
{ {
onSuccess(data) { onSuccess(data) {
showToast('Success', 'Assignment submitted successfully.', 'check') toast.success(__('Assignment submitted successfully'))
if (router.currentRoute.value.name == 'AssignmentSubmission') { if (router.currentRoute.value.name == 'AssignmentSubmission') {
router.push({ router.push({
name: 'AssignmentSubmission', name: 'AssignmentSubmission',
@@ -370,7 +371,7 @@ const addNewSubmission = () => {
submissionResource.reload() submissionResource.reload()
}, },
onError(err) { onError(err) {
showToast('Error', err.messages?.[0] || err, 'x') toast.error(err.messages?.[0] || err)
}, },
} }
) )

View File

@@ -86,9 +86,9 @@ import {
ListRows, ListRows,
ListView, ListView,
ListRowItem, ListRowItem,
toast,
} from 'frappe-ui' } from 'frappe-ui'
import { Plus, Trash2 } from 'lucide-vue-next' import { Plus, Trash2 } from 'lucide-vue-next'
import { showToast } from '@/utils'
const readOnlyMode = window.read_only_mode const readOnlyMode = window.read_only_mode
const showCourseModal = ref(false) const showCourseModal = ref(false)
@@ -152,7 +152,7 @@ const removeCourses = (selections, unselectAll) => {
{ {
onSuccess(data) { onSuccess(data) {
courses.reload() courses.reload()
showToast(__('Success'), __('Courses deleted successfully'), 'check') toast.success(__('Courses deleted successfully'))
unselectAll() unselectAll()
}, },
} }

View File

@@ -2,7 +2,12 @@
<div v-if="batch.data" class="border-2 rounded-md p-5 lg:w-72"> <div v-if="batch.data" class="border-2 rounded-md p-5 lg:w-72">
<div <div
v-if="batch.data.seat_count && seats_left > 0" v-if="batch.data.seat_count && seats_left > 0"
class="text-xs bg-green-100 text-green-700 float-right px-2 py-0.5 rounded-md" class="text-sm bg-green-100 text-green-700 px-2 py-1 rounded-md"
:class="
batch.data.amount || batch.data.courses.length
? 'float-right'
: 'w-fit mb-4'
"
> >
{{ seats_left }} {{ seats_left }}
<span v-if="seats_left > 1"> <span v-if="seats_left > 1">
@@ -117,9 +122,9 @@
</template> </template>
<script setup> <script setup>
import { inject, computed } from 'vue' import { inject, computed } from 'vue'
import { Badge, Button, createResource } from 'frappe-ui' import { Badge, Button, createResource, toast } from 'frappe-ui'
import { BookOpen, Clock, Globe } from 'lucide-vue-next' import { BookOpen, Clock, Globe } from 'lucide-vue-next'
import { formatNumberIntoCurrency, formatTime, showToast } from '@/utils' import { formatNumberIntoCurrency, formatTime } from '@/utils'
import DateRange from '@/components/Common/DateRange.vue' import DateRange from '@/components/Common/DateRange.vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
@@ -151,11 +156,7 @@ const enrollInBatch = () => {
{}, {},
{ {
onSuccess(data) { onSuccess(data) {
showToast( toast.success(__('You have been enrolled in this batch'))
__('Success'),
__('You have been enrolled in this batch'),
'check'
)
router.push({ router.push({
name: 'Batch', name: 'Batch',
params: { params: {

View File

@@ -1,12 +1,11 @@
<template> <template>
<div class=""> <div v-if="batch.data" class="">
<div class="w-full flex items-center justify-between pb-4"> <div class="w-full flex items-center justify-between pb-4">
<div class="font-medium text-ink-gray-7"> <div class="font-medium text-ink-gray-7">
{{ __('Statistics') }} {{ __('Statistics') }}
</div> </div>
</div> </div>
<div class="grid grid-cols-4 gap-5 mb-8"> <div class="grid grid-cols-4 gap-5 mb-8">
<NumberChart <NumberChart
class="border rounded-md" class="border rounded-md"
:config="{ title: __('Students'), value: students.data?.length || 0 }" :config="{ title: __('Students'), value: students.data?.length || 0 }"
@@ -14,7 +13,10 @@
<NumberChart <NumberChart
class="border rounded-md" class="border rounded-md"
:config="{ title: __('Certified'), value: certificationCount.data || 0 }" :config="{
title: __('Certified'),
value: certificationCount.data || 0,
}"
/> />
<NumberChart <NumberChart
@@ -79,26 +81,26 @@
product: 'HomePod', product: 'HomePod',
sales: 200, sales: 200,
}, },
], ],
title: __('Batch Summary'), title: __('Batch Summary'),
subtitle: __('Progress of students in courses and assessments'), subtitle: __('Progress of students in courses and assessments'),
xAxis: { xAxis: {
key: 'product', key: 'product',
title: 'Product', title: 'Product',
type: 'category', type: 'category',
},
yAxis: {
title: __('Number of Students'),
},
swapXY: true,
series: [
{
name: 'sales',
type: 'bar',
}, },
yAxis: { ],
title: __('Number of Students'), }"
}, />
swapXY: true,
series: [
{
name: 'sales',
type: 'bar',
},
],
}"
/>
<div v-if="showProgressChart" class="mb-8"> <div v-if="showProgressChart" class="mb-8">
<div class="text-ink-gray-7 font-medium"> <div class="text-ink-gray-7 font-medium">
@@ -231,9 +233,10 @@
</div> </div>
<StudentModal <StudentModal
:batch="props.batch.name" :batch="props.batch.data.name"
v-model="showStudentModal" v-model="showStudentModal"
v-model:reloadStudents="students" v-model:reloadStudents="students"
v-model:batchModal="props.batch"
/> />
<BatchStudentProgress <BatchStudentProgress
:student="selectedStudent" :student="selectedStudent"
@@ -255,6 +258,7 @@ import {
ListView, ListView,
ListRowItem, ListRowItem,
NumberChart, NumberChart,
toast,
} from 'frappe-ui' } from 'frappe-ui'
import { import {
BookOpen, BookOpen,
@@ -266,7 +270,6 @@ import {
} from 'lucide-vue-next' } from 'lucide-vue-next'
import { ref, watch } from 'vue' import { ref, watch } from 'vue'
import StudentModal from '@/components/Modals/StudentModal.vue' import StudentModal from '@/components/Modals/StudentModal.vue'
import { showToast } from '@/utils'
import ProgressBar from '@/components/ProgressBar.vue' import ProgressBar from '@/components/ProgressBar.vue'
import BatchStudentProgress from '@/components/Modals/BatchStudentProgress.vue' import BatchStudentProgress from '@/components/Modals/BatchStudentProgress.vue'
import ApexChart from 'vue3-apexcharts' import ApexChart from 'vue3-apexcharts'
@@ -290,15 +293,15 @@ const props = defineProps({
const students = createResource({ const students = createResource({
url: 'lms.lms.utils.get_batch_students', url: 'lms.lms.utils.get_batch_students',
cache: ['students', props.batch.name],
params: { params: {
batch: props.batch?.name, batch: props.batch?.data?.name,
}, },
auto: true, auto: true,
onSuccess(data) { onSuccess(data) {
chartData.value = getChartData() chartData.value = getChartData()
showProgressChart.value = showProgressChart.value =
data.length && (props.batch?.courses?.length || assessmentCount.value) data.length &&
(props.batch?.data?.courses?.length || assessmentCount.value)
}, },
}) })
@@ -355,7 +358,8 @@ const removeStudents = (selections, unselectAll) => {
{ {
onSuccess(data) { onSuccess(data) {
students.reload() students.reload()
showToast(__('Success'), __('Students deleted successfully'), 'check') props.batch.reload()
toast.success(__('Students deleted successfully'))
unselectAll() unselectAll()
}, },
} }
@@ -366,10 +370,8 @@ const getChartData = () => {
let data = [] let data = []
console.log(students.data) console.log(students.data)
students.data.forEach(row => { students.data.forEach((row) => {
row.assessments.forEach(assessment => { row.assessments.forEach((assessment) => {})
})
}) })
/* let categories = {} /* let categories = {}
@@ -476,7 +478,7 @@ const certificationCount = createResource({
params: { params: {
doctype: 'LMS Certificate', doctype: 'LMS Certificate',
filters: { filters: {
batch_name: props.batch.name, batch_name: props.batch?.data?.name,
}, },
}, },
auto: true, auto: true,

View File

@@ -5,10 +5,11 @@
{{ label }} {{ label }}
</div> </div>
<Button @click="() => showCategoryForm()"> <Button @click="() => showCategoryForm()">
<template #icon> <template #prefix>
<Plus v-if="!showForm" class="h-3 w-3 stroke-1.5" /> <Plus v-if="!showForm" class="h-3 w-3 stroke-1.5" />
<X v-else class="h-3 w-3 stroke-1.5" /> <X v-else class="h-3 w-3 stroke-1.5" />
</template> </template>
{{ showForm ? __('Close') : __('New') }}
</Button> </Button>
</div> </div>
@@ -28,12 +29,11 @@
</div> </div>
<div class="overflow-y-scroll"> <div class="overflow-y-scroll">
<div class="text-base divide-y space-y-2"> <div class="text-base space-y-2">
<FormControl <FormControl
:value="cat.category" :value="cat.category"
type="text" type="text"
v-for="cat in categories.data" v-for="cat in categories.data"
class=""
@change.stop="(e) => update(cat.name, e.target.value)" @change.stop="(e) => update(cat.name, e.target.value)"
/> />
</div> </div>

View File

@@ -92,7 +92,10 @@
{{ option.label }} {{ option.label }}
</div> </div>
<div <div
v-if="option.description" v-if="
option.description &&
option.description != option.label
"
class="text-xs text-ink-gray-7" class="text-xs text-ink-gray-7"
v-html="option.description" v-html="option.description"
></div> ></div>

View File

@@ -34,7 +34,7 @@
<Button <Button
variant="ghost" variant="ghost"
class="w-full !justify-start" class="w-full !justify-start"
label="Create New" :label="__('Create New')"
@click="attrs.onCreate(value, close)" @click="attrs.onCreate(value, close)"
> >
<template #prefix> <template #prefix>

View File

@@ -4,78 +4,91 @@
{{ label }} {{ label }}
<span class="text-ink-red-3" v-if="required">*</span> <span class="text-ink-red-3" v-if="required">*</span>
</label> </label>
<div class="grid grid-cols-3 gap-2"> <div class="w-full">
<Button <Combobox v-model="selectedValue" nullable>
ref="emails" <Popover class="w-full" v-model:show="showOptions">
v-for="value in values" <template #target="{ togglePopover }">
:key="value" <ComboboxInput
:label="value" ref="search"
theme="gray" class="search-input form-input w-full focus-visible:!ring-0"
variant="subtle" type="text"
class="rounded-md word-break-all" :value="query"
@keydown.delete.capture.stop="removeLastValue" @change="
> (e) => {
<template #suffix> query = e.target.value
<X @click="removeValue(value)" class="h-4 w-4 stroke-1.5" /> showOptions = true
</template> }
</Button> "
<div class=""> autocomplete="off"
<Combobox v-model="selectedValue" nullable> @focus="() => togglePopover()"
<Popover class="w-full" v-model:show="showOptions"> @keydown.delete.capture.stop="removeLastValue"
<template #target="{ togglePopover }"> />
<ComboboxInput </template>
ref="search" <template #body="{ isOpen, close }">
class="search-input form-input w-full focus-visible:!ring-0" <div v-show="isOpen">
type="text" <div
:value="query" class="mt-1 rounded-lg bg-surface-white py-1 text-base border-2"
@change=" >
(e) => { <ComboboxOptions
query = e.target.value class="my-1 min-h-[6rem] max-h-[12rem] overflow-y-auto px-1.5"
showOptions = true static
}
"
autocomplete="off"
@focus="() => togglePopover()"
@keydown.delete.capture.stop="removeLastValue"
/>
</template>
<template #body="{ isOpen }">
<div v-show="isOpen">
<div
class="mt-1 rounded-lg bg-surface-white py-1 text-base border-2"
> >
<ComboboxOptions <ComboboxOption
class="my-1 max-h-[12rem] overflow-y-auto px-1.5" v-for="option in options"
static :key="option.value"
:value="option"
v-slot="{ active }"
> >
<ComboboxOption <li
v-for="option in options" :class="[
:key="option.value" 'flex cursor-pointer items-center rounded px-2 py-1 text-base',
:value="option" { 'bg-surface-gray-2': active },
v-slot="{ active }" ]"
> >
<li <div class="flex flex-col gap-1 p-1">
:class="[ <div class="text-base font-medium text-ink-gray-8">
'flex cursor-pointer items-center rounded px-2 py-1 text-base', {{ option.description }}
{ 'bg-surface-gray-2': active },
]"
>
<div class="flex flex-col gap-1 p-1">
<div class="text-base font-medium text-ink-gray-8">
{{ option.description }}
</div>
<div class="text-sm text-ink-gray-5">
{{ option.value }}
</div>
</div> </div>
</li> <div class="text-sm text-ink-gray-5">
</ComboboxOption> {{ option.value }}
</ComboboxOptions> </div>
</div> </div>
</li>
</ComboboxOption>
<div
v-if="attrs.onCreate"
class="absolute bottom-2 left-1 w-[98%] pt-2 bg-white border-t"
>
<Button
variant="ghost"
class="w-full !justify-start"
:label="__('Create New')"
@click="attrs.onCreate(close)"
>
<template #prefix>
<Plus class="h-4 w-4 stroke-1.5" />
</template>
</Button>
</div>
</ComboboxOptions>
</div> </div>
</template> </div>
</Popover> </template>
</Combobox> </Popover>
</Combobox>
</div>
<div v-if="values.length" class="grid grid-cols-2 gap-2 mt-1">
<div
v-for="value in values"
class="flex items-center justify-between break-all bg-surface-gray-2 text-ink-gray-7 word-wrap p-2 rounded-md mr-2"
>
<span class="break-all">
{{ value }}
</span>
<X
class="size-4 stroke-1.5 cursor-pointer"
@click="removeValue(value)"
/>
</div> </div>
</div> </div>
<!-- <ErrorMessage class="mt-2 pl-2" v-if="error" :message="error" /> --> <!-- <ErrorMessage class="mt-2 pl-2" v-if="error" :message="error" /> -->
@@ -90,9 +103,9 @@ import {
ComboboxOption, ComboboxOption,
} from '@headlessui/vue' } from '@headlessui/vue'
import { createResource, Popover, Button } from 'frappe-ui' import { createResource, Popover, Button } from 'frappe-ui'
import { ref, computed, nextTick } from 'vue' import { ref, computed, nextTick, useAttrs } from 'vue'
import { watchDebounced } from '@vueuse/core' import { watchDebounced } from '@vueuse/core'
import { X } from 'lucide-vue-next' import { X, Plus } from 'lucide-vue-next'
const props = defineProps({ const props = defineProps({
label: { label: {
@@ -124,7 +137,7 @@ const props = defineProps({
}) })
const values = defineModel() const values = defineModel()
const attrs = useAttrs()
const emails = ref([]) const emails = ref([])
const search = ref(null) const search = ref(null)
const error = ref(null) const error = ref(null)

View File

@@ -146,8 +146,8 @@
<script setup> <script setup>
import { BookOpen, Users, Star, GraduationCap } from 'lucide-vue-next' import { BookOpen, Users, Star, GraduationCap } from 'lucide-vue-next'
import { computed, inject } from 'vue' import { computed, inject } from 'vue'
import { Badge, Button, createResource } from 'frappe-ui' import { Badge, Button, createResource, toast } from 'frappe-ui'
import { showToast, formatAmount } from '@/utils/' import { formatAmount } from '@/utils/'
import { capture } from '@/telemetry' import { capture } from '@/telemetry'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import CertificationLinks from '@/components/CertificationLinks.vue' import CertificationLinks from '@/components/CertificationLinks.vue'
@@ -172,11 +172,7 @@ const video_link = computed(() => {
function enrollStudent() { function enrollStudent() {
if (!user.data) { if (!user.data) {
showToast( toast.success(__('You need to login first to enroll for this course'))
__('Please Login'),
__('You need to login first to enroll for this course'),
'alert-circle'
)
setTimeout(() => { setTimeout(() => {
window.location.href = `/login?redirect-to=${window.location.pathname}` window.location.href = `/login?redirect-to=${window.location.pathname}`
}, 1000) }, 1000)
@@ -192,11 +188,7 @@ function enrollStudent() {
capture('enrolled_in_course', { capture('enrolled_in_course', {
course: props.course.data.name, course: props.course.data.name,
}) })
showToast( toast.success(__('You have been enrolled in this course'))
__('Success'),
__('You have been enrolled in this course'),
'check'
)
setTimeout(() => { setTimeout(() => {
router.push({ router.push({
name: 'Lesson', name: 'Lesson',

View File

@@ -147,7 +147,7 @@
/> />
</template> </template>
<script setup> <script setup>
import { Button, createResource, Tooltip } from 'frappe-ui' import { Button, createResource, Tooltip, toast } from 'frappe-ui'
import { getCurrentInstance, inject, ref } from 'vue' import { getCurrentInstance, inject, ref } from 'vue'
import Draggable from 'vuedraggable' import Draggable from 'vuedraggable'
import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/vue' import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/vue'
@@ -162,7 +162,6 @@ import {
} from 'lucide-vue-next' } from 'lucide-vue-next'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import ChapterModal from '@/components/Modals/ChapterModal.vue' import ChapterModal from '@/components/Modals/ChapterModal.vue'
import { showToast } from '@/utils'
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
@@ -215,7 +214,7 @@ const deleteLesson = createResource({
}, },
onSuccess() { onSuccess() {
outline.reload() outline.reload()
showToast('Success', 'Lesson deleted successfully', 'check') toast.success(__('Lesson deleted successfully'))
}, },
}) })
@@ -230,7 +229,7 @@ const updateLessonIndex = createResource({
} }
}, },
onSuccess() { onSuccess() {
showToast('Success', 'Lesson moved successfully', 'check') toast.success(__('Lesson moved successfully'))
}, },
}) })
@@ -288,7 +287,7 @@ const deleteChapter = createResource({
}, },
onSuccess() { onSuccess() {
outline.reload() outline.reload()
showToast('Success', 'Chapter deleted successfully', 'check') toast.success(__('Chapter deleted successfully'))
}, },
}) })
@@ -317,11 +316,7 @@ const redirectToChapter = (chapter) => {
event.preventDefault() event.preventDefault()
if (props.allowEdit) return if (props.allowEdit) return
if (!user.data) { if (!user.data) {
showToast( toast.success(__('Please enroll for this course to view this lesson'))
__('You are not enrolled'),
__('Please enroll for this course to view this lesson'),
'alert-circle'
)
return return
} }

View File

@@ -93,12 +93,11 @@
</div> </div>
</template> </template>
<script setup> <script setup>
import { createResource, TextEditor, Button, Dropdown } from 'frappe-ui' import { createResource, TextEditor, Button, Dropdown, toast } from 'frappe-ui'
import { timeAgo } from '../utils' import { timeAgo } from '../utils'
import UserAvatar from '@/components/UserAvatar.vue' import UserAvatar from '@/components/UserAvatar.vue'
import { ChevronLeft, MoreHorizontal } from 'lucide-vue-next' import { ChevronLeft, MoreHorizontal } from 'lucide-vue-next'
import { ref, inject, onMounted } from 'vue' import { ref, inject, onMounted } from 'vue'
import { createToast } from '../utils'
const showTopics = defineModel('showTopics') const showTopics = defineModel('showTopics')
const newReply = ref('') const newReply = ref('')
@@ -192,14 +191,7 @@ const postReply = () => {
replies.reload() replies.reload()
}, },
onError(err) { onError(err) {
createToast({ toast.error(err.messages?.[0] || err)
title: 'Error',
text: err.messages?.[0] || err,
icon: 'x',
iconClasses: 'bg-surface-red-5 text-ink-white rounded-md p-px',
position: 'top-center',
timeout: 10,
})
}, },
} }
) )

View File

@@ -0,0 +1,24 @@
<template>
<div class="flex flex-col items-center justify-center mt-60">
<GraduationCap class="size-10 mx-auto stroke-1 text-ink-gray-5" />
<div class="text-lg font-semibold text-ink-gray-7 mb-2.5">
{{ __('No {0}').format(type?.toLowerCase()) }}
</div>
<div
class="leading-5 text-base w-2/5 text-base text-center text-ink-gray-7"
>
{{
__(
'There are no {0} currently. Keep an eye out, fresh learning experiences are on the way!'
).format(type?.toLowerCase())
}}
</div>
</div>
</template>
<script setup lang="ts">
import { BookOpen, GraduationCap } from 'lucide-vue-next'
const props = defineProps({
type: String,
})
</script>

View File

@@ -17,10 +17,11 @@
:debounce="300" :debounce="300"
/> />
<Button @click="() => (showForm = !showForm)"> <Button @click="() => (showForm = !showForm)">
<template #icon> <template #prefix>
<Plus v-if="!showForm" class="h-3 w-3 stroke-1.5" /> <Plus v-if="!showForm" class="size-4 stroke-1.5" />
<X v-else class="h-3 w-3 stroke-1.5" /> <X v-else class="size-4 stroke-1.5" />
</template> </template>
{{ showForm ? __('Close') : __('New') }}
</Button> </Button>
</div> </div>
</div> </div>

View File

@@ -17,10 +17,11 @@
:debounce="300" :debounce="300"
/> />
<Button @click="() => (showForm = !showForm)"> <Button @click="() => (showForm = !showForm)">
<template #icon> <template #prefix>
<Plus v-if="!showForm" class="h-3 w-3 stroke-1.5" /> <Plus v-if="!showForm" class="size-4 stroke-1.5" />
<X v-else class="h-3 w-3 stroke-1.5" /> <X v-else class="size-4 stroke-1.5" />
</template> </template>
{{ showForm ? __('Close') : __('New') }}
</Button> </Button>
</div> </div>
</div> </div>

View File

@@ -31,6 +31,7 @@
<div class="mb-4"> <div class="mb-4">
<div class="mb-1.5 text-sm text-ink-gray-5"> <div class="mb-1.5 text-sm text-ink-gray-5">
{{ __('Announcement') }} {{ __('Announcement') }}
<span class="text-ink-red-3">*</span>
</div> </div>
<TextEditor <TextEditor
:fixedMenu="true" :fixedMenu="true"
@@ -43,9 +44,8 @@
</Dialog> </Dialog>
</template> </template>
<script setup> <script setup>
import { Dialog, Input, TextEditor, createResource } from 'frappe-ui' import { Dialog, Input, TextEditor, createResource, toast } from 'frappe-ui'
import { reactive } from 'vue' import { reactive } from 'vue'
import { showToast } from '@/utils/'
const show = defineModel() const show = defineModel()
@@ -87,22 +87,21 @@ const makeAnnouncement = (close) => {
{ {
validate() { validate() {
if (!props.students.length) { if (!props.students.length) {
return 'No students in this batch' return __('No students in this batch')
} }
if (!announcement.subject) { if (!announcement.subject) {
return 'Subject is required' return __('Subject is required')
}
if (!announcement.announcement) {
return __('Announcement is required')
} }
}, },
onSuccess() { onSuccess() {
close() close()
showToast( toast.success(__('Announcement has been sent successfully'))
__('Success'),
__('Announcement has been sent successfully'),
'check'
)
}, },
onError(err) { onError(err) {
showToast(__('Error'), __(err.messages?.[0] || err), 'alert-circle') toast.error(__(err.messages?.[0] || err))
}, },
} }
) )

View File

@@ -25,21 +25,39 @@
v-model="assessment" v-model="assessment"
:doctype="assessmentType" :doctype="assessmentType"
:label="__('Assessment')" :label="__('Assessment')"
:onCreate="
(value, close) => {
close()
if (assessmentType === 'LMS Quiz') {
router.push({
name: 'QuizForm',
params: {
quizID: 'new',
},
})
} else if (assessmentType === 'LMS Assignment') {
router.push({
name: 'Assignments',
})
}
}
"
/> />
</div> </div>
</template> </template>
</Dialog> </Dialog>
</template> </template>
<script setup> <script setup>
import { Dialog, FormControl, createResource } from 'frappe-ui' import { Dialog, FormControl, createResource, toast } from 'frappe-ui'
import Link from '@/components/Controls/Link.vue' import Link from '@/components/Controls/Link.vue'
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import { showToast } from '@/utils' import { useRouter } from 'vue-router'
const show = defineModel() const show = defineModel()
const assessmentType = ref(null) const assessmentType = ref(null)
const assessment = ref(null) const assessment = ref(null)
const assessments = defineModel('assessments') const assessments = defineModel('assessments')
const router = useRouter()
const props = defineProps({ const props = defineProps({
batch: { batch: {
@@ -70,7 +88,7 @@ const addAssessment = (close) => {
{ {
onSuccess(data) { onSuccess(data) {
assessments.value.reload() assessments.value.reload()
showToast(__('Success'), __('Assessment added successfully'), 'check') toast.success(__('Assessment added successfully'))
close() close()
}, },
} }

View File

@@ -37,7 +37,7 @@
@change="(val) => (assignment.question = val)" @change="(val) => (assignment.question = val)"
:editable="true" :editable="true"
:fixedMenu="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]" editorClass="prose-sm max-w-none border-b border-x bg-surface-gray-2 rounded-b-md py-1 px-2 min-h-[7rem] max-h-[18rem] overflow-y-auto"
/> />
</div> </div>
</div> </div>
@@ -64,9 +64,8 @@
</Dialog> </Dialog>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { Button, Dialog, FormControl, TextEditor } from 'frappe-ui' import { Button, Dialog, FormControl, TextEditor, toast } from 'frappe-ui'
import { computed, reactive, watch } from 'vue' import { computed, reactive, watch } from 'vue'
import { showToast } from '@/utils'
const show = defineModel() const show = defineModel()
const assignments = defineModel<Assignments>('assignments') const assignments = defineModel<Assignments>('assignments')
@@ -123,11 +122,7 @@ const saveAssignment = () => {
{ {
onSuccess() { onSuccess() {
show.value = false show.value = false
showToast( toast.success(__('Assignment created successfully'))
__('Success'),
__('Assignment created successfully'),
'check'
)
}, },
} }
) )
@@ -140,11 +135,7 @@ const saveAssignment = () => {
{ {
onSuccess() { onSuccess() {
show.value = false show.value = false
showToast( toast.success(__('Assignment updated successfully'))
__('Success'),
__('Assignment updated successfully'),
'check'
)
}, },
} }
) )

View File

@@ -19,32 +19,43 @@
v-model="course" v-model="course"
:label="__('Course')" :label="__('Course')"
:required="true" :required="true"
:onCreate="
(value, close) => {
close()
router.push({
name: 'CourseForm',
params: {
courseName: 'new',
},
})
}
"
/> />
<Link <Link
doctype="Course Evaluator" doctype="Course Evaluator"
v-model="evaluator" v-model="evaluator"
:label="__('Evaluator')" :label="__('Evaluator')"
:onCreate="(value, close) => openSettings(close)" :onCreate="(value, close) => openSettings('Evaluators', close)"
class="mt-4" class="mt-4"
/> />
</template> </template>
</Dialog> </Dialog>
</template> </template>
<script setup> <script setup>
import { Dialog, createResource } from 'frappe-ui' import { Dialog, createResource, toast } from 'frappe-ui'
import { ref, inject } from 'vue' import { ref, inject } from 'vue'
import Link from '@/components/Controls/Link.vue' import Link from '@/components/Controls/Link.vue'
import { showToast } from '@/utils'
import { useOnboarding } from 'frappe-ui/frappe' import { useOnboarding } from 'frappe-ui/frappe'
import { useSettings } from '@/stores/settings' import { openSettings } from '@/utils'
import { useRouter } from 'vue-router'
const show = defineModel() const show = defineModel()
const course = ref(null) const course = ref(null)
const evaluator = ref(null) const evaluator = ref(null)
const user = inject('$user') const user = inject('$user')
const courses = defineModel('courses') const courses = defineModel('courses')
const router = useRouter()
const { updateOnboardingStep } = useOnboarding('learning') const { updateOnboardingStep } = useOnboarding('learning')
const settingsStore = useSettings()
const props = defineProps({ const props = defineProps({
batch: { batch: {
@@ -83,15 +94,9 @@ const addCourse = (close) => {
evaluator.value = null evaluator.value = null
}, },
onError(err) { onError(err) {
showToast('Error', err.message[0] || err, 'x') toast.error(err.messages?.[0] || err)
}, },
} }
) )
} }
const openSettings = (close) => {
close()
settingsStore.activeTab = 'Evaluators'
settingsStore.isSettingsOpen = true
}
</script> </script>

View File

@@ -14,7 +14,13 @@
<div class="text-xl font-semibold"> <div class="text-xl font-semibold">
{{ student.full_name }} {{ student.full_name }}
</div> </div>
<Badge :theme="student.progress === 100 ? 'green' : 'red'"> <Badge
v-if="
Object.keys(student.assessments).length ||
Object.keys(student.courses).length
"
:theme="student.progress === 100 ? 'green' : 'red'"
>
{{ student.progress }}% {{ __('Complete') }} {{ student.progress }}% {{ __('Complete') }}
</Badge> </Badge>
</div> </div>
@@ -26,7 +32,10 @@
<div class="space-y-8"> <div class="space-y-8">
<!-- Assessments --> <!-- Assessments -->
<div class="space-y-2 text-sm"> <div
v-if="Object.keys(student.assessments).length"
class="space-y-2 text-sm"
>
<div class="flex items-center border-b pb-1 font-medium"> <div class="flex items-center border-b pb-1 font-medium">
<span class="flex-1"> <span class="flex-1">
{{ __('Assessment') }} {{ __('Assessment') }}
@@ -73,7 +82,10 @@
</div> </div>
<!-- Courses --> <!-- Courses -->
<div class="space-y-2 text-sm"> <div
v-if="Object.keys(student.courses).length"
class="space-y-2 text-sm"
>
<div class="flex items-center border-b pb-1 font-medium"> <div class="flex items-center border-b pb-1 font-medium">
<span class="flex-1"> <span class="flex-1">
{{ __('Courses') }} {{ __('Courses') }}

View File

@@ -62,9 +62,8 @@
</template> </template>
<script setup> <script setup>
import { inject, reactive } from 'vue' import { inject, reactive } from 'vue'
import { createResource, Dialog, FormControl, Switch } from 'frappe-ui' import { createResource, Dialog, FormControl, Switch, toast } from 'frappe-ui'
import Link from '@/components/Controls/Link.vue' import Link from '@/components/Controls/Link.vue'
import { showToast } from '@/utils'
const show = defineModel() const show = defineModel()
const dayjs = inject('$dayjs') const dayjs = inject('$dayjs')
@@ -112,13 +111,13 @@ const generateCertificates = (close) => {
}, },
{ {
onError(err) { onError(err) {
showToast(__('Error'), err.messages?.[0] || err, 'x') toast.error(err.messages?.[0] || err)
}, },
} }
) )
}) })
close() close()
showToast(__('Success'), __('Certificates generated successfully'), 'check') toast.success(__('Certificates generated successfully'))
} }
const getCourses = () => { const getCourses = () => {

View File

@@ -76,9 +76,10 @@ import {
FileUploader, FileUploader,
FormControl, FormControl,
Switch, Switch,
toast,
} from 'frappe-ui' } from 'frappe-ui'
import { reactive, watch, inject } from 'vue' import { reactive, watch, inject } from 'vue'
import { showToast, getFileSize } from '@/utils/' import { 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 { useOnboarding } from 'frappe-ui/frappe' import { useOnboarding } from 'frappe-ui/frappe'
@@ -150,21 +151,17 @@ const addChapter = async (close) => {
onSuccess(data) { onSuccess(data) {
cleanChapter() cleanChapter()
outline.value.reload() outline.value.reload()
showToast( toast.success(__('Chapter added successfully'))
__('Success'),
__('Chapter added successfully'),
'check'
)
}, },
onError(err) { onError(err) {
showToast(__('Error'), err.messages?.[0] || err, 'x') toast.error(err.messages?.[0] || err)
}, },
} }
) )
close() close()
}, },
onError(err) { onError(err) {
showToast(__('Error'), err.messages?.[0] || err, 'x') toast.error(err.messages?.[0] || err)
}, },
} }
) )
@@ -196,11 +193,11 @@ const editChapter = (close) => {
}, },
onSuccess() { onSuccess() {
outline.value.reload() outline.value.reload()
showToast(__('Success'), __('Chapter updated successfully'), 'check') toast.success(__('Chapter updated successfully'))
close() close()
}, },
onError(err) { onError(err) {
showToast(__('Error'), err.messages?.[0] || err, 'x') toast.error(err.messages?.[0] || err)
}, },
} }
) )

View File

@@ -34,9 +34,15 @@
</Dialog> </Dialog>
</template> </template>
<script setup> <script setup>
import { Dialog, FormControl, TextEditor, createResource } from 'frappe-ui' import {
Dialog,
FormControl,
TextEditor,
createResource,
toast,
} from 'frappe-ui'
import { reactive } from 'vue' import { reactive } from 'vue'
import { showToast, singularize } from '@/utils' import { singularize } from '@/utils'
const topics = defineModel('reloadTopics') const topics = defineModel('reloadTopics')
@@ -115,7 +121,7 @@ const submitTopic = (close) => {
) )
}, },
onError(err) { onError(err) {
showToast('Error', err.message, 'x') toast.error(err.messages?.[0] || err)
}, },
} }
) )

View File

@@ -93,10 +93,11 @@ import {
Button, Button,
createResource, createResource,
TextEditor, TextEditor,
toast,
} from 'frappe-ui' } from 'frappe-ui'
import { reactive, watch } from 'vue' import { reactive, watch } from 'vue'
import { FileText, X } from 'lucide-vue-next' import { FileText, X } from 'lucide-vue-next'
import { getFileSize, showToast, escapeHTML } from '@/utils' import { getFileSize, escapeHTML } from '@/utils'
const reloadProfile = defineModel('reloadProfile') const reloadProfile = defineModel('reloadProfile')
@@ -155,7 +156,7 @@ const saveProfile = (close) => {
reloadProfile.value.reload() reloadProfile.value.reload()
}, },
onError(err) { onError(err) {
showToast('Error', err.messages?.[0] || err, 'x') toast.error(err.messages?.[0] || err)
}, },
} }
) )

View File

@@ -68,7 +68,7 @@
<script setup> <script setup>
import { Dialog, createResource, Select, FormControl } from 'frappe-ui' import { Dialog, createResource, Select, FormControl } from 'frappe-ui'
import { reactive, watch, inject } from 'vue' import { reactive, watch, inject } from 'vue'
import { createToast, formatTime } from '@/utils/' import { formatTime } from '@/utils/'
const user = inject('$user') const user = inject('$user')
const dayjs = inject('$dayjs') const dayjs = inject('$dayjs')
@@ -148,14 +148,7 @@ function submitEvaluation(close) {
unavailabilityMessage = false unavailabilityMessage = false
} }
createToast({ toast.warning(__('Evaluator is unavailable'))
title: unavailabilityMessage ? __('Evaluator is Unavailable') : '',
text: message,
icon: unavailabilityMessage ? 'alert-circle' : 'x',
iconClasses: 'bg-yellow-600 text-ink-white rounded-md p-px',
position: 'top-center',
timeout: 10,
})
}, },
}) })
} }

View File

@@ -144,6 +144,7 @@ import {
Tabs, Tabs,
Tooltip, Tooltip,
Textarea, Textarea,
toast,
} from 'frappe-ui' } from 'frappe-ui'
import { import {
User, User,
@@ -157,7 +158,7 @@ import {
ClipboardList, ClipboardList,
} from 'lucide-vue-next' } from 'lucide-vue-next'
import { inject, reactive, watch, ref, computed } from 'vue' import { inject, reactive, watch, ref, computed } from 'vue'
import { formatTime, showToast } from '@/utils' import { formatTime } from '@/utils'
import Rating from '@/components/Controls/Rating.vue' import Rating from '@/components/Controls/Rating.vue'
import Link from '@/components/Controls/Link.vue' import Link from '@/components/Controls/Link.vue'
@@ -252,7 +253,7 @@ const saveEvaluation = () => {
} else { } else {
show.value = false show.value = false
} }
showToast(__('Success'), __('Evaluation saved successfully'), 'check') toast.success(__('Evaluation saved successfully'))
}, },
} }
) )
@@ -307,7 +308,7 @@ const saveCertificate = () => {
{}, {},
{ {
onSuccess: () => { onSuccess: () => {
showToast(__('Success'), __('Certificate saved successfully'), 'check') toast.success(__('Certificate saved successfully'))
}, },
} }
) )

View File

@@ -64,10 +64,10 @@
</Dialog> </Dialog>
</template> </template>
<script setup> <script setup>
import { Dialog, FileUploader, Button, createResource } from 'frappe-ui' import { Dialog, FileUploader, Button, createResource, toast } from 'frappe-ui'
import { FileText } from 'lucide-vue-next' import { FileText } from 'lucide-vue-next'
import { ref, inject } from 'vue' import { ref, inject } from 'vue'
import { createToast, getFileSize } from '@/utils/' import { getFileSize } from '@/utils/'
const resume = ref(null) const resume = ref(null)
const show = defineModel() const show = defineModel()
@@ -112,24 +112,12 @@ const submitResume = (close) => {
} }
}, },
onSuccess() { onSuccess() {
createToast({ toast.success('Your application has been submitted successfully')
title: 'Success',
text: 'Your application has been submitted',
icon: 'check',
iconClasses: 'bg-surface-green-3 text-ink-white rounded-md p-px',
})
application.value.reload() application.value.reload()
close() close()
}, },
onError(err) { onError(err) {
createToast({ toast.error(err.messages?.[0] || err)
title: 'Error',
text: err.messages?.[0] || err,
icon: 'x',
iconClasses: 'bg-surface-red-5 text-ink-white rounded-md p-px',
position: 'top-center',
timeout: 10,
})
}, },
} }
) )

View File

@@ -94,9 +94,10 @@ import {
Tooltip, Tooltip,
FormControl, FormControl,
Autocomplete, Autocomplete,
toast,
} from 'frappe-ui' } from 'frappe-ui'
import { reactive, inject, onMounted } from 'vue' import { reactive, inject, onMounted } from 'vue'
import { getTimezones, createToast, getUserTimezone } from '@/utils/' import { getTimezones, getUserTimezone } from '@/utils/'
const liveClasses = defineModel('reloadLiveClasses') const liveClasses = defineModel('reloadLiveClasses')
const show = defineModel() const show = defineModel()
@@ -202,14 +203,7 @@ const submitLiveClass = (close) => {
close() close()
}, },
onError(err) { onError(err) {
createToast({ toast.error(err.messages?.[0] || err)
title: 'Error',
text: err.messages?.[0] || err,
icon: 'x',
iconClasses: 'bg-surface-red-5 text-ink-white rounded-md p-px',
position: 'top-center',
timeout: 10,
})
}, },
}) })
} }

View File

@@ -30,11 +30,10 @@
</Dialog> </Dialog>
</template> </template>
<script setup> <script setup>
import { Dialog, createResource } from 'frappe-ui' import { Dialog, createResource, toast } from 'frappe-ui'
import Link from '@/components/Controls/Link.vue' import Link from '@/components/Controls/Link.vue'
import { reactive, watch } from 'vue' import { reactive, watch } from 'vue'
import IconPicker from '@/components/Controls/IconPicker.vue' import IconPicker from '@/components/Controls/IconPicker.vue'
import { showToast } from '@/utils'
const sidebar = defineModel('reloadSidebar') const sidebar = defineModel('reloadSidebar')
const show = defineModel() const show = defineModel()
@@ -78,10 +77,10 @@ const addWebPage = (close) => {
onSuccess() { onSuccess() {
sidebar.value.reload() sidebar.value.reload()
close() close()
showToast('Success', 'Web page added to sidebar', 'check') toast.success(__('Web page added to sidebar'))
}, },
onError(err) { onError(err) {
showToast('Error', err.message[0] || err, 'x') toast.error(err.message[0] || err)
close() close()
}, },
} }

View File

@@ -121,10 +121,10 @@ import {
createResource, createResource,
Switch, Switch,
Button, Button,
toast,
} from 'frappe-ui' } from 'frappe-ui'
import { computed, watch, reactive, ref, inject } from 'vue' import { computed, watch, reactive, ref, inject } from 'vue'
import Link from '@/components/Controls/Link.vue' import Link from '@/components/Controls/Link.vue'
import { showToast } from '@/utils'
import { useOnboarding } from 'frappe-ui/frappe' import { useOnboarding } from 'frappe-ui/frappe'
const show = defineModel() const show = defineModel()
@@ -260,7 +260,7 @@ const addQuestion = () => {
}) })
}, },
onError(err) { onError(err) {
showToast(__('Error'), __(err.messages?.[0] || err), 'x') toast.error(err.messages?.[0] || err)
}, },
} }
) )
@@ -278,12 +278,12 @@ const addQuestionRow = (question) => {
updateOnboardingStep('create_first_quiz') updateOnboardingStep('create_first_quiz')
show.value = false show.value = false
showToast(__('Success'), __('Question added successfully'), 'check') toast.success(__('Question added successfully'))
quiz.value.reload() quiz.value.reload()
show.value = false show.value = false
}, },
onError(err) { onError(err) {
showToast(__('Error'), __(err.messages?.[0] || err), 'x') toast.error(err.messages?.[0] || err)
show.value = false show.value = false
}, },
} }
@@ -328,18 +328,14 @@ const updateQuestion = () => {
{ {
onSuccess() { onSuccess() {
show.value = false show.value = false
showToast( toast.success(__('Question updated successfully'))
__('Success'),
__('Question updated successfully'),
'check'
)
quiz.value.reload() quiz.value.reload()
}, },
} }
) )
}, },
onError(err) { onError(err) {
showToast(__('Error'), __(err.messages?.[0] || err), 'x') toast.error(err.messages?.[0] || err)
}, },
} }
) )

View File

@@ -32,10 +32,9 @@
</Dialog> </Dialog>
</template> </template>
<script setup> <script setup>
import { Dialog, Textarea, createResource } from 'frappe-ui' import { Dialog, Textarea, createResource, toast } from 'frappe-ui'
import { reactive } from 'vue' import { reactive } from 'vue'
import Rating from '@/components/Controls/Rating.vue' import Rating from '@/components/Controls/Rating.vue'
import { createToast } from '@/utils/'
const show = defineModel() const show = defineModel()
const reviews = defineModel('reloadReviews') const reviews = defineModel('reloadReviews')
@@ -78,11 +77,7 @@ function submitReview(close) {
hasReviewed.value.reload() hasReviewed.value.reload()
}, },
onError(err) { onError(err) {
createToast({ toast.error(err.messages?.[0] || err)
text: err.messages?.[0] || err,
icon: 'x',
iconClasses: 'text-ink-red-4 bg-surface-red-4',
})
}, },
}) })
close() close()

View File

@@ -1,5 +1,5 @@
<template> <template>
<Dialog v-model="show" :options="{ size: '4xl' }"> <Dialog v-model="show" :options="{ size: '5xl' }">
<template #body> <template #body>
<div class="flex h-[calc(100vh_-_8rem)]"> <div class="flex h-[calc(100vh_-_8rem)]">
<div class="flex w-52 shrink-0 flex-col bg-surface-gray-2 p-2"> <div class="flex w-52 shrink-0 flex-col bg-surface-gray-2 p-2">

View File

@@ -19,19 +19,25 @@
doctype="User" doctype="User"
v-model="student" v-model="student"
:filters="{ ignore_user_type: 1 }" :filters="{ ignore_user_type: 1 }"
:onCreate="
(value, close) => {
openSettings('Members', close)
}
"
/> />
</div> </div>
</template> </template>
</Dialog> </Dialog>
</template> </template>
<script setup> <script setup>
import { Dialog, createResource } from 'frappe-ui' import { Dialog, createResource, toast } from 'frappe-ui'
import { ref, inject } from 'vue' import { ref, inject } from 'vue'
import Link from '@/components/Controls/Link.vue' import Link from '@/components/Controls/Link.vue'
import { showToast } from '@/utils'
import { useOnboarding } from 'frappe-ui/frappe' import { useOnboarding } from 'frappe-ui/frappe'
import { openSettings } from '@/utils'
const students = defineModel('reloadStudents') const students = defineModel('reloadStudents')
const batchModal = defineModel('batchModal')
const student = ref() const student = ref()
const user = inject('$user') const user = inject('$user')
const { updateOnboardingStep } = useOnboarding('learning') const { updateOnboardingStep } = useOnboarding('learning')
@@ -66,11 +72,12 @@ const addStudent = (close) => {
updateOnboardingStep('add_batch_student') updateOnboardingStep('add_batch_student')
students.value.reload() students.value.reload()
batchModal.value.reload()
student.value = null student.value = null
close() close()
}, },
onError(err) { onError(err) {
showToast(__('Error'), __(err.messages?.[0] || err), 'x') toast.error(err.messages?.[0] || err)
}, },
} }
) )

View File

@@ -291,9 +291,9 @@ import {
ListView, ListView,
TextEditor, TextEditor,
FormControl, FormControl,
toast,
} from 'frappe-ui' } from 'frappe-ui'
import { ref, watch, reactive, inject, computed } from 'vue' import { ref, watch, reactive, inject, computed } from 'vue'
import { createToast, showToast } from '@/utils/'
import { CheckCircle, XCircle, MinusCircle } from 'lucide-vue-next' import { CheckCircle, XCircle, MinusCircle } from 'lucide-vue-next'
import { timeAgo } from '@/utils' import { timeAgo } from '@/utils'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
@@ -494,12 +494,7 @@ const getAnswers = () => {
const checkAnswer = () => { const checkAnswer = () => {
let answers = getAnswers() let answers = getAnswers()
if (!answers.length) { if (!answers.length) {
createToast({ toast.warning(__('Please select an option'))
title: 'Please select an option',
icon: 'alert-circle',
iconClasses: 'text-yellow-600 bg-yellow-100 rounded-full',
position: 'top-center',
})
return return
} }
@@ -589,7 +584,7 @@ const createSubmission = () => {
const errorTitle = err?.message || '' const errorTitle = err?.message || ''
if (errorTitle.includes('MaximumAttemptsExceededError')) { if (errorTitle.includes('MaximumAttemptsExceededError')) {
const errorMessage = err.messages?.[0] || err const errorMessage = err.messages?.[0] || err
showToast(__('Error'), __(errorMessage), 'x') toast.error(__(errorMessage))
setTimeout(() => { setTimeout(() => {
window.location.reload() window.location.reload()
}, 3000) }, 3000)

View File

@@ -27,9 +27,8 @@
</template> </template>
<script setup> <script setup>
import { Button, Badge } from 'frappe-ui' import { Button, Badge, toast } from 'frappe-ui'
import SettingFields from '@/components/SettingFields.vue' import SettingFields from '@/components/SettingFields.vue'
import { showToast } from '@/utils'
const props = defineProps({ const props = defineProps({
fields: { fields: {
@@ -61,7 +60,7 @@ const update = () => {
{}, {},
{ {
onError(err) { onError(err) {
showToast(__('Error'), err.messages?.[0] || err, 'x') toast.error(err.messages?.[0] || err)
}, },
} }
) )

View File

@@ -21,14 +21,28 @@
</header> </header>
<div class="md:w-3/4 md:mx-auto py-5 mx-5"> <div class="md:w-3/4 md:mx-auto py-5 mx-5">
<div class="grid grid-cols-3 gap-5 mb-5"> <div class="flex items-center justify-between mb-5">
<FormControl v-model="titleFilter" :placeholder="__('Search by title')" /> <div
<FormControl v-if="assignmentCount"
v-model="typeFilter" class="text-xl font-semibold text-ink-gray-7 mb-4"
type="select" >
:options="assignmentTypes" {{ __('{0} Assignments').format(assignmentCount) }}
:placeholder="__('Type')" </div>
/> <div
v-if="assignments.data?.length || assigmentCount > 0"
class="grid grid-cols-2 gap-5"
>
<FormControl
v-model="titleFilter"
:placeholder="__('Search by title')"
/>
<FormControl
v-model="typeFilter"
type="select"
:options="assignmentTypes"
:placeholder="__('Type')"
/>
</div>
</div> </div>
<ListView <ListView
v-if="assignments.data?.length" v-if="assignments.data?.length"
@@ -46,22 +60,7 @@
}" }"
> >
</ListView> </ListView>
<div <EmptyState v-else type="Assignments" />
v-else
class="text-center p-5 text-ink-gray-5 mt-52 w-3/4 md:w-1/2 mx-auto space-y-2"
>
<Pencil class="size-10 mx-auto stroke-1 text-ink-gray-4" />
<div class="text-xl font-medium">
{{ __('No assignments found') }}
</div>
<div class="leading-5">
{{
__(
'You have not created any assignments yet. To create a new assignment, click on the "New" button above.'
)
}}
</div>
</div>
<div <div
v-if="assignments.data && assignments.hasNextPage" v-if="assignments.data && assignments.hasNextPage"
class="flex justify-center my-5" class="flex justify-center my-5"
@@ -81,16 +80,18 @@
import { import {
Breadcrumbs, Breadcrumbs,
Button, Button,
call,
createListResource, createListResource,
FormControl, FormControl,
ListView, ListView,
usePageMeta, usePageMeta,
} from 'frappe-ui' } from 'frappe-ui'
import { computed, inject, onMounted, ref, watch } from 'vue' import { computed, inject, onMounted, ref, watch } from 'vue'
import { Plus, Pencil } from 'lucide-vue-next' import { Plus } from 'lucide-vue-next'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { sessionStore } from '../stores/session' import { sessionStore } from '../stores/session'
import AssignmentForm from '@/components/Modals/AssignmentForm.vue' import AssignmentForm from '@/components/Modals/AssignmentForm.vue'
import EmptyState from '@/components/EmptyState.vue'
const user = inject('$user') const user = inject('$user')
const dayjs = inject('$dayjs') const dayjs = inject('$dayjs')
@@ -98,6 +99,7 @@ const titleFilter = ref('')
const typeFilter = ref('') const typeFilter = ref('')
const showAssignmentForm = ref(false) const showAssignmentForm = ref(false)
const assignmentID = ref('new') const assignmentID = ref('new')
const assignmentCount = ref(0)
const { brand } = sessionStore() const { brand } = sessionStore()
const router = useRouter() const router = useRouter()
const readOnlyMode = window.read_only_mode const readOnlyMode = window.read_only_mode
@@ -106,7 +108,7 @@ onMounted(() => {
if (!user.data?.is_moderator && !user.data?.is_instructor) { if (!user.data?.is_moderator && !user.data?.is_instructor) {
router.push({ name: 'Courses' }) router.push({ name: 'Courses' })
} }
getAssignmentCount()
titleFilter.value = router.currentRoute.value.query.title titleFilter.value = router.currentRoute.value.query.title
typeFilter.value = router.currentRoute.value.query.type typeFilter.value = router.currentRoute.value.query.type
}) })
@@ -179,6 +181,14 @@ const assignmentColumns = computed(() => {
] ]
}) })
const getAssignmentCount = () => {
call('frappe.client.get_count', {
doctype: 'LMS Assignment',
}).then((data) => {
assignmentCount.value = data
})
}
const assignmentTypes = computed(() => { const assignmentTypes = computed(() => {
let types = ['', 'Document', 'Image', 'PDF', 'URL', 'Text'] let types = ['', 'Document', 'Image', 'PDF', 'URL', 'Text']
return types.map((type) => { return types.map((type) => {

View File

@@ -67,7 +67,7 @@
<BatchDashboard :batch="batch" :isStudent="isStudent" /> <BatchDashboard :batch="batch" :isStudent="isStudent" />
</div> </div>
<div v-else-if="tab.label == 'Dashboard'"> <div v-else-if="tab.label == 'Dashboard'">
<BatchStudents :batch="batch.data" /> <BatchStudents :batch="batch" />
</div> </div>
<div v-else-if="tab.label == 'Classes'"> <div v-else-if="tab.label == 'Classes'">
<LiveClass :batch="batch.data.name" /> <LiveClass :batch="batch.data.name" />
@@ -357,6 +357,9 @@ watch(tabIndex, () => {
const canMakeAnnouncement = () => { const canMakeAnnouncement = () => {
if (readOnlyMode) return false if (readOnlyMode) return false
if (!batch.data?.students?.length) return false
return user.data?.is_moderator || user.data?.is_evaluator return user.data?.is_moderator || user.data?.is_evaluator
} }

View File

@@ -6,67 +6,45 @@
<Breadcrumbs :items="breadcrumbs" /> <Breadcrumbs :items="breadcrumbs" />
</header> </header>
<div class="m-5 pb-10"> <div class="m-5 pb-10">
<div> <div class="flex justify-between w-full">
<div class="text-3xl font-semibold text-ink-gray-9"> <div class="md:w-2/3">
{{ batch.data.title }} <div class="text-3xl font-semibold text-ink-gray-9">
</div> {{ batch.data.title }}
<div class="my-3 leading-6 text-ink-gray-7">
{{ batch.data.description }}
</div>
<div
class="flex flex-col gap-2 lg:gap-0 lg:flex-row lg:items-center space-x-0 md:space-x-5 lg:w-1/2"
>
<div
v-if="batch.data?.courses?.length"
class="flex items-center text-ink-gray-7"
>
<BookOpen class="h-4 w-4 mr-2 stroke-1.5" />
<span> {{ batch.data?.courses?.length }} {{ __('Courses') }} </span>
</div> </div>
<span v-if="batch.data?.courses?.length" class="hidden lg:block" <div class="my-3 leading-6 text-ink-gray-7">
>&middot;</span {{ batch.data.description }}
>
<DateRange
:startDate="batch.data.start_date"
:endDate="batch.data.end_date"
/>
<span class="hidden lg:block" v-if="batch.data.start_date"
>&middot;</span
>
<div class="flex items-center text-ink-gray-7">
<Clock class="h-4 w-4 mr-2 stroke-1.5" />
<span>
{{ formatTime(batch.data.start_time) }} -
{{ formatTime(batch.data.end_time) }}
</span>
</div> </div>
</div> <div class="flex avatar-group overlap">
<div class="flex avatar-group overlap mt-3"> <div
<div class="h-6 mr-1"
class="h-6 mr-1" :class="{
:class="{ 'avatar-group overlap': batch.data.instructors.length > 1,
'avatar-group overlap': batch.data.instructors.length > 1, }"
}" >
> <UserAvatar
<UserAvatar v-for="instructor in batch.data.instructors"
v-for="instructor in batch.data.instructors" :user="instructor"
:user="instructor" />
/> </div>
<CourseInstructors :instructors="batch.data.instructors" />
</div> </div>
<CourseInstructors :instructors="batch.data.instructors" />
</div>
</div>
<div class="grid lg:grid-cols-[60%,20%] gap-4 lg:gap-20 mt-10">
<div class="order-2 lg:order-none">
<div <div
class="ProseMirror prose prose-table:table-fixed prose-td:p-2 prose-th:p-2 prose-td:border prose-th:border prose-td:border-outline-gray-2 prose-th:border-outline-gray-2 prose-td:relative prose-th:relative prose-th:bg-surface-gray-2 prose-sm max-w-none !whitespace-normal mt-6" class="ProseMirror prose prose-table:table-fixed prose-td:p-2 prose-th:p-2 prose-td:border prose-th:border prose-td:border-outline-gray-2 prose-th:border-outline-gray-2 prose-td:relative prose-th:relative prose-th:bg-surface-gray-2 prose-sm max-w-none !whitespace-normal mt-10"
v-html="batch.data.batch_details" v-html="batch.data.batch_details"
></div> ></div>
</div>
<div class="hidden md:block">
<BatchOverlay :batch="batch" />
</div>
</div>
<!-- <div class="grid lg:grid-cols-[60%,20%] gap-4 lg:gap-20 mt-10">
<div class="order-2 lg:order-none">
</div> </div>
<div class="order-1 lg:order-none"> <div class="order-1 lg:order-none">
<BatchOverlay :batch="batch" /> <BatchOverlay :batch="batch" />
</div> </div>
</div> </div> -->
<div v-if="batch.data.courses.length"> <div v-if="batch.data.courses.length">
<div class="flex items-center mt-10"> <div class="flex items-center mt-10">
<div class="text-2xl font-semibold"> <div class="text-2xl font-semibold">

View File

@@ -8,13 +8,13 @@
{{ __('Save') }} {{ __('Save') }}
</Button> </Button>
</header> </header>
<div class="w-3/4 mx-auto py-5"> <div class="py-5">
<div class=""> <div class="px-20 pb-5 space-y-5 border-b mb-5">
<div class="text-lg text-ink-gray-9 font-semibold mb-4"> <div class="text-lg text-ink-gray-9 font-semibold mb-4">
{{ __('Details') }} {{ __('Details') }}
</div> </div>
<div class="space-y-10 mb-4"> <div class="grid grid-cols-2 gap-5">
<div class="space-y-4"> <div class="space-y-5">
<FormControl <FormControl
v-model="batch.title" v-model="batch.title"
:label="__('Title')" :label="__('Title')"
@@ -26,31 +26,162 @@
doctype="User" doctype="User"
:label="__('Instructors')" :label="__('Instructors')"
:required="true" :required="true"
:onCreate="(close) => openSettings('Members', close)"
:filters="{ ignore_user_type: 1 }" :filters="{ ignore_user_type: 1 }"
/> />
</div> </div>
<FormControl
v-model="batch.description"
:label="__('Short Description')"
type="textarea"
:rows="8"
:placeholder="__('Short description of the batch')"
:required="true"
/>
</div>
</div>
<div class="grid grid-cols-2 gap-10"> <div class="px-20 pb-5 space-y-5 border-b mb-5">
<div class="flex flex-col space-y-5"> <div class="text-lg text-ink-gray-9 font-semibold mb-4">
<FormControl {{ __('Settings') }}
v-model="batch.published" </div>
type="checkbox" <div class="grid grid-cols-3 gap-5">
:label="__('Published')" <FormControl
/> v-model="batch.published"
<FormControl type="checkbox"
v-model="batch.allow_self_enrollment" :label="__('Published')"
type="checkbox" />
:label="__('Allow self enrollment')" <FormControl
/> v-model="batch.allow_self_enrollment"
<FormControl type="checkbox"
v-model="batch.certification" :label="__('Allow self enrollment')"
type="checkbox" />
:label="__('Certification')" <FormControl
/> v-model="batch.certification"
</div> type="checkbox"
:label="__('Certification')"
/>
</div>
</div>
<div class="px-20 pb-5 space-y-5 border-b mb-5">
<div class="text-lg text-ink-gray-9 font-semibold mb-4">
{{ __('Date and Time') }}
</div>
<div class="grid grid-cols-3 gap-10">
<div class="space-y-5">
<FormControl
v-model="batch.start_date"
:label="__('Start Date')"
type="date"
class="mb-4"
:required="true"
/>
<FormControl
v-model="batch.end_date"
:label="__('End Date')"
type="date"
class="mb-4"
:required="true"
/>
</div>
<div class="space-y-5">
<FormControl
v-model="batch.start_time"
:label="__('Start Time')"
type="time"
class="mb-4"
:required="true"
/>
<FormControl
v-model="batch.end_time"
:label="__('End Time')"
type="time"
class="mb-4"
:required="true"
/>
</div>
<div class="space-y-5">
<FormControl
v-model="batch.timezone"
:label="__('Timezone')"
type="text"
:placeholder="__('Example: IST (+5:30)')"
class="mb-4"
:required="true"
/>
<FormControl
v-model="batch.evaluation_end_date"
:label="__('Evaluation End Date')"
type="date"
class="mb-4"
/>
</div>
</div>
</div>
<div class="px-20 pb-5 space-y-5 border-b mb-5">
<div>
<label class="block text-sm text-ink-gray-5 mb-1">
{{ __('Batch Details') }}
<span class="text-ink-red-3">*</span>
</label>
<TextEditor
:content="batch.batch_details"
@change="(val) => (batch.batch_details = 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] max-h-[20rem] overflow-y-scroll mb-4"
/>
</div>
</div>
<div class="px-20 pb-5 space-y-5 border-b mb-5">
<div class="text-lg text-ink-gray-9 font-semibold mb-4">
{{ __('Configurations') }}
</div>
<div class="grid grid-cols-3 gap-10">
<div class="space-y-5">
<FormControl
v-model="batch.seat_count"
:label="__('Seat Count')"
type="number"
class="mb-4"
:placeholder="__('Number of seats available')"
/>
<Link
doctype="Email Template"
:label="__('Email Template')"
v-model="batch.confirmation_email_template"
/>
</div>
<div class="space-y-5">
<FormControl
v-model="batch.medium"
type="select"
:options="[
{
label: 'Online',
value: 'Online',
},
{
label: 'Offline',
value: 'Offline',
},
]"
:label="__('Medium')"
class="mb-4"
/>
<Link
doctype="LMS Category"
:label="__('Category')"
v-model="batch.category"
:onCreate="(value, close) => openSettings('Categories', close)"
/>
</div>
<div class="space-y-5">
<div> <div>
<div class="text-xs text-ink-gray-5 mb-2"> <div class="text-xs text-ink-gray-5">
{{ __('Meta Image') }} {{ __('Meta Image') }}
</div> </div>
<FileUploader <FileUploader
@@ -70,11 +201,9 @@
<Button @click="openFileSelector"> <Button @click="openFileSelector">
{{ __('Upload') }} {{ __('Upload') }}
</Button> </Button>
<div class="mt-2 text-ink-gray-5 text-sm"> <div class="mt-1 text-ink-gray-5 text-sm leading-5">
{{ {{
__( __('Appears when the batch URL is shared on socials')
'Appears when the batch URL is shared on any online platform'
)
}} }}
</div> </div>
</div> </div>
@@ -106,119 +235,16 @@
</div> </div>
</div> </div>
<div class="my-10"> <div class="px-20 pb-5 space-y-5">
<div class="text-lg text-ink-gray-9 font-semibold mb-4"> <div class="text-lg text-ink-gray-9 font-semibold">
{{ __('Date and Time') }} {{ __('Pricing') }}
</div>
<div class="grid grid-cols-3 gap-10">
<div>
<FormControl
v-model="batch.start_date"
:label="__('Start Date')"
type="date"
class="mb-4"
:required="true"
/>
<FormControl
v-model="batch.end_date"
:label="__('End Date')"
type="date"
class="mb-4"
:required="true"
/>
</div>
<div>
<FormControl
v-model="batch.start_time"
:label="__('Start Time')"
type="time"
class="mb-4"
:required="true"
/>
<FormControl
v-model="batch.end_time"
:label="__('End Time')"
type="time"
class="mb-4"
:required="true"
/>
</div>
<div>
<FormControl
v-model="batch.timezone"
:label="__('Timezone')"
type="text"
:placeholder="__('Example: IST (+5:30)')"
class="mb-4"
:required="true"
/>
</div>
</div>
</div>
<div class="mb-10">
<div class="text-lg text-ink-gray-9 font-semibold mb-4">
{{ __('Settings') }}
</div>
<div class="grid grid-cols-3 gap-10">
<div>
<FormControl
v-model="batch.seat_count"
:label="__('Seat Count')"
type="number"
class="mb-4"
:placeholder="__('Number of seats available')"
/>
<FormControl
v-model="batch.evaluation_end_date"
:label="__('Evaluation End Date')"
type="date"
class="mb-4"
/>
</div>
<div>
<FormControl
v-model="batch.medium"
type="select"
:options="[
{
label: 'Online',
value: 'Online',
},
{
label: 'Offline',
value: 'Offline',
},
]"
:label="__('Medium')"
class="mb-4"
/>
<Link
doctype="LMS Category"
:label="__('Category')"
v-model="batch.category"
/>
</div>
<div>
<Link
doctype="Email Template"
:label="__('Email Template')"
v-model="batch.confirmation_email_template"
/>
</div>
</div>
</div>
<div class="">
<div class="text-lg text-ink-gray-9 font-semibold mb-4">
{{ __('Payment') }}
</div> </div>
<FormControl <FormControl
v-model="batch.paid_batch" v-model="batch.paid_batch"
type="checkbox" type="checkbox"
:label="__('Paid Batch')" :label="__('Paid Batch')"
/> />
<div class="grid grid-cols-3 gap-10 mt-4"> <div v-if="batch.paid_batch" class="grid grid-cols-3 gap-5">
<FormControl <FormControl
v-model="batch.amount" v-model="batch.amount"
:label="__('Amount')" :label="__('Amount')"
@@ -232,33 +258,6 @@
/> />
</div> </div>
</div> </div>
<div class="my-10">
<div class="text-lg text-ink-gray-9 font-semibold mb-4">
{{ __('Description') }}
</div>
<FormControl
v-model="batch.description"
:label="__('Short Description')"
type="textarea"
class="my-4"
:placeholder="__('Short description of the batch')"
:required="true"
/>
<div>
<label class="block text-sm text-ink-gray-5 mb-1">
{{ __('Batch Details') }}
<span class="text-ink-red-3">*</span>
</label>
<TextEditor
:content="batch.batch_details"
@change="(val) => (batch.batch_details = 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] mb-4"
/>
</div>
</div>
</div> </div>
</div> </div>
</template> </template>
@@ -279,15 +278,16 @@ import {
TextEditor, TextEditor,
createResource, createResource,
usePageMeta, usePageMeta,
toast,
} from 'frappe-ui' } from 'frappe-ui'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
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 { useOnboarding } from 'frappe-ui/frappe' import { useOnboarding } from 'frappe-ui/frappe'
import { sessionStore } from '../stores/session' import { sessionStore } from '../stores/session'
import MultiSelect from '@/components/Controls/MultiSelect.vue' import MultiSelect from '@/components/Controls/MultiSelect.vue'
import Link from '@/components/Controls/Link.vue' import Link from '@/components/Controls/Link.vue'
import { openSettings } from '@/utils'
const router = useRouter() const router = useRouter()
const user = inject('$user') const user = inject('$user')
@@ -459,7 +459,7 @@ const createNewBatch = () => {
}) })
}, },
onError(err) { onError(err) {
showToast('Message', err.messages?.[0] || err, 'alert-circle') toast.error(err.messages?.[0] || err)
}, },
} }
) )
@@ -478,7 +478,7 @@ const editBatchDetails = () => {
}) })
}, },
onError(err) { onError(err) {
showToast('Message', err.messages?.[0] || err, 'alert-circle') toast.error(err.messages?.[0] || err)
}, },
} }
) )

View File

@@ -20,12 +20,14 @@
</header> </header>
<div class="p-5 pb-10"> <div class="p-5 pb-10">
<div <div
v-if="batchCount"
class="flex flex-col lg:flex-row space-y-4 lg:space-y-0 lg:items-center justify-between mb-5" class="flex flex-col lg:flex-row space-y-4 lg:space-y-0 lg:items-center justify-between mb-5"
> >
<div class="text-lg text-ink-gray-9 font-semibold"> <div class="text-lg text-ink-gray-9 font-semibold">
{{ __('All Batches') }} {{ __('All Batches') }}
</div> </div>
<div <div
v-if="batches.data?.length || batchCount"
class="flex flex-col space-y-2 lg:space-y-0 lg:flex-row lg:items-center lg:space-x-4" class="flex flex-col space-y-2 lg:space-y-0 lg:flex-row lg:items-center lg:space-x-4"
> >
<TabButtons <TabButtons
@@ -70,22 +72,8 @@
<BatchCard :batch="batch" /> <BatchCard :batch="batch" />
</router-link> </router-link>
</div> </div>
<div <EmptyState v-else-if="!batches.list.loading" type="Batches" />
v-else-if="!batches.list.loading"
class="flex flex-col items-center justify-center text-sm text-ink-gray-5 mt-48"
>
<BookOpen class="size-10 mx-auto stroke-1 text-ink-gray-4" />
<div class="text-lg font-medium mb-1">
{{ __('No batches found') }}
</div>
<div class="leading-5 w-2/5 text-center">
{{
__(
'There are no batches matching the criteria. Keep an eye out, fresh learning experiences are on the way soon!'
)
}}
</div>
</div>
<div <div
v-if="!batches.list.loading && batches.hasNextPage" v-if="!batches.list.loading && batches.hasNextPage"
class="flex justify-center mt-5" class="flex justify-center mt-5"
@@ -100,6 +88,7 @@
import { import {
Breadcrumbs, Breadcrumbs,
Button, Button,
call,
createListResource, createListResource,
FormControl, FormControl,
Select, Select,
@@ -107,9 +96,10 @@ import {
usePageMeta, usePageMeta,
} from 'frappe-ui' } from 'frappe-ui'
import { computed, inject, onMounted, ref, watch } from 'vue' import { computed, inject, onMounted, ref, watch } from 'vue'
import { BookOpen, Plus } from 'lucide-vue-next' import { Plus } from 'lucide-vue-next'
import { sessionStore } from '@/stores/session' import { sessionStore } from '@/stores/session'
import BatchCard from '@/components/BatchCard.vue' import BatchCard from '@/components/BatchCard.vue'
import EmptyState from '@/components/EmptyState.vue'
const user = inject('$user') const user = inject('$user')
const dayjs = inject('$dayjs') const dayjs = inject('$dayjs')
@@ -125,10 +115,12 @@ const is_student = computed(() => user.data?.is_student)
const currentTab = ref(is_student.value ? 'All' : 'Upcoming') const currentTab = ref(is_student.value ? 'All' : 'Upcoming')
const orderBy = ref('start_date') const orderBy = ref('start_date')
const readOnlyMode = window.read_only_mode const readOnlyMode = window.read_only_mode
const batchCount = ref(0)
onMounted(() => { onMounted(() => {
setFiltersFromQuery() setFiltersFromQuery()
updateBatches() updateBatches()
getBatchCount()
categories.value = [ categories.value = [
{ {
label: '', label: '',
@@ -306,6 +298,14 @@ const canCreateBatch = () => {
return false return false
} }
const getBatchCount = () => {
call('frappe.client.get_count', {
doctype: 'LMS Batch',
}).then((data) => {
batchCount.value = data
})
}
const breadcrumbs = computed(() => [ const breadcrumbs = computed(() => [
{ {
label: __('Batches'), label: __('Batches'),

View File

@@ -156,9 +156,9 @@ import {
FormControl, FormControl,
Breadcrumbs, Breadcrumbs,
usePageMeta, usePageMeta,
toast,
} from 'frappe-ui' } from 'frappe-ui'
import { reactive, inject, onMounted, computed } from 'vue' import { reactive, inject, onMounted, computed } from 'vue'
import { showToast } from '@/utils/'
import { sessionStore } from '../stores/session' import { sessionStore } from '../stores/session'
import Link from '@/components/Controls/Link.vue' import Link from '@/components/Controls/Link.vue'
import NotPermitted from '@/components/NotPermitted.vue' import NotPermitted from '@/components/NotPermitted.vue'
@@ -259,7 +259,7 @@ const generatePaymentLink = () => {
window.location.href = data window.location.href = data
}, },
onError(err) { onError(err) {
showToast(__('Error'), err.messages?.[0] || err, 'x') toast.error(err.messages?.[0] || err)
}, },
} }
) )
@@ -333,14 +333,7 @@ const validateAddress = () => {
} }
const showError = (err) => { const showError = (err) => {
createToast({ toast.error(err.messages?.[0] || err)
title: 'Error',
text: err.messages?.[0] || err,
icon: 'x',
iconClasses: 'bg-surface-red-5 text-ink-white rounded-md p-px',
position: 'top-center',
timeout: 10,
})
} }
const changeCurrency = (country) => { const changeCurrency = (country) => {

View File

@@ -3,7 +3,7 @@
class="sticky flex items-center justify-between top-0 z-10 border-b bg-surface-white px-3 py-2.5 sm:px-5" class="sticky flex items-center justify-between top-0 z-10 border-b bg-surface-white px-3 py-2.5 sm:px-5"
> >
<Breadcrumbs :items="breadcrumbs" /> <Breadcrumbs :items="breadcrumbs" />
<router-link :to="{ name: 'Batches' }"> <router-link :to="{ name: 'Batches', query: { certification: true } }">
<Button> <Button>
<template #prefix> <template #prefix>
<GraduationCap class="h-4 w-4 stroke-1.5" /> <GraduationCap class="h-4 w-4 stroke-1.5" />
@@ -101,22 +101,7 @@
</Button> </Button>
</div> </div>
</div> </div>
<div <EmptyState v-else type="Certified Members" />
v-else
class="flex flex-col items-center justify-center text-sm text-ink-gray-5 mt-48"
>
<BookOpen class="size-10 mx-auto stroke-1 text-ink-gray-4" />
<div class="text-lg font-medium mb-1">
{{ __('No certified members') }}
</div>
<div class="leading-5 w-2/5 text-center">
{{
__(
'No certified members found. Please check again later or get certified yourself.'
)
}}
</div>
</div>
</template> </template>
<script setup> <script setup>
import { import {
@@ -130,8 +115,9 @@ import {
usePageMeta, usePageMeta,
} from 'frappe-ui' } from 'frappe-ui'
import { computed, inject, onMounted, ref } from 'vue' import { computed, inject, onMounted, ref } from 'vue'
import { BookOpen, GraduationCap } from 'lucide-vue-next' import { GraduationCap } from 'lucide-vue-next'
import { sessionStore } from '../stores/session' import { sessionStore } from '../stores/session'
import EmptyState from '@/components/EmptyState.vue'
const currentCategory = ref('') const currentCategory = ref('')
const filters = ref({}) const filters = ref({})

View File

@@ -19,62 +19,112 @@
</Button> </Button>
</div> </div>
</header> </header>
<div class="mt-5 mb-10"> <div class="mt-5 mb-5">
<div class="container mb-5"> <div class="px-10 pb-5 mb-5 space-y-5 border-b">
<div class="text-lg font-semibold mb-4"> <div class="text-lg font-semibold mb-4">
{{ __('Details') }} {{ __('Details') }}
</div> </div>
<FormControl <div class="grid grid-cols-2 gap-5">
v-model="course.title" <FormControl
:label="__('Title')" v-model="course.title"
class="mb-4" :label="__('Title')"
:required="true" :required="true"
/> />
<FormControl <Link
v-model="course.short_introduction" doctype="LMS Category"
:label="__('Short Introduction')" v-model="course.category"
:placeholder=" :label="__('Category')"
__( :onCreate="(value, close) => openSettings('Categories', close)"
'A one line introduction to the course that appears on the course card'
)
"
class="mb-4"
:required="true"
/>
<div class="mb-4">
<div class="mb-1.5 text-sm text-ink-gray-5">
{{ __('Course Description') }}
<span class="text-ink-red-3">*</span>
</div>
<TextEditor
:content="course.description"
@change="(val) => (course.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]"
/> />
</div> </div>
<div class="mb-4"> <div class="grid grid-cols-2 gap-5">
<div class="text-xs text-ink-gray-5 mb-2"> <MultiSelect
{{ __('Course Image') }} v-model="instructors"
<span class="text-ink-red-3">*</span> doctype="User"
:label="__('Instructors')"
:filters="{ ignore_user_type: 1 }"
:onCreate="(close) => openSettings('Members', close)"
:required="true"
/>
<div>
<div class="mb-1.5 text-xs text-ink-gray-5">
{{ __('Tags') }}
</div>
<div class="flex items-center">
<div
v-if="course.tags"
v-for="tag in course.tags?.split(', ')"
class="flex items-center bg-surface-gray-2 text-ink-gray-7 p-2 rounded-md mr-2"
>
{{ tag }}
<X
class="stroke-1.5 w-3 h-3 ml-2 cursor-pointer"
@click="removeTag(tag)"
/>
</div>
<FormControl
v-model="newTag"
:placeholder="__('Add a keyword and then press enter')"
class="w-full"
@keyup.enter="updateTags()"
id="tags"
/>
</div>
</div> </div>
<FileUploader </div>
v-if="!course.course_image" <div class="grid grid-cols-2 gap-5">
:fileTypes="['image/*']" <FormControl
:validateFile="validateFile" v-model="course.short_introduction"
@success="(file) => saveImage(file)" type="textarea"
> :rows="4"
<template :label="__('Short Introduction')"
v-slot="{ file, progress, uploading, openFileSelector }" :placeholder="
__(
'A one line introduction to the course that appears on the course card'
)
"
:required="true"
/>
<div class="mb-4">
<div class="text-xs text-ink-gray-5 mb-2">
{{ __('Course Image') }}
<span class="text-ink-red-3">*</span>
</div>
<FileUploader
v-if="!course.course_image"
:fileTypes="['image/*']"
:validateFile="validateFile"
@success="(file) => saveImage(file)"
> >
<div class="flex items-center"> <template
<div class="border rounded-md w-fit py-5 px-20"> v-slot="{ file, progress, uploading, openFileSelector }"
<Image class="size-5 stroke-1 text-ink-gray-7" /> >
<div class="flex items-center">
<div class="border rounded-md w-fit py-5 px-20">
<Image class="size-5 stroke-1 text-ink-gray-7" />
</div>
<div class="ml-4">
<Button @click="openFileSelector">
{{ __('Upload') }}
</Button>
<div class="mt-1 text-ink-gray-5 text-sm leading-5">
{{
__('Appears on the course card in the course list')
}}
</div>
</div>
</div> </div>
</template>
</FileUploader>
<div v-else class="mb-4">
<div class="flex items-center">
<img
:src="course.course_image.file_url"
class="border rounded-md w-40"
/>
<div class="ml-4"> <div class="ml-4">
<Button @click="openFileSelector"> <Button @click="removeImage()">
{{ __('Upload') }} {{ __('Remove') }}
</Button> </Button>
<div class="mt-2 text-ink-gray-5 text-sm"> <div class="mt-2 text-ink-gray-5 text-sm">
{{ {{
@@ -83,85 +133,17 @@
</div> </div>
</div> </div>
</div> </div>
</template>
</FileUploader>
<div v-else class="mb-4">
<div class="flex items-center">
<img
:src="course.course_image.file_url"
class="border rounded-md w-40"
/>
<div class="ml-4">
<Button @click="removeImage()">
{{ __('Remove') }}
</Button>
<div class="mt-2 text-ink-gray-5 text-sm">
{{ __('Appears on the course card in the course list') }}
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
<FormControl
v-model="course.video_link"
:label="__('Preview Video')"
:placeholder="
__(
'Paste the youtube link of a short video introducing the course'
)
"
class="mb-4"
/>
<div class="mb-4">
<div class="mb-1.5 text-xs text-ink-gray-5">
{{ __('Tags') }}
</div>
<div class="flex items-center">
<div
v-if="course.tags"
v-for="tag in course.tags?.split(', ')"
class="flex items-center bg-surface-gray-2 text-ink-gray-7 p-2 rounded-md mr-2"
>
{{ tag }}
<X
class="stroke-1.5 w-3 h-3 ml-2 cursor-pointer"
@click="removeTag(tag)"
/>
</div>
<FormControl
v-model="newTag"
:placeholder="__('Add a keyword and then press enter')"
class="w-72"
@keyup.enter="updateTags()"
id="tags"
/>
</div>
</div>
<div class="w-1/2 mb-4">
<Link
doctype="LMS Category"
v-model="course.category"
:label="__('Category')"
:onCreate="(value, close) => openSettings(close)"
/>
</div>
<MultiSelect
v-model="instructors"
doctype="User"
:label="__('Instructors')"
:filters="{ ignore_user_type: 1 }"
:required="true"
/>
</div> </div>
<div class="container border-t">
<div class="text-lg font-semibold mt-5 mb-4"> <div class="px-10 pb-5 mb-5 space-y-5 border-b">
<div class="text-lg font-semibold">
{{ __('Settings') }} {{ __('Settings') }}
</div> </div>
<div class="grid grid-cols-2 gap-10 mb-4"> <div class="grid grid-cols-2 gap-5">
<div <div class="flex flex-col space-y-5">
v-if="user.data?.is_moderator"
class="flex flex-col space-y-4"
>
<FormControl <FormControl
type="checkbox" type="checkbox"
v-model="course.published" v-model="course.published"
@@ -171,10 +153,9 @@
v-model="course.published_on" v-model="course.published_on"
:label="__('Published On')" :label="__('Published On')"
type="date" type="date"
class="mb-5"
/> />
</div> </div>
<div class="flex flex-col space-y-3"> <div class="flex flex-col space-y-5">
<FormControl <FormControl
type="checkbox" type="checkbox"
v-model="course.upcoming" v-model="course.upcoming"
@@ -193,7 +174,34 @@
</div> </div>
</div> </div>
</div> </div>
<div class="container border-t space-y-4">
<div class="px-10 pb-5 mb-5 space-y-5 border-b">
<div class="">
<div class="mb-1.5 text-sm text-ink-gray-5">
{{ __('Course Description') }}
<span class="text-ink-red-3">*</span>
</div>
<TextEditor
:content="course.description"
@change="(val) => (course.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]"
/>
</div>
<FormControl
v-model="course.video_link"
:label="__('Preview Video')"
:placeholder="
__(
'Paste the youtube link of a short video introducing the course'
)
"
/>
</div>
<div class="px-10 pb-5 space-y-5">
<div class="text-lg font-semibold mt-5"> <div class="text-lg font-semibold mt-5">
{{ __('Pricing and Certification') }} {{ __('Pricing and Certification') }}
</div> </div>
@@ -214,19 +222,31 @@
:label="__('Paid Certificate')" :label="__('Paid Certificate')"
/> />
</div> </div>
<FormControl v-model="course.course_price" :label="__('Amount')" /> <div class="grid grid-cols-2 gap-5">
<Link <div class="space-y-5">
doctype="Currency" <FormControl
v-model="course.currency" v-if="course.paid_course || course.paid_certificate"
:filters="{ enabled: 1 }" v-model="course.course_price"
:label="__('Currency')" :label="__('Amount')"
/> />
<Link <Link
v-if="course.paid_certificate" v-if="course.paid_certificate"
doctype="Course Evaluator" doctype="Course Evaluator"
v-model="course.evaluator" v-model="course.evaluator"
:label="__('Evaluator')" :label="__('Evaluator')"
/> :onCreate="
(value, close) => openSettings('Evaluators', close)
"
/>
</div>
<Link
v-if="course.paid_course || course.paid_certificate"
doctype="Currency"
v-model="course.currency"
:filters="{ enabled: 1 }"
:label="__('Currency')"
/>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -250,6 +270,7 @@ import {
FormControl, FormControl,
FileUploader, FileUploader,
usePageMeta, usePageMeta,
toast,
} from 'frappe-ui' } from 'frappe-ui'
import { import {
inject, inject,
@@ -261,13 +282,12 @@ import {
watch, watch,
getCurrentInstance, getCurrentInstance,
} from 'vue' } from 'vue'
import { showToast } from '@/utils'
import { Image, Trash2, X } from 'lucide-vue-next' import { Image, Trash2, X } from 'lucide-vue-next'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { capture } from '@/telemetry' import { capture } from '@/telemetry'
import { useOnboarding } from 'frappe-ui/frappe' import { useOnboarding } from 'frappe-ui/frappe'
import { sessionStore } from '../stores/session' import { sessionStore } from '../stores/session'
import { useSettings } from '@/stores/settings' import { openSettings } from '@/utils'
import Link from '@/components/Controls/Link.vue' import Link from '@/components/Controls/Link.vue'
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'
@@ -277,7 +297,6 @@ const newTag = ref('')
const { brand } = sessionStore() const { brand } = sessionStore()
const router = useRouter() const router = useRouter()
const instructors = ref([]) const instructors = ref([])
const settingsStore = useSettings()
const app = getCurrentInstance() const app = getCurrentInstance()
const { updateOnboardingStep } = useOnboarding('learning') const { updateOnboardingStep } = useOnboarding('learning')
const { $dialog } = app.appContext.config.globalProperties const { $dialog } = app.appContext.config.globalProperties
@@ -429,10 +448,10 @@ const submitCourse = () => {
}, },
{ {
onSuccess() { onSuccess() {
showToast('Success', 'Course updated successfully', 'check') toast.success(__('Course updated successfully'))
}, },
onError(err) { onError(err) {
showToast('Error', err.messages?.[0] || err, 'x') toast.error(err.messages?.[0] || err)
}, },
} }
) )
@@ -446,14 +465,14 @@ const submitCourse = () => {
} }
capture('course_created') capture('course_created')
showToast('Success', 'Course created successfully', 'check') toast.success(__('Course created successfully'))
router.push({ router.push({
name: 'CourseForm', name: 'CourseForm',
params: { courseName: data.name }, params: { courseName: data.name },
}) })
}, },
onError(err) { onError(err) {
showToast('Error', err.messages?.[0] || err, 'x') toast.error(err.messages?.[0] || err)
}, },
}) })
} }
@@ -467,7 +486,7 @@ const deleteCourse = createResource({
} }
}, },
onSuccess() { onSuccess() {
showToast(__('Success'), __('Course deleted successfully'), 'check') toast.success(__('Course deleted successfully'))
router.push({ name: 'Courses' }) router.push({ name: 'Courses' })
}, },
}) })
@@ -531,12 +550,6 @@ const removeImage = () => {
course.course_image = null course.course_image = null
} }
const openSettings = (close) => {
close()
settingsStore.activeTab = 'Categories'
settingsStore.isSettingsOpen = true
}
const check_permission = () => { const check_permission = () => {
let user_is_instructor = false let user_is_instructor = false
if (user.data?.is_moderator) return if (user.data?.is_moderator) return

View File

@@ -20,12 +20,14 @@
</header> </header>
<div class="p-5 pb-10"> <div class="p-5 pb-10">
<div <div
v-if="courseCount"
class="flex flex-col lg:flex-row space-y-4 lg:space-y-0 lg:items-center justify-between mb-5" class="flex flex-col lg:flex-row space-y-4 lg:space-y-0 lg:items-center justify-between mb-5"
> >
<div class="text-lg text-ink-gray-9 font-semibold"> <div class="text-lg text-ink-gray-9 font-semibold">
{{ __('All Courses') }} {{ __('All Courses') }}
</div> </div>
<div <div
v-if="courses.data?.length || courseCount"
class="flex flex-col space-y-2 lg:space-y-0 lg:flex-row lg:items-center lg:space-x-4" class="flex flex-col space-y-2 lg:space-y-0 lg:flex-row lg:items-center lg:space-x-4"
> >
<TabButtons :buttons="courseTabs" v-model="currentTab" /> <TabButtons :buttons="courseTabs" v-model="currentTab" />
@@ -66,22 +68,7 @@
<CourseCard :course="course" /> <CourseCard :course="course" />
</router-link> </router-link>
</div> </div>
<div <EmptyState v-else-if="!courses.list.loading" type="Courses" />
v-else-if="!courses.list.loading"
class="flex flex-col items-center justify-center text-sm text-ink-gray-5 italic mt-48"
>
<BookOpen class="size-10 mx-auto stroke-1 text-ink-gray-4" />
<div class="text-lg font-medium mb-1">
{{ __('No courses found') }}
</div>
<div class="leading-5 w-2/5 text-center">
{{
__(
'There are no courses matching the criteria. Keep an eye out, fresh learning experiences are on the way soon!'
)
}}
</div>
</div>
<div <div
v-if="!courses.list.loading && courses.hasNextPage" v-if="!courses.list.loading && courses.hasNextPage"
class="flex justify-center mt-5" class="flex justify-center mt-5"
@@ -104,10 +91,11 @@ import {
usePageMeta, usePageMeta,
} from 'frappe-ui' } from 'frappe-ui'
import { computed, inject, onMounted, ref, watch } from 'vue' import { computed, inject, onMounted, ref, watch } from 'vue'
import { BookOpen, Plus } from 'lucide-vue-next' import { Plus } from 'lucide-vue-next'
import { sessionStore } from '@/stores/session' import { sessionStore } from '@/stores/session'
import { canCreateCourse } from '@/utils' import { canCreateCourse } from '@/utils'
import CourseCard from '@/components/CourseCard.vue' import CourseCard from '@/components/CourseCard.vue'
import EmptyState from '@/components/EmptyState.vue'
import router from '../router' import router from '../router'
const user = inject('$user') const user = inject('$user')
@@ -121,12 +109,12 @@ const certification = ref(false)
const filters = ref({}) const filters = ref({})
const currentTab = ref('Live') const currentTab = ref('Live')
const { brand } = sessionStore() const { brand } = sessionStore()
const readOnlyMode = window.read_only_mode const courseCount = ref(0)
onMounted(() => { onMounted(() => {
identifyUserPersona()
setFiltersFromQuery() setFiltersFromQuery()
updateCourses() updateCourses()
getCourseCount()
categories.value = [ categories.value = [
{ {
label: '', label: '',
@@ -175,19 +163,23 @@ const identifyUserPersona = async () => {
if (user.data?.is_system_manager && !user.data?.developer_mode) { if (user.data?.is_system_manager && !user.data?.developer_mode) {
let personaCaptured = await isPersonaCaptured() let personaCaptured = await isPersonaCaptured()
if (personaCaptured) return if (personaCaptured) return
if (!courseCount.value) {
call('frappe.client.get_count', { router.push({
doctype: 'LMS Course', name: 'PersonaForm',
}).then((data) => { })
if (!data) { }
router.push({
name: 'PersonaForm',
})
}
})
} }
} }
const getCourseCount = () => {
call('frappe.client.get_count', {
doctype: 'LMS Course',
}).then((data) => {
courseCount.value = data
identifyUserPersona()
})
}
const updateCourses = () => { const updateCourses = () => {
updateFilters() updateFilters()
courses.update({ courses.update({

View File

@@ -94,6 +94,12 @@
{{ dayjs(job.data.creation).fromNow() }} {{ dayjs(job.data.creation).fromNow() }}
</Badge> </Badge>
<Badge size="lg"> <Badge size="lg">
<template #prefix>
<ClipboardType class="size-3 stroke-2 text-ink-gray-7" />
</template>
{{ job.data.type }}
</Badge>
<Badge v-if="applicationCount.data" size="lg">
<template #prefix> <template #prefix>
<SquareUserRound class="size-3 stroke-2 text-ink-gray-7" /> <SquareUserRound class="size-3 stroke-2 text-ink-gray-7" />
</template> </template>
@@ -102,12 +108,6 @@
applicationCount.data == 1 ? __('applicant') : __('applicants') applicationCount.data == 1 ? __('applicant') : __('applicants')
}} }}
</Badge> </Badge>
<Badge size="lg">
<template #prefix>
<ClipboardType class="size-3 stroke-2 text-ink-gray-7" />
</template>
{{ job.data.type }}
</Badge>
</div> </div>
</div> </div>

View File

@@ -9,7 +9,7 @@
</Button> </Button>
</header> </header>
<div class="py-5"> <div class="py-5">
<div class="container border-b mb-4 pb-4"> <div class="container border-b mb-4 pb-5">
<div class="text-lg font-semibold mb-4"> <div class="text-lg font-semibold mb-4">
{{ __('Job Details') }} {{ __('Job Details') }}
</div> </div>
@@ -20,6 +20,15 @@
:label="__('Title')" :label="__('Title')"
:required="true" :required="true"
/> />
<FormControl
v-model="job.type"
:label="__('Type')"
type="select"
:options="jobTypes"
:required="true"
/>
</div>
<div class="space-y-4">
<FormControl <FormControl
v-model="job.location" v-model="job.location"
:label="__('City')" :label="__('City')"
@@ -31,17 +40,8 @@
:label="__('Country')" :label="__('Country')"
:required="true" :required="true"
/> />
</div>
<div>
<FormControl
v-model="job.type"
:label="__('Type')"
type="select"
:options="jobTypes"
class="mb-4"
:required="true"
/>
<FormControl <FormControl
v-if="jobName != 'new'"
v-model="job.status" v-model="job.status"
:label="__('Status')" :label="__('Status')"
type="select" type="select"
@@ -51,7 +51,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="container border-b mb-4 pb-4"> <div class="container border-b mb-4 pb-5">
<div class="text-lg font-semibold mb-4"> <div class="text-lg font-semibold mb-4">
{{ __('Company Details') }} {{ __('Company Details') }}
</div> </div>
@@ -145,12 +145,13 @@ import {
TextEditor, TextEditor,
FileUploader, FileUploader,
usePageMeta, usePageMeta,
toast,
} from 'frappe-ui' } from 'frappe-ui'
import { computed, onMounted, reactive, inject } from 'vue' import { computed, onMounted, reactive, inject } from 'vue'
import { FileText, X } from 'lucide-vue-next' import { FileText, X } from 'lucide-vue-next'
import { sessionStore } from '@/stores/session' import { sessionStore } from '@/stores/session'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { getFileSize, showToast } from '../utils' import { getFileSize } from '@/utils'
const user = inject('$user') const user = inject('$user')
const router = useRouter() const router = useRouter()
@@ -259,7 +260,7 @@ const createNewJob = () => {
}) })
}, },
onError(err) { onError(err) {
showToast('Error', err.messages?.[0] || err, 'x') toast.error(err.messages?.[0] || err)
}, },
} }
) )
@@ -278,7 +279,7 @@ const editJobDetails = () => {
}) })
}, },
onError(err) { onError(err) {
showToast('Error', err.messages?.[0] || err, 'x') toast.error(err.messages?.[0] || err)
}, },
} }
) )

View File

@@ -26,15 +26,17 @@
</header> </header>
<div> <div>
<div <div
v-if="jobCount"
class="flex flex-col lg:flex-row space-y-4 lg:space-y-0 lg:items-center justify-between w-full md:w-4/5 mx-auto p-5" class="flex flex-col lg:flex-row space-y-4 lg:space-y-0 lg:items-center justify-between w-full md:w-4/5 mx-auto p-5"
> >
<div <div class="text-xl font-semibold text-ink-gray-7 mb-4 md:mb-0">
v-if="jobCount"
class="text-xl font-semibold text-ink-gray-7 mb-4 md:mb-0"
>
{{ __('{0} Open Jobs').format(jobCount) }} {{ __('{0} Open Jobs').format(jobCount) }}
</div> </div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-2">
<div
v-if="jobs.data?.length || jobCount > 0"
class="grid grid-cols-1 md:grid-cols-3 gap-2"
>
<FormControl <FormControl
type="text" type="text"
:placeholder="__('Search')" :placeholder="__('Search')"
@@ -79,21 +81,7 @@
</router-link> </router-link>
</div> </div>
</div> </div>
<div <EmptyState v-else type="Job Openings" />
v-else
class="flex flex-col items-center justify-center text-sm text-ink-gray-5 mt-56"
>
<Laptop class="size-10 mx-auto stroke-1 text-ink-gray-4" />
<div class="text-lg font-medium mb-1">
{{ __('No jobs found') }}
</div>
<div class="leading-5 w-2/5 text-center">
{{ __('There are no jobs available at the moment.') }}
</div>
<div class="leading-5 w-1/5 text-center">
{{ __('Post a new job or check again later.') }}
</div>
</div>
</div> </div>
</div> </div>
</template> </template>
@@ -106,11 +94,12 @@ import {
FormControl, FormControl,
usePageMeta, usePageMeta,
} from 'frappe-ui' } from 'frappe-ui'
import { Laptop, Plus, Search } from 'lucide-vue-next' import { Plus, Search } from 'lucide-vue-next'
import { sessionStore } from '../stores/session' import { sessionStore } from '../stores/session'
import { inject, computed, ref, onMounted, watch } from 'vue' import { inject, computed, ref, onMounted, watch } from 'vue'
import JobCard from '@/components/JobCard.vue' import JobCard from '@/components/JobCard.vue'
import Link from '@/components/Controls/Link.vue' import Link from '@/components/Controls/Link.vue'
import EmptyState from '@/components/EmptyState.vue'
const user = inject('$user') const user = inject('$user')
const jobType = ref(null) const jobType = ref(null)

View File

@@ -334,7 +334,6 @@ const props = defineProps({
onMounted(() => { onMounted(() => {
startTimer() startTimer()
enablePlyr()
document.addEventListener('fullscreenchange', attachFullscreenEvent) document.addEventListener('fullscreenchange', attachFullscreenEvent)
}) })
@@ -473,6 +472,7 @@ watch(
() => lesson.data, () => lesson.data,
(data) => { (data) => {
setupLesson(data) setupLesson(data)
enablePlyr()
} }
) )

View File

@@ -84,6 +84,7 @@ import {
createResource, createResource,
FormControl, FormControl,
usePageMeta, usePageMeta,
toast,
} from 'frappe-ui' } from 'frappe-ui'
import { import {
computed, computed,
@@ -97,7 +98,7 @@ import { sessionStore } from '../stores/session'
import EditorJS from '@editorjs/editorjs' import EditorJS from '@editorjs/editorjs'
import LessonHelp from '@/components/LessonHelp.vue' import LessonHelp from '@/components/LessonHelp.vue'
import { ChevronRight } from 'lucide-vue-next' import { ChevronRight } from 'lucide-vue-next'
import { createToast, getEditorTools, enablePlyr } from '@/utils' import { getEditorTools, enablePlyr } from '@/utils'
import { capture } from '@/telemetry' import { capture } from '@/telemetry'
import { useOnboarding } from 'frappe-ui/frappe' import { useOnboarding } from 'frappe-ui/frappe'
@@ -410,14 +411,14 @@ const createNewLesson = () => {
updateOnboardingStep('create_first_lesson') updateOnboardingStep('create_first_lesson')
capture('lesson_created') capture('lesson_created')
showToast('Success', 'Lesson created successfully', 'check') toast.success(__('Lesson created successfully'))
lessonDetails.reload() lessonDetails.reload()
}, },
} }
) )
}, },
onError(err) { onError(err) {
showToast('Error', err.message, 'x') toast.error(err.messages?.[0] || err)
}, },
} }
) )
@@ -434,11 +435,11 @@ const editCurrentLesson = () => {
}, },
onSuccess() { onSuccess() {
showSuccessMessage showSuccessMessage
? showToast('Success', 'Lesson updated successfully', 'check') ? toast.success(__('Lesson updated successfully'))
: '' : ''
}, },
onError(err) { onError(err) {
showToast('Error', err.message, 'x') toast.error(err.message)
}, },
} }
) )
@@ -453,20 +454,6 @@ const validateLesson = () => {
} }
} }
const showToast = (title, text, icon) => {
createToast({
title: title,
text: text,
icon: icon,
iconClasses:
icon == 'check'
? 'bg-surface-green-3 text-ink-white rounded-md p-px'
: 'bg-surface-red-5 text-ink-white rounded-md p-px',
position: icon == 'check' ? 'bottom-right' : 'top-center',
timeout: icon == 'check' ? 5 : 10,
})
}
const breadcrumbs = computed(() => { const breadcrumbs = computed(() => {
let crumbs = [ let crumbs = [
{ {

View File

@@ -1,6 +1,6 @@
<template> <template>
<div class="flex h-screen overflow-hidden sm:bg-gray-50"> <div class="flex h-screen overflow-hidden sm:bg-gray-50">
<div class="relative h-full z-10 mx-auto pt-8 sm:w-max sm:pt-32"> <div class="relative h-full z-10 mx-auto sm:w-max pt-40">
<div class="mx-auto flex items-center justify-center space-x-2"> <div class="mx-auto flex items-center justify-center space-x-2">
<LMSLogo class="size-7" /> <LMSLogo class="size-7" />
<span <span
@@ -18,7 +18,7 @@
<div class="mb-5"> <div class="mb-5">
<div class="text-sm text-gray-700 mb-2"> <div class="text-sm text-gray-700 mb-2">
{{ __('What is your main use case for Frappe Learning?') }} {{ __('What is your use case for Frappe Learning?') }}
</div> </div>
<FormControl <FormControl
v-model="persona.useCase" v-model="persona.useCase"
@@ -29,12 +29,12 @@
<div class="mb-5"> <div class="mb-5">
<div class="text-sm text-gray-700 mb-2"> <div class="text-sm text-gray-700 mb-2">
{{ __('How many students are you planning to teach?') }} {{ __('What best describes your role?') }}
</div> </div>
<FormControl <FormControl
v-model="persona.noOfStudents" v-model="persona.role"
type="select" type="select"
:options="noOfStudentsOptions" :options="roleOptions"
/> />
</div> </div>
@@ -65,7 +65,7 @@ const router = useRouter()
const { brand } = sessionStore() const { brand } = sessionStore()
const persona = reactive({ const persona = reactive({
noOfStudents: null, role: null,
useCase: null, useCase: null,
}) })
@@ -97,6 +97,24 @@ const skipPersonaForm = () => {
}) })
} }
const roleOptions = computed(() => {
const options = [
'Trainer / Instructor',
'Freelancer / Consultant',
'HR / L&D Professional',
'School / University Admin',
'Software Developer',
'Community Manager',
'Business Owner / Team Lead',
'Other',
]
return options.map((option) => ({
label: option,
value: option,
}))
})
const noOfStudentsOptions = computed(() => { const noOfStudentsOptions = computed(() => {
const options = [ const options = [
'Less than 50', 'Less than 50',

View File

@@ -141,9 +141,9 @@
</div> </div>
</template> </template>
<script setup> <script setup>
import { createResource, FormControl, Button, Badge } from 'frappe-ui' import { createResource, FormControl, Button, Badge, toast } from 'frappe-ui'
import { computed, reactive, ref, onMounted, inject } from 'vue' import { computed, reactive, ref, onMounted, inject } from 'vue'
import { showToast, convertToTitleCase } from '@/utils' import { convertToTitleCase } from '@/utils'
import { Plus, X, Check, CircleAlert } from 'lucide-vue-next' import { Plus, X, Check, CircleAlert } from 'lucide-vue-next'
const user = inject('$user') const user = inject('$user')
@@ -198,7 +198,7 @@ const createSlot = createResource({
} }
}, },
onSuccess() { onSuccess() {
showToast('Success', 'Slot added successfully', 'check') toast.success(__('Slot added successfully'))
evaluator.reload() evaluator.reload()
showSlotsTemplate.value = 0 showSlotsTemplate.value = 0
newSlot.day = '' newSlot.day = ''
@@ -206,7 +206,7 @@ const createSlot = createResource({
newSlot.end_time = '' newSlot.end_time = ''
}, },
onError(err) { onError(err) {
showToast('Error', err.messages?.[0] || err, 'x') toast.error(err.messages?.[0] || err)
}, },
}) })
@@ -221,10 +221,10 @@ const updateSlot = createResource({
} }
}, },
onSuccess() { onSuccess() {
showToast('Success', 'Availability updated successfully', 'check') toast.success(__('Availability updated successfully'))
}, },
onError(err) { onError(err) {
showToast('Error', err.messages?.[0] || err, 'x') toast.error(err.messages?.[0] || err)
}, },
}) })
@@ -237,11 +237,11 @@ const deleteSlot = createResource({
} }
}, },
onSuccess() { onSuccess() {
showToast('Success', 'Slot deleted successfully', 'check') toast.success(__('Slot deleted successfully'))
evaluator.reload() evaluator.reload()
}, },
onError(err) { onError(err) {
showToast('Error', err.messages?.[0] || err, 'x') toast.error(err.messages?.[0] || err)
}, },
}) })
@@ -256,10 +256,10 @@ const updateUnavailability = createResource({
} }
}, },
onSuccess() { onSuccess() {
showToast('Success', 'Unavailability updated successfully', 'check') toast.success(__('Unavailability updated successfully'))
}, },
onError(err) { onError(err) {
showToast('Error', err.messages?.[0] || err, 'x') toast.error(err.messages?.[0] || err)
}, },
}) })

View File

@@ -44,9 +44,9 @@
</div> </div>
</template> </template>
<script setup> <script setup>
import { FormControl, createResource } from 'frappe-ui' import { FormControl, createResource, toast } from 'frappe-ui'
import { ref } from 'vue' import { ref } from 'vue'
import { showToast, convertToTitleCase } from '@/utils' import { convertToTitleCase } from '@/utils'
import { CircleAlert } from 'lucide-vue-next' import { CircleAlert } from 'lucide-vue-next'
const moderator = ref(false) const moderator = ref(false)
@@ -102,7 +102,7 @@ const changeRole = (role) => {
}, },
{ {
onSuccess(data) { onSuccess(data) {
showToast('Success', 'Role updated successfully', 'check') toast.success(__('Role updated successfully'))
}, },
} }
) )

View File

@@ -168,6 +168,7 @@
ignore_user_type: 1, ignore_user_type: 1,
}" }"
:label="__('Program Member')" :label="__('Program Member')"
:onCreate="(value, close) => openSettings('Members', close)"
/> />
</template> </template>
</Dialog> </Dialog>
@@ -187,12 +188,13 @@ import {
ListHeaderItem, ListHeaderItem,
ListSelectBanner, ListSelectBanner,
usePageMeta, usePageMeta,
toast,
} from 'frappe-ui' } from 'frappe-ui'
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import { Plus, Trash2 } from 'lucide-vue-next' import { Plus, Trash2 } from 'lucide-vue-next'
import { showToast } from '@/utils/'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { sessionStore } from '../stores/session' import { sessionStore } from '@/stores/session'
import { openSettings } from '@/utils'
import Draggable from 'vuedraggable' import Draggable from 'vuedraggable'
import Link from '@/components/Controls/Link.vue' import Link from '@/components/Controls/Link.vue'
@@ -229,11 +231,11 @@ const addProgramCourse = () => {
onSuccess(data) { onSuccess(data) {
showDialog.value = false showDialog.value = false
course.value = null course.value = null
showToast(__('Success'), __('Course added to program'), 'check') toast.success(__('Course added to program'))
program.reload() program.reload()
}, },
onError(err) { onError(err) {
showToast('Error', err.messages?.[0] || err, 'x') toast.error(err.messages?.[0] || err)
}, },
} }
) )
@@ -251,11 +253,11 @@ const addProgramMember = () => {
onSuccess(data) { onSuccess(data) {
showDialog.value = false showDialog.value = false
member.value = null member.value = null
showToast(__('Success'), __('Member added to program'), 'check') toast.success(__('Member added to program'))
program.reload() program.reload()
}, },
onError(err) { onError(err) {
showToast('Error', err.messages?.[0] || err, 'x') toast.error(err.messages?.[0] || err)
}, },
} }
) )
@@ -272,11 +274,11 @@ const remove = (selections, unselectAll, doctype) => {
{ {
onSuccess(data) { onSuccess(data) {
unselectAll() unselectAll()
showToast(__('Success'), __('Items removed successfully'), 'check') toast.success(__('Items removed successfully'))
program.reload() program.reload()
}, },
onError(err) { onError(err) {
showToast('Error', err.messages?.[0] || err, 'x') toast.error(err.messages?.[0] || err)
}, },
} }
) )
@@ -298,11 +300,11 @@ const updateOrder = (e) => {
}, },
{ {
onSuccess(data) { onSuccess(data) {
showToast(__('Success'), __('Course moved successfully'), 'check') toast.success(__('Course moved successfully'))
program.reload() program.reload()
}, },
onError(err) { onError(err) {
showToast('Error', err.messages?.[0] || err, 'x') toast.error(err.messages?.[0] || err)
}, },
} }
) )

View File

@@ -82,22 +82,7 @@
</div> </div>
</div> </div>
</div> </div>
<div <EmptyState v-else type="Programs" />
v-else
class="text-center p-5 text-ink-gray-5 mt-52 w-3/4 md:w-1/2 mx-auto space-y-2"
>
<BookOpen class="size-10 mx-auto stroke-1 text-ink-gray-4" />
<div class="text-xl font-medium">
{{ __('No programs found') }}
</div>
<div class="leading-5">
{{
__(
'There are no programs available at the moment. Keep an eye out, fresh learning experiences are on the way soon!'
)
}}
</div>
</div>
<Dialog <Dialog
v-model="showDialog" v-model="showDialog"
@@ -127,13 +112,14 @@ import {
Dialog, Dialog,
FormControl, FormControl,
usePageMeta, usePageMeta,
toast,
} from 'frappe-ui' } from 'frappe-ui'
import { computed, inject, onMounted, ref } from 'vue' import { computed, inject, onMounted, ref } from 'vue'
import { BookOpen, Edit, Plus, LockKeyhole } from 'lucide-vue-next' import { Edit, Plus, LockKeyhole } from 'lucide-vue-next'
import CourseCard from '@/components/CourseCard.vue' import CourseCard from '@/components/CourseCard.vue'
import EmptyState from '@/components/EmptyState.vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { sessionStore } from '../stores/session' import { sessionStore } from '../stores/session'
import { showToast } from '@/utils'
import { useSettings } from '@/stores/settings' import { useSettings } from '@/stores/settings'
const { brand } = sessionStore() const { brand } = sessionStore()
@@ -198,7 +184,7 @@ const enrollMember = (program, course) => {
} }
}) })
.catch((err) => { .catch((err) => {
showToast('Error', err.messages?.[0] || err, 'x') toast.error(err.messages?.[0] || err)
}) })
} }

View File

@@ -198,6 +198,7 @@ import {
ListSelectBanner, ListSelectBanner,
Button, Button,
usePageMeta, usePageMeta,
toast,
} from 'frappe-ui' } from 'frappe-ui'
import { import {
computed, computed,
@@ -210,7 +211,7 @@ import {
} from 'vue' } from 'vue'
import { sessionStore } from '../stores/session' import { sessionStore } from '../stores/session'
import { Plus, Trash2 } from 'lucide-vue-next' import { Plus, Trash2 } from 'lucide-vue-next'
import { showToast, updateDocumentTitle } from '@/utils' import { updateDocumentTitle } from '@/utils'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import Question from '@/components/Modals/Question.vue' import Question from '@/components/Modals/Question.vue'
@@ -340,14 +341,14 @@ const createQuiz = () => {
{}, {},
{ {
onSuccess(data) { onSuccess(data) {
showToast(__('Success'), __('Quiz created successfully'), 'check') toast.success(__('Quiz created successfully'))
router.push({ router.push({
name: 'QuizForm', name: 'QuizForm',
params: { quizID: data.name }, params: { quizID: data.name },
}) })
}, },
onError(err) { onError(err) {
showToast(__('Error'), __(err.messages?.[0] || err), 'x') toast.error(err.messages?.[0] || err)
}, },
} }
) )
@@ -359,10 +360,10 @@ const updateQuiz = () => {
{ {
onSuccess(data) { onSuccess(data) {
quiz.total_marks = data.total_marks quiz.total_marks = data.total_marks
showToast(__('Success'), __('Quiz updated successfully'), 'check') toast.success(__('Quiz updated successfully'))
}, },
onError(err) { onError(err) {
showToast(__('Error'), __(err.messages?.[0] || err), 'x') toast.error(err.messages?.[0] || err)
}, },
} }
) )
@@ -428,7 +429,7 @@ const deleteQuestions = (selections, unselectAll) => {
}, },
{ {
onSuccess() { onSuccess() {
showToast(__('Success'), __('Questions deleted successfully'), 'check') toast.success(__('Questions deleted successfully'))
quizDetails.reload() quizDetails.reload()
unselectAll() unselectAll()
}, },

View File

@@ -2,10 +2,10 @@
<header <header
class="sticky top-0 z-10 flex items-center justify-between border-b bg-surface-white px-3 py-2.5 sm:px-5" class="sticky top-0 z-10 flex items-center justify-between border-b bg-surface-white px-3 py-2.5 sm:px-5"
> >
<Breadcrumbs v-if="submisisonDetails.doc" :items="breadcrumbs" /> <Breadcrumbs v-if="submissionDetails.doc" :items="breadcrumbs" />
<div class="space-x-2"> <div class="space-x-2">
<Badge <Badge
v-if="submisisonDetails.isDirty" v-if="submissionDetails.isDirty"
:label="__('Not Saved')" :label="__('Not Saved')"
variant="subtle" variant="subtle"
theme="orange" theme="orange"
@@ -15,19 +15,19 @@
</Button> </Button>
</div> </div>
</header> </header>
<div v-if="submisisonDetails.doc" class="w-1/2 mx-auto py-5 space-y-5"> <div v-if="submissionDetails.doc" class="w-2/3 border-x mx-auto py-5">
<div class="text-xl font-semibold text-ink-gray-9"> <div class="text-xl px-10 font-semibold text-ink-gray-9 mb-5">
{{ submisisonDetails.doc.member_name }} {{ submissionDetails.doc.member_name }}
</div> </div>
<div class="space-y-4 border p-5 rounded-md"> <div class="space-y-4 border-b pb-5 px-10">
<div class="grid grid-cols-2 gap-5"> <div class="grid grid-cols-2 gap-5">
<FormControl <FormControl
v-model="submisisonDetails.doc.quiz_title" v-model="submissionDetails.doc.quiz_title"
:label="__('Quiz')" :label="__('Quiz')"
:disabled="true" :disabled="true"
/> />
<FormControl <FormControl
v-model="submisisonDetails.doc.member_name" v-model="submissionDetails.doc.member_name"
:label="__('Member')" :label="__('Member')"
:disabled="true" :disabled="true"
/> />
@@ -35,39 +35,39 @@
<div class="grid grid-cols-2 gap-5"> <div class="grid grid-cols-2 gap-5">
<FormControl <FormControl
v-model="submisisonDetails.doc.score" v-model="submissionDetails.doc.score"
:label="__('Score')" :label="__('Score')"
:disabled="true" :disabled="true"
/> />
<FormControl <FormControl
v-model="submisisonDetails.doc.percentage" v-model="submissionDetails.doc.percentage"
:label="__('Percentage')" :label="__('Percentage')"
:disabled="true" :disabled="true"
/> />
</div> </div>
</div> </div>
<div <div class="divide-y">
v-for="(row, index) in submisisonDetails.doc.result" <div
class="border p-5 rounded-md space-y-4" v-for="(row, index) in submissionDetails.doc.result"
> class="py-5 px-10 space-y-4"
<div class="flex items-start space-x-1 font-semibold text-ink-gray-9"> >
<!-- <span> <div class="text-ink-gray-9">
{{ index + 1 }}. <span class="font-semibold"> {{ __('Question') }}: </span>
</span> --> <span class="leading-5" v-html="row.question"> </span>
<span class="leading-5" v-html="row.question"> </span> </div>
</div> <div class="">
<div class="leading-5 text-ink-gray-7 space-x-1"> <span class="font-semibold"> {{ __('Answer') }} </span>
<span> {{ __('Answer') }}: </span> <span class="leading-5" v-html="row.answer"></span>
<span v-html="row.answer"></span> </div>
</div> <div class="grid grid-cols-2 gap-5">
<div class="grid grid-cols-2 gap-5"> <FormControl v-model="row.marks" :label="__('Marks')" />
<FormControl v-model="row.marks" :label="__('Marks')" /> <FormControl
<FormControl v-model="row.marks_out_of"
v-model="row.marks_out_of" :label="__('Marks out of')"
:label="__('Marks out of')" :disabled="true"
:disabled="true" />
/> </div>
</div> </div>
</div> </div>
</div> </div>
@@ -80,10 +80,10 @@ import {
Button, Button,
Badge, Badge,
usePageMeta, usePageMeta,
toast,
} from 'frappe-ui' } from 'frappe-ui'
import { computed, onBeforeUnmount, onMounted, inject } from 'vue' import { computed, onBeforeUnmount, onMounted, inject } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { showToast } from '@/utils'
import { sessionStore } from '@/stores/session' import { sessionStore } from '@/stores/session'
const { brand } = sessionStore() const { brand } = sessionStore()
@@ -119,7 +119,7 @@ const props = defineProps({
}, },
}) })
const submisisonDetails = createDocumentResource({ const submissionDetails = createDocumentResource({
doctype: 'LMS Quiz Submission', doctype: 'LMS Quiz Submission',
name: props.submission, name: props.submission,
auto: true, auto: true,
@@ -132,22 +132,22 @@ const breadcrumbs = computed(() => {
route: { route: {
name: 'QuizSubmissionList', name: 'QuizSubmissionList',
params: { params: {
quizID: submisisonDetails.doc.quiz, quizID: submissionDetails.doc.quiz,
}, },
}, },
}, },
{ {
label: submisisonDetails.doc.quiz_title, label: submissionDetails.doc.quiz_title,
}, },
] ]
}) })
const saveSubmission = () => { const saveSubmission = () => {
submisisonDetails.save.submit( submissionDetails.save.submit(
{}, {},
{ {
onError(err) { onError(err) {
showToast(__('Error'), __(err.messages?.[0] || err), 'x') toast.error(err.messages?.[0] || err)
}, },
} }
) )
@@ -155,7 +155,7 @@ const saveSubmission = () => {
usePageMeta(() => { usePageMeta(() => {
return { return {
title: `${submisisonDetails.doc.quiz_title}`, title: `${submissionDetails.doc?.quiz_title}`,
icon: brand.favicon, icon: brand.favicon,
} }
}) })

View File

@@ -40,18 +40,7 @@
</Button> </Button>
</div> </div>
</div> </div>
<div <EmptyState v-else />
v-else
class="text-center p-5 text-ink-gray-5 mt-52 w-3/4 md:w-1/2 mx-auto space-y-2"
>
<BookOpen class="size-10 mx-auto stroke-1 text-ink-gray-4" />
<div class="text-xl font-medium">
{{ __('No submissions') }}
</div>
<div class="leading-5">
{{ __('No quiz submissions found. Please check again later.') }}
</div>
</div>
</template> </template>
<script setup> <script setup>
import { import {
@@ -65,10 +54,10 @@ import {
ListHeaderItem, ListHeaderItem,
usePageMeta, usePageMeta,
} from 'frappe-ui' } from 'frappe-ui'
import { BookOpen } from 'lucide-vue-next'
import { computed, onMounted, inject } from 'vue' import { computed, onMounted, inject } from 'vue'
import { sessionStore } from '../stores/session' import { sessionStore } from '../stores/session'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import EmptyState from '@/components/EmptyState.vue'
const { brand } = sessionStore() const { brand } = sessionStore()
const router = useRouter() const router = useRouter()

View File

@@ -21,6 +21,9 @@
</router-link> </router-link>
</header> </header>
<div v-if="quizzes.data?.length" class="md:w-3/4 md:mx-auto py-5 mx-5"> <div v-if="quizzes.data?.length" class="md:w-3/4 md:mx-auto py-5 mx-5">
<div v-if="quizCount" class="text-xl font-semibold text-ink-gray-7 mb-4">
{{ __('{0} Quizzes').format(quizCount) }}
</div>
<ListView <ListView
:columns="quizColumns" :columns="quizColumns"
:rows="quizzes.data" :rows="quizzes.data"
@@ -53,27 +56,13 @@
</Button> </Button>
</div> </div>
</div> </div>
<div <EmptyState v-else type="Quizzes" />
v-else
class="text-center p-5 text-ink-gray-5 mt-52 w-3/4 md:w-1/2 mx-auto space-y-2"
>
<BookOpen class="size-10 mx-auto stroke-1 text-ink-gray-4" />
<div class="text-xl font-medium">
{{ __('No quizzes found') }}
</div>
<div class="leading-5">
{{
__(
'You have not created any quizzes yet. To create a new quiz, click on the "New Quiz" button above.'
)
}}
</div>
</div>
</template> </template>
<script setup> <script setup>
import { import {
Breadcrumbs, Breadcrumbs,
Button, Button,
call,
createListResource, createListResource,
ListView, ListView,
ListRows, ListRows,
@@ -83,19 +72,22 @@ import {
usePageMeta, usePageMeta,
} from 'frappe-ui' } from 'frappe-ui'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { computed, inject, onMounted } from 'vue' import { computed, inject, onMounted, ref } from 'vue'
import { BookOpen, Plus } from 'lucide-vue-next' import { Plus } from 'lucide-vue-next'
import { sessionStore } from '@/stores/session' import { sessionStore } from '@/stores/session'
import EmptyState from '@/components/EmptyState.vue'
const { brand } = sessionStore() const { brand } = sessionStore()
const user = inject('$user') const user = inject('$user')
const router = useRouter() const router = useRouter()
const quizCount = ref(0)
const readOnlyMode = window.read_only_mode const readOnlyMode = window.read_only_mode
onMounted(() => { onMounted(() => {
if (!user.data?.is_moderator && !user.data?.is_instructor) { if (!user.data?.is_moderator && !user.data?.is_instructor) {
router.push({ name: 'Courses' }) router.push({ name: 'Courses' })
} }
getQuizCount()
}) })
const quizFilter = computed(() => { const quizFilter = computed(() => {
@@ -114,6 +106,14 @@ const quizzes = createListResource({
orderBy: 'modified desc', orderBy: 'modified desc',
}) })
const getQuizCount = () => {
call('frappe.client.get_count', {
doctype: 'LMS Quiz',
}).then((data) => {
quizCount.value = data
})
}
const quizColumns = computed(() => { const quizColumns = computed(() => {
return [ return [
{ {

View File

@@ -1,9 +1,10 @@
import { toast } from 'frappe-ui'
import { useTimeAgo } from '@vueuse/core' import { useTimeAgo } from '@vueuse/core'
import { Quiz } from '@/utils/quiz' import { Quiz } from '@/utils/quiz'
import { Assignment } from '@/utils/assignment' import { Assignment } from '@/utils/assignment'
import { Upload } from '@/utils/upload' import { Upload } from '@/utils/upload'
import { Markdown } from '@/utils/markdownParser' import { Markdown } from '@/utils/markdownParser'
import { useSettings } from '@/stores/settings'
import { usersStore } from '@/stores/user'
import Header from '@editorjs/header' import Header from '@editorjs/header'
import Paragraph from '@editorjs/paragraph' import Paragraph from '@editorjs/paragraph'
import { CodeBox } from '@/utils/code' import { CodeBox } from '@/utils/code'
@@ -14,19 +15,11 @@ import dayjs from '@/utils/dayjs'
import Embed from '@editorjs/embed' import Embed from '@editorjs/embed'
import SimpleImage from '@editorjs/simple-image' import SimpleImage from '@editorjs/simple-image'
import Table from '@editorjs/table' import Table from '@editorjs/table'
import { usersStore } from '../stores/user'
import Plyr from 'plyr' import Plyr from 'plyr'
import 'plyr/dist/plyr.css' import 'plyr/dist/plyr.css'
const readOnlyMode = window.read_only_mode const readOnlyMode = window.read_only_mode
export function createToast(options) {
toast({
position: 'bottom-right',
...options,
})
}
export function timeAgo(date) { export function timeAgo(date) {
return useTimeAgo(date).value return useTimeAgo(date).value
} }
@@ -97,26 +90,6 @@ export function getFileSize(file_size) {
return value return value
} }
export function showToast(title, text, icon, iconClasses = null) {
if (!iconClasses) {
if (icon == 'check') {
iconClasses = 'bg-surface-green-3 text-ink-white rounded-md p-px'
} else if (icon == 'alert-circle') {
iconClasses = 'bg-yellow-600 text-ink-white rounded-md p-px'
} else {
iconClasses = 'bg-surface-red-5 text-ink-white rounded-md p-px'
}
}
createToast({
title: title,
text: htmlToText(text),
icon: icon,
iconClasses: iconClasses,
position: icon == 'check' ? 'bottom-right' : 'top-center',
timeout: icon != 'check' ? 10 : 5,
})
}
export function getImgDimensions(imgSrc) { export function getImgDimensions(imgSrc) {
return new Promise((resolve) => { return new Promise((resolve) => {
let img = new Image() let img = new Image()
@@ -558,24 +531,33 @@ export const enablePlyr = () => {
const videoElement = document.getElementsByClassName('video-player') const videoElement = document.getElementsByClassName('video-player')
if (videoElement.length === 0) return if (videoElement.length === 0) return
const src = videoElement[0].getAttribute('src') Array.from(videoElement).forEach((video) => {
if (src) { const src = video.getAttribute('src')
let videoID = src.split('/').pop() if (src) {
videoElement[0].setAttribute('data-plyr-embed-id', videoID) let videoID = src.split('/').pop()
} video.setAttribute('data-plyr-embed-id', videoID)
new Plyr('.video-player', { }
youtube: { new Plyr(video, {
noCookie: true, youtube: {
}, noCookie: true,
controls: [ },
'play-large', controls: [
'play', 'play-large',
'progress', 'play',
'current-time', 'progress',
'mute', 'current-time',
'volume', 'mute',
'fullscreen', 'volume',
], 'fullscreen',
}) ],
}, 500) })
}, 500)
})
}
export const openSettings = (category, close) => {
const settingsStore = useSettings()
close()
settingsStore.activeTab = category
settingsStore.isSettingsOpen = true
} }

View File

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

View File

@@ -1 +1 @@
__version__ = "2.28.0" __version__ = "2.28.1"

View File

@@ -21,7 +21,7 @@ app_license = "AGPL"
# include js, css files in header of web template # include js, css files in header of web template
web_include_css = "lms.bundle.css" web_include_css = "lms.bundle.css"
# web_include_css = "/assets/lms/css/lms.css" # web_include_css = "/assets/lms/css/lms.css"
web_include_js = ["website.bundle.js"] web_include_js = []
# include custom scss in every website theme (without file extension ".scss") # include custom scss in every website theme (without file extension ".scss")
# website_theme_scss = "lms/public/scss/website" # website_theme_scss = "lms/public/scss/website"

View File

@@ -21,9 +21,9 @@ from lms.lms.utils import (
class LMSBatch(Document): class LMSBatch(Document):
def validate(self): def validate(self):
if self.seat_count: self.validate_seats_left()
self.validate_seats_left()
self.validate_batch_end_date() self.validate_batch_end_date()
self.validate_batch_time()
self.validate_duplicate_courses() self.validate_duplicate_courses()
self.validate_payments_app() self.validate_payments_app()
self.validate_amount_and_currency() self.validate_amount_and_currency()
@@ -40,6 +40,11 @@ class LMSBatch(Document):
if self.end_date < self.start_date: if self.end_date < self.start_date:
frappe.throw(_("Batch end date cannot be before the batch start date")) frappe.throw(_("Batch end date cannot be before the batch start date"))
def validate_batch_time(self):
if self.start_time and self.end_time:
if get_time(self.start_time) >= get_time(self.end_time):
frappe.throw(_("Batch start time cannot be greater than or equal to end time."))
def validate_duplicate_courses(self): def validate_duplicate_courses(self):
courses = [row.course for row in self.courses] courses = [row.course for row in self.courses]
duplicates = {course for course in courses if courses.count(course) > 1} duplicates = {course for course in courses if courses.count(course) > 1}
@@ -94,6 +99,9 @@ class LMSBatch(Document):
enrollment.save() enrollment.save()
def validate_seats_left(self): def validate_seats_left(self):
if cint(self.seat_count) < 0:
frappe.throw(_("Seat count cannot be negative."))
students = frappe.db.count("LMS Batch Enrollment", {"batch": self.name}) students = frappe.db.count("LMS Batch Enrollment", {"batch": self.name})
if cint(self.seat_count) < students: if cint(self.seat_count) < students:
frappe.throw(_("There are no seats available in this batch.")) frappe.throw(_("There are no seats available in this batch."))
@@ -208,86 +216,6 @@ def authenticate():
return response.json()["access_token"] return response.json()["access_token"]
@frappe.whitelist()
def create_batch(
title,
start_date,
end_date,
description=None,
batch_details=None,
batch_details_raw=None,
meta_image=None,
seat_count=0,
start_time=None,
end_time=None,
medium="Online",
category=None,
paid_batch=0,
amount=0,
currency=None,
amount_usd=0,
name=None,
published=0,
evaluation_end_date=None,
):
frappe.only_for("Moderator")
if name:
doc = frappe.get_doc("LMS Batch", name)
else:
doc = frappe.get_doc({"doctype": "LMS Batch"})
doc.update(
{
"title": title,
"start_date": start_date,
"end_date": end_date,
"description": description,
"batch_details": batch_details,
"batch_details_raw": batch_details_raw,
"meta_image": meta_image,
"seat_count": seat_count,
"start_time": start_time,
"end_time": end_time,
"medium": medium,
"category": category,
"paid_batch": paid_batch,
"amount": amount,
"currency": currency,
"amount_usd": amount_usd,
"published": published,
"evaluation_end_date": evaluation_end_date,
}
)
doc.save()
return doc
@frappe.whitelist()
def add_course(course, parent, name=None, evaluator=None):
frappe.only_for("Moderator")
if frappe.db.exists("Batch Course", {"course": course, "parent": parent}):
frappe.throw(_("Course already added to the batch."))
if name:
doc = frappe.get_doc("Batch Course", name)
else:
doc = frappe.new_doc("Batch Course")
doc.update(
{
"course": course,
"evaluator": evaluator,
"parent": parent,
"parentfield": "courses",
"parenttype": "LMS Batch",
}
)
doc.save()
return doc.name
@frappe.whitelist() @frappe.whitelist()
def get_batch_timetable(batch): def get_batch_timetable(batch):
timetable = frappe.get_all( timetable = frappe.get_all(

View File

@@ -96,9 +96,7 @@ def set_total_marks(questions):
@frappe.whitelist() @frappe.whitelist()
def quiz_summary(quiz, results): def quiz_summary(quiz, results):
score = 0
results = results and json.loads(results) results = results and json.loads(results)
is_open_ended = False
percentage = 0 percentage = 0
quiz_details = frappe.db.get_value( quiz_details = frappe.db.get_value(
@@ -108,7 +106,32 @@ def quiz_summary(quiz, results):
as_dict=1, as_dict=1,
) )
data = process_results(results, quiz)
results = data["results"]
score = data["score"]
is_open_ended = data["is_open_ended"]
score_out_of = quiz_details.total_marks score_out_of = quiz_details.total_marks
percentage = (score / score_out_of) * 100 if score_out_of else 0
submission = create_submission(
quiz, results, score_out_of, quiz_details.passing_percentage
)
save_progress_after_quiz(quiz_details, percentage)
return {
"score": score,
"score_out_of": score_out_of,
"submission": submission.name,
"pass": percentage == quiz_details.passing_percentage,
"percentage": percentage,
"is_open_ended": is_open_ended,
}
def process_results(results, quiz):
score = 0
is_open_ended = False
for result in results: for result in results:
question_details = frappe.db.get_value( question_details = frappe.db.get_value(
@@ -123,55 +146,28 @@ def quiz_summary(quiz, results):
result["marks_out_of"] = question_details.marks result["marks_out_of"] = question_details.marks
if question_details.type != "Open Ended": if question_details.type != "Open Ended":
correct = result["is_correct"][0] if len(result["is_correct"]) > 0:
for point in result["is_correct"]: correct = result["is_correct"][0]
correct = correct and point for point in result["is_correct"]:
result["is_correct"] = correct correct = correct and point
result["is_correct"] = correct
else:
result["is_correct"] = 0
marks = question_details.marks if correct else 0 marks = question_details.marks if correct else 0
result["marks"] = marks result["marks"] = marks
score += marks score += marks
else: else:
result["is_correct"] = 0
is_open_ended = True is_open_ended = True
result["is_correct"] = 0
percentage = (score / score_out_of) * 100 result["answer"] = re.sub(
result["answer"] = re.sub( r'<img[^>]*src\s*=\s*["\'](?=data:)(.*?)["\']', _save_file, result["answer"]
r'<img[^>]*src\s*=\s*["\'](?=data:)(.*?)["\']', _save_file, result["answer"] )
)
submission = frappe.new_doc("LMS Quiz Submission")
# Score and percentage are calculated by the controller function
submission.update(
{
"doctype": "LMS Quiz Submission",
"quiz": quiz,
"result": results,
"score": 0,
"score_out_of": score_out_of,
"member": frappe.session.user,
"percentage": 0,
"passing_percentage": quiz_details.passing_percentage,
}
)
submission.save(ignore_permissions=True)
if (
percentage >= quiz_details.passing_percentage
and quiz_details.lesson
and quiz_details.course
):
save_progress(quiz_details.lesson, quiz_details.course)
elif not quiz_details.passing_percentage:
save_progress(quiz_details.lesson, quiz_details.course)
return { return {
"results": results,
"score": score, "score": score,
"score_out_of": score_out_of,
"submission": submission.name,
"pass": percentage == quiz_details.passing_percentage,
"percentage": percentage,
"is_open_ended": is_open_ended, "is_open_ended": is_open_ended,
} }
@@ -219,6 +215,36 @@ def get_corrupted_image_msg():
return _("Image: Corrupted Data Stream") return _("Image: Corrupted Data Stream")
def create_submission(quiz, results, score_out_of, passing_percentage):
submission = frappe.new_doc("LMS Quiz Submission")
# Score and percentage are calculated by the controller function
submission.update(
{
"doctype": "LMS Quiz Submission",
"quiz": quiz,
"result": results,
"score": 0,
"score_out_of": score_out_of,
"member": frappe.session.user,
"percentage": 0,
"passing_percentage": passing_percentage,
}
)
submission.save(ignore_permissions=True)
return submission
def save_progress_after_quiz(quiz_details, percentage):
if (
percentage >= quiz_details.passing_percentage
and quiz_details.lesson
and quiz_details.course
):
save_progress(quiz_details.lesson, quiz_details.course)
elif not quiz_details.passing_percentage:
save_progress(quiz_details.lesson, quiz_details.course)
@frappe.whitelist() @frappe.whitelist()
def get_question_details(question): def get_question_details(question):
if frappe.db.exists("LMS Quiz Question", question): if frappe.db.exists("LMS Quiz Question", question):

View File

@@ -403,7 +403,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2025-04-22 16:05:27.914422", "modified": "2025-05-14 12:43:22.749850",
"modified_by": "sayali@frappe.io", "modified_by": "sayali@frappe.io",
"module": "LMS", "module": "LMS",
"name": "LMS Settings", "name": "LMS Settings",
@@ -425,6 +425,16 @@
"read": 1, "read": 1,
"role": "LMS Student", "role": "LMS Student",
"share": 1 "share": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"role": "Moderator",
"share": 1,
"write": 1
} }
], ],
"row_format": "Dynamic", "row_format": "Dynamic",

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,450 +0,0 @@
frappe.ready(() => {
setup_file_size();
pin_header();
$(".enroll-in-course").click((e) => {
enroll_in_course(e);
});
$(".notify-me").click((e) => {
notify_user(e);
});
$(".nav-link").click((e) => {
change_hash(e);
});
if (window.location.hash) {
open_tab();
}
if (window.location.pathname == "/statistics") {
generate_graph("New Signups", "#new-signups");
generate_graph("Course Enrollments", "#course-enrollments");
generate_graph("Lesson Completion", "#lesson-completion");
generate_course_completion_graph();
}
expand_the_active_chapter();
$(".chapter-title")
.unbind()
.click((e) => {
rotate_chapter_icon(e);
});
$(".no-preview").click((e) => {
show_no_preview_dialog(e);
});
$("#create-batch").click((e) => {
open_batch_dialog(e);
});
$("#course-filter").change((e) => {
filter_courses(e);
});
});
const pin_header = () => {
const el = document.querySelector(".sticky");
if (el) {
const observer = new IntersectionObserver(
([e]) =>
e.target.classList.toggle("is-pinned", e.intersectionRatio < 1),
{ threshold: [1] }
);
observer.observe(el);
}
};
const setup_file_size = () => {
frappe.provide("frappe.form.formatters");
frappe.form.formatters.FileSize = file_size;
};
const file_size = (value) => {
if (value > 1048576) {
value = flt(flt(value) / 1048576, 1) + "M";
} else if (value > 1024) {
value = flt(flt(value) / 1024, 1) + "K";
}
return value;
};
const enroll_in_course = (e) => {
e.preventDefault();
let course = $(e.currentTarget).attr("data-course");
if (frappe.session.user == "Guest") {
window.location.href = `/login?redirect-to=/courses/${course}`;
return;
}
let batch = $(e.currentTarget).attr("data-batch");
batch = batch ? decodeURIComponent(batch) : "";
frappe.call({
method: "lms.lms.doctype.lms_enrollment.lms_enrollment.create_membership",
args: {
batch: batch ? batch : "",
course: course,
},
callback: (data) => {
if (data.message == "OK") {
$(".no-preview-modal").modal("hide");
frappe.show_alert(
{
message: __("Enrolled successfully"),
indicator: "green",
},
3
);
setTimeout(function () {
window.location.href = `/courses/${course}/learn/1.1`;
}, 1000);
}
},
});
};
const notify_user = (e) => {
e.preventDefault();
var course = decodeURIComponent($("#outline-heading").attr("data-course"));
if (frappe.session.user == "Guest") {
window.location.href = `/login?redirect-to=/courses/${course}`;
return;
}
frappe.call({
method: "lms.lms.doctype.lms_course_interest.lms_course_interest.capture_interest",
args: {
course: course,
},
callback: (data) => {
$(".no-preview-modal").modal("hide");
frappe.show_alert(
{
message: __(
"You have opted to be notified for this course. You will receive an email when the course becomes available."
),
indicator: "green",
},
3
);
setTimeout(() => {
window.location.reload();
}, 3000);
},
});
};
const generate_graph = (chart_name, element, type = "line") => {
let date = frappe.datetime;
frappe.call({
method: "lms.lms.utils.get_chart_data",
args: {
chart_name: chart_name,
timespan: "Select Date Range",
timegrain: "Daily",
from_date: date.add_days(date.get_today(), -30),
to_date: date.add_days(date.get_today(), +1),
},
callback: (data) => {
render_chart(data.message, chart_name, element, type);
},
});
};
const render_chart = (data, chart_name, element, type) => {
const chart = new frappe.Chart(element, {
title: chart_name,
data: data,
type: type,
height: 250,
colors: ["#4563f1"],
axisOptions: {
xIsSeries: 1,
},
lineOptions: {
regionFill: 1,
},
});
};
const generate_course_completion_graph = () => {
frappe.call({
method: "lms.lms.utils.get_course_completion_data",
callback: (data) => {
render_chart(
data.message,
"Course Completion",
"#course-completion",
"pie"
);
},
});
};
const change_hash = (e) => {
window.location.hash = $(e.currentTarget).attr("href");
};
const open_tab = () => {
$(`a[href="${window.location.hash}"]`).click();
};
const expand_the_first_chapter = () => {
let elements = $(".course-home-outline .collapse");
elements.each((i, element) => {
if (i < 1) {
show_section(element);
return false;
}
});
};
const expand_the_active_chapter = () => {
let selector = $(".course-home-headings.title");
if (selector.length && $(".course-details-page").length) {
expand_for_course_details(selector);
} else if ($(".active-lesson").length) {
/* For course home page */
selector = $(".active-lesson");
show_section(selector.parent().parent());
} else {
/* If no active chapter then exapand the first chapter */
expand_the_first_chapter();
}
};
const expand_for_course_details = (selector) => {
$(".lesson-info").removeClass("active-lesson");
$(".lesson-info").each((i, elem) => {
if ($(elem).data("lesson") == selector.data("lesson")) {
$(elem).addClass("active-lesson");
show_section($(elem).parent().parent());
}
});
};
const show_section = (element) => {
$(element).addClass("show");
$(element)
.siblings(".chapter-title")
.children(".chapter-icon")
.css("transform", "rotate(90deg)");
$(element).siblings(".chapter-title").attr("aria-expanded", true);
};
const rotate_chapter_icon = (e) => {
let icon = $(e.currentTarget).children(".chapter-icon");
if (icon.css("transform") == "none") {
icon.css("transform", "rotate(90deg)");
} else {
icon.css("transform", "none");
}
};
const show_no_preview_dialog = (e) => {
$("#no-preview-modal").modal("show");
};
const open_batch_dialog = () => {
this.batch_dialog = new frappe.ui.Dialog({
title: __("New Batch"),
fields: [
{
fieldtype: "Data",
label: __("Title"),
fieldname: "title",
reqd: 1,
default: batch_info && batch_info.title,
},
{
fieldtype: "Check",
label: __("Published"),
fieldname: "published",
default: batch_info && batch_info.published,
},
{
fieldtype: "Section Break",
},
{
fieldtype: "Date",
label: __("Start Date"),
fieldname: "start_date",
reqd: 1,
default: batch_info && batch_info.start_date,
},
{
fieldtype: "Date",
label: __("End Date"),
fieldname: "end_date",
reqd: 1,
default: batch_info && batch_info.end_date,
},
{
fieldtype: "Column Break",
},
{
fieldtype: "Time",
label: __("Start Time"),
fieldname: "start_time",
default: batch_info && batch_info.start_time,
reqd: 1,
},
{
fieldtype: "Time",
label: __("End Time"),
fieldname: "end_time",
default: batch_info && batch_info.end_time,
reqd: 1,
},
{
fieldtype: "Section Break",
},
{
fieldtype: "Select",
label: __("Medium"),
fieldname: "medium",
options: ["Online", "Offline"],
default: (batch_info && batch_info.medium) || "Online",
},
{
fieldtype: "Link",
label: __("Category"),
fieldname: "category",
options: "LMS Category",
only_select: 1,
default: batch_info && batch_info.category,
},
{
fieldtype: "Column Break",
},
{
fieldtype: "Int",
label: __("Seat Count"),
fieldname: "seat_count",
default: batch_info && batch_info.seat_count,
},
{
fieldtype: "Date",
label: __("Evaluation End Date"),
fieldname: "evaluation_end_date",
default: batch_info && batch_info.evaluation_end_date,
},
{
fieldtype: "Section Break",
},
{
fieldtype: "Small Text",
label: __("Description"),
fieldname: "description",
default: batch_info && batch_info.description,
reqd: 1,
},
{
fieldtype: "Text Editor",
label: __("Batch Details"),
fieldname: "batch_details",
default: batch_info && batch_info.batch_details,
reqd: 1,
},
{
fieldtype: "HTML Editor",
label: __("Batch Details Raw"),
fieldname: "batch_details_raw",
default: batch_info && batch_info.batch_details_raw,
},
{
fieldtype: "Attach Image",
label: __("Meta Image"),
fieldname: "meta_image",
default: batch_info && batch_info.meta_image,
},
{
fieldtype: "Section Break",
label: __("Pricing"),
fieldname: "pricing",
},
{
fieldtype: "Check",
label: __("Paid Batch"),
fieldname: "paid_batch",
default: batch_info && batch_info.paid_batch,
},
{
fieldtype: "Currency",
label: __("Amount"),
fieldname: "amount",
default: batch_info && batch_info.amount,
mandatory_depends_on: "paid_batch",
depends_on: "paid_batch",
},
{
fieldtype: "Link",
label: __("Currency"),
fieldname: "currency",
options: "Currency",
default: batch_info && batch_info.currency,
mandatory_depends_on: "paid_batch",
depends_on: "paid_batch",
only_select: 1,
},
{
fieldtype: "Currency",
label: __("Amount (USD)"),
fieldname: "amount_usd",
depends_on: "paid_batch",
description: __(
"If you set an amount here, then the USD equivalent setting will not get applied."
),
},
],
primary_action_label: __("Save"),
primary_action: (values) => {
save_batch(values);
},
});
this.batch_dialog.show();
};
const save_batch = (values) => {
let args = {};
if (batch_info) {
args = Object.assign(batch_info, values);
} else {
args = values;
}
frappe.call({
method: "lms.lms.doctype.lms_batch.lms_batch.create_batch",
args: args,
callback: (r) => {
if (r.message) {
frappe.show_alert({
message: batch_info
? __("Batch Updated")
: __("Batch Created"),
indicator: "green",
});
this.batch_dialog.hide();
window.location.href = `/batches/details/${r.message.name}`;
}
},
});
};
const filter_courses = (e) => {
const course_lists = $(".course-cards-parent");
const filter = $(e.currentTarget).val();
course_lists.each((i, list) => {
const course_cards = $(list).children(".course-card");
course_cards.sort((a, b) => {
var value1 = $(a).data(filter);
var value2 = $(b).data(filter);
return value1 > value2 ? -1 : value1 < value2 ? 1 : 0;
});
$(list).append(course_cards);
});
};

View File

@@ -1,14 +0,0 @@
frappe.ready(() => {
hide_profile_and_dashboard_for_guest_users();
});
const hide_profile_and_dashboard_for_guest_users = () => {
if (frappe.session.user == "Guest") {
let links = $(".nav-link").filter(
(i, elem) =>
$(elem).text().trim() === "My Profile" ||
$(elem).text().trim() === "Dashboard"
);
links.length && links.each((i, elem) => $(elem).addClass("hide"));
}
};

View File

@@ -1,5 +0,0 @@
import "./profile.js";
import "./common_functions.js";
import "../../../../frappe/frappe/public/js/frappe/ui/chart.js";
import "../../../../frappe/frappe/public/js/frappe/ui/keyboard.js";
import "../../../../frappe/frappe/public/js/frappe/event_emitter.js";

View File

@@ -11,9 +11,11 @@
<b>{{ _("Batch Start Date:") }}</b> {{ frappe.utils.format_date(start_date, "medium") }} <b>{{ _("Batch Start Date:") }}</b> {{ frappe.utils.format_date(start_date, "medium") }}
</p> </p>
{% if medium %}
<p> <p>
<b>{{ _("Medium:") }}</b> {{ medium }} <b>{{ _("Medium:") }}</b> {{ medium }}
</p> </p>
{% endif %}
<p> <p>
<b>{{ _("Timings:") }}</b> {{ frappe.utils.format_time(start_time, "hh:mm a") }} <b>{{ _("Timings:") }}</b> {{ frappe.utils.format_time(start_time, "hh:mm a") }}

View File

@@ -1,31 +0,0 @@
{% extends "templates/base.html" %}
{% block content %}
{% include "public/icons/symbol-defs.svg" %}
{% include "lms/templates/onboarding_header.html" %}
{% block page_content %}
Hello, world!
{% endblock %}
{% endblock %}
{%- block script -%}
{{ super() }}
{% if frappe.get_system_settings("enable_telemetry") %}
{% set telemetry_boot_info = get_telemetry_boot_info() %}
<script>
const telemetry_boot_info = {{ get_telemetry_boot_info() }}
if (telemetry_boot_info && Object.keys(telemetry_boot_info).length)
Object.assign(frappe.boot, telemetry_boot_info)
</script>
{% endif %}
<script>
frappe.router = {
slug(name) {
return name.toLowerCase().replace(/ /g, "-");
},
};
frappe.utils.make_event_emitter(frappe.router)
</script>
{{ include_script("telemetry.bundle.js") }}
{%- endblock -%}

158
yarn.lock
View File

@@ -129,9 +129,9 @@
"@lezer/highlight" "^1.0.0" "@lezer/highlight" "^1.0.0"
"@codemirror/view@^6.0.0", "@codemirror/view@^6.22.0", "@codemirror/view@^6.23.0", "@codemirror/view@^6.27.0", "@codemirror/view@^6.35.0": "@codemirror/view@^6.0.0", "@codemirror/view@^6.22.0", "@codemirror/view@^6.23.0", "@codemirror/view@^6.27.0", "@codemirror/view@^6.35.0":
version "6.36.7" version "6.36.8"
resolved "https://registry.yarnpkg.com/@codemirror/view/-/view-6.36.7.tgz#f3711d3fea457a3eec1c09a76b8a132f98df17b1" resolved "https://registry.yarnpkg.com/@codemirror/view/-/view-6.36.8.tgz#d72e59d2b2d99bb5f177178f63e11cef6e605b94"
integrity sha512-kCWGW/chWGPgZqfZ36Um9Iz0X2IVpmCjg1P/qY6B6a2ecXtWRRAigmpJ6YgUQ5lTWXMyyVdfmpzhLZmsZQMbtg== integrity sha512-yoRo4f+FdnD01fFt4XpfpMCcCAo9QvZOtbrXExn4SqzH32YC6LgzqxfLZw/r6Ge65xyY03mK/UfUqrVw1gFiFg==
dependencies: dependencies:
"@codemirror/state" "^6.5.0" "@codemirror/state" "^6.5.0"
style-mod "^4.1.0" style-mod "^4.1.0"
@@ -496,17 +496,17 @@
mlly "^1.7.4" mlly "^1.7.4"
"@inquirer/confirm@^5.0.0": "@inquirer/confirm@^5.0.0":
version "5.1.9" version "5.1.10"
resolved "https://registry.yarnpkg.com/@inquirer/confirm/-/confirm-5.1.9.tgz#c858b6a3decb458241ec36ca9a9117477338076a" resolved "https://registry.yarnpkg.com/@inquirer/confirm/-/confirm-5.1.10.tgz#de3732cb7ae9333bd3e354afee6a6ef8cf28d951"
integrity sha512-NgQCnHqFTjF7Ys2fsqK2WtnA8X1kHyInyG+nMIuHowVTIgIuS10T4AznI/PvbqSpJqjCUqNBlKGh1v3bwLFL4w== integrity sha512-FxbQ9giWxUWKUk2O5XZ6PduVnH2CZ/fmMKMBkH71MHJvWr7WL5AHKevhzF1L5uYWB2P548o1RzVxrNd3dpmk6g==
dependencies: dependencies:
"@inquirer/core" "^10.1.10" "@inquirer/core" "^10.1.11"
"@inquirer/type" "^3.0.6" "@inquirer/type" "^3.0.6"
"@inquirer/core@^10.1.10": "@inquirer/core@^10.1.11":
version "10.1.10" version "10.1.11"
resolved "https://registry.yarnpkg.com/@inquirer/core/-/core-10.1.10.tgz#222a374e3768536a1eb0adf7516c436d5f4a291d" resolved "https://registry.yarnpkg.com/@inquirer/core/-/core-10.1.11.tgz#4022032b5b6b35970e1c3fcfc522bc250ef8810d"
integrity sha512-roDaKeY1PYY0aCqhRmXihrHjoSW2A00pV3Ke5fTpMCkzcGF64R8e0lw3dK+eLEHwS4vB5RnW1wuQmvzoRul8Mw== integrity sha512-BXwI/MCqdtAhzNQlBEFE7CEflhPkl/BqvAuV/aK6lW3DClIfYVDWPP/kXuXHtBWC7/EEbNqd/1BGq2BGBBnuxw==
dependencies: dependencies:
"@inquirer/figures" "^1.0.11" "@inquirer/figures" "^1.0.11"
"@inquirer/type" "^3.0.6" "@inquirer/type" "^3.0.6"
@@ -1170,9 +1170,9 @@
integrity sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg== integrity sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==
"@types/node@*": "@types/node@*":
version "22.15.16" version "22.15.17"
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.15.16.tgz#685cf0338ad9f5b14860f50a6ac2c3ebd58582cd" resolved "https://registry.yarnpkg.com/@types/node/-/node-22.15.17.tgz#355ccec95f705b664e4332bb64a7f07db30b7055"
integrity sha512-3pr+KjwpVujqWqOKT8mNR+rd09FqhBLwg+5L/4t0cNYBzm/yEiYGCxWttjaPBsLtAo+WFNoXzGJfolM1JuRXoA== integrity sha512-wIX2aSZL5FE+MR0JlvF87BNVrtFWf6AE6rxSE9X7OwnVvoyCQjpzSRJ+M87se/4QCkCiebQAqrJ0y6fwIyi7nw==
dependencies: dependencies:
undici-types "~6.21.0" undici-types "~6.21.0"
@@ -1224,9 +1224,9 @@
integrity sha512-kqf7SGFoG+80aZG6Pf+gsZIVvGSCKE98JbiWqcCV9cThtg91Jav0yvYFC9Zb+jKetNGF6ZKeoaxgZfND21fWKw== integrity sha512-kqf7SGFoG+80aZG6Pf+gsZIVvGSCKE98JbiWqcCV9cThtg91Jav0yvYFC9Zb+jKetNGF6ZKeoaxgZfND21fWKw==
"@vitejs/plugin-vue@^5.0.3": "@vitejs/plugin-vue@^5.0.3":
version "5.2.3" version "5.2.4"
resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-5.2.3.tgz#71a8fc82d4d2e425af304c35bf389506f674d89b" resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz#9e8a512eb174bfc2a333ba959bbf9de428d89ad8"
integrity sha512-IYSLEQj4LgZZuoVpdSUCw3dIynTWQgPlaRP6iAvMle4My0HdYwr5g5wQAfwOeHQBmYwEkqF70nRpSilr6PoUDg== integrity sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==
"@vitest/expect@2.1.9": "@vitest/expect@2.1.9":
version "2.1.9" version "2.1.9"
@@ -1756,9 +1756,9 @@ camelcase-css@^2.0.1:
integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA== integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==
caniuse-lite@^1.0.30001702, caniuse-lite@^1.0.30001716: caniuse-lite@^1.0.30001702, caniuse-lite@^1.0.30001716:
version "1.0.30001717" version "1.0.30001718"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001717.tgz#5d9fec5ce09796a1893013825510678928aca129" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001718.tgz#dae13a9c80d517c30c6197515a96131c194d8f82"
integrity sha512-auPpttCq6BDEG8ZAuHJIplGw6GODhjw+/11e7IjpnYCxZcW/ONgPs0KVBJ0d1bY3e2+7PRe5RCLyP+PfwVgkYw== integrity sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==
capital-case@^1.0.4: capital-case@^1.0.4:
version "1.0.4" version "1.0.4"
@@ -2061,7 +2061,7 @@ cross-spawn@^5.0.1:
shebang-command "^1.2.0" shebang-command "^1.2.0"
which "^1.2.9" which "^1.2.9"
cross-spawn@^7.0.0, cross-spawn@^7.0.3, cross-spawn@^7.0.6: cross-spawn@^7.0.0, cross-spawn@^7.0.6:
version "7.0.6" version "7.0.6"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f"
integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==
@@ -2322,9 +2322,9 @@ ee-first@1.1.1:
integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==
electron-to-chromium@^1.5.149: electron-to-chromium@^1.5.149:
version "1.5.151" version "1.5.152"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.151.tgz#5edd6c17e1b2f14b4662c41b9379f96cc8c2bb7c" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.152.tgz#bcdd39567e291b930ec26b930031137a05593695"
integrity sha512-Rl6uugut2l9sLojjS4H4SAr3A4IgACMLgpuEMPYCVcKydzfyPrn5absNRju38IhQOf/NwjJY8OGWjlteqYeBCA== integrity sha512-xBOfg/EBaIlVsHipHl2VdTPJRSvErNUaqW8ejTq5OlOlIYx1wOllCHsAvAIrr55jD1IYEfdR86miUEt8H5IeJg==
emoji-regex@^10.3.0: emoji-regex@^10.3.0:
version "10.4.0" version "10.4.0"
@@ -2541,21 +2541,6 @@ execa@4.1.0:
signal-exit "^3.0.2" signal-exit "^3.0.2"
strip-final-newline "^2.0.0" strip-final-newline "^2.0.0"
execa@^8.0.1:
version "8.0.1"
resolved "https://registry.yarnpkg.com/execa/-/execa-8.0.1.tgz#51f6a5943b580f963c3ca9c6321796db8cc39b8c"
integrity sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==
dependencies:
cross-spawn "^7.0.3"
get-stream "^8.0.1"
human-signals "^5.0.0"
is-stream "^3.0.0"
merge-stream "^2.0.0"
npm-run-path "^5.1.0"
onetime "^6.0.0"
signal-exit "^4.1.0"
strip-final-newline "^3.0.0"
executable@^4.1.1: executable@^4.1.1:
version "4.1.1" version "4.1.1"
resolved "https://registry.yarnpkg.com/executable/-/executable-4.1.1.tgz#41532bff361d3e57af4d763b70582db18f5d133c" resolved "https://registry.yarnpkg.com/executable/-/executable-4.1.1.tgz#41532bff361d3e57af4d763b70582db18f5d133c"
@@ -2677,9 +2662,9 @@ finalhandler@1.1.2:
unpipe "~1.0.0" unpipe "~1.0.0"
flexsearch@*: flexsearch@*:
version "0.8.161" version "0.8.164"
resolved "https://registry.yarnpkg.com/flexsearch/-/flexsearch-0.8.161.tgz#fe964fe1841a99b580ba19f853603d1747fb3bc8" resolved "https://registry.yarnpkg.com/flexsearch/-/flexsearch-0.8.164.tgz#2d1277249d6dec8eb745358fa64543ddbddc9e05"
integrity sha512-3dlwyJpl7PN4vRTp0pV8FkCULPzeuK6JzZYE3KhLiv5vbhUPGUqiOWgBbjlpCC4Rl1aW7B0lJeOS0Cr8G0N12g== integrity sha512-tauQG+NlwSWW6uL3BIJdHNEXYiei2xTR3H/mrjNadAOwvXgiLbLvLzQWSJvD31Yn0+1lAxG2NVRndywGnDZZiA==
flexsearch@0.7.21: flexsearch@0.7.21:
version "0.7.21" version "0.7.21"
@@ -2835,11 +2820,6 @@ get-stream@^5.0.0, get-stream@^5.1.0:
dependencies: dependencies:
pump "^3.0.0" pump "^3.0.0"
get-stream@^8.0.1:
version "8.0.1"
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-8.0.1.tgz#def9dfd71742cd7754a7761ed43749a27d02eca2"
integrity sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==
getos@^3.2.1: getos@^3.2.1:
version "3.2.1" version "3.2.1"
resolved "https://registry.yarnpkg.com/getos/-/getos-3.2.1.tgz#0134d1f4e00eb46144c5a9c0ac4dc087cbb27dc5" resolved "https://registry.yarnpkg.com/getos/-/getos-3.2.1.tgz#0134d1f4e00eb46144c5a9c0ac4dc087cbb27dc5"
@@ -3045,11 +3025,6 @@ human-signals@^1.1.1:
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3"
integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw== integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==
human-signals@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-5.0.0.tgz#42665a284f9ae0dade3ba41ebc37eb4b852f3a28"
integrity sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==
iconv-lite@0.6.3: iconv-lite@0.6.3:
version "0.6.3" version "0.6.3"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501"
@@ -3058,9 +3033,9 @@ iconv-lite@0.6.3:
safer-buffer ">= 2.1.2 < 3.0.0" safer-buffer ">= 2.1.2 < 3.0.0"
idb-keyval@^6.2.0: idb-keyval@^6.2.0:
version "6.2.1" version "6.2.2"
resolved "https://registry.yarnpkg.com/idb-keyval/-/idb-keyval-6.2.1.tgz#94516d625346d16f56f3b33855da11bfded2db33" resolved "https://registry.yarnpkg.com/idb-keyval/-/idb-keyval-6.2.2.tgz#b0171b5f73944854a3291a5cdba8e12768c4854a"
integrity sha512-8Sb3veuYCyrZL+VBt9LJfZjLUPWVvqn8tG28VqYNFCo43KHcKuq+b4EiXGeuaLAQWL2YmyDgMp2aSpH9JHsEQg== integrity sha512-yjD9nARJ/jb1g+CvD0tlhUHOrJ9Sy0P8T9MF3YaLlHnSRpwPfpTX0XIvpmw3gAJUmEu3FiICLBDPXVwyEvrleg==
ieee754@^1.1.13: ieee754@^1.1.13:
version "1.2.1" version "1.2.1"
@@ -3173,11 +3148,6 @@ is-stream@^2.0.0:
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077"
integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==
is-stream@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-3.0.0.tgz#e6bfd7aa6bef69f4f472ce9bb681e3e57b4319ac"
integrity sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==
is-typedarray@~1.0.0: is-typedarray@~1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
@@ -3349,20 +3319,20 @@ linkifyjs@^4.2.0:
integrity sha512-DRSlB9DKVW04c4SUdGvKK5FR6be45lTU9M76JnngqPeeGDqPwYc0zdUErtsNVMtxPXgUWV4HbXbnC4sNyBxkYg== integrity sha512-DRSlB9DKVW04c4SUdGvKK5FR6be45lTU9M76JnngqPeeGDqPwYc0zdUErtsNVMtxPXgUWV4HbXbnC4sNyBxkYg==
lint-staged@>=10: lint-staged@>=10:
version "15.5.2" version "16.0.0"
resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-15.5.2.tgz#beff028fd0681f7db26ffbb67050a21ed4d059a3" resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-16.0.0.tgz#31826709bde6a62542431da3055f038e386a20db"
integrity sha512-YUSOLq9VeRNAo/CTaVmhGDKG+LBtA8KF1X4K5+ykMSwWST1vDxJRB2kv2COgLb1fvpCo+A/y9A0G0znNVmdx4w== integrity sha512-sUCprePs6/rbx4vKC60Hez6X10HPkpDJaGcy3D1NdwR7g1RcNkWL8q9mJMreOqmHBTs+1sNFp+wOiX9fr+hoOQ==
dependencies: dependencies:
chalk "^5.4.1" chalk "^5.4.1"
commander "^13.1.0" commander "^13.1.0"
debug "^4.4.0" debug "^4.4.0"
execa "^8.0.1"
lilconfig "^3.1.3" lilconfig "^3.1.3"
listr2 "^8.2.5" listr2 "^8.3.3"
micromatch "^4.0.8" micromatch "^4.0.8"
nano-spawn "^1.0.0"
pidtree "^0.6.0" pidtree "^0.6.0"
string-argv "^0.3.2" string-argv "^0.3.2"
yaml "^2.7.0" yaml "^2.7.1"
listr2@^3.8.3: listr2@^3.8.3:
version "3.14.0" version "3.14.0"
@@ -3378,7 +3348,7 @@ listr2@^3.8.3:
through "^2.3.8" through "^2.3.8"
wrap-ansi "^7.0.0" wrap-ansi "^7.0.0"
listr2@^8.2.5: listr2@^8.3.3:
version "8.3.3" version "8.3.3"
resolved "https://registry.yarnpkg.com/listr2/-/listr2-8.3.3.tgz#815fc8f738260ff220981bf9e866b3e11e8121bf" resolved "https://registry.yarnpkg.com/listr2/-/listr2-8.3.3.tgz#815fc8f738260ff220981bf9e866b3e11e8121bf"
integrity sha512-LWzX2KsqcB1wqQ4AHgYb4RsDXauQiqhjLk+6hjbaeHG4zpjjVAB6wC/gz6X0l+Du1cN3pUB5ZlrvTbhGSNnUQQ== integrity sha512-LWzX2KsqcB1wqQ4AHgYb4RsDXauQiqhjLk+6hjbaeHG4zpjjVAB6wC/gz6X0l+Du1cN3pUB5ZlrvTbhGSNnUQQ==
@@ -3597,11 +3567,6 @@ mimic-fn@^2.1.0:
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
mimic-fn@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-4.0.0.tgz#60a90550d5cb0b239cca65d893b1a53b29871ecc"
integrity sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==
mimic-function@^5.0.0: mimic-function@^5.0.0:
version "5.0.1" version "5.0.1"
resolved "https://registry.yarnpkg.com/mimic-function/-/mimic-function-5.0.1.tgz#acbe2b3349f99b9deaca7fb70e48b83e94e67076" resolved "https://registry.yarnpkg.com/mimic-function/-/mimic-function-5.0.1.tgz#acbe2b3349f99b9deaca7fb70e48b83e94e67076"
@@ -3665,9 +3630,9 @@ ms@^2.1.1, ms@^2.1.3:
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
msw@^2.7.0: msw@^2.7.0:
version "2.7.6" version "2.8.2"
resolved "https://registry.yarnpkg.com/msw/-/msw-2.7.6.tgz#1471ce4311f4c173f287dced31dee211b6958deb" resolved "https://registry.yarnpkg.com/msw/-/msw-2.8.2.tgz#a35545275403da472f4ed2152cd3c77000db544c"
integrity sha512-P+rwn43ktxN8ghcl8q+hSAUlEi0PbJpDhGmDkw4zeUnRj3hBCVynWD+dTu38yLYKCE9ZF1OYcvpy7CTBRcqkZA== integrity sha512-ugu8RBgUj6//RD0utqDDPdS+QIs36BKYkDAM6u59hcMVtFM4PM0vW4l3G1R+1uCWP2EWFUG8reT/gPXVEtx7/w==
dependencies: dependencies:
"@bundled-es-modules/cookie" "^2.0.1" "@bundled-es-modules/cookie" "^2.0.1"
"@bundled-es-modules/statuses" "^1.0.1" "@bundled-es-modules/statuses" "^1.0.1"
@@ -3702,6 +3667,11 @@ mz@^2.7.0:
object-assign "^4.0.1" object-assign "^4.0.1"
thenify-all "^1.0.0" thenify-all "^1.0.0"
nano-spawn@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/nano-spawn/-/nano-spawn-1.0.1.tgz#c8e4c1e133e567e3efba44041dcfb12113d861b6"
integrity sha512-BfcvzBlUTxSDWfT+oH7vd6CbUV+rThLLHCIym/QO6GGLBsyVXleZs00fto2i2jzC/wPiBYk5jyOmpXWg4YopiA==
nanoid@^3.3.8: nanoid@^3.3.8:
version "3.3.11" version "3.3.11"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b"
@@ -3742,13 +3712,6 @@ npm-run-path@^4.0.0:
dependencies: dependencies:
path-key "^3.0.0" path-key "^3.0.0"
npm-run-path@^5.1.0:
version "5.3.0"
resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-5.3.0.tgz#e23353d0ebb9317f174e93417e4a4d82d0249e9f"
integrity sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==
dependencies:
path-key "^4.0.0"
nwsapi@^2.2.2: nwsapi@^2.2.2:
version "2.2.20" version "2.2.20"
resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.20.tgz#22e53253c61e7b0e7e93cef42c891154bcca11ef" resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.20.tgz#22e53253c61e7b0e7e93cef42c891154bcca11ef"
@@ -3795,13 +3758,6 @@ onetime@^5.1.0:
dependencies: dependencies:
mimic-fn "^2.1.0" mimic-fn "^2.1.0"
onetime@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/onetime/-/onetime-6.0.0.tgz#7c24c18ed1fd2e9bca4bd26806a33613c77d34b4"
integrity sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==
dependencies:
mimic-fn "^4.0.0"
onetime@^7.0.0: onetime@^7.0.0:
version "7.0.0" version "7.0.0"
resolved "https://registry.yarnpkg.com/onetime/-/onetime-7.0.0.tgz#9f16c92d8c9ef5120e3acd9dd9957cceecc1ab60" resolved "https://registry.yarnpkg.com/onetime/-/onetime-7.0.0.tgz#9f16c92d8c9ef5120e3acd9dd9957cceecc1ab60"
@@ -3902,11 +3858,6 @@ path-key@^3.0.0, path-key@^3.1.0:
resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==
path-key@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/path-key/-/path-key-4.0.0.tgz#295588dc3aee64154f877adb9d780b81c554bf18"
integrity sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==
path-parse@^1.0.7: path-parse@^1.0.7:
version "1.0.7" version "1.0.7"
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
@@ -4540,9 +4491,9 @@ section-matter@^1.0.0:
kind-of "^6.0.0" kind-of "^6.0.0"
semver@^7.5.3: semver@^7.5.3:
version "7.7.1" version "7.7.2"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.1.tgz#abd5098d82b18c6c81f6074ff2647fd3e7220c9f" resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.2.tgz#67d99fdcd35cec21e6f8b87a7fd515a33f982b58"
integrity sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA== integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==
sentence-case@^3.0.4: sentence-case@^3.0.4:
version "3.0.4" version "3.0.4"
@@ -4877,11 +4828,6 @@ strip-final-newline@^2.0.0:
resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad"
integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==
strip-final-newline@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-3.0.0.tgz#52894c313fbff318835280aed60ff71ebf12b8fd"
integrity sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==
style-mod@^4.0.0, style-mod@^4.1.0: style-mod@^4.0.0, style-mod@^4.1.0:
version "4.1.2" version "4.1.2"
resolved "https://registry.yarnpkg.com/style-mod/-/style-mod-4.1.2.tgz#ca238a1ad4786520f7515a8539d5a63691d7bf67" resolved "https://registry.yarnpkg.com/style-mod/-/style-mod-4.1.2.tgz#ca238a1ad4786520f7515a8539d5a63691d7bf67"
@@ -5223,9 +5169,9 @@ unplugin-vue-components@^28.4.1:
unplugin-utils "^0.2.4" unplugin-utils "^0.2.4"
unplugin@^2.2.0, unplugin@^2.3.2: unplugin@^2.2.0, unplugin@^2.3.2:
version "2.3.2" version "2.3.3"
resolved "https://registry.yarnpkg.com/unplugin/-/unplugin-2.3.2.tgz#36c93a1662b70c97a2e2fc45c0e78fa09f7a4984" resolved "https://registry.yarnpkg.com/unplugin/-/unplugin-2.3.3.tgz#f83507e4484008e400f3d831a628eaede22c954f"
integrity sha512-3n7YA46rROb3zSj8fFxtxC/PqoyvYQ0llwz9wtUPUutr9ig09C8gGo5CWCwHrUzlqC1LLR43kxp5vEIyH1ac1w== integrity sha512-DN4DgiS13HFrAapoRmmoa9B35QzmQVRH2k58HelO28htXVNEEFZ8CGlZL0aRHXOXtz9McwY6lqaZjcc15uWMow==
dependencies: dependencies:
acorn "^8.14.1" acorn "^8.14.1"
picomatch "^4.0.2" picomatch "^4.0.2"
@@ -5563,7 +5509,7 @@ yallist@^2.1.2:
resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"
integrity sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A== integrity sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==
yaml@^2.3.4, yaml@^2.7.0: yaml@^2.3.4, yaml@^2.7.1:
version "2.7.1" version "2.7.1"
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.7.1.tgz#44a247d1b88523855679ac7fa7cda6ed7e135cf6" resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.7.1.tgz#44a247d1b88523855679ac7fa7cda6ed7e135cf6"
integrity sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ== integrity sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==