feat: batch creation

This commit is contained in:
Jannat Patel
2024-03-15 21:54:02 +05:30
parent 83a1b03bb7
commit 63bcbb6506
33 changed files with 1536 additions and 887 deletions

View File

@@ -12,12 +12,13 @@
"@editorjs/editorjs": "^2.29.0",
"@editorjs/embed": "^2.7.0",
"@editorjs/header": "^2.8.1",
"@editorjs/list": "^1.9.0",
"@editorjs/image": "^2.9.0",
"@editorjs/nested-list": "^1.4.2",
"@editorjs/paragraph": "^2.11.3",
"chart.js": "^4.4.1",
"dayjs": "^1.11.6",
"feather-icons": "^4.28.0",
"frappe-ui": "^0.1.31",
"frappe-ui": "^0.1.35",
"lucide-vue-next": "^0.309.0",
"markdown-it": "^14.0.0",
"pinia": "^2.0.33",

View File

@@ -0,0 +1,141 @@
<template>
<div>
<div class="flex items-center justify-between mb-4">
<div class="text-xl font-semibold">
{{ __('Courses') }}
</div>
<Button
v-if="user.data?.is_moderator"
variant="solid"
@click="openCourseModal()"
>
<template #prefix>
<Plus class="h-4 w-4" />
</template>
{{ __('Add Course') }}
</Button>
</div>
<div v-if="courses.data?.length">
<ListView
:columns="getCoursesColumns()"
:rows="courses.data"
row-key="name"
:options="{ showTooltip: false }"
>
<ListHeader
class="mb-2 grid items-center space-x-4 rounded bg-gray-100 p-2"
>
<ListHeaderItem :item="item" v-for="item in getCoursesColumns()">
<template #prefix="{ item }">
<component
v-if="item.icon"
:is="item.icon"
class="h-4 w-4 stroke-1.5 ml-4"
/>
</template>
</ListHeaderItem>
</ListHeader>
<ListRows>
<ListRow :row="row" v-for="row in courses.data">
<template #default="{ column, item }">
<ListRowItem :item="row[column.key]" :align="column.align">
<div>
{{ row[column.key] }}
</div>
</ListRowItem>
</template>
</ListRow>
</ListRows>
<ListSelectBanner>
<template #actions="{ unselectAll, selections }">
<div class="flex gap-2">
<Button variant="ghost" @click="removeCourses(selections)">
<Trash2 class="h-4 w-4 stroke-1.5" />
</Button>
</div>
</template>
</ListSelectBanner>
</ListView>
</div>
<BatchCourseModal
v-model="showCourseModal"
:batch="batch"
v-model:courses="courses"
/>
</div>
</template>
<script setup>
import { ref, inject } from 'vue'
import BatchCourseModal from '@/components/Modals/BatchCourseModal.vue'
import {
createResource,
Button,
ListHeader,
ListHeaderItem,
ListSelectBanner,
ListRow,
ListRows,
ListView,
ListRowItem,
} from 'frappe-ui'
import { Plus, Trash2 } from 'lucide-vue-next'
const showCourseModal = ref(false)
const user = inject('$user')
const props = defineProps({
batch: {
type: String,
required: true,
},
})
const courses = createResource({
url: 'lms.lms.utils.get_batch_courses',
params: {
batch: props.batch,
},
cache: ['batchCourses', props.batchName],
auto: true,
})
const openCourseModal = () => {
showCourseModal.value = true
}
const getCoursesColumns = () => {
return [
{
label: 'Title',
key: 'title',
},
{
label: 'Lessons',
key: 'lesson_count',
},
{
label: 'Enrollments',
key: 'enrollment_count',
},
]
}
const removeCourse = createResource({
url: 'frappe.client.delete',
makeParams(values) {
return {
doctype: 'Batch Course',
name: values.course,
}
},
})
const removeCourses = (selections) => {
console.log(selections)
selections.forEach(async (course) => {
removeCourse.submit({ course })
await setTimeout(1000)
})
courses.reload()
}
</script>

View File

@@ -73,11 +73,21 @@
>
{{ __('Enroll Now') }}
</Button>
<Button v-if="user?.data?.is_moderator" class="w-full mt-2">
<span>
{{ __('Edit') }}
</span>
</Button>
<router-link
v-if="user?.data?.is_moderator"
:to="{
name: 'BatchCreation',
params: {
batchName: batch.data.name,
},
}"
>
<Button class="w-full mt-2">
<span>
{{ __('Edit') }}
</span>
</Button>
</router-link>
</div>
</template>
<script setup>

View File

@@ -55,16 +55,14 @@
<Button variant="ghost" @click="removeStudents(selections)">
<Trash2 class="h-4 w-4 stroke-1.5" />
</Button>
<Button
variant="ghost"
label="Unselect all"
@click="unselectAll.toString()"
/>
</div>
</template>
</ListSelectBanner>
</ListView>
</div>
<div v-else class="text-sm italic text-gray-600">
{{ __('There are no students in this batch.') }}
</div>
<StudentModal
:batch="props.batch"
v-model="showStudentModal"

View File

