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
run: |
cd ~/frappe-bench/apps/lms
yarn add cypress@^10 --no-lockfile
yarn add cypress@^10 --no-lockfile -W
- name: UI Tests
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",
rules: {
"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",
adminPassword: "admin",
testUser: "frappe@example.com",

View File

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

View File

@@ -47,6 +47,7 @@ declare module 'vue' {
Discussions: typeof import('./src/components/Discussions.vue')['default']
EditCoverImage: typeof import('./src/components/Modals/EditCoverImage.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']
Evaluators: typeof import('./src/components/Evaluators.vue')['default']
Event: typeof import('./src/components/Modals/Event.vue')['default']

View File

@@ -27,7 +27,7 @@
"codemirror-editor-vue3": "^2.8.0",
"dayjs": "^1.11.6",
"feather-icons": "^4.28.0",
"frappe-ui": "^0.1.141",
"frappe-ui": "^0.1.143",
"highlight.js": "^11.11.1",
"lucide-vue-next": "^0.383.0",
"markdown-it": "^14.0.0",

View File

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

View File

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

View File

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

View File

@@ -86,9 +86,9 @@ import {
ListRows,
ListView,
ListRowItem,
toast,
} from 'frappe-ui'
import { Plus, Trash2 } from 'lucide-vue-next'
import { showToast } from '@/utils'
const readOnlyMode = window.read_only_mode
const showCourseModal = ref(false)
@@ -152,7 +152,7 @@ const removeCourses = (selections, unselectAll) => {
{
onSuccess(data) {
courses.reload()
showToast(__('Success'), __('Courses deleted successfully'), 'check')
toast.success(__('Courses deleted successfully'))
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.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 }}
<span v-if="seats_left > 1">
@@ -117,9 +122,9 @@
</template>
<script setup>
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 { formatNumberIntoCurrency, formatTime, showToast } from '@/utils'
import { formatNumberIntoCurrency, formatTime } from '@/utils'
import DateRange from '@/components/Common/DateRange.vue'
import { useRouter } from 'vue-router'
@@ -151,11 +156,7 @@ const enrollInBatch = () => {
{},
{
onSuccess(data) {
showToast(
__('Success'),
__('You have been enrolled in this batch'),
'check'
)
toast.success(__('You have been enrolled in this batch'))
router.push({
name: 'Batch',
params: {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -93,12 +93,11 @@
</div>
</template>
<script setup>
import { createResource, TextEditor, Button, Dropdown } from 'frappe-ui'
import { createResource, TextEditor, Button, Dropdown, toast } from 'frappe-ui'
import { timeAgo } from '../utils'
import UserAvatar from '@/components/UserAvatar.vue'
import { ChevronLeft, MoreHorizontal } from 'lucide-vue-next'
import { ref, inject, onMounted } from 'vue'
import { createToast } from '../utils'
const showTopics = defineModel('showTopics')
const newReply = ref('')
@@ -192,14 +191,7 @@ const postReply = () => {
replies.reload()
},
onError(err) {
createToast({
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,
})
toast.error(err.messages?.[0] || err)
},
}
)

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"
/>
<Button @click="() => (showForm = !showForm)">
<template #icon>
<Plus v-if="!showForm" class="h-3 w-3 stroke-1.5" />
<X v-else class="h-3 w-3 stroke-1.5" />
<template #prefix>
<Plus v-if="!showForm" class="size-4 stroke-1.5" />
<X v-else class="size-4 stroke-1.5" />
</template>
{{ showForm ? __('Close') : __('New') }}
</Button>
</div>
</div>

View File

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

View File

@@ -31,6 +31,7 @@
<div class="mb-4">
<div class="mb-1.5 text-sm text-ink-gray-5">
{{ __('Announcement') }}
<span class="text-ink-red-3">*</span>
</div>
<TextEditor
:fixedMenu="true"
@@ -43,9 +44,8 @@
</Dialog>
</template>
<script setup>
import { Dialog, Input, TextEditor, createResource } from 'frappe-ui'
import { Dialog, Input, TextEditor, createResource, toast } from 'frappe-ui'
import { reactive } from 'vue'
import { showToast } from '@/utils/'
const show = defineModel()
@@ -87,22 +87,21 @@ const makeAnnouncement = (close) => {
{
validate() {
if (!props.students.length) {
return 'No students in this batch'
return __('No students in this batch')
}
if (!announcement.subject) {
return 'Subject is required'
return __('Subject is required')
}
if (!announcement.announcement) {
return __('Announcement is required')
}
},
onSuccess() {
close()
showToast(
__('Success'),
__('Announcement has been sent successfully'),
'check'
)
toast.success(__('Announcement has been sent successfully'))
},
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"
:doctype="assessmentType"
: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>
</template>
</Dialog>
</template>
<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 { computed, ref } from 'vue'
import { showToast } from '@/utils'
import { useRouter } from 'vue-router'
const show = defineModel()
const assessmentType = ref(null)
const assessment = ref(null)
const assessments = defineModel('assessments')
const router = useRouter()
const props = defineProps({
batch: {
@@ -70,7 +88,7 @@ const addAssessment = (close) => {
{
onSuccess(data) {
assessments.value.reload()
showToast(__('Success'), __('Assessment added successfully'), 'check')
toast.success(__('Assessment added successfully'))
close()
},
}

View File

@@ -37,7 +37,7 @@
@change="(val) => (assignment.question = 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]"
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>
@@ -64,9 +64,8 @@
</Dialog>
</template>
<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 { showToast } from '@/utils'
const show = defineModel()
const assignments = defineModel<Assignments>('assignments')
@@ -123,11 +122,7 @@ const saveAssignment = () => {
{
onSuccess() {
show.value = false
showToast(
__('Success'),
__('Assignment created successfully'),
'check'
)
toast.success(__('Assignment created successfully'))
},
}
)
@@ -140,11 +135,7 @@ const saveAssignment = () => {
{
onSuccess() {
show.value = false
showToast(
__('Success'),
__('Assignment updated successfully'),
'check'
)
toast.success(__('Assignment updated successfully'))
},
}
)

View File

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

View File

@@ -14,7 +14,13 @@
<div class="text-xl font-semibold">
{{ student.full_name }}
</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') }}
</Badge>
</div>
@@ -26,7 +32,10 @@
<div class="space-y-8">
<!-- 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">
<span class="flex-1">
{{ __('Assessment') }}
@@ -73,7 +82,10 @@
</div>
<!-- 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">
<span class="flex-1">
{{ __('Courses') }}

View File

@@ -62,9 +62,8 @@
</template>
<script setup>
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 { showToast } from '@/utils'
const show = defineModel()
const dayjs = inject('$dayjs')
@@ -112,13 +111,13 @@ const generateCertificates = (close) => {
},
{
onError(err) {
showToast(__('Error'), err.messages?.[0] || err, 'x')
toast.error(err.messages?.[0] || err)
},
}
)
})
close()
showToast(__('Success'), __('Certificates generated successfully'), 'check')
toast.success(__('Certificates generated successfully'))
}
const getCourses = () => {

View File

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

View File

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

View File

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

View File

@@ -68,7 +68,7 @@
<script setup>
import { Dialog, createResource, Select, FormControl } from 'frappe-ui'
import { reactive, watch, inject } from 'vue'
import { createToast, formatTime } from '@/utils/'
import { formatTime } from '@/utils/'
const user = inject('$user')
const dayjs = inject('$dayjs')
@@ -148,14 +148,7 @@ function submitEvaluation(close) {
unavailabilityMessage = false
}
createToast({
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,
})
toast.warning(__('Evaluator is unavailable'))
},
})
}

View File

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

View File

@@ -64,10 +64,10 @@
</Dialog>
</template>
<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 { ref, inject } from 'vue'
import { createToast, getFileSize } from '@/utils/'
import { getFileSize } from '@/utils/'
const resume = ref(null)
const show = defineModel()
@@ -112,24 +112,12 @@ const submitResume = (close) => {
}
},
onSuccess() {
createToast({
title: 'Success',
text: 'Your application has been submitted',
icon: 'check',
iconClasses: 'bg-surface-green-3 text-ink-white rounded-md p-px',
})
toast.success('Your application has been submitted successfully')
application.value.reload()
close()
},
onError(err) {
createToast({
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,
})
toast.error(err.messages?.[0] || err)
},
}
)

View File

@@ -94,9 +94,10 @@ import {
Tooltip,
FormControl,
Autocomplete,
toast,
} from 'frappe-ui'
import { reactive, inject, onMounted } from 'vue'
import { getTimezones, createToast, getUserTimezone } from '@/utils/'
import { getTimezones, getUserTimezone } from '@/utils/'
const liveClasses = defineModel('reloadLiveClasses')
const show = defineModel()
@@ -202,14 +203,7 @@ const submitLiveClass = (close) => {
close()
},
onError(err) {
createToast({
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,
})
toast.error(err.messages?.[0] || err)
},
})
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -21,14 +21,28 @@
</header>
<div class="md:w-3/4 md:mx-auto py-5 mx-5">
<div class="grid grid-cols-3 gap-5 mb-5">
<FormControl v-model="titleFilter" :placeholder="__('Search by title')" />
<FormControl
v-model="typeFilter"
type="select"
:options="assignmentTypes"
:placeholder="__('Type')"
/>
<div class="flex items-center justify-between mb-5">
<div
v-if="assignmentCount"
class="text-xl font-semibold text-ink-gray-7 mb-4"
>
{{ __('{0} Assignments').format(assignmentCount) }}
</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>
<ListView
v-if="assignments.data?.length"
@@ -46,22 +60,7 @@
}"
>
</ListView>
<div
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>
<EmptyState v-else type="Assignments" />
<div
v-if="assignments.data && assignments.hasNextPage"
class="flex justify-center my-5"
@@ -81,16 +80,18 @@
import {
Breadcrumbs,
Button,
call,
createListResource,
FormControl,
ListView,
usePageMeta,
} from 'frappe-ui'
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 { sessionStore } from '../stores/session'
import AssignmentForm from '@/components/Modals/AssignmentForm.vue'
import EmptyState from '@/components/EmptyState.vue'
const user = inject('$user')
const dayjs = inject('$dayjs')
@@ -98,6 +99,7 @@ const titleFilter = ref('')
const typeFilter = ref('')
const showAssignmentForm = ref(false)
const assignmentID = ref('new')
const assignmentCount = ref(0)
const { brand } = sessionStore()
const router = useRouter()
const readOnlyMode = window.read_only_mode
@@ -106,7 +108,7 @@ onMounted(() => {
if (!user.data?.is_moderator && !user.data?.is_instructor) {
router.push({ name: 'Courses' })
}
getAssignmentCount()
titleFilter.value = router.currentRoute.value.query.title
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(() => {
let types = ['', 'Document', 'Image', 'PDF', 'URL', 'Text']
return types.map((type) => {

View File

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

View File

@@ -6,67 +6,45 @@
<Breadcrumbs :items="breadcrumbs" />
</header>
<div class="m-5 pb-10">
<div>
<div class="text-3xl font-semibold text-ink-gray-9">
{{ batch.data.title }}
</div>
<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 class="flex justify-between w-full">
<div class="md:w-2/3">
<div class="text-3xl font-semibold text-ink-gray-9">
{{ batch.data.title }}
</div>
<span v-if="batch.data?.courses?.length" class="hidden lg:block"
>&middot;</span
>
<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 class="my-3 leading-6 text-ink-gray-7">
{{ batch.data.description }}
</div>
</div>
<div class="flex avatar-group overlap mt-3">
<div
class="h-6 mr-1"
:class="{
'avatar-group overlap': batch.data.instructors.length > 1,
}"
>
<UserAvatar
v-for="instructor in batch.data.instructors"
:user="instructor"
/>
<div class="flex avatar-group overlap">
<div
class="h-6 mr-1"
:class="{
'avatar-group overlap': batch.data.instructors.length > 1,
}"
>
<UserAvatar
v-for="instructor in batch.data.instructors"
:user="instructor"
/>
</div>
<CourseInstructors :instructors="batch.data.instructors" />
</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
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"
></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 class="order-1 lg:order-none">
<BatchOverlay :batch="batch" />
</div>
</div>
</div> -->
<div v-if="batch.data.courses.length">
<div class="flex items-center mt-10">
<div class="text-2xl font-semibold">

View File

@@ -8,13 +8,13 @@
{{ __('Save') }}
</Button>
</header>
<div class="w-3/4 mx-auto py-5">
<div class="">
<div class="py-5">
<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">
{{ __('Details') }}
</div>
<div class="space-y-10 mb-4">
<div class="space-y-4">
<div class="grid grid-cols-2 gap-5">
<div class="space-y-5">
<FormControl
v-model="batch.title"
:label="__('Title')"
@@ -26,31 +26,162 @@
doctype="User"
:label="__('Instructors')"
:required="true"
:onCreate="(close) => openSettings('Members', close)"
:filters="{ ignore_user_type: 1 }"
/>
</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="flex flex-col space-y-5">
<FormControl
v-model="batch.published"
type="checkbox"
:label="__('Published')"
/>
<FormControl
v-model="batch.allow_self_enrollment"
type="checkbox"
:label="__('Allow self enrollment')"
/>
<FormControl
v-model="batch.certification"
type="checkbox"
:label="__('Certification')"
/>
</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">
{{ __('Settings') }}
</div>
<div class="grid grid-cols-3 gap-5">
<FormControl
v-model="batch.published"
type="checkbox"
:label="__('Published')"
/>
<FormControl
v-model="batch.allow_self_enrollment"
type="checkbox"
:label="__('Allow self enrollment')"
/>
<FormControl
v-model="batch.certification"
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 class="text-xs text-ink-gray-5 mb-2">
<div class="text-xs text-ink-gray-5">
{{ __('Meta Image') }}
</div>
<FileUploader
@@ -70,11 +201,9 @@
<Button @click="openFileSelector">
{{ __('Upload') }}
</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 any online platform'
)
__('Appears when the batch URL is shared on socials')
}}
</div>
</div>
@@ -106,119 +235,16 @@
</div>
</div>
<div class="my-10">
<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>
<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 class="px-20 pb-5 space-y-5">
<div class="text-lg text-ink-gray-9 font-semibold">
{{ __('Pricing') }}
</div>
<FormControl
v-model="batch.paid_batch"
type="checkbox"
: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
v-model="batch.amount"
:label="__('Amount')"
@@ -232,33 +258,6 @@
/>
</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>
</template>
@@ -279,15 +278,16 @@ import {
TextEditor,
createResource,
usePageMeta,
toast,
} from 'frappe-ui'
import { useRouter } from 'vue-router'
import { showToast } from '@/utils'
import { Image } from 'lucide-vue-next'
import { capture } from '@/telemetry'
import { useOnboarding } from 'frappe-ui/frappe'
import { sessionStore } from '../stores/session'
import MultiSelect from '@/components/Controls/MultiSelect.vue'
import Link from '@/components/Controls/Link.vue'
import { openSettings } from '@/utils'
const router = useRouter()
const user = inject('$user')
@@ -459,7 +459,7 @@ const createNewBatch = () => {
})
},
onError(err) {
showToast('Message', err.messages?.[0] || err, 'alert-circle')
toast.error(err.messages?.[0] || err)
},
}
)
@@ -478,7 +478,7 @@ const editBatchDetails = () => {
})
},
onError(err) {
showToast('Message', err.messages?.[0] || err, 'alert-circle')
toast.error(err.messages?.[0] || err)
},
}
)