@@ -103,8 +103,8 @@ import { computed, inject } from 'vue'
import { Button, createResource } from 'frappe-ui'
import { createToast } from '@/utils/'
import { useRouter } from 'vue-router'
const router = useRouter()
const router = useRouter()
const user = inject('$user')
const props = defineProps({

View File

@@ -4,10 +4,10 @@
v-if="title && (outline.data?.length || allowEdit)"
class="flex items-center justify-between mb-4"
>
<div class="text-lg font-semibold">
<div class="font-semibold" :class="allowEdit ? 'text-base' : 'text-lg'">
{{ __(title) }}
</div>
<Button v-if="allowEdit" @click="openChapterModal()">
<Button size="sm" v-if="allowEdit" @click="openChapterModal()">
{{ __('Add Chapter') }}
</Button>
<!-- <span class="font-medium cursor-pointer" @click="expandAllChapters()">

View File

@@ -0,0 +1,99 @@
<template>
<div v-if="youtube">
<iframe
class="youtube-video"
:src="getYouTubeVideoSource(youtube)"
width="100%"
height="400"
frameborder="0"
allowfullscreen
></iframe>
</div>
<div v-for="block in content.split('\n\n')">
<div v-if="block.includes('{{ YouTubeVideo')">
<iframe
class="youtube-video"
:src="getYouTubeVideoSource(block)"
width="100%"
height="400"
frameborder="0"
allowfullscreen
></iframe>
</div>
<div v-else-if="block.includes('{{ Quiz')">
<Quiz :quiz="getId(block)" />
</div>
<div v-else-if="block.includes('{{ Video')">
<video controls width="100%" controlsList="nodownload">
<source :src="getId(block)" type="video/mp4" />
</video>
</div>
<div v-else-if="block.includes('{{ PDF')">
<iframe
:src="getPDFSource(block)"
width="100%"
height="400"
frameborder="0"
allowfullscreen
></iframe>
</div>
<div v-else-if="block.includes('{{ Audio')">
<audio width="100%" controls controlsList="nodownload">
<source :src="getId(block)" type="audio/mp3" />
</audio>
</div>
<div v-else-if="block.includes('{{ Embed')">
<iframe
width="100%"
height="400"
:src="getId(block)"
frameborder="0"
allowfullscreen
>
</iframe>
</div>
<div v-else v-html="markdown.render(block)"></div>
</div>
<div v-if="quizId">
<Quiz :quiz="quizId" />
</div>
</template>
<script setup>
import Quiz from '@/components/QuizBlock.vue'
import MarkdownIt from 'markdown-it'
const markdown = new MarkdownIt({
html: true,
linkify: true,
})
const props = defineProps({
content: {
type: String,
required: true,
},
youtube: {
type: String,
required: false,
},
quizId: {
type: String,
required: false,
},
})
const getYouTubeVideoSource = (block) => {
if (block.includes('{{')) {
block = getId(block)
}
return `https://www.youtube.com/embed/${block}`
}
const getPDFSource = (block) => {
return `${getId(block)}#toolbar=0`
}
const getId = (block) => {
return block.match(/\(["']([^"']+?)["']\)/)[1]
}
</script>

View File

@@ -0,0 +1,137 @@
<template>
<div class="text-lg font-semibold">
{{ __('Components') }}
</div>
<div class="mt-5">
<div class="">
<div class="text-xs text-gray-600 mb-1">
{{ __('Select an Editor') }}
</div>
<Select v-model="currentEditor" :options="getEditorOptions()" />
</div>
<div class="flex mt-4">
<Link
v-model="quiz"
class="flex-1"
doctype="LMS Quiz"
:label="__('Select a Quiz')"
/>
<Button @click="addQuiz()" class="self-end ml-2">
<template #icon>
<Plus class="h-4 w-4 stroke-1.5" />
</template>
</Button>
</div>
<div class="mt-4">
<div class="text-xs text-gray-600 mb-1">
{{ __('Add an image, video, pdf or audio.') }}
</div>
<div class="flex">
<FileUploader
v-if="!file"
:fileTypes="['image/*', 'video/*', 'audio/*', '.pdf']"
:validateFile="validateFile"
@success="(data) => addFile(data)"
>
<template v-slot="{ file, progress, uploading, openFileSelector }">
<div class="">
<Button @click="openFileSelector" :loading="uploading">
{{
uploading
? __('Uploading {0}%').format(progress)
: __('Upload an File')
}}
</Button>
</div>
</template>
</FileUploader>
<div v-else class="">
<div class="flex items-center">
<div class="border rounded-md p-2 mr-2">
<FileText class="h-4 w-4 stroke-1.5 text-gray-700" />
</div>
<div class="flex flex-col">
<span class="text-xs">
{{ file.file_name }}
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import Link from '@/components/Controls/Link.vue'
import { FileUploader, Button, Select } from 'frappe-ui'
import { Plus, FileText } from 'lucide-vue-next'
import { ref, watch } from 'vue'
const quiz = ref(null)
const file = ref(null)
const lessonEditor = ref(null)
const instructorEditor = ref(null)
const currentEditor = ref('Lesson Content')
const props = defineProps({
editor: {
required: true,
},
notesEditor: {
required: true,
},
})
const addQuiz = () => {
getCurrentEditor().caret.setToLastBlock('end', 0)
if (quiz.value) {
getCurrentEditor().blocks.insert('quiz', {
quiz: quiz.value,
})
quiz.value = null
}
}
const addFile = (data) => {
getCurrentEditor().caret.setToLastBlock('end', 0)
getCurrentEditor().blocks.insert('upload', data)
}
const getBlocksCount = () => {
return getCurrentEditor().blocks.getBlocksCount
}
const validateFile = (file) => {
let extension = file.name.split('.').pop().toLowerCase()
if (!['jpg', 'jpeg', 'png', 'mp4', 'mov', 'mp3'].includes(extension)) {
return 'Only image and video files are allowed.'
}
}
const getEditorOptions = () => {
return [
{
label: 'Lesson Content',
value: 'Lesson Content',
},
{
label: 'Instructor Content',
value: 'Instructor Content',
},
]
}
const getCurrentEditor = () => {
return currentEditor.value == 'Lesson Content'
? lessonEditor.value
: instructorEditor.value
}
watch(
() => [props.editor, props.notesEditor],
([newEditor, newNotesEditor], [oldEditor, oldNotesEditor]) => {
lessonEditor.value = newEditor
instructorEditor.value = newNotesEditor
}
)
</script>

View File

@@ -9,7 +9,7 @@
<Plus class="h-4 w-4" />
</template>
<span>
{{ __('Create') }}
{{ __('Add Live Class') }}
</span>
</Button>
<div class="text-lg font-semibold mb-4">
@@ -88,7 +88,7 @@ const props = defineProps({
const liveClasses = createListResource({
doctype: 'LMS Live Class',
filters: {
batch: props.batchName,
batch_name: props.batch,
date: ['>=', new Date()],
},
fields: [

View File

@@ -0,0 +1,68 @@
<template>
<Dialog
v-model="show"
:options="{
title: __('Add a course'),
size: 'sm',
actions: [
{
label: __('Submit'),
variant: 'solid',
onClick: (close) => addCourse(close),
},
],
}"
>
<template #body-content>
<Link doctype="LMS Course" v-model="course" />
</template>
</Dialog>
</template>
<script setup>
import { Dialog, createResource } from 'frappe-ui'
import { ref, defineModel } from 'vue'
import Link from '@/components/Controls/Link.vue'
import { showToast } from '@/utils'
const show = defineModel()
const course = ref(null)
const courses = defineModel('courses')
const props = defineProps({
batch: {
type: String,
default: null,
},
})
const createBatchCourse = createResource({
url: 'frappe.client.insert',
makeParams(values) {
return {
doc: {
doctype: 'Batch Course',
parent: props.batch,
parenttype: 'LMS Batch',
parentfield: 'courses',
course: course.value,
},
}
},
})
const addCourse = (close) => {
createBatchCourse.submit(
{},
{
onSuccess() {
courses.value.reload()
close()
course.value = null
},
onError(err) {
showToast('Error', err.message[0] || err, 'x')
},
}
)
}
</script>

View File

@@ -1,192 +0,0 @@
<template>
<Dialog
v-model="show"
:options="{
title: __('Create a Batch'),
size: '3xl',
actions: [
{
label: __('Save'),
variant: 'solid',
onClick: (close) => createBatch(close),
},
],
}"
>
<template #body-content>
<div>
<div class="grid grid-cols-3 gap-4">
<div>
<FormControl
v-model="batch.title"
:label="__('Title')"
class="mb-4"
/>
<FormControl
v-model="batch.published"
type="checkbox"
:label="__('Published')"
/>
</div>
<div>
<FormControl
v-model="batch.start_date"
:label="__('Start Date')"
type="date"
class="mb-4"
/>
<FormControl
v-model="batch.end_date"
:label="__('End Date')"
type="date"
class="mb-4"
/>
</div>
<div>
<FormControl
v-model="batch.start_time"
:label="__('Start Time')"
type="time"
class="mb-4"
/>
<FormControl
v-model="batch.end_time"
:label="__('End Time')"
type="time"
class="mb-4"
/>
</div>
</div>
<div class="grid grid-cols-3 gap-4 mt-4 border-t pt-4">
<div>
<FormControl
v-model="batch.seat_count"
:label="__('Seat Count')"
type="number"
class="mb-4"
/>
<FormControl
v-model="batch.evaluation_end_date"
:label="__('Evaluation End Date')"
type="date"
class="mb-4"
/>
</div>
<div>
<FormControl
v-model="batch.medium"
:label="__('Medium')"
class="mb-4"
/>
<FormControl
v-model="batch.category"
:label="__('Category')"
class="mb-4"
/>
</div>
<div>
<FileUploader
v-if="!batch.meta_image"
:fileTypes="['image/*']"
:validateFile="validateFile"
@success="
(file) => {
batch.meta_image.value = file
}
"
>
<template
v-slot="{ file, progress, uploading, openFileSelector }"
>
<div class="mb-4">
<Button @click="openFileSelector" :loading="uploading">
{{
uploading ? `Uploading ${progress}%` : 'Upload an image'
}}
</Button>
</div>
</template>
</FileUploader>
</div>
</div>
<div class="border-t pt-4 mb-4">
<FormControl
v-model="batch.paid_batch"
type="checkbox"
:label="__('Paid Batch')"
/>
<FormControl
v-model="batch.amount"
:label="__('Amount')"
type="number"
class="my-4"
/>
<Link
doctype="Currency"
v-model="batch.currency"
:filters="{ enabled: 1 }"
:label="__('Currency')"
/>
</div>
<div class="grid grid-cols-2 gap-4 border-y pt-4 mb-4"></div>
<FormControl
v-model="batch.description"
:label="__('Description')"
type="textarea"
class="mb-4"
/>
<div>
<label class="block text-sm text-gray-600 mb-1">
{{ __('Batch Details') }}
</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-gray-100 rounded-b-md py-1 px-2 min-h-[7rem] mb-4"
/>
</div>
<FormControl
v-model="batch.batch_details_raw"
:label="__('Batch Details Raw')"
type="textarea"
class="mb-4"
/>
</div>
</template>
</Dialog>
</template>
<script setup>
import {
Dialog,
FormControl,
TextEditor,
FileUploader,
Button,
} from 'frappe-ui'
import { reactive, defineModel } from 'vue'
import Link from '@/components/Controls/Link.vue'
const show = defineModel()
const batch = reactive({
title: '',
published: false,
start_date: '',
end_date: '',
start_time: '',
end_time: '',
medium: '',
category: '',
seat_count: 0,
evaluation_end_date: '',
description: '',
batch_details: '',
batch_details_raw: '',
meta_image: '',
paid_batch: false,
amount: 0,
currency: '',
})
</script>

View File

@@ -3,7 +3,7 @@
v-model="show"
:options="{
title: __('Add a Student'),
size: 'xl',
size: 'sm',
actions: [
{
label: 'Submit',

View File

@@ -45,25 +45,7 @@
<template #default="{ tab }">
<div class="pt-5 px-10 pb-10">
<div v-if="tab.label == 'Courses'">
<div class="text-xl font-semibold">
{{ __('Courses') }}
</div>
<div
class="grid grid-cols-1 sm:grid-cols-1 md:grid-cols-2 gap-8 mt-5"
>
<div v-for="course in courses.data">
<router-link
:to="{
name: 'CourseDetail',
params: {
courseName: course.name,
},
}"
>
<CourseCard :key="course.name" :course="course" />
</router-link>
</div>
</div>
<BatchCourses :batch="batch.data.name" />
</div>
<div v-else-if="tab.label == 'Dashboard'">
<BatchDashboard :batch="batch" :isStudent="isStudent" />
@@ -94,9 +76,10 @@
</Tabs>
</div>
<div class="p-5">
<div class="text-2xl font-semibold mb-3">
<div class="text-2xl font-semibold mb-2">
{{ batch.data.title }}
</div>
<div v-html="batch.data.description" class="leading-5 mb-4"></div>
<div class="flex items-center mb-3">
<Calendar class="h-4 w-4 stroke-1.5 mr-2 text-gray-700" />
<span>
@@ -111,7 +94,6 @@
{{ formatTime(batch.data.end_time) }}
</span>
</div>
<div v-html="batch.data.description"></div>
</div>
<AnnouncementModal
v-model="showAnnouncementModal"
@@ -180,8 +162,8 @@ import {
MessageCircle,
} from 'lucide-vue-next'
import { formatTime } from '@/utils'
import CourseCard from '@/components/CourseCard.vue'
import BatchDashboard from '@/components/BatchDashboard.vue'
import BatchCourses from '@/components/BatchCourses.vue'
import LiveClass from '@/components/LiveClass.vue'
import BatchStudents from '@/components/BatchStudents.vue'
import Assessments from '@/components/Assessments.vue'
@@ -213,7 +195,7 @@ const breadcrumbs = computed(() => {
let crumbs = [{ label: 'All Batches', route: { name: 'Batches' } }]
if (!isStudent.value) {
crumbs.push({
label: batch.data?.title,
label: 'Details',
route: {
name: 'BatchDetail',
params: {
@@ -275,15 +257,6 @@ const tabs = computed(() => {
return batchTabs
})
const courses = createResource({
url: 'lms.lms.utils.get_batch_courses',
params: {
batch: props.batchName,
},
cache: ['batchCourses', props.batchName],
auto: true,
})
const redirectToLogin = () => {
window.location.href = `/login?redirect-to=/batches`
}

View File

@@ -0,0 +1,390 @@
<template>
<div class="h-screen text-base">
<header
class="sticky top-0 z-10 flex items-center justify-between border-b bg-white px-3 py-2.5 sm:px-5"
>
<Breadcrumbs class="h-7" :items="breadcrumbs" />
<Button variant="solid" @click="saveBatch()">
{{ __('Save') }}
</Button>
</header>
<div class="py-5">
<div class="container">
<div class="text-lg font-semibold mb-4">
{{ __('Details') }}
</div>
<div class="grid grid-cols-2 gap-10">
<div>
<FormControl
v-model="batch.title"
:label="__('Title')"
class="mb-4"
/>
<FormControl
v-model="batch.description"
:label="__('Description')"
type="textarea"
class="mb-4"
/>
</div>
<div>
<FormControl
v-model="batch.published"
type="checkbox"
:label="__('Published')"
/>
<FileUploader
v-if="!batch.image"
class="mt-4"
:fileTypes="['image/*']"
:validateFile="validateFile"
@success="(file) => saveImage(file)"
>
<template
v-slot="{ file, progress, uploading, openFileSelector }"
>
<div class="mb-4">
<Button @click="openFileSelector" :loading="uploading">
{{
uploading ? `Uploading ${progress}%` : 'Upload an image'
}}
</Button>
</div>
</template>
</FileUploader>
<div v-else class="mt-4">
<div class="text-xs text-gray-600 mb-1">
{{ __('Meta Image') }}
</div>
<div class="flex items-center">
<div class="border rounded-md p-2 mr-2">
<FileText class="h-5 w-5 stroke-1.5 text-gray-700" />
</div>
<div class="flex flex-col">
<span>
{{ batch.image.file_name }}
</span>
<span class="text-sm text-gray-500 mt-1">
{{ getFileSize(batch.image.file_size) }}
</span>
</div>
<X
@click="removeImage()"
class="bg-gray-200 rounded-md cursor-pointer stroke-1.5 w-5 h-5 p-1 ml-4"
/>
</div>
</div>
</div>
</div>
</div>
<div class="container border-b mb-5">
<div>
<label class="block text-sm text-gray-600 mb-1">
{{ __('Batch Details') }}
</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-gray-100 rounded-b-md py-1 px-2 min-h-[7rem] mb-4"
/>
</div>
</div>
<div class="container border-b mb-5">
<div class="text-lg font-semibold mb-4">
{{ __('Settings') }}
</div>
<div class="grid grid-cols-2 gap-10">
<div>
<FormControl
v-model="batch.start_date"
:label="__('Start Date')"
type="date"
class="mb-4"
/>
<FormControl
v-model="batch.end_date"
:label="__('End Date')"
type="date"
class="mb-4"
/>
<FormControl
v-model="batch.start_time"
:label="__('Start Time')"
type="time"
class="mb-4"
/>
<FormControl
v-model="batch.end_time"
:label="__('End Time')"
type="time"
class="mb-4"
/>
</div>
<div>
<FormControl
v-model="batch.seat_count"
:label="__('Seat Count')"
type="number"
class="mb-4"
/>
<FormControl
v-model="batch.evaluation_end_date"
:label="__('Evaluation End Date')"
type="date"
class="mb-4"
/>
<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>
</div>
<div class="container">
<div class="text-lg font-semibold mb-4">
{{ __('Payment') }}
</div>
<div>
<FormControl
v-model="batch.paid_batch"
type="checkbox"
:label="__('Paid Batch')"
/>
<FormControl
v-model="batch.amount"
:label="__('Amount')"
type="number"
class="my-4"
/>
<Link
doctype="Currency"
v-model="batch.currency"
:filters="{ enabled: 1 }"
:label="__('Currency')"
/>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { computed, onMounted, inject, reactive } from 'vue'
import {
Breadcrumbs,
FormControl,
FileUploader,
Button,
TextEditor,
createResource,
} from 'frappe-ui'
import Link from '@/components/Controls/Link.vue'
import { useRouter } from 'vue-router'
import { getFileSize } from '../utils'
import { X, FileText } from 'lucide-vue-next'
import { showToast } from '../utils'
const router = useRouter()
const user = inject('$user')
const props = defineProps({
batchName: {
type: String,
required: true,
},
})
const batch = reactive({
title: '',
published: false,
description: '',
batch_details: '',
start_date: '',
end_date: '',
start_time: '',
end_time: '',
evaluation_end_date: '',
seat_count: '',
medium: '',
category: '',
image: null,
paid_batch: false,
currency: '',
amount: 0,
})
onMounted(() => {
if (!user.data) window.location.href = '/login'
if (props.batchName != 'new') {
batchDetail.reload()
}
})
const newBatch = createResource({
url: 'frappe.client.insert',
makeParams(values) {
return {
doc: {
doctype: 'LMS Batch',
meta_image: batch.image.file_url,
...batch,
},
}
},
})
const batchDetail = createResource({
url: 'frappe.client.get',
makeParams(values) {
return {
doctype: 'LMS Batch',
name: props.batchName,
}
},
onSuccess(data) {
Object.keys(data).forEach((key) => {
if (Object.hasOwn(batch, key)) batch[key] = data[key]
})
let checkboxes = ['published', 'paid_batch']
for (let idx in checkboxes) {
let key = checkboxes[idx]
batch[key] = batch[key] ? true : false
}
if (data.meta_image) imageResource.reload({ image: data.meta_image })
},
})
const editBatch = createResource({
url: 'frappe.client.set_value',
makeParams(values) {
return {
doctype: 'LMS Batch',
name: props.batchName,
fieldname: {
meta_image: batch.image.file_url,
...batch,
},
}
},
})
const imageResource = createResource({
url: 'lms.lms.api.get_file_info',
makeParams(values) {
return {
file_url: values.image,
}
},
auto: false,
onSuccess(data) {
batch.image = data
},
})
const saveBatch = () => {
if (batchDetail.data) {
editBatchDetails()
} else {
createNewBatch()
}
}
const createNewBatch = () => {
newBatch.submit(
{},
{
onSuccess(data) {
router.push({
name: 'BatchDetail',
params: {
batchName: data.name,
},
})
},
onError(err) {
showToast('Error', err.messages?.[0] || err, 'x')
},
}
)
}
const editBatchDetails = () => {
editBatch.submit(
{},
{
onSuccess(data) {
router.push({
name: 'BatchDetail',
params: {
batchName: data.name,
},
})
},
onError(err) {
showToast('Error', err.messages?.[0] || err, 'x')
},
}
)
}
const saveImage = (file) => {
batch.image = file
}
const removeImage = () => {
batch.image = null
}
const validateFile = (file) => {
let extension = file.name.split('.').pop().toLowerCase()
if (!['jpg', 'jpeg', 'png'].includes(extension)) {
return 'Only image file is allowed.'
}
}
const breadcrumbs = computed(() => {
let crumbs = [
{
label: 'Batches',
route: {
name: 'Batches',
},
},
]
if (batchDetail.data) {
crumbs.push({
label: batchDetail.data.title,
route: {
name: 'BatchDetail',
params: {
batchName: props.batchName,
},
},
})
}
crumbs.push({
label: props.batchName == 'new' ? 'New Batch' : 'Edit Batch',
route: { name: 'BatchCreation', params: { batchName: props.batchName } },
})
return crumbs
})
</script>

View File

@@ -45,9 +45,11 @@
<BatchOverlay :batch="batch" />
</div>
</div>
<div>
<div class="text-2xl font-semibold mt-10">
{{ __('Courses') }}
<div v-if="batch.data.courses.length">
<div class="flex items-center mt-10">
<div class="text-2xl font-semibold">
{{ __('Courses') }}
</div>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-8 mt-5">
<div
@@ -78,10 +80,10 @@
</div>
</template>
<script setup>
import { Breadcrumbs, createResource } from 'frappe-ui'
import { Breadcrumbs, createResource, Button } from 'frappe-ui'
import { BookOpen, Calendar, Clock } from 'lucide-vue-next'
import { formatTime } from '../utils'
import { computed, inject } from 'vue'
import { computed, inject, ref } from 'vue'
import BatchOverlay from '@/components/BatchOverlay.vue'
import CourseCard from '@/components/CourseCard.vue'
import { useRouter } from 'vue-router'

View File

@@ -9,8 +9,9 @@
/>
<div class="flex">
<router-link
v-if="user.data"
:to="{
name: 'CreateBatch',
name: 'BatchCreation',
params: { batchName: 'new' },
}"
>
@@ -88,7 +89,6 @@ import BatchCard from '@/components/BatchCard.vue'
import { inject, ref, computed } from 'vue'
const user = inject('$user')
const showBatchModal = ref(false)
const batches = createListResource({
doctype: 'LMS Batch',

View File

@@ -31,7 +31,7 @@
v-if="courses.data.length == 0 && courses.list.loading"
class="p-5 text-base text-gray-700"
>
Loading Courses...
{{ __('Loading Courses...') }}
</div>
<Tabs
v-else

View File

@@ -1,37 +0,0 @@
<template>
<div class="h-screen text-base">
<header
class="sticky top-0 z-10 flex items-center justify-between border-b bg-white px-3 py-2.5 sm:px-5"
>
<Breadcrumbs class="h-7" :items="breadcrumbs" />
</header>
<div>Batch creation</div>
</div>
</template>
<script setup>
import { computed } from 'vue'
import { Breadcrumbs } from 'frappe-ui'
const props = defineProps({
batchName: {
type: String,
required: true,
},
})
const breadcrumbs = computed(() => {
let crumbs = [
{
label: 'Batches',
route: {
name: 'Batches',
},
},
]
crumbs.push({
label: props.batchName == 'new' ? 'New Batch' : 'Edit Batch',
route: { name: 'CreateBatch', params: { batchName: props.batchName } },
})
return crumbs
})
</script>

View File

@@ -8,10 +8,10 @@
<Breadcrumbs class="h-7" :items="breadcrumbs" />
<div class="flex items-center">
<router-link
v-if="courseResource.doc"
v-if="courseResource.data"
:to="{
name: 'CourseDetail',
params: { courseName: courseResource.doc.name },
params: { courseName: courseResource.data.name },
}"
>
<Button>
@@ -30,7 +30,7 @@
<div class="mt-5 mb-10">
<div class="container mb-5">
<div class="text-lg font-semibold mb-4">
{{ __('Course Details') }}
{{ __('Details') }}
</div>
<FormControl
v-model="course.title"
@@ -55,14 +55,10 @@
/>
</div>
<FileUploader
v-if="!image"
v-if="!course.course_image"
:fileTypes="['image/*']"
:validateFile="validateFile"
@success="
(file) => {
image = file
}
"
@success="(file) => saveImage(file)"
>
<template
v-slot="{ file, progress, uploading, openFileSelector }"
@@ -86,10 +82,10 @@
</div>
<div class="flex flex-col">
<span>
{{ image.file_name }}
{{ course.course_image.file_name }}
</span>
<span class="text-sm text-gray-500 mt-1">
{{ getFileSize(image.file_size) }}
{{ getFileSize(course.course_image.file_size) }}
</span>
</div>
<X
@@ -104,12 +100,12 @@
class="mb-4"
/>
<div>
<div class="mb-1.5 text-sm text-gray-700">
<div class="mb-1.5 text-xs text-gray-600">
{{ __('Tags') }}
</div>
<div class="flex items-center">
<div
v-for="tag in getTags"
v-for="tag in course.tags.split(', ')"
class="flex items-center bg-gray-100 p-2 rounded-md mr-2"
>
{{ tag }}
@@ -124,7 +120,7 @@
</div>
<div class="container border-t">
<div class="text-lg font-semibold mt-5 mb-4">
{{ __('Course Settings') }}
{{ __('Settings') }}
</div>
<div class="flex items-center justify-between mb-5">
<FormControl
@@ -146,7 +142,7 @@
</div>
<div class="container border-t">
<div class="text-lg font-semibold mt-5 mb-4">
{{ __('Course Pricing') }}
{{ __('Pricing') }}
</div>
<div class="mb-4">
<FormControl
@@ -172,9 +168,9 @@
<div class="border-l px-5 pt-5">
<!-- <CreateOutline v-if="courseResource.doc" :course="courseResource.doc"/> -->
<CourseOutline
v-if="courseResource.doc"
:courseName="courseResource.doc.name"
:title="courseResource.doc.title"
v-if="courseResource.data"
:courseName="courseResource.data.name"
:title="course.title"
:allowEdit="true"
/>
</div>
@@ -192,15 +188,13 @@ import {
FileUploader,
} from 'frappe-ui'
import { inject, onMounted, computed, ref, reactive } from 'vue'
import { convertToTitleCase, createToast, getFileSize } from '../utils'
import { convertToTitleCase, showToast, getFileSize } from '../utils'
import Link from '@/components/Controls/Link.vue'
import { FileText, X } from 'lucide-vue-next'
import CourseOutline from '@/components/CourseOutline.vue'
const user = inject('$user')
const tags = ref('')
const newTag = ref('')
const image = ref(null)
const props = defineProps({
courseName: {
@@ -208,84 +202,6 @@ const props = defineProps({
},
})
const breadcrumbs = computed(() => {
let crumbs = [
{
label: 'Courses',
route: { name: 'Courses' },
},
]
if (courseResource.doc) {
crumbs.push({
label: courseResource.doc?.title,
route: { name: 'CourseDetail', params: { courseName: props.courseName } },
})
}
crumbs.push({
label: props.courseName == 'new' ? 'New Course' : 'Edit Course',
route: { name: 'CreateCourse', params: { courseName: props.courseName } },
})
return crumbs
})
const courseResource = createDocumentResource({
doctype: 'LMS Course',
name: props.courseName,
auto: false,
onSuccess(data) {
tags.value = data.tags
imageResource.reload({ image: data.image })
Object.assign(course, data)
course.published = data.published ? true : false
course.upcoming = data.upcoming ? true : false
course.disable_self_learning = data.disable_self_learning ? true : false
course.paid_course = data.paid_course ? true : false
},
})
const imageResource = createResource({
url: 'lms.lms.api.get_file_info',
makeParams(values) {
return {
file_url: values.image,
}
},
auto: false,
onSuccess(data) {
image.value = data
},
})
onMounted(() => {
if (!user.data?.is_moderator || !user.data?.is_instructor) {
window.location.href = '/login'
}
if (props.courseName !== 'new') {
courseResource.reload()
}
})
/* const course = computed(() => {
return {
title: courseResource.doc?.title || '',
short_introduction: courseResource.doc?.short_introduction || '',
description: courseResource.doc?.description || '',
video_link: courseResource.doc?.video_link || '',
course_image: courseResource.doc?.image || null,
tags: courseResource.doc?.tags || '',
published: courseResource.doc?.published ? true : false,
upcoming: courseResource.doc?.upcoming ? true : false,
disable_self_learning: courseResource.doc?.disable_self_learning
? true
: false,
course_image: image.value,
paid_course: courseResource.doc?.paid_course ? true : false,
course_price: courseResource.doc?.course_price || '',
currency: courseResource.doc?.currency || '',
image: courseResource.doc?.image || null,
}
}) */
const course = reactive({
title: '',
short_introduction: '',
@@ -301,10 +217,13 @@ const course = reactive({
currency: '',
})
const getTags = computed(() => {
return courseResource.doc?.tags
? courseResource.doc.tags.split(', ')
: tags.value?.split(', ')
onMounted(() => {
if (!user.data?.is_moderator || !user.data?.is_instructor) {
window.location.href = '/login'
}
if (props.courseName !== 'new') {
courseResource.reload()
}
})
const courseCreationResource = createResource({
@@ -313,24 +232,82 @@ const courseCreationResource = createResource({
return {
doc: {
doctype: 'LMS Course',
image: image.value.file_url,
image: course.course_image.file_url,
...values,
},
}
},
})
const courseEditResource = createResource({
url: 'frappe.client.set_value',
auto: false,
makeParams(values) {
return {
doctype: 'LMS Course',
name: values.course,
fieldname: {
image: course.course_image.file_url,
...course,
},
}
},
})
const courseResource = createResource({
url: 'frappe.client.get',
makeParams(values) {
return {
doctype: 'LMS Course',
name: props.courseName,
}
},
auto: false,
onSuccess(data) {
Object.keys(data).forEach((key) => {
if (Object.hasOwn(course, key)) course[key] = data[key]
})
let checkboxes = [
'published',
'upcoming',
'disable_self_learning',
'paid_course',
]
for (let idx in checkboxes) {
let key = checkboxes[idx]
course[key] = course[key] ? true : false
}
if (data.image) imageResource.reload({ image: data.image })
},
})
const imageResource = createResource({
url: 'lms.lms.api.get_file_info',
makeParams(values) {
return {
file_url: values.image,
}
},
auto: false,
onSuccess(data) {
course.course_image = data
},
})
const getTags = computed(() => {
return courseResource.doc?.tags
? courseResource.doc.tags.split(', ')
: tags.value?.split(', ')
})
const submitCourse = () => {
if (courseResource.doc) {
courseResource.setValue.submit(
if (courseResource.data) {
courseEditResource.submit(
{
image: image.value?.file_url || null,
...course.value,
course: courseResource.data.name,
},
{
validate() {
return validateMandatoryFields()
},
onSuccess() {
showToast('Success', 'Course updated successfully', 'check')
},
@@ -340,10 +317,7 @@ const submitCourse = () => {
}
)
} else {
courseCreationResource.submit(course.value, {
validate() {
return validateMandatoryFields()
},
courseCreationResource.submit(course, {
onSuccess() {
showToast('Success', 'Course created successfully', 'check')
},
@@ -363,15 +337,12 @@ const validateMandatoryFields = () => {
'course_image',
]
for (const field of mandatory_fields) {
if (!course.value[field]) {
if (!course[field]) {
let fieldLabel = convertToTitleCase(field.split('_').join(' '))
return `${fieldLabel} is mandatory`
}
}
if (
course.value.paid_course &&
(!course.value.course_price || !course.value.currency)
) {
if (course.paid_course && (!course.course_price || !course.currency)) {
return 'Course price and currency are mandatory for paid courses'
}
}
@@ -385,35 +356,44 @@ const validateFile = (file) => {
const updateTags = () => {
if (newTag.value) {
tags.value = tags.value ? `${tags.value}, ${newTag.value}` : newTag.value
course.tags = course.tags ? `${course.tags}, ${newTag.value}` : newTag.value
newTag.value = ''
}
}
const removeTag = (tag) => {
tags.value = tags.value
course.tags = course.tags
?.split(', ')
.filter((t) => t !== tag)
.join(', ')
newTag.value = ''
}
const showToast = (title, text, icon) => {
createToast({
title: title,
text: text,
icon: icon,
iconClasses:
icon == 'check'
? 'bg-green-600 text-white rounded-md p-px'
: 'bg-red-600 text-white rounded-md p-px',
position: icon == 'check' ? 'bottom-right' : 'top-center',
timeout: icon == 'check' ? 5 : 10,
})
const saveImage = (file) => {
course.course_image = file
}
const removeImage = () => {
image.value = null
course.value.course_image = null
course.course_image = null
}
const breadcrumbs = computed(() => {
let crumbs = [
{
label: 'Courses',
route: { name: 'Courses' },
},
]
if (courseResource.data) {
crumbs.push({
label: course.title,
route: { name: 'CourseDetail', params: { courseName: props.courseName } },
})
}
crumbs.push({
label: props.courseName == 'new' ? 'New Course' : 'Edit Course',
route: { name: 'CreateCourse', params: { courseName: props.courseName } },
})
return crumbs
})
</script>

View File

@@ -1,7 +1,7 @@
<template>
<div class="h-screen text-base">
<div class="grid grid-cols-[75%,25%] h-full">
<div>
<div class="border-r">
<header
class="sticky top-0 z-10 flex items-center justify-between border-b bg-white px-3 py-2.5 sm:px-5"
>
@@ -10,53 +10,56 @@
{{ __('Save') }}
</Button>
</header>
<div class="w-5/6 mx-auto py-5">
<div class="flex items-center justify-between mb-5">
<div class="text-lg font-semibold">
{{ __('Lesson Details') }}
<div class="py-5">
<div class="w-5/6 mx-auto">
<FormControl v-model="lesson.title" label="Title" class="mb-4" />
<FormControl
v-model="lesson.include_in_preview"
type="checkbox"
label="Include in Preview"
/>
</div>
<div class="border-t mt-4">
<div class="w-5/6 mx-auto pt-4">
<div
class="flex justify-between cursor-pointer"
@click="
() => {
openInstructorEditor = !openInstructorEditor
}
"
>
<label class="block font-medium text-gray-600 mb-1">
{{ __('Instructor Notes') }}
</label>
<ChevronRight
class="stroke-2 h-5 w-5 text-gray-600"
:class="{
'rotate-90 transform duration-200': openInstructorEditor,
'duration-200': !openInstructorEditor,
}"
/>
</div>
<div
v-show="openInstructorEditor"
id="instructor-notes"
class="py-3"
></div>
</div>
</div>
<FormControl v-model="lesson.title" label="Title" class="mb-4" />
<FormControl
v-model="lesson.include_in_preview"
type="checkbox"
label="Include in Preview"
/>
<div class="mt-4">
<label class="block text-xs text-gray-600 mb-1">
{{ __('Instructor Notes') }}
</label>
<div
id="instructor-notes"
class="border rounded-md px-10 py-3"
></div>
</div>
<div class="mt-4">
<label class="block text-xs text-gray-600 mb-1">
{{ __('Content') }}
</label>
<div id="content" class="border rounded-md py-3"></div>
<div class="border-t mt-4">
<div class="w-5/6 mx-auto pt-4">
<label class="block font-medium text-gray-600 mb-1">
{{ __('Content') }}
</label>
<div id="content" class="py-3"></div>
</div>
</div>
</div>
</div>
<div class="border-l px-5 pt-5">
<div class="text-lg font-semibold">
{{ __('Components') }}
</div>
<div class="mt-5">
<div class="flex">
<Link
v-model="quiz"
class="flex-1"
doctype="LMS Quiz"
:label="__('Select a Quiz')"
/>
<Button @click="addQuiz()" class="self-end ml-2">
<template #icon>
<Plus class="h-4 w-4 stroke-1.5" />
</template>
</Button>
</div>
<div class="">
<div class="sticky top-0 p-5">
<LessonPlugins :editor="editor" :notesEditor="instructorEditor" />
</div>
</div>
</div>
@@ -73,13 +76,14 @@ import {
import { computed, reactive, onMounted, inject, ref } from 'vue'
import EditorJS from '@editorjs/editorjs'
import { createToast } from '../utils'
import Link from '@/components/Controls/Link.vue'
import { Plus } from 'lucide-vue-next'
import LessonPlugins from '@/components/LessonPlugins.vue'
import { getEditorTools } from '../utils'
import { ChevronRight } from 'lucide-vue-next'
let editor
const editor = ref(null)
const instructorEditor = ref(null)
const user = inject('$user')
const quiz = ref(null)
const openInstructorEditor = ref(false)
const props = defineProps({
courseName: {
@@ -100,7 +104,8 @@ onMounted(() => {
if (!user.data?.is_moderator || !user.data?.is_instructor) {
window.location.href = '/login'
}
editor = renderEditor('content')
editor.value = renderEditor('content')
instructorEditor.value = renderEditor('instructor-notes')
})
const renderEditor = (holder) => {
@@ -132,8 +137,27 @@ const lessonDetails = createResource({
lesson[key] = data.lesson[key]
})
lesson.include_in_preview = data.include_in_preview ? true : false
editor.isReady.then(() => {
editor.render(JSON.parse(data.lesson.content))
editor.value.isReady.then(() => {
if (data.lesson.content) {
editor.value.render(JSON.parse(data.lesson.content))
} else if (data.lesson.body) {
let blocks = convertToJSON(data.lesson)
editor.value.render({
blocks: blocks,
})
}
})
instructorEditor.value.isReady.then(() => {
if (data.lesson.instructor_content) {
instructorEditor.value.render(
JSON.parse(data.lesson.instructor_content)
)
} else if (data.lesson.instructor_notes) {
let blocks = convertToJSON(data.lesson)
instructorEditor.value.render({
blocks: blocks,
})
}
})
}
},
@@ -180,30 +204,106 @@ const lessonReference = createResource({
},
})
const saveLesson = () => {
editor.save().then((outputData) => {
lesson.content = JSON.stringify(outputData)
if (lessonDetails.data?.lesson) {
editLesson.submit(
{
lesson: lessonDetails.data.lesson.name,
const convertToJSON = (lessonData) => {
let blocks = []
lessonData.body.split('\n').forEach((block) => {
if (block.includes('{{ YouTubeVideo')) {
let youtubeID = block.match(/\(["']([^"']+?)["']\)/)[1]
if (!youtubeID.includes('https://'))
youtubeID = `https://www.youtube.com/embed/${youtubeID}`
blocks.push({
type: 'embed',
data: {
service: 'youtube',
embed: youtubeID,
},
{
validate() {
return validateLesson()
},
onSuccess() {
showToast('Success', 'Lesson updated successfully', 'check')
},
onError(err) {
showToast('Error', err.message, 'x')
},
}
)
})
} else if (block.includes('{{ Quiz')) {
let quiz = block.match(/\(["']([^"']+?)["']\)/)[1]
blocks.push({
type: 'quiz',
data: {
quiz: quiz,
},
})
} else if (block.includes('{{ Video')) {
let video = block.match(/\(["']([^"']+?)["']\)/)[1]
blocks.push({
type: 'upload',
data: {
file_url: video,
file_type: 'video',
},
})
} else if (block.includes('{{ Audio')) {
let audio = block.match(/\(["']([^"']+?)["']\)/)[1]
blocks.push({
type: 'upload',
data: {
file_url: audio,
file_type: 'audio',
},
})
} else if (block.includes('{{ PDF')) {
let pdf = block.match(/\(["']([^"']+?)["']\)/)[1]
blocks.push({
type: 'upload',
data: {
file_url: pdf,
file_type: 'pdf',
},
})
} else if (block.includes('{{ Embed')) {
let embed = block.match(/\(["']([^"']+?)["']\)/)[1]
blocks.push({
type: 'embed',
data: {
service: embed.split('|||')[0],
embed: embed.split('|||')[1],
},
})
} else if (block.includes('![]')) {
let image = block.match(/\((.*?)\)/)[1]
blocks.push({
type: 'upload',
data: {
file_url: image,
file_type: 'image',
},
})
} else if (block.includes('#')) {
let level = (block.match(/#/g) || []).length
blocks.push({
type: 'header',
data: {
text: block.replace(/#/g, '').trim(),
level: level,
},
})
} else {
createNewLesson()
blocks.push({
type: 'paragraph',
data: {
text: block,
},
})
}
})
return blocks
}
const saveLesson = () => {
editor.value.save().then((outputData) => {
lesson.content = JSON.stringify(outputData)
instructorEditor.value.save().then((outputData) => {
lesson.instructor_content = JSON.stringify(outputData)
if (lessonDetails.data?.lesson) {
editCurrentLesson()
} else {
createNewLesson()
}
})
})
}
const createNewLesson = () => {
@@ -230,6 +330,25 @@ const createNewLesson = () => {
)
}
const editCurrentLesson = () => {
editLesson.submit(
{
lesson: lessonDetails.data.lesson.name,
},
{
validate() {
return validateLesson()
},
onSuccess() {
showToast('Success', 'Lesson updated successfully', 'check')
},
onError(err) {
showToast('Error', err.message, 'x')
},
}
)
}
const validateLesson = () => {
if (!lesson.title) {
return 'Title is required'
@@ -239,20 +358,6 @@ const validateLesson = () => {
}
}
const addQuiz = () => {
if (quiz.value) {
editor.blocks.insert(
'quiz',
{
quiz: quiz.value,
},
{},
editor.blocks.getBlocksCount()
)
quiz.value = null
}
}
const showToast = (title, text, icon) => {
createToast({
title: title,
@@ -306,3 +411,16 @@ const breadcrumbs = computed(() => {
return crumbs
})
</script>
<style>
.embed-tool__caption {
display: none;
}
.ce-toolbar__actions {
right: 108%;
}
.ce-block__content {
max-width: none;
}
</style>

View File

@@ -0,0 +1,2 @@
<template></template>
<script setup></script>

View File

@@ -8,12 +8,14 @@
:items="[{ label: __('Jobs'), route: { name: 'Jobs' } }]"
/>
<div class="flex">
<Button v-if="user.data?.name" variant="solid">
<template #prefix>
<Plus class="h-4 w-4" />
</template>
{{ __('New Job') }}
</Button>
<router-link v-if="user.data?.name">
<Button variant="solid">
<template #prefix>
<Plus class="h-4 w-4" />
</template>
{{ __('New Job') }}
</Button>
</router-link>
</div>
</header>
<div v-if="jobs.data">

View File

@@ -6,7 +6,7 @@
<Breadcrumbs class="h-7" :items="breadcrumbs" />
</header>
<div class="grid grid-cols-[70%,30%] h-full">
<div v-if="lesson.data.no_preview" class="border-r-2 text-center pt-10">
<div v-if="lesson.data.no_preview" class="border-r-1 text-center pt-10">
<p class="mb-4">
{{
__(
@@ -111,74 +111,38 @@
</span>
</div>
<div
v-if="lesson.data.content"
v-for="content in JSON.parse(lesson.data.content).blocks"
v-if="lesson.data.instructor_content && allowInstructorContent()"
class="bg-gray-100 p-3 rounded-md mt-6"
>
<div class="text-gray-600 font-medium">
{{ __('Instructor Notes') }}
</div>
<div
id="instructor-content"
class="ProseMirror prose prose-table:table-fixed prose-td:p-2 prose-th:p-2 prose-td:border prose-th:border prose-td:border-gray-300 prose-th:border-gray-300 prose-td:relative prose-th:relative prose-th:bg-gray-100 prose-sm max-w-none !whitespace-normal"
></div>
</div>
<div
v-else-if="lesson.data.instructor_notes"
class="ProseMirror prose prose-table:table-fixed prose-td:p-2 prose-th:p-2 prose-td:border prose-th:border prose-td:border-gray-300 prose-th:border-gray-300 prose-td:relative prose-th:relative prose-th:bg-gray-100 prose-sm max-w-none !whitespace-normal mt-6"
>
<LessonContent :data="lesson.data.instructor_notes" />
</div>
<div
v-if="lesson.data.content"
class="ProseMirror prose prose-table:table-fixed prose-td:p-2 prose-th:p-2 prose-td:border prose-th:border prose-td:border-gray-300 prose-th:border-gray-300 prose-td:relative prose-th:relative prose-th:bg-gray-100 prose-sm max-w-none !whitespace-normal mt-5"
>
<div id="editor"></div>
</div>
<div
v-else
class="ProseMirror prose prose-table:table-fixed prose-td:p-2 prose-th:p-2 prose-td:border prose-th:border prose-td:border-gray-300 prose-th:border-gray-300 prose-td:relative prose-th:relative prose-th:bg-gray-100 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-gray-300 prose-th:border-gray-300 prose-td:relative prose-th:relative prose-th:bg-gray-100 prose-sm max-w-none !whitespace-normal mt-5"
>
<div v-if="lesson.data.youtube">
<iframe
class="youtube-video"
:src="getYouTubeVideoSource(lesson.data.youtube)"
width="100%"
height="400"
frameborder="0"
allowfullscreen
></iframe>
</div>
<div v-for="block in lesson.data.body.split('\n\n\n')">
<div v-if="block.includes('{{ YouTubeVideo')">
<iframe
class="youtube-video"
:src="getYouTubeVideoSource(block)"
width="100%"
height="400"
frameborder="0"
allowfullscreen
></iframe>
</div>
<div v-else-if="block.includes('{{ Quiz')">
<Quiz :quiz="getId(block)" />
</div>
<div v-else-if="block.includes('{{ Video')">
<video controls width="100%" controlsList="nodownload">
<source :src="getId(block)" type="video/mp4" />
</video>
</div>
<div v-else-if="block.includes('{{ PDF')">
<iframe
:src="getPDFSource(block)"
width="100%"
height="400"
frameborder="0"
allowfullscreen
></iframe>
</div>
<div v-else-if="block.includes('{{ Audio')">
<audio width="100%" controls controlsList="nodownload">
<source :src="getId(block)" type="audio/mp3" />
</audio>
</div>
<div v-else-if="block.includes('{{ Embed')">
<iframe
width="100%"
height="400"
:src="getId(block)"
frameborder="0"
allowfullscreen
>
</iframe>
</div>
<div v-else v-html="markdown.render(block)"></div>
</div>
<div v-if="lesson.data.quiz_id">
<Quiz :quiz="lesson.data.quiz_id" />
</div>
<LessonContent
:content="lesson.data.body"
:youtube="lesson.data.youtube"
:quizId="lesson.data.quiz_id"
/>
</div>
<div class="mt-20">
<Discussions
@@ -221,21 +185,15 @@ import { computed, watch, ref, inject, createApp } from 'vue'
import CourseOutline from '@/components/CourseOutline.vue'
import UserAvatar from '@/components/UserAvatar.vue'
import { useRoute } from 'vue-router'
import MarkdownIt from 'markdown-it'
import { ChevronLeft, ChevronRight } from 'lucide-vue-next'
import Discussions from '@/components/Discussions.vue'
import Quiz from '@/components/QuizBlock.vue'
import { getEditorTools } from '../utils'
import EditorJS from '@editorjs/editorjs'
import LessonContent from '@/components/LessonContent.vue'
const user = inject('$user')
const route = useRoute()
let editor
const markdown = new MarkdownIt({
html: true,
linkify: true,
})
let editor, instructorEditor
const props = defineProps({
courseName: {
@@ -264,26 +222,31 @@ const lesson = createResource({
},
auto: true,
onSuccess(data) {
console.log(data)
if (data.membership)
current_lesson.submit({
name: data.membership.name,
lesson_name: data.name,
})
renderEditor()
if (data.content) editor = renderEditor('editor', data.content)
if (data.instructor_content)
instructorEditor = renderEditor(
'instructor-content',
data.instructor_content
)
markProgress(data)
},
})
const renderEditor = () => {
if (lesson.data?.content) {
editor = new EditorJS({
holder: 'editor',
tools: getEditorTools(),
data: JSON.parse(lesson.data.content),
readOnly: true,
})
}
const renderEditor = (holder, content) => {
return new EditorJS({
holder: holder,
tools: getEditorTools(),
data: JSON.parse(content),
readOnly: true,
defaultBlock: 'embed', // editor adds an empty block at the top, so to avoid that added default block as embed
})
}
const markProgress = (data) => {
@@ -349,21 +312,6 @@ watch(
}
)
const getYouTubeVideoSource = (block) => {
if (block.includes('{{')) {
block = getId(block)
}
return `https://www.youtube.com/embed/${block}`
}
const getPDFSource = (block) => {
return `${getId(block)}#toolbar=0`
}
const getId = (block) => {
return block.match(/\(["']([^"']+?)["']\)/)[1]
}
const redirectToLogin = () => {
window.location.href = `/login?redirect_to=/courses/${props.courseName}/learn/${route.params.chapterNumber}-${route.params.lessonNumber}`
}
@@ -377,12 +325,14 @@ const allowDiscussions = () => {
}
const allowEdit = () => {
if (user.data?.is_instructor) {
return true
}
if (lesson.data?.instructor.includes(user.data?.name)) {
return true
}
if (user.data?.is_moderator) return true
if (lesson.data?.instructors.includes(user.data?.name)) return true
return false
}
const allowInstructorContent = () => {
if (user.data?.is_moderator) return true
if (lesson.data?.instructors.includes(user.data?.name)) return true
return false
}
</script>
@@ -437,4 +387,12 @@ const allowEdit = () => {
text-decoration: underline;
font-weight: 500;
}
.codex-editor__redactor {
padding-bottom: 0 !important;
}
.embed-tool__caption {
display: none;
}
</style>

View File

@@ -84,8 +84,14 @@ const routes = [
},
{
path: '/batches/:batchName/edit',
name: 'CreateBatch',
component: () => import('@/pages/CreateBatch.vue'),
name: 'BatchCreation',
component: () => import('@/pages/BatchCreation.vue'),
props: true,
},
{
path: '/batches/:batchName/edit',
name: 'JobCreation',
component: () => import('@/pages/JobCreation.vue'),
props: true,
},
]

View File

@@ -2,10 +2,11 @@ import { toast } from 'frappe-ui'
import { useDateFormat, useTimeAgo } from '@vueuse/core'
import { BookOpen, Users, TrendingUp, Briefcase } from 'lucide-vue-next'
import { Quiz } from '@/utils/quiz'
import { Upload } from '@/utils/upload'
import Header from '@editorjs/header'
import Paragraph from '@editorjs/paragraph'
import List from '@editorjs/list'
import Embed from '@editorjs/embed'
import NestedList from '@editorjs/nested-list'
export function createToast(options) {
toast({
@@ -69,10 +70,25 @@ export function getFileSize(file_size) {
return value
}
export function showToast(title, text, icon) {
createToast({
title: title,
text: text,
icon: icon,
iconClasses:
icon == 'check'
? 'bg-green-600 text-white rounded-md p-px'
: 'bg-red-600 text-white rounded-md p-px',
position: icon == 'check' ? 'bottom-right' : 'top-center',
timeout: icon == 'check' ? 5 : 10,
})
}
export function getEditorTools() {
return {
header: Header,
quiz: Quiz,
upload: Upload,
paragraph: {
class: Paragraph,
inlineToolbar: true,
@@ -80,9 +96,15 @@ export function getEditorTools() {
preserveBlank: true,
},
},
list: List,
list: {
class: NestedList,
config: {
defaultStyle: 'ordered',
},
},
embed: {
class: Embed,
inlineToolbar: false,
config: {
services: {
youtube: true,

View File

@@ -4,13 +4,6 @@ import { usersStore } from '../stores/user'
import translationPlugin from '../translation'
export class Quiz {
static get toolbox() {
return {
title: 'Quiz',
icon: `<img src="/assets/lms/icons/quiz.svg" width="15" height="15">`,
}
}
constructor({ data, api, readOnly }) {
this.data = data
this.readOnly = readOnly
@@ -42,7 +35,7 @@ export class Quiz {
app.mount(this.wrapper)
return
}
return `<div class='border rounded-md p-10 text-center'>
return `<div class='border rounded-md p-10 text-center mb-2'>
<span class="font-medium">
Quiz: ${quiz}
</span>

View File

@@ -0,0 +1,43 @@
export class Upload {
constructor({ data, api, readOnly }) {
this.data = data
this.readOnly = readOnly
}
static get isReadOnlySupported() {
return true
}
render() {
this.wrapper = document.createElement('div')
this.wrapper.innerHTML = this.renderUpload(this.data)
return this.wrapper
}
renderUpload(file) {
if (file.file_type == 'video') {
return `<video controls width='100%' controls controlsList='nodownload' class="mb-4">
<source src=${encodeURI(file.file_url)} type='video/mp4'>
</video>`
} else if (file.file_type == 'audio') {
return `<audio controls width='100%' controls controlsList='nodownload' class="mb-4">
<source src=${encodeURI(file.file_url)} type='audio/mp3'>
</audio>`
} else if (file.file_type == 'pdf') {
return `<iframe src="${encodeURI(
file.file_url
)}#toolbar=0" width='100%' height='700px' class="mb-4"></iframe>`
} else {
return `<img class="mb-4" src=${encodeURI(
file.file_url
)} width='100%'>`
}
}
save(blockContent) {
return {
file_url: this.data.file_url,
file_type: this.data.file_type,
}
}
}

View File

@@ -154,6 +154,7 @@ def get_user_info():
user["roles"] = frappe.get_roles(user.name)
user.is_instructor = "Course Creator" in user.roles
user.is_moderator = "Moderator" in user.roles
user.is_evaluator = "Batch Evaluator" in user.roles
return user

View File

@@ -23,9 +23,11 @@
"column_break_15",
"file_type",
"section_break_11",
"body",
"instructor_notes",
"content",
"body",
"column_break_cjmf",
"instructor_content",
"instructor_notes",
"help_section",
"help"
],
@@ -143,11 +145,20 @@
"fieldname": "content",
"fieldtype": "Text",
"label": "Content"
},
{
"fieldname": "column_break_cjmf",
"fieldtype": "Column Break"
},
{
"fieldname": "instructor_content",
"fieldtype": "Text",
"label": "Instructor Content"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2024-03-04 19:40:57.359033",
"modified": "2024-03-14 14:25:22.464111",
"modified_by": "Administrator",
"module": "LMS",
"name": "Course Lesson",

View File

@@ -1339,6 +1339,7 @@ def get_lesson(course, chapter, lesson):
"instructor_notes",
"course",
"content",
"instructor_content",
],
as_dict=True,
)
@@ -1761,7 +1762,15 @@ def get_lesson_creation_details(course, chapter, lesson):
lesson_details = frappe.db.get_value(
"Course Lesson",
lesson_name,
["name", "title", "include_in_preview", "body", "content", "instructor_notes"],
[
"name",
"title",
"include_in_preview",
"body",
"content",
"instructor_notes",
"instructor_content",
],
as_dict=1,
)

View File

@@ -83,4 +83,5 @@ lms.patches.v1_0.create_batch_source
lms.patches.v1_0.batch_tabs_settings
execute:frappe.delete_doc("Notification", "Assignment Submission Notification")
lms.patches.v1_0.change_jobs_url #19-01-2024
lms.patches.v1_0.custom_perm_for_discussions #14-01-2024
lms.patches.v1_0.custom_perm_for_discussions #14-01-2024
lms.patches.v1_0.rename_evaluator_role

View File

@@ -0,0 +1,6 @@
import frappe
def execute():
if frappe.db.exists("Role", "Class Evaluator"):
frappe.rename_doc("Role", "Class Evaluator", "Batch Evaluator")

417
yarn.lock
View File

@@ -203,14 +203,19 @@
"@lezer/highlight" "^1.0.0"
"@codemirror/view@^6.0.0", "@codemirror/view@^6.16.0", "@codemirror/view@^6.23.0":
version "6.25.0"
resolved "https://registry.yarnpkg.com/@codemirror/view/-/view-6.25.0.tgz#7bf6cd15d1285e9354a83e39ee35cbf8f3957119"
integrity sha512-XnMGOm6qXB8znzCko0N7k97qZayVdvqpA0JebxA5fHtgBjC/XlCPhH9TK92TahsoCKMPQlaTCUep06Dwj/+GXQ==
version "6.26.0"
resolved "https://registry.yarnpkg.com/@codemirror/view/-/view-6.26.0.tgz#ab5a85aa8ebfb953cb5534e07d0a3751f9a3869a"
integrity sha512-nSSmzONpqsNzshPOxiKhK203R6BvABepugAe34QfQDbNDslyjkqBuKgrK5ZBvqNXpfxz5iLrlGTmEfhbQyH46A==
dependencies:
"@codemirror/state" "^6.4.0"
style-mod "^4.1.0"
w3c-keyname "^2.2.4"
"@codexteam/icons@^0.0.2":
version "0.0.2"
resolved "https://registry.yarnpkg.com/@codexteam/icons/-/icons-0.0.2.tgz#9183996a38b75a93506890373a015e3a2a369264"
integrity sha512-KdeKj3TwaTHqM3IXd5YjeJP39PBUZTb+dtHjGlf5+b0VgsxYD4qzsZkb11lzopZbAuDsHaZJmAYQ8LFligIT6Q==
"@codexteam/icons@^0.0.4":
version "0.0.4"
resolved "https://registry.yarnpkg.com/@codexteam/icons/-/icons-0.0.4.tgz#8b72dcd3f3a1b0d880bdceb2abebd74b46d3ae13"
@@ -221,32 +226,37 @@
resolved "https://registry.yarnpkg.com/@codexteam/icons/-/icons-0.0.5.tgz#d17f39b6a0497c6439f57dd42711817a3dd3679c"
integrity sha512-s6H2KXhLz2rgbMZSkRm8dsMJvyUNZsEjxobBEg9ztdrb1B2H3pEzY6iTwI4XUPJWJ3c3qRKwV4TrO3J5jUdoQA==
"@codexteam/icons@^0.0.6":
version "0.0.6"
resolved "https://registry.yarnpkg.com/@codexteam/icons/-/icons-0.0.6.tgz#5553ada48dddf5940851ccc142cfe17835c36ad3"
integrity sha512-L7Q5PET8PjKcBT5wp7VR+FCjwCi5PUp7rd/XjsgQ0CI5FJz0DphyHGRILMuDUdCW2MQT9NHbVr4QP31vwAkS/A==
"@codexteam/icons@^0.3.0":
version "0.3.0"
resolved "https://registry.yarnpkg.com/@codexteam/icons/-/icons-0.3.0.tgz#62380b4053d487a257de443864b5c72dafab95e6"
integrity sha512-fJE9dfFdgq8xU+sbsxjH0Kt8Yeatw9xHBJWb77DhRkEXz3OCoIS6hrRC1ewHEryxzIjxD8IyQrRq2f+Gz3BcmA==
"@docsearch/css@3.5.2", "@docsearch/css@^3.5.2":
version "3.5.2"
resolved "https://registry.yarnpkg.com/@docsearch/css/-/css-3.5.2.tgz#610f47b48814ca94041df969d9fcc47b91fc5aac"
integrity sha512-SPiDHaWKQZpwR2siD0KQUwlStvIAnEyK6tAE2h2Wuoq8ue9skzhlyVQ1ddzOxX6khULnAALDiR/isSF3bnuciA==
"@docsearch/css@3.6.0", "@docsearch/css@^3.5.2":
version "3.6.0"
resolved "https://registry.yarnpkg.com/@docsearch/css/-/css-3.6.0.tgz#0e9f56f704b3a34d044d15fd9962ebc1536ba4fb"
integrity sha512-+sbxb71sWre+PwDK7X2T8+bhS6clcVMLwBPznX45Qu6opJcgRjAp7gYSDzVFp187J+feSj5dNBN1mJoi6ckkUQ==
"@docsearch/js@^3.5.2":
version "3.5.2"
resolved "https://registry.yarnpkg.com/@docsearch/js/-/js-3.5.2.tgz#a11cb2e7e62890e9e940283fed6972ecf632629d"
integrity sha512-p1YFTCDflk8ieHgFJYfmyHBki1D61+U9idwrLh+GQQMrBSP3DLGKpy0XUJtPjAOPltcVbqsTjiPFfH7JImjUNg==
version "3.6.0"
resolved "https://registry.yarnpkg.com/@docsearch/js/-/js-3.6.0.tgz#f9e46943449b9092d874944f7a80bcc071004cfb"
integrity sha512-QujhqINEElrkIfKwyyyTfbsfMAYCkylInLYMRqHy7PHc8xTBQCow73tlo/Kc7oIwBrCLf0P3YhjlOeV4v8hevQ==
dependencies:
"@docsearch/react" "3.5.2"
"@docsearch/react" "3.6.0"
preact "^10.0.0"
"@docsearch/react@3.5.2":
version "3.5.2"
resolved "https://registry.yarnpkg.com/@docsearch/react/-/react-3.5.2.tgz#2e6bbee00eb67333b64906352734da6aef1232b9"
integrity sha512-9Ahcrs5z2jq/DcAvYtvlqEBHImbm4YJI8M9y0x6Tqg598P40HTEkX7hsMcIuThI+hTFxRGZ9hll0Wygm2yEjng==
"@docsearch/react@3.6.0":
version "3.6.0"
resolved "https://registry.yarnpkg.com/@docsearch/react/-/react-3.6.0.tgz#b4f25228ecb7fc473741aefac592121e86dd2958"
integrity sha512-HUFut4ztcVNmqy9gp/wxNbC7pTOHhgVVkHVGCACTuLhUKUhKAF9KYHJtMiLUJxEqiFLQiuri1fWF8zqwM/cu1w==
dependencies:
"@algolia/autocomplete-core" "1.9.3"
"@algolia/autocomplete-preset-algolia" "1.9.3"
"@docsearch/css" "3.5.2"
"@docsearch/css" "3.6.0"
algoliasearch "^4.19.1"
"@editorjs/checklist@^1.6.0":
@@ -273,12 +283,19 @@
dependencies:
"@codexteam/icons" "^0.0.5"
"@editorjs/list@^1.9.0":
version "1.9.0"
resolved "https://registry.yarnpkg.com/@editorjs/list/-/list-1.9.0.tgz#5242095d0cee38671d852b02aa02293f54400f2c"
integrity sha512-BQEvZW4vi0O0dBvGNljiKxiE89vMSHoM2Tu2OzKUndoj7pY9AxqpgCh1qvwIVsJAlG4Lbt/vBFQilnoStMmI6A==
"@editorjs/image@^2.9.0":
version "2.9.0"
resolved "https://registry.yarnpkg.com/@editorjs/image/-/image-2.9.0.tgz#0c83252d569a0dc3af14c3f7d16b6df033b9c37b"
integrity sha512-xItihKJFiWJ06SMtLWQZvzHv4LRPNAFZYaHAXesBFzXvWwUrtVaVMcNSf0eNnw3InrPO3Po1vZRRgpsT+Ya3Bg==
dependencies:
"@codexteam/icons" "^0.0.4"
"@codexteam/icons" "^0.0.6"
"@editorjs/nested-list@^1.4.2":
version "1.4.2"
resolved "https://registry.yarnpkg.com/@editorjs/nested-list/-/nested-list-1.4.2.tgz#2b47b9c3ee1ce11dec02eae0b176bd4107360847"
integrity sha512-qb1dAoJ+bihqmlR3822TC2GuIxEjTCLTZsZVWNces3uJIZ+W4019G3IJKBt/MOOgz4Evzad/RvUEKwPCPe6YOQ==
dependencies:
"@codexteam/icons" "^0.0.2"
"@editorjs/paragraph@^2.11.3":
version "2.11.3"
@@ -680,9 +697,9 @@
integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==
"@polka/url@^1.0.0-next.24":
version "1.0.0-next.24"
resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.24.tgz#58601079e11784d20f82d0585865bb42305c4df3"
integrity sha512-2LuNTFBIO0m7kKIQvvPHN6UE63VjpmL9rnEEaOOaiSPbZK+zUOYIzBAWcED+3XYzhYsd/0mD57VdxAEqqV52CQ==
version "1.0.0-next.25"
resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.25.tgz#f077fdc0b5d0078d30893396ff4827a13f99e817"
integrity sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ==
"@popperjs/core@^2.11.2", "@popperjs/core@^2.9.0":
version "2.11.8"
@@ -694,96 +711,70 @@
resolved "https://registry.yarnpkg.com/@remirror/core-constants/-/core-constants-2.0.2.tgz#f05eccdc69e3a65e7d524b52548f567904a11a1a"
integrity sha512-dyHY+sMF0ihPus3O27ODd4+agdHMEmuRdyiZJ2CCWjPV5UFmn17ZbElvk6WOGVE4rdCJKZQCrPV2BcikOMLUGQ==
"@remirror/core-helpers@^3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@remirror/core-helpers/-/core-helpers-3.0.0.tgz#3a35c2346bc23ebc3cee585b7840b5567755c5f1"
integrity sha512-tusEgQJIqg4qKj6HSBUFcyRnWnziw3neh4T9wOmsPGHFC3w9kl5KSrDb9UAgE8uX6y32FnS7vJ955mWOl3n50A==
dependencies:
"@remirror/core-constants" "^2.0.2"
"@remirror/types" "^1.0.1"
"@types/object.omit" "^3.0.0"
"@types/object.pick" "^1.3.2"
"@types/throttle-debounce" "^2.1.0"
case-anything "^2.1.13"
dash-get "^1.0.2"
deepmerge "^4.3.1"
fast-deep-equal "^3.1.3"
make-error "^1.3.6"
object.omit "^3.0.0"
object.pick "^1.3.0"
throttle-debounce "^3.0.1"
"@rollup/rollup-android-arm-eabi@4.13.0":
version "4.13.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.13.0.tgz#b98786c1304b4ff8db3a873180b778649b5dff2b"
integrity sha512-5ZYPOuaAqEH/W3gYsRkxQATBW3Ii1MfaT4EQstTnLKViLi2gLSQmlmtTpGucNP3sXEpOiI5tdGhjdE111ekyEg==
"@remirror/types@^1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@remirror/types/-/types-1.0.1.tgz#768502497a0fbbc23338a1586b893f729310cf70"
integrity sha512-VlZQxwGnt1jtQ18D6JqdIF+uFZo525WEqrfp9BOc3COPpK4+AWCgdnAWL+ho6imWcoINlGjR/+3b6y5C1vBVEA==
dependencies:
type-fest "^2.19.0"
"@rollup/rollup-android-arm64@4.13.0":
version "4.13.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.13.0.tgz#8833679af11172b1bf1ab7cb3bad84df4caf0c9e"
integrity sha512-BSbaCmn8ZadK3UAQdlauSvtaJjhlDEjS5hEVVIN3A4bbl3X+otyf/kOJV08bYiRxfejP3DXFzO2jz3G20107+Q==
"@rollup/rollup-android-arm-eabi@4.12.0":
version "4.12.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.12.0.tgz#38c3abd1955a3c21d492af6b1a1dca4bb1d894d6"
integrity sha512-+ac02NL/2TCKRrJu2wffk1kZ+RyqxVUlbjSagNgPm94frxtr+XDL12E5Ll1enWskLrtrZ2r8L3wED1orIibV/w==
"@rollup/rollup-darwin-arm64@4.13.0":
version "4.13.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.13.0.tgz#ef02d73e0a95d406e0eb4fd61a53d5d17775659b"
integrity sha512-Ovf2evVaP6sW5Ut0GHyUSOqA6tVKfrTHddtmxGQc1CTQa1Cw3/KMCDEEICZBbyppcwnhMwcDce9ZRxdWRpVd6g==
"@rollup/rollup-android-arm64@4.12.0":
version "4.12.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.12.0.tgz#3822e929f415627609e53b11cec9a4be806de0e2"
integrity sha512-OBqcX2BMe6nvjQ0Nyp7cC90cnumt8PXmO7Dp3gfAju/6YwG0Tj74z1vKrfRz7qAv23nBcYM8BCbhrsWqO7PzQQ==
"@rollup/rollup-darwin-x64@4.13.0":
version "4.13.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.13.0.tgz#3ce5b9bcf92b3341a5c1c58a3e6bcce0ea9e7455"
integrity sha512-U+Jcxm89UTK592vZ2J9st9ajRv/hrwHdnvyuJpa5A2ngGSVHypigidkQJP+YiGL6JODiUeMzkqQzbCG3At81Gg==
"@rollup/rollup-darwin-arm64@4.12.0":
version "4.12.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.12.0.tgz#6c082de71f481f57df6cfa3701ab2a7afde96f69"
integrity sha512-X64tZd8dRE/QTrBIEs63kaOBG0b5GVEd3ccoLtyf6IdXtHdh8h+I56C2yC3PtC9Ucnv0CpNFJLqKFVgCYe0lOQ==
"@rollup/rollup-linux-arm-gnueabihf@4.13.0":
version "4.13.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.13.0.tgz#3d3d2c018bdd8e037c6bfedd52acfff1c97e4be4"
integrity sha512-8wZidaUJUTIR5T4vRS22VkSMOVooG0F4N+JSwQXWSRiC6yfEsFMLTYRFHvby5mFFuExHa/yAp9juSphQQJAijQ==
"@rollup/rollup-darwin-x64@4.12.0":
version "4.12.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.12.0.tgz#c34ca0d31f3c46a22c9afa0e944403eea0edcfd8"
integrity sha512-cc71KUZoVbUJmGP2cOuiZ9HSOP14AzBAThn3OU+9LcA1+IUqswJyR1cAJj3Mg55HbjZP6OLAIscbQsQLrpgTOg==
"@rollup/rollup-linux-arm64-gnu@4.13.0":
version "4.13.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.13.0.tgz#5fc8cc978ff396eaa136d7bfe05b5b9138064143"
integrity sha512-Iu0Kno1vrD7zHQDxOmvweqLkAzjxEVqNhUIXBsZ8hu8Oak7/5VTPrxOEZXYC1nmrBVJp0ZcL2E7lSuuOVaE3+w==
"@rollup/rollup-linux-arm-gnueabihf@4.12.0":
version "4.12.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.12.0.tgz#48e899c1e438629c072889b824a98787a7c2362d"
integrity sha512-a6w/Y3hyyO6GlpKL2xJ4IOh/7d+APaqLYdMf86xnczU3nurFTaVN9s9jOXQg97BE4nYm/7Ga51rjec5nfRdrvA==
"@rollup/rollup-linux-arm64-musl@4.13.0":
version "4.13.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.13.0.tgz#f2ae7d7bed416ffa26d6b948ac5772b520700eef"
integrity sha512-C31QrW47llgVyrRjIwiOwsHFcaIwmkKi3PCroQY5aVq4H0A5v/vVVAtFsI1nfBngtoRpeREvZOkIhmRwUKkAdw==
"@rollup/rollup-linux-arm64-gnu@4.12.0":
version "4.12.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.12.0.tgz#788c2698a119dc229062d40da6ada8a090a73a68"
integrity sha512-0fZBq27b+D7Ar5CQMofVN8sggOVhEtzFUwOwPppQt0k+VR+7UHMZZY4y+64WJ06XOhBTKXtQB/Sv0NwQMXyNAA==
"@rollup/rollup-linux-riscv64-gnu@4.13.0":
version "4.13.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.13.0.tgz#303d57a328ee9a50c85385936f31cf62306d30b6"
integrity sha512-Oq90dtMHvthFOPMl7pt7KmxzX7E71AfyIhh+cPhLY9oko97Zf2C9tt/XJD4RgxhaGeAraAXDtqxvKE1y/j35lA==
"@rollup/rollup-linux-arm64-musl@4.12.0":
version "4.12.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.12.0.tgz#3882a4e3a564af9e55804beeb67076857b035ab7"
integrity sha512-eTvzUS3hhhlgeAv6bfigekzWZjaEX9xP9HhxB0Dvrdbkk5w/b+1Sxct2ZuDxNJKzsRStSq1EaEkVSEe7A7ipgQ==
"@rollup/rollup-linux-x64-gnu@4.13.0":
version "4.13.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.13.0.tgz#f672f6508f090fc73f08ba40ff76c20b57424778"
integrity sha512-yUD/8wMffnTKuiIsl6xU+4IA8UNhQ/f1sAnQebmE/lyQ8abjsVyDkyRkWop0kdMhKMprpNIhPmYlCxgHrPoXoA==
"@rollup/rollup-linux-riscv64-gnu@4.12.0":
version "4.12.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.12.0.tgz#0c6ad792e1195c12bfae634425a3d2aa0fe93ab7"
integrity sha512-ix+qAB9qmrCRiaO71VFfY8rkiAZJL8zQRXveS27HS+pKdjwUfEhqo2+YF2oI+H/22Xsiski+qqwIBxVewLK7sw==
"@rollup/rollup-linux-x64-musl@4.13.0":
version "4.13.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.13.0.tgz#d2f34b1b157f3e7f13925bca3288192a66755a89"
integrity sha512-9RyNqoFNdF0vu/qqX63fKotBh43fJQeYC98hCaf89DYQpv+xu0D8QFSOS0biA7cGuqJFOc1bJ+m2rhhsKcw1hw==
"@rollup/rollup-linux-x64-gnu@4.12.0":
version "4.12.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.12.0.tgz#9d62485ea0f18d8674033b57aa14fb758f6ec6e3"
integrity sha512-TenQhZVOtw/3qKOPa7d+QgkeM6xY0LtwzR8OplmyL5LrgTWIXpTQg2Q2ycBf8jm+SFW2Wt/DTn1gf7nFp3ssVA==
"@rollup/rollup-win32-arm64-msvc@4.13.0":
version "4.13.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.13.0.tgz#8ffecc980ae4d9899eb2f9c4ae471a8d58d2da6b"
integrity sha512-46ue8ymtm/5PUU6pCvjlic0z82qWkxv54GTJZgHrQUuZnVH+tvvSP0LsozIDsCBFO4VjJ13N68wqrKSeScUKdA==
"@rollup/rollup-linux-x64-musl@4.12.0":
version "4.12.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.12.0.tgz#50e8167e28b33c977c1f813def2b2074d1435e05"
integrity sha512-LfFdRhNnW0zdMvdCb5FNuWlls2WbbSridJvxOvYWgSBOYZtgBfW9UGNJG//rwMqTX1xQE9BAodvMH9tAusKDUw==
"@rollup/rollup-win32-ia32-msvc@4.13.0":
version "4.13.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.13.0.tgz#a7505884f415662e088365b9218b2b03a88fc6f2"
integrity sha512-P5/MqLdLSlqxbeuJ3YDeX37srC8mCflSyTrUsgbU1c/U9j6l2g2GiIdYaGD9QjdMQPMSgYm7hgg0551wHyIluw==
"@rollup/rollup-win32-arm64-msvc@4.12.0":
version "4.12.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.12.0.tgz#68d233272a2004429124494121a42c4aebdc5b8e"
integrity sha512-JPDxovheWNp6d7AHCgsUlkuCKvtu3RB55iNEkaQcf0ttsDU/JZF+iQnYcQJSk/7PtT4mjjVG8N1kpwnI9SLYaw==
"@rollup/rollup-win32-ia32-msvc@4.12.0":
version "4.12.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.12.0.tgz#366ca62221d1689e3b55a03f4ae12ae9ba595d40"
integrity sha512-fjtuvMWRGJn1oZacG8IPnzIV6GF2/XG+h71FKn76OYFqySXInJtseAqdprVTDTyqPxQOG9Exak5/E9Z3+EJ8ZA==
"@rollup/rollup-win32-x64-msvc@4.12.0":
version "4.12.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.12.0.tgz#9ffdf9ed133a7464f4ae187eb9e1294413fab235"
integrity sha512-ZYmr5mS2wd4Dew/JjT0Fqi2NPB/ZhZ2VvPp7SmvPZb4Y1CG/LRcS6tcRo2cYU7zLK5A7cdbhWnnWmUjoI4qapg==
"@rollup/rollup-win32-x64-msvc@4.13.0":
version "4.13.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.13.0.tgz#6abd79db7ff8d01a58865ba20a63cfd23d9e2a10"
integrity sha512-UKXUQNbO3DOhzLRwHSpa0HnhhCgNODvfoPWv2FCXme8N/ANFfhIPMGuOT+QuKd16+B5yxZ0HdpNlqPvTMS1qfw==
"@shikijs/core@1.1.7", "@shikijs/core@^1.1.5":
version "1.1.7"
@@ -1118,27 +1109,12 @@
integrity sha512-6L6VymKTzYSrEf4Nev4Xa1LCHKrlTlYCBMTlQKFuddo1CvQcE52I0mwfOJayueUC7MJuXOeHTcIU683lzd0cUA==
"@types/node@*":
version "20.11.24"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792"
integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long==
version "20.11.28"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.28.tgz#4fd5b2daff2e580c12316e457473d68f15ee6f66"
integrity sha512-M/GPWVS2wLkSkNHVeLkrF2fD5Lx5UC4PxA0uZcKc6QqbIQUJyW1jVjueJYi1z8n0I5PxYrtpnPnWglE+y9A0KA==
dependencies:
undici-types "~5.26.4"
"@types/object.omit@^3.0.0":
version "3.0.3"
resolved "https://registry.yarnpkg.com/@types/object.omit/-/object.omit-3.0.3.tgz#cc52b1d9774c1619b5c6fc50229d087f01eabd68"
integrity sha512-xrq4bQTBGYY2cw+gV4PzoG2Lv3L0pjZ1uXStRRDQoATOYW1lCsFQHhQ+OkPhIcQoqLjAq7gYif7D14Qaa6Zbew==
"@types/object.pick@^1.3.2":
version "1.3.4"
resolved "https://registry.yarnpkg.com/@types/object.pick/-/object.pick-1.3.4.tgz#1a38b6e69a35f36ec2dcc8b9f5ffd555c1c4d7fc"
integrity sha512-5PjwB0uP2XDp3nt5u5NJAG2DORHIRClPzWT/TTZhJ2Ekwe8M5bA9tvPdi9NO/n2uvu2/ictat8kgqvLfcIE1SA==
"@types/throttle-debounce@^2.1.0":
version "2.1.0"
resolved "https://registry.yarnpkg.com/@types/throttle-debounce/-/throttle-debounce-2.1.0.tgz#1c3df624bfc4b62f992d3012b84c56d41eab3776"
integrity sha512-5eQEtSCoESnh2FsiLTxE121IiE60hnMqcb435fShf4bpLRjEu1Eoekht23y6zXS9Ts3l+Szu3TARnTsA0GkOkQ==
"@types/web-bluetooth@^0.0.20":
version "0.0.20"
resolved "https://registry.yarnpkg.com/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz#f066abfcd1cbe66267cdbbf0de010d8a41b41597"
@@ -1202,27 +1178,27 @@
integrity sha512-LgPscpE3Vs0x96PzSSB4IGVSZXZBZHpfxs+ZA1d+VEPwHdOXowy/Y2CsvCAIFrf+ssVU1pD1jidj505EpUnfbA==
"@vue/devtools-api@^7.0.14":
version "7.0.16"
resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-7.0.16.tgz#aa199ebb597bab697876d9555feb35c98415a782"
integrity sha512-fZG2CG8624qphMf4aj59zNHckMx1G3lxODUuyM9USKuLznXCh66TP+tEbPOCcml16hA0GizJ4D8w6F34hrfbcw==
version "7.0.17"
resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-7.0.17.tgz#6251541c3a9f6a4e0cbdef56b2c17d762aa115b0"
integrity sha512-UWU9tqzUBv+ttUxYLaQcL5IxSSdF+i6yheFiEtz7mh88YZUYkxpEmT43iKBs3YsC54ROwPD2iZIndnju6PWfOQ==
dependencies:
"@vue/devtools-kit" "^7.0.16"
"@vue/devtools-kit" "^7.0.17"
"@vue/devtools-kit@^7.0.16":
version "7.0.16"
resolved "https://registry.yarnpkg.com/@vue/devtools-kit/-/devtools-kit-7.0.16.tgz#83f906078c6a7dc74db4c24859c3989f17c9741f"
integrity sha512-IA8SSGiZbNgOi4wLT3mRvd71Q9KE0KvMfGk6haa2GZ6bL2K/xMA8Fvvj3o1maspfUXrGcCXutaqbLqbGx/espQ==
"@vue/devtools-kit@^7.0.17":
version "7.0.17"
resolved "https://registry.yarnpkg.com/@vue/devtools-kit/-/devtools-kit-7.0.17.tgz#d918d1717a4837fd1226e2e0510c78d026e2ddfe"
integrity sha512-znPLSOoTP3RnR9fvkq5M+nnpEA+WocybzOo5ID73vYkE0/n0VcfU8Ld0j4AHQjV/omTdAzh6QLpPlUYdIHXg+w==
dependencies:
"@vue/devtools-shared" "^7.0.16"
"@vue/devtools-shared" "^7.0.17"
hookable "^5.5.3"
mitt "^3.0.1"
perfect-debounce "^1.0.0"
speakingurl "^14.0.1"
"@vue/devtools-shared@^7.0.16":
version "7.0.16"
resolved "https://registry.yarnpkg.com/@vue/devtools-shared/-/devtools-shared-7.0.16.tgz#a18ea93d5aa60ca4e1df83d3116b663582899e09"
integrity sha512-Lew4FrGjDjmanaUWSueNE1Rre83k7jQpttc17MaoVw0eARWU5DgZ1F/g9GNUMZXVjbP9rwE+LL3gd9XfXCfkvA==
"@vue/devtools-shared@^7.0.17":
version "7.0.17"
resolved "https://registry.yarnpkg.com/@vue/devtools-shared/-/devtools-shared-7.0.17.tgz#e893d8f55deeffeed35038eddf8ed63466590ee8"
integrity sha512-QNg2TMQBFFffRbTKE9NjytXBywGR77p2UMi/gJ0ow58S+1jkAvL8ikU/JnSs9ePvsVtspHX32m2cdfe4DJ4ygw==
dependencies:
rfdc "^1.3.1"
@@ -1426,9 +1402,9 @@ balanced-match@^1.0.0:
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
binary-extensions@^2.0.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
version "2.3.0"
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522"
integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==
birpc@^0.1.1:
version "0.1.1"
@@ -1483,9 +1459,9 @@ camelcase-css@^2.0.1:
integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==
caniuse-lite@^1.0.30001587, caniuse-lite@^1.0.30001591:
version "1.0.30001593"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001593.tgz#7cda1d9e5b0cad6ebab4133b1f239d4ea44fe659"
integrity sha512-UWM1zlo3cZfkpBysd7AS+z+v007q9G1+fLTUU42rQnY6t2axoogPW/xol6T7juU5EUoOhML4WgBIdG+9yYqAjQ==
version "1.0.30001597"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001597.tgz#8be94a8c1d679de23b22fbd944232aa1321639e6"
integrity sha512-7LjJvmQU6Sj7bL0j5b5WY/3n7utXUJvAe1lxhsHDbLmwX9mdL86Yjtr+5SRCyf8qME4M7pU2hswj0FpyBVCv9w==
capital-case@^1.0.4:
version "1.0.4"
@@ -1496,11 +1472,6 @@ capital-case@^1.0.4:
tslib "^2.0.3"
upper-case-first "^2.0.2"
case-anything@^2.1.13:
version "2.1.13"
resolved "https://registry.yarnpkg.com/case-anything/-/case-anything-2.1.13.tgz#0cdc16278cb29a7fcdeb072400da3f342ba329e9"
integrity sha512-zlOQ80VrQ2Ue+ymH5OuM/DlDq64mEm+B9UTdHULv5osUMD6HalNTblf2b1u/m6QecjsnOkBpqVZ+XPwIVsy7Ng==
chalk@5.3.0:
version "5.3.0"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.3.0.tgz#67c20a7ebef70e7f3970a01f90fa210cb6860385"
@@ -1687,11 +1658,6 @@ csstype@^3.1.3:
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81"
integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==
dash-get@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/dash-get/-/dash-get-1.0.2.tgz#4c9e9ad5ef04c4bf9d3c9a451f6f7997298dcc7c"
integrity sha512-4FbVrHDwfOASx7uQVxeiCTo7ggSdYZbqs8lH+WU6ViypPlDbe9y6IP5VVUDQBv9DcnyaiPT5XT0UWHgJ64zLeQ==
data-urls@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-3.0.2.tgz#9cf24a477ae22bcef5cd5f6f0bfbc1d2d3be9143"
@@ -1725,11 +1691,6 @@ decimal.js@^10.4.2:
resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.4.3.tgz#1044092884d245d1b7f65725fa4ad4c6f781cc23"
integrity sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==
deepmerge@^4.3.1:
version "4.3.1"
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a"
integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==
defu@^6.1.2:
version "6.1.4"
resolved "https://registry.yarnpkg.com/defu/-/defu-6.1.4.tgz#4e0c9cf9ff68fe5f3d7f2765cc1a012dfdcb0479"
@@ -1788,9 +1749,9 @@ ee-first@1.1.1:
integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==
electron-to-chromium@^1.4.668:
version "1.4.690"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.690.tgz#dd5145d45c49c08a9a6f7454127e660bdf9a3fa7"
integrity sha512-+2OAGjUx68xElQhydpcbqH50hE8Vs2K6TkAeLhICYfndb67CVH0UsZaijmRUE3rHlIxU1u0jxwhgVe6fK3YANA==
version "1.4.707"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.707.tgz#77904f87432b8b50b8b8b654ba3940d2bef48d63"
integrity sha512-qRq74Mo7ChePOU6GHdfAJ0NREXU8vQTlVlfWz3wNygFay6xrd/fY2J7oGHwrhFeU30OVctGLdTh/FcnokTWpng==
emoji-regex@^10.3.0:
version "10.3.0"
@@ -1968,11 +1929,6 @@ extend-shallow@^2.0.1:
dependencies:
is-extendable "^0.1.0"
fast-deep-equal@^3.1.3:
version "3.1.3"
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
fast-glob@^3.3.0:
version "3.3.2"
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129"
@@ -2053,10 +2009,10 @@ fraction.js@^4.3.7:
resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7"
integrity sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==
frappe-ui@^0.1.31:
version "0.1.33"
resolved "https://registry.yarnpkg.com/frappe-ui/-/frappe-ui-0.1.33.tgz#ede5bc4dde1ac0f7fbc3b7a4b6e4a1fa7c20fd5e"
integrity sha512-scWkhNh70zmFXaPZlMFT+0DhX1CY/JI3hK6ehZBGQcRfX79GVo9ST6JJ+Odv/nxVc7/r4CDBpOPWGdVrCBy+4Q==
frappe-ui@^0.1.35:
version "0.1.35"
resolved "https://registry.yarnpkg.com/frappe-ui/-/frappe-ui-0.1.35.tgz#fde57d5935745e761c7bdf7857c3103d4ef58358"
integrity sha512-8oxA4sADc/WPG9yo39L1ZHYCsrITq2Y8K/m73jSIzY+N4Y34qSHUvbg004ylmFkfAhTDEcINqub3vifTElWZQw==
dependencies:
"@headlessui/vue" "^1.7.14"
"@popperjs/core" "^2.11.2"
@@ -2167,9 +2123,9 @@ gray-matter@^4.0.3:
strip-bom-string "^1.0.0"
hasown@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.1.tgz#26f48f039de2c0f8d3356c223fb8d50253519faa"
integrity sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==
version "2.0.2"
resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003"
integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==
dependencies:
function-bind "^1.1.2"
@@ -2298,13 +2254,6 @@ is-extendable@^0.1.0:
resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89"
integrity sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==
is-extendable@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4"
integrity sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==
dependencies:
is-plain-object "^2.0.4"
is-extglob@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
@@ -2339,13 +2288,6 @@ is-number@^7.0.0:
resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
is-plain-object@^2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677"
integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==
dependencies:
isobject "^3.0.1"
is-potential-custom-element-name@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5"
@@ -2361,11 +2303,6 @@ isexe@^2.0.0:
resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==
isobject@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df"
integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==
jackspeak@^2.3.5:
version "2.3.6"
resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-2.3.6.tgz#647ecc472238aee4b06ac0e461acc21a8c505ca8"
@@ -2564,11 +2501,6 @@ magic-string@^0.30.7:
dependencies:
"@jridgewell/sourcemap-codec" "^1.4.15"
make-error@^1.3.6:
version "1.3.6"
resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2"
integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==
mark.js@8.11.1:
version "8.11.1"
resolved "https://registry.yarnpkg.com/mark.js/-/mark.js-8.11.1.tgz#180f1f9ebef8b0e638e4166ad52db879beb2ffc5"
@@ -2799,20 +2731,6 @@ object-hash@^3.0.0:
resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-3.0.0.tgz#73f97f753e7baffc0e2cc9d6e079079744ac82e9"
integrity sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==
object.omit@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/object.omit/-/object.omit-3.0.0.tgz#0e3edc2fce2ba54df5577ff529f6d97bd8a522af"
integrity sha512-EO+BCv6LJfu+gBIF3ggLicFebFLN5zqzz/WWJlMFfkMyGth+oBkhxzDl0wx2W4GkLzuQs/FsSkXZb2IMWQqmBQ==
dependencies:
is-extendable "^1.0.0"
object.pick@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747"
integrity sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==
dependencies:
isobject "^3.0.1"
on-finished@~2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947"
@@ -3000,9 +2918,9 @@ postcss-selector-parser@6.0.10:
util-deprecate "^1.0.2"
postcss-selector-parser@^6.0.11:
version "6.0.15"
resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.15.tgz#11cc2b21eebc0b99ea374ffb9887174855a01535"
integrity sha512-rEYkQOMUCEMhsKbK66tbEU9QVIxbhN18YiniAwA7XQYTVBqrBy+P2p5JcdqsHgKM2zWylp8d7J6eszocfds5Sw==
version "6.0.16"
resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.16.tgz#3b88b9f5c5abd989ef4e2fc9ec8eedd34b20fb04"
integrity sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw==
dependencies:
cssesc "^3.0.0"
util-deprecate "^1.0.2"
@@ -3166,12 +3084,11 @@ prosemirror-tables@^1.3.5:
prosemirror-view "^1.13.3"
prosemirror-trailing-node@^2.0.7:
version "2.0.7"
resolved "https://registry.yarnpkg.com/prosemirror-trailing-node/-/prosemirror-trailing-node-2.0.7.tgz#ba782a7929f18bcae650b1c7082a2d10443eab19"
integrity sha512-8zcZORYj/8WEwsGo6yVCRXFMOfBo0Ub3hCUvmoWIZYfMP26WqENU0mpEP27w7mt8buZWuGrydBewr0tOArPb1Q==
version "2.0.8"
resolved "https://registry.yarnpkg.com/prosemirror-trailing-node/-/prosemirror-trailing-node-2.0.8.tgz#233ddcbda72de06f9b5d758d2a65a8cac482ea10"
integrity sha512-ujRYhSuhQb1Jsarh1IHqb2KoSnRiD7wAMDGucP35DN7j5af6X7B18PfdPIrbwsPTqIAj0fyOvxbuPsWhNvylmA==
dependencies:
"@remirror/core-constants" "^2.0.2"
"@remirror/core-helpers" "^3.0.0"
escape-string-regexp "^4.0.0"
prosemirror-transform@^1.0.0, prosemirror-transform@^1.1.0, prosemirror-transform@^1.2.1, prosemirror-transform@^1.7.3, prosemirror-transform@^1.8.0:
@@ -3269,25 +3186,25 @@ rollup@^3.27.1:
fsevents "~2.3.2"
rollup@^4.2.0:
version "4.12.0"
resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.12.0.tgz#0b6d1e5f3d46bbcf244deec41a7421dc54cc45b5"
integrity sha512-wz66wn4t1OHIJw3+XU7mJJQV/2NAfw5OAk6G6Hoo3zcvz/XOfQ52Vgi+AN4Uxoxi0KBBwk2g8zPrTDA4btSB/Q==
version "4.13.0"
resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.13.0.tgz#dd2ae144b4cdc2ea25420477f68d4937a721237a"
integrity sha512-3YegKemjoQnYKmsBlOHfMLVPPA5xLkQ8MHLLSw/fBrFaVkEayL51DilPpNNLq1exr98F2B1TzrV0FUlN3gWRPg==
dependencies:
"@types/estree" "1.0.5"
optionalDependencies:
"@rollup/rollup-android-arm-eabi" "4.12.0"
"@rollup/rollup-android-arm64" "4.12.0"
"@rollup/rollup-darwin-arm64" "4.12.0"
"@rollup/rollup-darwin-x64" "4.12.0"
"@rollup/rollup-linux-arm-gnueabihf" "4.12.0"
"@rollup/rollup-linux-arm64-gnu" "4.12.0"
"@rollup/rollup-linux-arm64-musl" "4.12.0"
"@rollup/rollup-linux-riscv64-gnu" "4.12.0"
"@rollup/rollup-linux-x64-gnu" "4.12.0"
"@rollup/rollup-linux-x64-musl" "4.12.0"
"@rollup/rollup-win32-arm64-msvc" "4.12.0"
"@rollup/rollup-win32-ia32-msvc" "4.12.0"
"@rollup/rollup-win32-x64-msvc" "4.12.0"
"@rollup/rollup-android-arm-eabi" "4.13.0"
"@rollup/rollup-android-arm64" "4.13.0"
"@rollup/rollup-darwin-arm64" "4.13.0"
"@rollup/rollup-darwin-x64" "4.13.0"
"@rollup/rollup-linux-arm-gnueabihf" "4.13.0"
"@rollup/rollup-linux-arm64-gnu" "4.13.0"
"@rollup/rollup-linux-arm64-musl" "4.13.0"
"@rollup/rollup-linux-riscv64-gnu" "4.13.0"
"@rollup/rollup-linux-x64-gnu" "4.13.0"
"@rollup/rollup-linux-x64-musl" "4.13.0"
"@rollup/rollup-win32-arm64-msvc" "4.13.0"
"@rollup/rollup-win32-ia32-msvc" "4.13.0"
"@rollup/rollup-win32-x64-msvc" "4.13.0"
fsevents "~2.3.2"
rope-sequence@^1.3.0:
@@ -3423,9 +3340,9 @@ snake-case@^3.0.4:
tslib "^2.0.3"
socket.io-client@^4.5.1, socket.io-client@^4.7.2:
version "4.7.4"
resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-4.7.4.tgz#5f0e060ff34ac0a4b4c5abaaa88e0d1d928c64c8"
integrity sha512-wh+OkeF0rAVCrABWQBaEjLfb7DVPotMbu0cgWgyR0v6eA4EoVnAwcIeIbcdTE3GT/H3kbdLl7OoH2+asoDRIIg==
version "4.7.5"
resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-4.7.5.tgz#919be76916989758bdc20eec63f7ee0ae45c05b7"
integrity sha512-sJ/tqHOCe7Z50JCBCXrsY3I2k03iOiUe+tj1OmKeD2lXPiGH/RUCdTZFoqVyN7l1MnpIzPrGtLcijffmeouNlQ==
dependencies:
"@socket.io/component-emitter" "~3.1.0"
debug "~4.3.2"
@@ -3604,11 +3521,6 @@ thenify-all@^1.0.0:
dependencies:
any-promise "^1.0.0"
throttle-debounce@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/throttle-debounce/-/throttle-debounce-3.0.1.tgz#32f94d84dfa894f786c9a1f290e7a645b6a19abb"
integrity sha512-dTEWWNu6JmeVXY0ZYoPuH5cRIwc0MeGbJwah9KUNYSJwommQpCzTySTpEe8Gs1J23aeWEuAobe4Ag7EHVt/LOg==
tippy.js@^6.3.7:
version "6.3.7"
resolved "https://registry.yarnpkg.com/tippy.js/-/tippy.js-6.3.7.tgz#8ccfb651d642010ed9a32ff29b0e9e19c5b8c61c"
@@ -3660,20 +3572,15 @@ tslib@^2.0.3:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae"
integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==
type-fest@^2.19.0:
version "2.19.0"
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b"
integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==
type-fest@^3.0.0:
version "3.13.1"
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-3.13.1.tgz#bb744c1f0678bea7543a2d1ec24e83e68e8c8706"
integrity sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==
typescript@^5.0.2:
version "5.3.3"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.3.tgz#b3ce6ba258e72e6305ba66f5c9b452aaee3ffe37"
integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==
version "5.4.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.2.tgz#0ae9cebcfae970718474fe0da2c090cad6577372"
integrity sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==
uc.micro@^1.0.1, uc.micro@^1.0.5:
version "1.0.6"
@@ -3776,9 +3683,9 @@ vite-node@0.28.4:
fsevents "~2.3.2"
vite@^5.0.11, vite@^5.1.3:
version "5.1.4"
resolved "https://registry.yarnpkg.com/vite/-/vite-5.1.4.tgz#14e9d3e7a6e488f36284ef13cebe149f060bcfb6"
integrity sha512-n+MPqzq+d9nMVTKyewqw6kSt+R3CkvF9QAKY8obiQn8g1fwTscKxyfaYnC632HtBXAQGc1Yjomphwn1dtwGAHg==
version "5.1.6"
resolved "https://registry.yarnpkg.com/vite/-/vite-5.1.6.tgz#706dae5fab9e97f57578469eef1405fc483943e4"
integrity sha512-yYIAZs9nVfRJ/AiOLCA91zzhjsHUgMjB+EigzFb6W2XTLO8JixBCKCjvhKZaye+NKYHCrkv3Oh50dH9EdLU2RA==
dependencies:
esbuild "^0.19.3"
postcss "^8.4.35"
@@ -3787,9 +3694,9 @@ vite@^5.0.11, vite@^5.1.3:
fsevents "~2.3.3"
vitepress@^1.0.0-alpha.29:
version "1.0.0-rc.44"
resolved "https://registry.yarnpkg.com/vitepress/-/vitepress-1.0.0-rc.44.tgz#01bce883761c22de42b9869a95f04bd02cbb8cdb"
integrity sha512-tO5taxGI7fSpBK1D8zrZTyJJERlyU9nnt0jHSt3fywfq3VKn977Hg0wUuTkEmwXlFYwuW26+6+3xorf4nD3XvA==
version "1.0.0-rc.45"
resolved "https://registry.yarnpkg.com/vitepress/-/vitepress-1.0.0-rc.45.tgz#1cb41f53fa084c224dd2d910137ef7b2e8c0c191"
integrity sha512-/OiYsu5UKpQKA2c0BAZkfyywjfauDjvXyv6Mo4Ra57m5n4Bxg1HgUGoth1CLH2vwUbR/BHvDA9zOM0RDvgeSVQ==
dependencies:
"@docsearch/css" "^3.5.2"
"@docsearch/js" "^3.5.2"
@@ -3950,6 +3857,6 @@ yaml@2.3.4:
integrity sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==
yaml@^2.3.4:
version "2.4.0"
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.4.0.tgz#2376db1083d157f4b3a452995803dbcf43b08140"
integrity sha512-j9iR8g+/t0lArF4V6NE/QCfT+CO7iLqrXAHZbJdo+LfjqP1vR8Fg5bSiaq6Q2lOD1AUEVrEVIgABvBFYojJVYQ==
version "2.4.1"
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.4.1.tgz#2e57e0b5e995292c25c75d2658f0664765210eed"
integrity sha512-pIXzoImaqmfOrL7teGUBt/T7ZDnyeGBWyXQBvOVhLkWLN37GXv8NMLK406UY6dS51JfcQHsmcW5cJ441bHg6Lg==