View File

@@ -20,12 +20,14 @@
</header>
<div class="p-5 pb-10">
<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"
>
<div class="text-lg text-ink-gray-9 font-semibold">
{{ __('All Batches') }}
</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"
>
<TabButtons
@@ -70,22 +72,8 @@
<BatchCard :batch="batch" />
</router-link>
</div>
<div
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>
<EmptyState v-else-if="!batches.list.loading" type="Batches" />
<div
v-if="!batches.list.loading && batches.hasNextPage"
class="flex justify-center mt-5"
@@ -100,6 +88,7 @@
import {
Breadcrumbs,
Button,
call,
createListResource,
FormControl,
Select,
@@ -107,9 +96,10 @@ import {
usePageMeta,
} from 'frappe-ui'
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 BatchCard from '@/components/BatchCard.vue'
import EmptyState from '@/components/EmptyState.vue'
const user = inject('$user')
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 orderBy = ref('start_date')
const readOnlyMode = window.read_only_mode
const batchCount = ref(0)
onMounted(() => {
setFiltersFromQuery()
updateBatches()
getBatchCount()
categories.value = [
{
label: '',
@@ -306,6 +298,14 @@ const canCreateBatch = () => {
return false
}
const getBatchCount = () => {
call('frappe.client.get_count', {
doctype: 'LMS Batch',
}).then((data) => {
batchCount.value = data
})
}
const breadcrumbs = computed(() => [
{
label: __('Batches'),

View File

@@ -156,9 +156,9 @@ import {
FormControl,
Breadcrumbs,
usePageMeta,
toast,
} from 'frappe-ui'
import { reactive, inject, onMounted, computed } from 'vue'
import { showToast } from '@/utils/'
import { sessionStore } from '../stores/session'
import Link from '@/components/Controls/Link.vue'
import NotPermitted from '@/components/NotPermitted.vue'
@@ -259,7 +259,7 @@ const generatePaymentLink = () => {
window.location.href = data
},
onError(err) {
showToast(__('Error'), err.messages?.[0] || err, 'x')
toast.error(err.messages?.[0] || err)
},
}
)
@@ -333,14 +333,7 @@ const validateAddress = () => {
}
const showError = (err) => {
createToast({
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,
})
toast.error(err.messages?.[0] || err)
}
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"
>
<Breadcrumbs :items="breadcrumbs" />
<router-link :to="{ name: 'Batches' }">
<router-link :to="{ name: 'Batches', query: { certification: true } }">
<Button>
<template #prefix>
<GraduationCap class="h-4 w-4 stroke-1.5" />
@@ -101,22 +101,7 @@
</Button>
</div>
</div>
<div
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>
<EmptyState v-else type="Certified Members" />
</template>
<script setup>
import {
@@ -130,8 +115,9 @@ import {
usePageMeta,
} from 'frappe-ui'
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 EmptyState from '@/components/EmptyState.vue'
const currentCategory = ref('')
const filters = ref({})

View File

@@ -19,62 +19,112 @@
</Button>
</div>
</header>
<div class="mt-5 mb-10">
<div class="container mb-5">
<div class="mt-5 mb-5">
<div class="px-10 pb-5 mb-5 space-y-5 border-b">
<div class="text-lg font-semibold mb-4">
{{ __('Details') }}
</div>
<FormControl
v-model="course.title"
:label="__('Title')"
class="mb-4"
:required="true"
/>
<FormControl
v-model="course.short_introduction"
:label="__('Short Introduction')"
:placeholder="
__(
'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 class="grid grid-cols-2 gap-5">
<FormControl
v-model="course.title"
:label="__('Title')"
:required="true"
/>
<Link
doctype="LMS Category"
v-model="course.category"
:label="__('Category')"
:onCreate="(value, close) => openSettings('Categories', close)"
/>
</div>
<div class="mb-4">
<div class="text-xs text-ink-gray-5 mb-2">
{{ __('Course Image') }}
<span class="text-ink-red-3">*</span>
<div class="grid grid-cols-2 gap-5">
<MultiSelect
v-model="instructors"
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>
<FileUploader
v-if="!course.course_image"
:fileTypes="['image/*']"
:validateFile="validateFile"
@success="(file) => saveImage(file)"
>
<template
v-slot="{ file, progress, uploading, openFileSelector }"
</div>
<div class="grid grid-cols-2 gap-5">
<FormControl
v-model="course.short_introduction"
type="textarea"
:rows="4"
:label="__('Short Introduction')"
: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">
<div class="border rounded-md w-fit py-5 px-20">
<Image class="size-5 stroke-1 text-ink-gray-7" />
<template
v-slot="{ file, progress, uploading, openFileSelector }"
>
<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>
</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="openFileSelector">
{{ __('Upload') }}
<Button @click="removeImage()">
{{ __('Remove') }}
</Button>
<div class="mt-2 text-ink-gray-5 text-sm">
{{
@@ -83,85 +133,17 @@
</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>
<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 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') }}
</div>
<div class="grid grid-cols-2 gap-10 mb-4">
<div
v-if="user.data?.is_moderator"
class="flex flex-col space-y-4"
>
<div class="grid grid-cols-2 gap-5">
<div class="flex flex-col space-y-5">
<FormControl
type="checkbox"
v-model="course.published"
@@ -171,10 +153,9 @@
v-model="course.published_on"
:label="__('Published On')"
type="date"
class="mb-5"
/>
</div>
<div class="flex flex-col space-y-3">
<div class="flex flex-col space-y-5">
<FormControl
type="checkbox"
v-model="course.upcoming"
@@ -193,7 +174,34 @@
</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">
{{ __('Pricing and Certification') }}
</div>
@@ -214,19 +222,31 @@
:label="__('Paid Certificate')"
/>
</div>
<FormControl v-model="course.course_price" :label="__('Amount')" />
<Link
doctype="Currency"
v-model="course.currency"
:filters="{ enabled: 1 }"
:label="__('Currency')"
/>
<Link
v-if="course.paid_certificate"
doctype="Course Evaluator"
v-model="course.evaluator"
:label="__('Evaluator')"
/>
<div class="grid grid-cols-2 gap-5">
<div class="space-y-5">
<FormControl
v-if="course.paid_course || course.paid_certificate"
v-model="course.course_price"
:label="__('Amount')"
/>
<Link
v-if="course.paid_certificate"
doctype="Course Evaluator"
v-model="course.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>
@@ -250,6 +270,7 @@ import {
FormControl,
FileUploader,
usePageMeta,
toast,
} from 'frappe-ui'
import {
inject,
@@ -261,13 +282,12 @@ import {
watch,
getCurrentInstance,
} from 'vue'
import { showToast } from '@/utils'
import { Image, Trash2, X } from 'lucide-vue-next'
import { useRouter } from 'vue-router'
import { capture } from '@/telemetry'
import { useOnboarding } from 'frappe-ui/frappe'
import { sessionStore } from '../stores/session'
import { useSettings } from '@/stores/settings'
import { openSettings } from '@/utils'
import Link from '@/components/Controls/Link.vue'
import CourseOutline from '@/components/CourseOutline.vue'
import MultiSelect from '@/components/Controls/MultiSelect.vue'
@@ -277,7 +297,6 @@ const newTag = ref('')
const { brand } = sessionStore()
const router = useRouter()
const instructors = ref([])
const settingsStore = useSettings()
const app = getCurrentInstance()
const { updateOnboardingStep } = useOnboarding('learning')
const { $dialog } = app.appContext.config.globalProperties
@@ -429,10 +448,10 @@ const submitCourse = () => {
},
{
onSuccess() {
showToast('Success', 'Course updated successfully', 'check')
toast.success(__('Course updated successfully'))
},
onError(err) {
showToast('Error', err.messages?.[0] || err, 'x')
toast.error(err.messages?.[0] || err)
},
}
)
@@ -446,14 +465,14 @@ const submitCourse = () => {
}
capture('course_created')
showToast('Success', 'Course created successfully', 'check')
toast.success(__('Course created successfully'))
router.push({
name: 'CourseForm',
params: { courseName: data.name },
})
},
onError(err) {
showToast('Error', err.messages?.[0] || err, 'x')
toast.error(err.messages?.[0] || err)
},
})
}
@@ -467,7 +486,7 @@ const deleteCourse = createResource({
}
},
onSuccess() {
showToast(__('Success'), __('Course deleted successfully'), 'check')
toast.success(__('Course deleted successfully'))
router.push({ name: 'Courses' })
},
})
@@ -531,12 +550,6 @@ const removeImage = () => {
course.course_image = null
}
const openSettings = (close) => {
close()
settingsStore.activeTab = 'Categories'
settingsStore.isSettingsOpen = true
}
const check_permission = () => {
let user_is_instructor = false
if (user.data?.is_moderator) return

View File

@@ -20,12 +20,14 @@
</header>
<div class="p-5 pb-10">
<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"
>
<div class="text-lg text-ink-gray-9 font-semibold">
{{ __('All Courses') }}
</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"
>
<TabButtons :buttons="courseTabs" v-model="currentTab" />
@@ -66,22 +68,7 @@
<CourseCard :course="course" />
</router-link>
</div>
<div
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>
<EmptyState v-else-if="!courses.list.loading" type="Courses" />
<div
v-if="!courses.list.loading && courses.hasNextPage"
class="flex justify-center mt-5"
@@ -104,10 +91,11 @@ import {
usePageMeta,
} from 'frappe-ui'
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 { canCreateCourse } from '@/utils'
import CourseCard from '@/components/CourseCard.vue'
import EmptyState from '@/components/EmptyState.vue'
import router from '../router'
const user = inject('$user')
@@ -121,12 +109,12 @@ const certification = ref(false)
const filters = ref({})
const currentTab = ref('Live')
const { brand } = sessionStore()
const readOnlyMode = window.read_only_mode
const courseCount = ref(0)
onMounted(() => {
identifyUserPersona()
setFiltersFromQuery()
updateCourses()
getCourseCount()
categories.value = [
{
label: '',
@@ -175,19 +163,23 @@ const identifyUserPersona = async () => {
if (user.data?.is_system_manager && !user.data?.developer_mode) {
let personaCaptured = await isPersonaCaptured()
if (personaCaptured) return
call('frappe.client.get_count', {
doctype: 'LMS Course',
}).then((data) => {
if (!data) {
router.push({
name: 'PersonaForm',
})
}
})
if (!courseCount.value) {
router.push({
name: 'PersonaForm',
})
}
}
}
const getCourseCount = () => {
call('frappe.client.get_count', {
doctype: 'LMS Course',
}).then((data) => {
courseCount.value = data
identifyUserPersona()
})
}
const updateCourses = () => {
updateFilters()
courses.update({

View File

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

View File

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

View File

@@ -26,15 +26,17 @@
</header>
<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"
>
<div
v-if="jobCount"
class="text-xl font-semibold text-ink-gray-7 mb-4 md:mb-0"
>
<div class="text-xl font-semibold text-ink-gray-7 mb-4 md:mb-0">
{{ __('{0} Open Jobs').format(jobCount) }}
</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
type="text"
:placeholder="__('Search')"
@@ -79,21 +81,7 @@
</router-link>
</div>
</div>
<div
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>
<EmptyState v-else type="Job Openings" />
</div>
</div>
</template>
@@ -106,11 +94,12 @@ import {
FormControl,
usePageMeta,
} from 'frappe-ui'
import { Laptop, Plus, Search } from 'lucide-vue-next'
import { Plus, Search } from 'lucide-vue-next'
import { sessionStore } from '../stores/session'
import { inject, computed, ref, onMounted, watch } from 'vue'
import JobCard from '@/components/JobCard.vue'
import Link from '@/components/Controls/Link.vue'
import EmptyState from '@/components/EmptyState.vue'
const user = inject('$user')
const jobType = ref(null)

View File

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

View File

@@ -84,6 +84,7 @@ import {
createResource,
FormControl,
usePageMeta,
toast,
} from 'frappe-ui'
import {
computed,
@@ -97,7 +98,7 @@ import { sessionStore } from '../stores/session'
import EditorJS from '@editorjs/editorjs'
import LessonHelp from '@/components/LessonHelp.vue'
import { ChevronRight } from 'lucide-vue-next'
import { createToast, getEditorTools, enablePlyr } from '@/utils'
import { getEditorTools, enablePlyr } from '@/utils'
import { capture } from '@/telemetry'
import { useOnboarding } from 'frappe-ui/frappe'
@@ -410,14 +411,14 @@ const createNewLesson = () => {
updateOnboardingStep('create_first_lesson')
capture('lesson_created')
showToast('Success', 'Lesson created successfully', 'check')
toast.success(__('Lesson created successfully'))
lessonDetails.reload()
},
}
)
},
onError(err) {
showToast('Error', err.message, 'x')
toast.error(err.messages?.[0] || err)
},
}
)
@@ -434,11 +435,11 @@ const editCurrentLesson = () => {
},
onSuccess() {
showSuccessMessage
? showToast('Success', 'Lesson updated successfully', 'check')
? toast.success(__('Lesson updated successfully'))
: ''
},
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(() => {
let crumbs = [
{

View File

@@ -1,6 +1,6 @@
<template>
<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">
<LMSLogo class="size-7" />
<span
@@ -18,7 +18,7 @@
<div class="mb-5">
<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>
<FormControl
v-model="persona.useCase"
@@ -29,12 +29,12 @@
<div class="mb-5">
<div class="text-sm text-gray-700 mb-2">
{{ __('How many students are you planning to teach?') }}
{{ __('What best describes your role?') }}
</div>
<FormControl
v-model="persona.noOfStudents"
v-model="persona.role"
type="select"
:options="noOfStudentsOptions"
:options="roleOptions"
/>
</div>
@@ -65,7 +65,7 @@ const router = useRouter()
const { brand } = sessionStore()
const persona = reactive({
noOfStudents: null,
role: 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 options = [
'Less than 50',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -40,18 +40,7 @@
</Button>
</div>
</div>
<div
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>
<EmptyState v-else />
</template>
<script setup>
import {
@@ -65,10 +54,10 @@ import {
ListHeaderItem,
usePageMeta,
} from 'frappe-ui'
import { BookOpen } from 'lucide-vue-next'
import { computed, onMounted, inject } from 'vue'
import { sessionStore } from '../stores/session'
import { useRouter } from 'vue-router'
import EmptyState from '@/components/EmptyState.vue'
const { brand } = sessionStore()
const router = useRouter()

View File

@@ -21,6 +21,9 @@
</router-link>
</header>
<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
:columns="quizColumns"
:rows="quizzes.data"
@@ -53,27 +56,13 @@
</Button>
</div>
</div>
<div
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>
<EmptyState v-else type="Quizzes" />
</template>
<script setup>
import {
Breadcrumbs,
Button,
call,
createListResource,
ListView,
ListRows,
@@ -83,19 +72,22 @@ import {
usePageMeta,
} from 'frappe-ui'
import { useRouter } from 'vue-router'
import { computed, inject, onMounted } from 'vue'
import { BookOpen, Plus } from 'lucide-vue-next'
import { computed, inject, onMounted, ref } from 'vue'
import { Plus } from 'lucide-vue-next'
import { sessionStore } from '@/stores/session'
import EmptyState from '@/components/EmptyState.vue'
const { brand } = sessionStore()
const user = inject('$user')
const router = useRouter()
const quizCount = ref(0)
const readOnlyMode = window.read_only_mode
onMounted(() => {
if (!user.data?.is_moderator && !user.data?.is_instructor) {
router.push({ name: 'Courses' })
}
getQuizCount()
})
const quizFilter = computed(() => {
@@ -114,6 +106,14 @@ const quizzes = createListResource({
orderBy: 'modified desc',
})
const getQuizCount = () => {
call('frappe.client.get_count', {
doctype: 'LMS Quiz',
}).then((data) => {
quizCount.value = data
})
}
const quizColumns = computed(() => {
return [
{

View File

@@ -1,9 +1,10 @@
import { toast } from 'frappe-ui'
import { useTimeAgo } from '@vueuse/core'
import { Quiz } from '@/utils/quiz'
import { Assignment } from '@/utils/assignment'
import { Upload } from '@/utils/upload'
import { Markdown } from '@/utils/markdownParser'
import { useSettings } from '@/stores/settings'
import { usersStore } from '@/stores/user'
import Header from '@editorjs/header'
import Paragraph from '@editorjs/paragraph'
import { CodeBox } from '@/utils/code'
@@ -14,19 +15,11 @@ import dayjs from '@/utils/dayjs'
import Embed from '@editorjs/embed'
import SimpleImage from '@editorjs/simple-image'
import Table from '@editorjs/table'
import { usersStore } from '../stores/user'
import Plyr from 'plyr'
import 'plyr/dist/plyr.css'
const readOnlyMode = window.read_only_mode
export function createToast(options) {
toast({
position: 'bottom-right',
...options,
})
}
export function timeAgo(date) {
return useTimeAgo(date).value
}
@@ -97,26 +90,6 @@ export function getFileSize(file_size) {
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) {
return new Promise((resolve) => {
let img = new Image()
@@ -558,24 +531,33 @@ export const enablePlyr = () => {
const videoElement = document.getElementsByClassName('video-player')
if (videoElement.length === 0) return
const src = videoElement[0].getAttribute('src')
if (src) {
let videoID = src.split('/').pop()
videoElement[0].setAttribute('data-plyr-embed-id', videoID)
}
new Plyr('.video-player', {
youtube: {
noCookie: true,
},
controls: [
'play-large',
'play',
'progress',
'current-time',
'mute',
'volume',
'fullscreen',
],
})
}, 500)
Array.from(videoElement).forEach((video) => {
const src = video.getAttribute('src')
if (src) {
let videoID = src.split('/').pop()
video.setAttribute('data-plyr-embed-id', videoID)
}
new Plyr(video, {
youtube: {
noCookie: true,
},
controls: [
'play-large',
'play',
'progress',
'current-time',
'mute',
'volume',
'fullscreen',
],
})
}, 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: {
allowedHosts: ['fs', 'persona'],
allowedHosts: ['fs', 'per2'],
},
resolve: {
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
web_include_css = "lms.bundle.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")
# website_theme_scss = "lms/public/scss/website"

View File

@@ -21,9 +21,9 @@ from lms.lms.utils import (
class LMSBatch(Document):
def validate(self):
if self.seat_count:
self.validate_seats_left()
self.validate_seats_left()
self.validate_batch_end_date()
self.validate_batch_time()
self.validate_duplicate_courses()
self.validate_payments_app()
self.validate_amount_and_currency()
@@ -40,6 +40,11 @@ class LMSBatch(Document):
if self.end_date < self.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):
courses = [row.course for row in self.courses]
duplicates = {course for course in courses if courses.count(course) > 1}
@@ -94,6 +99,9 @@ class LMSBatch(Document):
enrollment.save()
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})
if cint(self.seat_count) < students:
frappe.throw(_("There are no seats available in this batch."))
@@ -208,86 +216,6 @@ def authenticate():
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()
def get_batch_timetable(batch):
timetable = frappe.get_all(

View File

@@ -96,9 +96,7 @@ def set_total_marks(questions):
@frappe.whitelist()
def quiz_summary(quiz, results):
score = 0
results = results and json.loads(results)
is_open_ended = False
percentage = 0
quiz_details = frappe.db.get_value(
@@ -108,7 +106,32 @@ def quiz_summary(quiz, results):
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
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:
question_details = frappe.db.get_value(
@@ -123,55 +146,28 @@ def quiz_summary(quiz, results):
result["marks_out_of"] = question_details.marks
if question_details.type != "Open Ended":
correct = result["is_correct"][0]
for point in result["is_correct"]:
correct = correct and point
result["is_correct"] = correct
if len(result["is_correct"]) > 0:
correct = result["is_correct"][0]
for point in result["is_correct"]:
correct = correct and point
result["is_correct"] = correct
else:
result["is_correct"] = 0
marks = question_details.marks if correct else 0
result["marks"] = marks
score += marks
else:
result["is_correct"] = 0
is_open_ended = True
percentage = (score / score_out_of) * 100
result["answer"] = re.sub(
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)
result["is_correct"] = 0
result["answer"] = re.sub(
r'<img[^>]*src\s*=\s*["\'](?=data:)(.*?)["\']', _save_file, result["answer"]
)
return {
"results": results,
"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,
}
@@ -219,6 +215,36 @@ def get_corrupted_image_msg():
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()
def get_question_details(question):
if frappe.db.exists("LMS Quiz Question", question):

View File

@@ -403,7 +403,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2025-04-22 16:05:27.914422",
"modified": "2025-05-14 12:43:22.749850",
"modified_by": "sayali@frappe.io",
"module": "LMS",
"name": "LMS Settings",
@@ -425,6 +425,16 @@
"read": 1,
"role": "LMS Student",
"share": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"role": "Moderator",
"share": 1,
"write": 1
}
],
"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") }}
</p>
{% if medium %}
<p>
<b>{{ _("Medium:") }}</b> {{ medium }}
</p>
{% endif %}
<p>
<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"
"@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"
resolved "https://registry.yarnpkg.com/@codemirror/view/-/view-6.36.7.tgz#f3711d3fea457a3eec1c09a76b8a132f98df17b1"
integrity sha512-kCWGW/chWGPgZqfZ36Um9Iz0X2IVpmCjg1P/qY6B6a2ecXtWRRAigmpJ6YgUQ5lTWXMyyVdfmpzhLZmsZQMbtg==
version "6.36.8"
resolved "https://registry.yarnpkg.com/@codemirror/view/-/view-6.36.8.tgz#d72e59d2b2d99bb5f177178f63e11cef6e605b94"
integrity sha512-yoRo4f+FdnD01fFt4XpfpMCcCAo9QvZOtbrXExn4SqzH32YC6LgzqxfLZw/r6Ge65xyY03mK/UfUqrVw1gFiFg==
dependencies:
"@codemirror/state" "^6.5.0"
style-mod "^4.1.0"
@@ -496,17 +496,17 @@
mlly "^1.7.4"
"@inquirer/confirm@^5.0.0":
version "5.1.9"
resolved "https://registry.yarnpkg.com/@inquirer/confirm/-/confirm-5.1.9.tgz#c858b6a3decb458241ec36ca9a9117477338076a"
integrity sha512-NgQCnHqFTjF7Ys2fsqK2WtnA8X1kHyInyG+nMIuHowVTIgIuS10T4AznI/PvbqSpJqjCUqNBlKGh1v3bwLFL4w==
version "5.1.10"
resolved "https://registry.yarnpkg.com/@inquirer/confirm/-/confirm-5.1.10.tgz#de3732cb7ae9333bd3e354afee6a6ef8cf28d951"
integrity sha512-FxbQ9giWxUWKUk2O5XZ6PduVnH2CZ/fmMKMBkH71MHJvWr7WL5AHKevhzF1L5uYWB2P548o1RzVxrNd3dpmk6g==
dependencies:
"@inquirer/core" "^10.1.10"
"@inquirer/core" "^10.1.11"
"@inquirer/type" "^3.0.6"
"@inquirer/core@^10.1.10":
version "10.1.10"
resolved "https://registry.yarnpkg.com/@inquirer/core/-/core-10.1.10.tgz#222a374e3768536a1eb0adf7516c436d5f4a291d"
integrity sha512-roDaKeY1PYY0aCqhRmXihrHjoSW2A00pV3Ke5fTpMCkzcGF64R8e0lw3dK+eLEHwS4vB5RnW1wuQmvzoRul8Mw==
"@inquirer/core@^10.1.11":
version "10.1.11"
resolved "https://registry.yarnpkg.com/@inquirer/core/-/core-10.1.11.tgz#4022032b5b6b35970e1c3fcfc522bc250ef8810d"
integrity sha512-BXwI/MCqdtAhzNQlBEFE7CEflhPkl/BqvAuV/aK6lW3DClIfYVDWPP/kXuXHtBWC7/EEbNqd/1BGq2BGBBnuxw==
dependencies:
"@inquirer/figures" "^1.0.11"
"@inquirer/type" "^3.0.6"
@@ -1170,9 +1170,9 @@
integrity sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==
"@types/node@*":
version "22.15.16"
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.15.16.tgz#685cf0338ad9f5b14860f50a6ac2c3ebd58582cd"
integrity sha512-3pr+KjwpVujqWqOKT8mNR+rd09FqhBLwg+5L/4t0cNYBzm/yEiYGCxWttjaPBsLtAo+WFNoXzGJfolM1JuRXoA==
version "22.15.17"
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.15.17.tgz#355ccec95f705b664e4332bb64a7f07db30b7055"
integrity sha512-wIX2aSZL5FE+MR0JlvF87BNVrtFWf6AE6rxSE9X7OwnVvoyCQjpzSRJ+M87se/4QCkCiebQAqrJ0y6fwIyi7nw==
dependencies:
undici-types "~6.21.0"
@@ -1224,9 +1224,9 @@
integrity sha512-kqf7SGFoG+80aZG6Pf+gsZIVvGSCKE98JbiWqcCV9cThtg91Jav0yvYFC9Zb+jKetNGF6ZKeoaxgZfND21fWKw==
"@vitejs/plugin-vue@^5.0.3":
version "5.2.3"
resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-5.2.3.tgz#71a8fc82d4d2e425af304c35bf389506f674d89b"
integrity sha512-IYSLEQj4LgZZuoVpdSUCw3dIynTWQgPlaRP6iAvMle4My0HdYwr5g5wQAfwOeHQBmYwEkqF70nRpSilr6PoUDg==
version "5.2.4"
resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz#9e8a512eb174bfc2a333ba959bbf9de428d89ad8"
integrity sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==
"@vitest/expect@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==
caniuse-lite@^1.0.30001702, caniuse-lite@^1.0.30001716:
version "1.0.30001717"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001717.tgz#5d9fec5ce09796a1893013825510678928aca129"
integrity sha512-auPpttCq6BDEG8ZAuHJIplGw6GODhjw+/11e7IjpnYCxZcW/ONgPs0KVBJ0d1bY3e2+7PRe5RCLyP+PfwVgkYw==
version "1.0.30001718"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001718.tgz#dae13a9c80d517c30c6197515a96131c194d8f82"
integrity sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==
capital-case@^1.0.4:
version "1.0.4"
@@ -2061,7 +2061,7 @@ cross-spawn@^5.0.1:
shebang-command "^1.2.0"
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"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f"
integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==
@@ -2322,9 +2322,9 @@ ee-first@1.1.1:
integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==
electron-to-chromium@^1.5.149:
version "1.5.151"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.151.tgz#5edd6c17e1b2f14b4662c41b9379f96cc8c2bb7c"
integrity sha512-Rl6uugut2l9sLojjS4H4SAr3A4IgACMLgpuEMPYCVcKydzfyPrn5absNRju38IhQOf/NwjJY8OGWjlteqYeBCA==
version "1.5.152"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.152.tgz#bcdd39567e291b930ec26b930031137a05593695"
integrity sha512-xBOfg/EBaIlVsHipHl2VdTPJRSvErNUaqW8ejTq5OlOlIYx1wOllCHsAvAIrr55jD1IYEfdR86miUEt8H5IeJg==
emoji-regex@^10.3.0:
version "10.4.0"
@@ -2541,21 +2541,6 @@ execa@4.1.0:
signal-exit "^3.0.2"
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:
version "4.1.1"
resolved "https://registry.yarnpkg.com/executable/-/executable-4.1.1.tgz#41532bff361d3e57af4d763b70582db18f5d133c"
@@ -2677,9 +2662,9 @@ finalhandler@1.1.2:
unpipe "~1.0.0"
flexsearch@*:
version "0.8.161"
resolved "https://registry.yarnpkg.com/flexsearch/-/flexsearch-0.8.161.tgz#fe964fe1841a99b580ba19f853603d1747fb3bc8"
integrity sha512-3dlwyJpl7PN4vRTp0pV8FkCULPzeuK6JzZYE3KhLiv5vbhUPGUqiOWgBbjlpCC4Rl1aW7B0lJeOS0Cr8G0N12g==
version "0.8.164"
resolved "https://registry.yarnpkg.com/flexsearch/-/flexsearch-0.8.164.tgz#2d1277249d6dec8eb745358fa64543ddbddc9e05"
integrity sha512-tauQG+NlwSWW6uL3BIJdHNEXYiei2xTR3H/mrjNadAOwvXgiLbLvLzQWSJvD31Yn0+1lAxG2NVRndywGnDZZiA==
flexsearch@0.7.21:
version "0.7.21"
@@ -2835,11 +2820,6 @@ get-stream@^5.0.0, get-stream@^5.1.0:
dependencies:
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:
version "3.2.1"
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"
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:
version "0.6.3"
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"
idb-keyval@^6.2.0:
version "6.2.1"
resolved "https://registry.yarnpkg.com/idb-keyval/-/idb-keyval-6.2.1.tgz#94516d625346d16f56f3b33855da11bfded2db33"
integrity sha512-8Sb3veuYCyrZL+VBt9LJfZjLUPWVvqn8tG28VqYNFCo43KHcKuq+b4EiXGeuaLAQWL2YmyDgMp2aSpH9JHsEQg==
version "6.2.2"
resolved "https://registry.yarnpkg.com/idb-keyval/-/idb-keyval-6.2.2.tgz#b0171b5f73944854a3291a5cdba8e12768c4854a"
integrity sha512-yjD9nARJ/jb1g+CvD0tlhUHOrJ9Sy0P8T9MF3YaLlHnSRpwPfpTX0XIvpmw3gAJUmEu3FiICLBDPXVwyEvrleg==
ieee754@^1.1.13:
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"
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:
version "1.0.0"
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==
lint-staged@>=10:
version "15.5.2"
resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-15.5.2.tgz#beff028fd0681f7db26ffbb67050a21ed4d059a3"
integrity sha512-YUSOLq9VeRNAo/CTaVmhGDKG+LBtA8KF1X4K5+ykMSwWST1vDxJRB2kv2COgLb1fvpCo+A/y9A0G0znNVmdx4w==
version "16.0.0"
resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-16.0.0.tgz#31826709bde6a62542431da3055f038e386a20db"
integrity sha512-sUCprePs6/rbx4vKC60Hez6X10HPkpDJaGcy3D1NdwR7g1RcNkWL8q9mJMreOqmHBTs+1sNFp+wOiX9fr+hoOQ==
dependencies:
chalk "^5.4.1"
commander "^13.1.0"
debug "^4.4.0"
execa "^8.0.1"
lilconfig "^3.1.3"
listr2 "^8.2.5"
listr2 "^8.3.3"
micromatch "^4.0.8"
nano-spawn "^1.0.0"
pidtree "^0.6.0"
string-argv "^0.3.2"
yaml "^2.7.0"
yaml "^2.7.1"
listr2@^3.8.3:
version "3.14.0"
@@ -3378,7 +3348,7 @@ listr2@^3.8.3:
through "^2.3.8"
wrap-ansi "^7.0.0"
listr2@^8.2.5:
listr2@^8.3.3:
version "8.3.3"
resolved "https://registry.yarnpkg.com/listr2/-/listr2-8.3.3.tgz#815fc8f738260ff220981bf9e866b3e11e8121bf"
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"
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:
version "5.0.1"
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==
msw@^2.7.0:
version "2.7.6"
resolved "https://registry.yarnpkg.com/msw/-/msw-2.7.6.tgz#1471ce4311f4c173f287dced31dee211b6958deb"
integrity sha512-P+rwn43ktxN8ghcl8q+hSAUlEi0PbJpDhGmDkw4zeUnRj3hBCVynWD+dTu38yLYKCE9ZF1OYcvpy7CTBRcqkZA==
version "2.8.2"
resolved "https://registry.yarnpkg.com/msw/-/msw-2.8.2.tgz#a35545275403da472f4ed2152cd3c77000db544c"
integrity sha512-ugu8RBgUj6//RD0utqDDPdS+QIs36BKYkDAM6u59hcMVtFM4PM0vW4l3G1R+1uCWP2EWFUG8reT/gPXVEtx7/w==
dependencies:
"@bundled-es-modules/cookie" "^2.0.1"
"@bundled-es-modules/statuses" "^1.0.1"
@@ -3702,6 +3667,11 @@ mz@^2.7.0:
object-assign "^4.0.1"
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:
version "3.3.11"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b"
@@ -3742,13 +3712,6 @@ npm-run-path@^4.0.0:
dependencies:
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:
version "2.2.20"
resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.20.tgz#22e53253c61e7b0e7e93cef42c891154bcca11ef"
@@ -3795,13 +3758,6 @@ onetime@^5.1.0:
dependencies:
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:
version "7.0.0"
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"
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:
version "1.0.7"
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"
semver@^7.5.3:
version "7.7.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.1.tgz#abd5098d82b18c6c81f6074ff2647fd3e7220c9f"
integrity sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==
version "7.7.2"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.2.tgz#67d99fdcd35cec21e6f8b87a7fd515a33f982b58"
integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==
sentence-case@^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"
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:
version "4.1.2"
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@^2.2.0, unplugin@^2.3.2:
version "2.3.2"
resolved "https://registry.yarnpkg.com/unplugin/-/unplugin-2.3.2.tgz#36c93a1662b70c97a2e2fc45c0e78fa09f7a4984"
integrity sha512-3n7YA46rROb3zSj8fFxtxC/PqoyvYQ0llwz9wtUPUutr9ig09C8gGo5CWCwHrUzlqC1LLR43kxp5vEIyH1ac1w==
version "2.3.3"
resolved "https://registry.yarnpkg.com/unplugin/-/unplugin-2.3.3.tgz#f83507e4484008e400f3d831a628eaede22c954f"
integrity sha512-DN4DgiS13HFrAapoRmmoa9B35QzmQVRH2k58HelO28htXVNEEFZ8CGlZL0aRHXOXtz9McwY6lqaZjcc15uWMow==
dependencies:
acorn "^8.14.1"
picomatch "^4.0.2"
@@ -5563,7 +5509,7 @@ yallist@^2.1.2:
resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"
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"
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.7.1.tgz#44a247d1b88523855679ac7fa7cda6ed7e135cf6"
integrity sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==