Merge pull request #2015 from pateljannat/enroll-students-in-course-from-ui

feat: course admin dashboard
This commit is contained in:
Jannat Patel
2026-01-29 19:53:27 +05:30
committed by GitHub
40 changed files with 2131 additions and 1216 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
{
"branches": ["develop"],
"branches": ["main"],
"plugins": [
"@semantic-release/commit-analyzer", {
"preset": "angular"
+20 -17
View File
@@ -11,7 +11,6 @@ describe("Course Creation", () => {
cy.get("button").contains("Create").click();
cy.get("span").contains("New Course").click();
cy.wait(500);
cy.url().should("include", "/courses/new/edit");
cy.get("label").contains("Title").type("Test Course");
cy.get("label")
@@ -35,21 +34,6 @@ describe("Course Creation", () => {
});
});
cy.get("label")
.contains("Preview Video")
.type("https://www.youtube.com/embed/-LPmw2Znl2c");
cy.get("[id=tags]").type("Learning{enter}Frappe{enter}ERPNext{enter}");
cy.get("label")
.contains("Category")
.parent()
.within(() => {
cy.get("button").click();
});
cy.get("[id^=headlessui-combobox-option-")
.should("be.visible")
.first()
.click();
/* Instructor */
cy.get("label")
.contains("Instructors")
@@ -69,13 +53,32 @@ describe("Course Creation", () => {
});
});
cy.button("Create").last().click();
// Edit Course Details
cy.wait(500);
cy.get("label")
.contains("Preview Video")
.type("https://www.youtube.com/embed/-LPmw2Znl2c");
cy.get("[id=tags]").type("Learning{enter}Frappe{enter}ERPNext{enter}");
cy.get("label")
.contains("Category")
.parent()
.within(() => {
cy.get("button").click();
});
cy.get("[id^=headlessui-combobox-option-")
.should("be.visible")
.first()
.click();
cy.get("label").contains("Published").click();
cy.get("label").contains("Published On").type("2021-01-01");
cy.button("Save").click();
// Add Chapter
cy.wait(1000);
cy.button("Add Chapter").click();
cy.button("Add").click();
cy.wait(1000);
cy.get("[data-dismissable-layer]")
+1 -1
View File
@@ -54,7 +54,6 @@ declare module 'vue' {
CourseCardOverlay: typeof import('./src/components/CourseCardOverlay.vue')['default']
CourseInstructors: typeof import('./src/components/CourseInstructors.vue')['default']
CourseOutline: typeof import('./src/components/CourseOutline.vue')['default']
CourseProgressSummary: typeof import('./src/components/Modals/CourseProgressSummary.vue')['default']
CourseReviews: typeof import('./src/components/CourseReviews.vue')['default']
CreateOutline: typeof import('./src/components/CreateOutline.vue')['default']
DateRange: typeof import('./src/components/Common/DateRange.vue')['default']
@@ -94,6 +93,7 @@ declare module 'vue' {
NoSidebarLayout: typeof import('./src/components/NoSidebarLayout.vue')['default']
Notes: typeof import('./src/components/Notes/Notes.vue')['default']
NotPermitted: typeof import('./src/components/NotPermitted.vue')['default']
NumberChartGraph: typeof import('./src/components/NumberChartGraph.vue')['default']
PageModal: typeof import('./src/components/Modals/PageModal.vue')['default']
PaymentGatewayDetails: typeof import('./src/components/Settings/PaymentGatewayDetails.vue')['default']
PaymentGateways: typeof import('./src/components/Settings/PaymentGateways.vue')['default']
+4 -5
View File
@@ -25,8 +25,7 @@
"@editorjs/paragraph": "2.11.3",
"@editorjs/simple-image": "1.6.0",
"@editorjs/table": "2.4.2",
"@vueuse/core": "10.4.1",
"@vueuse/router": "12.7.0",
"@vueuse/core": "^14.1.0",
"ace-builds": "1.36.2",
"apexcharts": "4.3.0",
"chart.js": "4.4.1",
@@ -34,7 +33,7 @@
"dayjs": "1.11.10",
"dompurify": "3.2.6",
"feather-icons": "4.28.0",
"frappe-ui": "^0.1.256",
"frappe-ui": "^0.1.261",
"highlight.js": "11.11.1",
"lucide-vue-next": "0.383.0",
"markdown-it": "14.0.0",
@@ -43,11 +42,11 @@
"socket.io-client": "4.7.2",
"thememirror": "2.0.1",
"typescript": "5.7.2",
"vue": "^3.5.0",
"vue": "^3.5.27",
"vue-chartjs": "5.3.0",
"vue-codemirror": "6.1.1",
"vue-draggable-next": "2.2.1",
"vue-router": "4.2.2",
"vue-router": "^4.6.4",
"vue3-apexcharts": "1.8.0",
"vuedraggable": "4.1.0"
},
@@ -19,9 +19,16 @@
showOptions = true
}
"
@click="
(e) => {
showOptions = true
nextTick(() => {
setFocus()
})
}
"
@focus="
() => {
showOptions = true
if (!filterOptions.data || filterOptions.data.length === 0) {
reload('')
}
@@ -55,7 +62,11 @@
>
<div class="flex flex-col gap-1 p-1">
<div class="text-base font-medium text-ink-gray-8">
{{ option.description }}
{{
option.value == option.label
? option.description
: option.label
}}
</div>
<div class="text-sm text-ink-gray-5">
{{ option.value }}
@@ -112,7 +123,7 @@ import {
} from '@headlessui/vue'
import { createResource, Popover, Button } from 'frappe-ui'
import { ref, computed, nextTick, useAttrs } from 'vue'
import { watchDebounced } from '@vueuse/core'
import { set, watchDebounced } from '@vueuse/core'
import { X, Plus } from 'lucide-vue-next'
const props = defineProps({
@@ -146,18 +157,20 @@ const props = defineProps({
const values = defineModel()
const attrs = useAttrs()
const emails = ref([])
const search = ref(null)
const error = ref(null)
const query = ref('')
const text = ref('')
const showOptions = ref(false)
const emit = defineEmits(['update:modelValue'])
const selectedValue = computed({
get: () => query.value || '',
set: (val) => {
query.value = ''
val?.value && addValue(val.value)
showOptions.value = false
emit('update:modelValue', values.value)
},
})
@@ -229,6 +242,7 @@ const addValue = (value) => {
const removeValue = (value) => {
values.value = values.value.filter((v) => v !== value)
emit('update:modelValue', values.value)
}
function setFocus() {
@@ -34,7 +34,7 @@
<img
v-if="type == 'image'"
:src="modelValue"
class="border rounded-md w-44 h-auto"
class="border rounded-md w-44 h-auto min-h-20 object-cover"
/>
<video v-else controls class="border rounded-md w-44 h-auto">
<source :src="modelValue" />
@@ -67,7 +67,7 @@ const emit = defineEmits<{
const props = withDefaults(
defineProps<{
modelValue: string
modelValue: string | null
label?: string
description?: string
type?: 'image' | 'video'
+8 -45
View File
@@ -37,7 +37,7 @@
<CertificationLinks :courseName="course.data.name" class="w-full" />
</div>
<router-link
v-else-if="course.data.paid_course"
v-else-if="course.data.paid_course && !isAdmin"
:to="{
name: 'Billing',
params: {
@@ -56,14 +56,14 @@
</Button>
</router-link>
<Badge
v-else-if="course.data.disable_self_learning"
v-else-if="course.data.disable_self_learning && !isAdmin"
theme="blue"
size="lg"
>
{{ __('Contact the Administrator to enroll for this course.') }}
{{ __('Contact the Administrator to enroll for this course') }}
</Badge>
<Button
v-else-if="!user.data?.is_moderator && !is_instructor()"
v-else-if="!isAdmin"
@click="enrollStudent()"
variant="solid"
class="w-full"
@@ -88,40 +88,11 @@
</template>
{{ __('Get Certificate') }}
</Button>
<Button
v-if="user.data?.is_moderator || is_instructor()"
class="w-full mt-2"
size="md"
@click="showProgressSummary"
>
<template #prefix>
<TrendingUp class="size-4 stroke-1.5" />
{{ __('Progress Summary') }}
</template>
</Button>
<router-link
v-if="user?.data?.is_moderator || is_instructor()"
:to="{
name: 'CourseForm',
params: {
courseName: course.data.name,
},
}"
>
<Button variant="subtle" class="w-full mt-2" size="md">
<template #prefix>
<Pencil class="size-4 stroke-1.5" />
</template>
<span>
{{ __('Edit') }}
</span>
</Button>
</router-link>
</div>
<div class="space-y-4">
<div
class="font-medium text-ink-gray-9"
:class="{ 'mt-8': !readOnlyMode }"
:class="{ 'mt-8': course.data.membership && !readOnlyMode }"
>
{{ __('This course has:') }}
</div>
@@ -168,12 +139,6 @@
</div>
</div>
</div>
<CourseProgressSummary
v-if="user.data?.is_moderator || is_instructor()"
v-model="showProgressModal"
:courseName="course.data.name"
:enrollments="course.data.enrollments"
/>
</template>
<script setup>
import {
@@ -191,12 +156,10 @@ import { Badge, Button, call, createResource, toast } from 'frappe-ui'
import { formatAmount } from '@/utils/'
import { useRouter } from 'vue-router'
import CertificationLinks from '@/components/CertificationLinks.vue'
import CourseProgressSummary from '@/components/Modals/CourseProgressSummary.vue'
import { useTelemetry } from 'frappe-ui/frappe'
const router = useRouter()
const user = inject('$user')
const showProgressModal = ref(false)
const readOnlyMode = window.read_only_mode
const { capture } = useTelemetry()
@@ -295,7 +258,7 @@ const fetchCertificate = () => {
})
}
const showProgressSummary = () => {
showProgressModal.value = true
}
const isAdmin = computed(() => {
return user.data?.is_moderator || is_instructor()
})
</script>
+5 -1
View File
@@ -15,7 +15,10 @@
{{ __(title) }}
</div>
<Button size="sm" v-if="allowEdit" @click="openChapterModal()">
{{ __('Add Chapter') }}
<template #prefix>
<Plus class="size-4 stroke-1.5" />
</template>
{{ __('Add') }}
</Button>
</div>
<div
@@ -174,6 +177,7 @@ import {
FilePenLine,
HelpCircle,
MonitorPlay,
Plus,
Trash2,
} from 'lucide-vue-next'
import { useRoute, useRouter } from 'vue-router'
@@ -23,10 +23,8 @@
(value, close) => {
close()
router.push({
name: 'CourseForm',
params: {
courseName: 'new',
},
name: 'Courses',
query: { newCourse: '1' },
})
}
"
@@ -1,231 +0,0 @@
<template>
<Dialog
v-model="show"
:options="{
title: __('Course Progress Summary'),
size: '5xl',
}"
>
<template #body-content>
<div
class="flex flex-col-reverse md:flex-row justify-between md:space-x-10 text-base mt-10"
>
<div class="w-full">
<div class="flex items-center justify-between space-x-5 mb-4">
<FormControl
v-model="searchFilter"
:placeholder="__('Search by Member')"
type="text"
class="w-full"
/>
</div>
<div class="max-h-[70vh] overflow-y-auto">
<ListView
v-if="progressList.loading || progressList.data?.length"
:columns="progressColumns"
:rows="progressList.data"
rowKey="name"
:options="{
selectable: false,
showTooltip: false,
}"
>
<ListHeader
class="mb-2 grid items-center space-x-4 rounded bg-surface-gray-2 p-2"
>
<ListHeaderItem
:item="item"
v-for="item in progressColumns"
:key="item.key"
>
<template #prefix="{ item }">
<FeatherIcon
:name="item.icon?.toString()"
class="h-4 w-4"
/>
</template>
</ListHeaderItem>
</ListHeader>
<ListRows v-for="row in progressList.data">
<router-link
:to="{
name: 'Profile',
params: { username: row.member_username },
}"
>
<ListRow :row="row">
<template #default="{ column, item }">
<ListRowItem
:item="row[column.key]"
:align="column.align"
>
<template #prefix>
<div v-if="column.key == 'member_name'">
<Avatar
class="flex items-center"
:image="row['member_image']"
:label="item"
size="sm"
/>
</div>
</template>
<div>
{{ row[column.key].toString() }}
</div>
</ListRowItem>
</template>
</ListRow>
</router-link>
</ListRows>
</ListView>
<div
v-if="progressList.data && progressList.hasNextPage"
class="flex justify-center my-5"
>
<Button @click="progressList.next()">
{{ __('Load More') }}
</Button>
</div>
</div>
</div>
<div class="mb-4 self-start w-full space-y-5">
<div
class="flex flex-col md:flex-row items-center space-y-2 md:space-y-0 md:space-x-4"
>
<NumberChart
class="border rounded-md w-full"
:config="{
title: __('Enrollments'),
value: memberCount || 0,
}"
/>
<NumberChart
class="border rounded-md w-full"
:config="{
title: __('Average Progress %'),
value: chartDetails.data?.average_progress || 0,
}"
/>
</div>
<DonutChart
:config="{
data: chartDetails.data?.progress_distribution || [],
title: __('Progress Distribution'),
categoryColumn: 'category',
valueColumn: 'count',
colors: [
getColor('red', 400),
getColor('amber', 400),
getColor('pink', 400),
getColor('blue', 400),
getColor('green', 400),
],
}"
/>
</div>
</div>
</template>
</Dialog>
</template>
<script setup lang="ts">
import {
Avatar,
Button,
createListResource,
createResource,
Dialog,
DonutChart,
FeatherIcon,
FormControl,
ListView,
ListHeader,
ListHeaderItem,
ListRows,
ListRow,
ListRowItem,
NumberChart,
} from 'frappe-ui'
import { computed, ref, watch } from 'vue'
import { getColor } from '@/utils'
const show = defineModel<boolean>({ default: false })
const searchFilter = ref<string | null>(null)
type Filters = {
course: string | undefined
member_name?: string[]
}
const props = defineProps<{
courseName?: string
enrollments?: number
}>()
const memberCount = ref<number>(props.enrollments || 0)
const chartDetails = createResource({
url: 'lms.lms.api.get_course_progress_distribution',
params: {
course: props.courseName,
},
auto: true,
})
const progressList = createListResource({
doctype: 'LMS Enrollment',
filters: {
course: props.courseName,
},
fields: [
'name',
'member',
'member_name',
'member_image',
'member_username',
'progress',
],
pageLength: 50,
auto: true,
})
watch([searchFilter], () => {
let filterApplied = false
let filters: Filters = {
course: props.courseName,
}
if (searchFilter.value) {
filters.member_name = ['like', `%${searchFilter.value}%`]
filterApplied = true
}
progressList.update({
filters: filters,
})
progressList.reload(
{},
{
onSuccess(data: any[]) {
memberCount.value = filterApplied ? data.length : props.enrollments || 0
},
}
)
})
const progressColumns = computed(() => {
return [
{
label: __('Member'),
key: 'member_name',
width: '60%',
icon: 'user',
},
{
label: __('Progress'),
key: 'progress',
align: 'right',
icon: 'trending-up',
},
]
})
</script>
@@ -0,0 +1,20 @@
<template>
<div class="border rounded-lg p-3 space-y-2">
<div class="text-ink-gray-5">
{{ __(title) }}
</div>
<div class="flex items-center space-x-2">
<slot name="prefix" />
<div class="font-semibold text-2xl">
{{ value }}
</div>
<slot name="suffix" />
</div>
</div>
</template>
<script setup lang="ts">
const props = defineProps<{
title: string
value: number | string
}>()
</script>
+4 -1
View File
@@ -1,6 +1,9 @@
<template>
<Tooltip :text="`${props.progress}%`">
<div class="w-full bg-surface-gray-3 rounded-full h-1">
<div
class="w-full bg-surface-gray-3 rounded-full h-1"
:class="$attrs.class"
>
<div
class="bg-surface-gray-7 rounded-full"
:class="progressBarHeight"
@@ -1,16 +1,25 @@
<template>
<div class="flex flex-col justify-between h-full">
<div class="flex flex-col h-full">
<div>
<div class="flex items-center justify-between">
<div class="font-semibold mb-1 text-ink-gray-9">
{{ __(label) }}
</div>
<Badge
v-if="isDirty"
:label="__('Not Saved')"
variant="subtle"
theme="orange"
/>
<div class="space-x-2">
<Badge
v-if="isDirty"
:label="__('Not Saved')"
variant="subtle"
theme="orange"
/>
<Button
variant="solid"
:loading="saveSettings.loading"
@click="update"
>
{{ __('Update') }}
</Button>
</div>
</div>
<div class="text-xs text-ink-gray-5">
{{ __(description) }}
@@ -19,11 +28,6 @@
<div class="overflow-y-auto">
<SettingFields :sections="sections" :data="branding.data" />
</div>
<div class="flex flex-row-reverse mt-auto">
<Button variant="solid" :loading="saveSettings.loading" @click="update">
{{ __('Update') }}
</Button>
</div>
</div>
</template>
<script setup>
+17 -15
View File
@@ -8,22 +8,24 @@
<h1 class="mb-3 px-2 pt-2 text-lg font-semibold text-ink-gray-9">
{{ __('Settings') }}
</h1>
<div v-for="tab in tabs" :key="tab.label">
<div
v-if="!tab.hideLabel"
class="mb-2 mt-3 flex cursor-pointer gap-1.5 px-1 text-base text-ink-gray-5 transition-all duration-300 ease-in-out"
>
<span>{{ __(tab.label) }}</span>
</div>
<nav class="space-y-1">
<div v-for="item in tab.items" @click="activeTab = item">
<SidebarLink
:link="item"
:key="item.label"
:activeTab="activeTab?.label"
/>
<div class="space-y-5">
<div v-for="tab in tabs" :key="tab.label">
<div
v-if="!tab.hideLabel"
class="mb-2 mt-3 flex cursor-pointer gap-1.5 px-1 text-base text-ink-gray-5 transition-all duration-300 ease-in-out"
>
<span>{{ __(tab.label) }}</span>
</div>
</nav>
<nav class="space-y-1">
<div v-for="item in tab.items" @click="activeTab = item">
<SidebarLink
:link="item"
:key="item.label"
:activeTab="activeTab?.label"
/>
</div>
</nav>
</div>
</div>
</div>
<div
@@ -1,12 +1,33 @@
<template>
<div class="flex flex-col h-full text-base">
<div class="flex items-center space-x-2 mb-8 -ml-1.5">
<ChevronLeft
class="size-5 stroke-1.5 text-ink-gray-7 cursor-pointer"
@click="emit('updateStep', 'list')"
/>
<div class="text-xl font-semibold text-ink-gray-9">
{{ __('Transaction Details') }}
<div class="flex items-center justify-between mb-10 -ml-1.5">
<div class="flex items-center space-x-2">
<ChevronLeft
class="size-5 stroke-1.5 text-ink-gray-7 cursor-pointer"
@click="emit('updateStep', 'list')"
/>
<div class="text-xl font-semibold text-ink-gray-9">
{{ __('Transaction Details') }}
</div>
</div>
<div class="space-x-2">
<Button
v-if="
transactionData?.payment_for_document_type &&
transactionData?.payment_for_document
"
@click="openDetails()"
>
{{ __('Open the ') }}
{{
transactionData.payment_for_document_type == 'LMS Course'
? __('Course')
: __('Batch')
}}
</Button>
<Button variant="solid" @click="saveTransaction()">
{{ __('Save') }}
</Button>
</div>
</div>
<div v-if="transactionData" class="overflow-y-auto">
@@ -21,6 +42,12 @@
type="checkbox"
v-model="transactionData.payment_for_certificate"
/>
<FormControl
:label="__('Member Consent')"
type="checkbox"
v-model="transactionData.member_consent"
:disabled="true"
/>
</div>
<div class="grid grid-cols-3 gap-5 mt-5">
@@ -28,22 +55,27 @@
:label="__('Member')"
doctype="User"
v-model="transactionData.member"
:required="true"
/>
<FormControl
:label="__('Billing Name')"
v-model="transactionData.billing_name"
:required="true"
/>
<Link
:label="__('Source')"
v-model="transactionData.source"
doctype="LMS Source"
/>
<Link
<FormControl
type="select"
:options="documentTypeOptions"
:label="__('Payment For Document Type')"
v-model="transactionData.payment_for_document_type"
doctype="DocType"
/>
<Link
v-if="transactionData.payment_for_document_type"
:label="__('Payment For Document')"
v-model="transactionData.payment_for_document"
:doctype="transactionData.payment_for_document_type"
@@ -58,8 +90,13 @@
:label="__('Currency')"
v-model="transactionData.currency"
doctype="Currency"
:required="true"
/>
<FormControl
:label="__('Amount')"
v-model="transactionData.amount"
:required="true"
/>
<FormControl :label="__('Amount')" v-model="transactionData.amount" />
<FormControl
v-if="transactionData.amount_with_gst"
:label="__('Amount with GST')"
@@ -103,6 +140,7 @@
:label="__('Address')"
v-model="transactionData.address"
doctype="Address"
:required="true"
/>
<FormControl :label="__('GSTIN')" v-model="transactionData.gstin" />
<FormControl :label="__('PAN')" v-model="transactionData.pan" />
@@ -116,25 +154,12 @@
/>
</div>
</div>
<div class="space-x-2 mt-auto ml-auto">
<Button @click="openDetails()">
{{ __('Open the ') }}
{{
data.payment_for_document_type == 'LMS Course'
? __('Course')
: __('Batch')
}}
</Button>
<Button variant="solid" @click="saveTransaction()">
{{ __('Save') }}
</Button>
</div>
</div>
</template>
<script setup lang="ts">
import { Button, FormControl } from 'frappe-ui'
import { Button, FormControl, toast } from 'frappe-ui'
import { useRouter } from 'vue-router'
import { ref, watch } from 'vue'
import { computed, ref, watch } from 'vue'
import { ChevronLeft } from 'lucide-vue-next'
import Link from '@/components/Controls/Link.vue'
@@ -148,21 +173,40 @@ const props = defineProps<{
data: any
}>()
watch(
() => props.data,
(newVal) => {
transactionData.value = newVal ? { ...newVal } : null
},
{ immediate: true }
)
const saveTransaction = () => {
if (props.data?.name) {
updateTransaction()
} else {
createTransaction()
}
}
const saveTransaction = (close: () => void) => {
props.transactions.value.setValue
const createTransaction = () => {
console.log(props.transactions)
props.transactions.insert
.submit({
...transactionData.value,
})
.then(() => {
close()
toast.success(__('Transaction created successfully'))
})
.catch((err: any) => {
toast.error(__(err.messages?.[0] || err))
console.error(err)
})
}
const updateTransaction = () => {
props.transactions.setValue
.submit({
...transactionData.value,
})
.then(() => {
toast.success(__('Transaction updated successfully'))
})
.catch((err: any) => {
toast.error(__(err.messages?.[0] || err))
console.error(err)
})
}
@@ -181,4 +225,48 @@ const openDetails = () => {
show.value = false
}
}
const emptyTransactionData = {
payment_received: false,
payment_for_certificate: false,
member: null,
billing_name: null,
source: null,
payment_for_document_type: null,
payment_for_document: null,
member_consent: false,
currency: null,
amount: null,
amount_with_gst: null,
coupon: null,
coupon_code: null,
discount_amount: null,
original_amount: null,
order_id: null,
payment_id: null,
gstin: null,
pan: null,
address: null,
}
watch(
() => props.data,
(newVal) => {
transactionData.value = newVal ? { ...newVal } : emptyTransactionData
},
{ immediate: true }
)
const documentTypeOptions = computed(() => {
return [
{
label: __('Course'),
value: 'LMS Course',
},
{
label: __('Batch'),
value: 'LMS Batch',
},
]
})
</script>
@@ -1,12 +1,20 @@
<template>
<div class="flex min-h-0 flex-col text-base">
<div class="mb-5">
<div class="text-xl font-semibold mb-1 text-ink-gray-9">
{{ __(label) }}
</div>
<div class="text-ink-gray-6 leading-5">
{{ __(description) }}
<div class="flex items-center justify-between mb-5">
<div>
<div class="text-xl font-semibold mb-1 text-ink-gray-9">
{{ __(label) }}
</div>
<div class="text-ink-gray-6 leading-5">
{{ __(description) }}
</div>
</div>
<Button @click="emit('updateStep', 'new', null)">
<template #prefix>
<FeatherIcon name="plus" class="h-4 w-4 stroke-1.5" />
</template>
{{ __('Add Transaction') }}
</Button>
</div>
<div class="flex items-center space-x-5 mb-4">
@@ -1,6 +1,13 @@
<template>
<TransactionDetails
v-if="step == 'new'"
:transactions="transactions"
:data="data"
v-model:show="show"
@updateStep="updateStep"
/>
<TransactionList
v-if="step === 'list'"
v-else-if="step === 'list'"
:label="props.label"
:description="props.description"
:transactions="transactions"
@@ -33,6 +40,8 @@ const updateStep = (newStep: 'list' | 'new' | 'edit', newData: any) => {
step.value = newStep
if (newData) {
data.value = newData
} else {
data.value = null
}
}
@@ -405,9 +405,13 @@ const steps = reactive([
minimize.value = true
let course = await getFirstCourse()
if (course) {
router.push({ name: 'CourseForm', params: { courseName: course } })
router.push({
name: 'CourseDetail',
params: { courseName: course },
hash: '#settings',
})
} else {
router.push({ name: 'CourseForm' })
router.push({ name: 'Courses', query: { newCourse: '1' } })
}
},
},
@@ -422,11 +426,12 @@ const steps = reactive([
let course = await getFirstCourse()
if (course) {
router.push({
name: 'CourseForm',
name: 'CourseDetail',
params: { courseName: course },
hash: '#settings',
})
} else {
router.push({ name: 'Courses' })
router.push({ name: 'Courses', query: { newCourse: '1' } })
}
},
},
-194
View File
@@ -1,194 +0,0 @@
<template>
<div v-if="course.data">
<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 class="h-7" :items="breadcrumbs" />
</header>
<div class="m-5">
<div class="flex justify-between w-full space-x-5">
<div class="md:w-2/3">
<div class="text-3xl font-semibold text-ink-gray-9">
{{ course.data.title }}
</div>
<div class="my-3 leading-6 text-ink-gray-7">
{{ course.data.short_introduction }}
</div>
<div class="flex items-center">
<Tooltip
v-if="parseInt(course.data.rating) > 0"
:text="__('Average Rating')"
class="flex items-center"
>
<Star class="size-4 text-transparent fill-yellow-500" />
<span class="ml-1 text-ink-gray-7">
{{ course.data.rating }}
</span>
</Tooltip>
<span v-if="parseInt(course.data.rating) > 0" class="mx-3"
>&middot;</span
>
<Tooltip
v-if="course.data.enrollment_count"
:text="__('Enrolled Students')"
class="flex items-center"
>
<Users class="h-4 w-4 text-ink-gray-7" />
<span class="ml-1">
{{ course.data.enrollment_count_formatted }}
</span>
</Tooltip>
<span v-if="course.data.enrollment_count" class="mx-3"
>&middot;</span
>
<div class="flex items-center">
<span
class="h-6 mr-1"
:class="{
'avatar-group overlap': course.data.instructors.length > 1,
}"
>
<UserAvatar
v-for="instructor in course.data.instructors"
:user="instructor"
/>
</span>
<CourseInstructors :instructors="course.data.instructors" />
</div>
</div>
<div v-if="course.data.tags" class="flex my-4 w-fit">
<Badge
theme="gray"
size="lg"
class="mr-2 text-ink-gray-9"
v-for="tag in course.data.tags.split(', ')"
>
{{ tag }}
</Badge>
</div>
<div class="md:hidden my-4">
<CourseCardOverlay :course="course" />
</div>
<div
v-html="course.data.description"
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"
></div>
<div class="mt-10">
<CourseOutline
:title="__('Course Outline')"
:courseName="course.data.name"
:showOutline="true"
:getProgress="course.data.membership ? true : false"
/>
</div>
<CourseReviews
:courseName="course.data.name"
:avg_rating="course.data.rating"
:membership="course.data.membership"
/>
</div>
<div class="hidden md:block">
<CourseCardOverlay :course="course" />
</div>
</div>
<RelatedCourses :courseName="course.data.name" />
</div>
</div>
</template>
<script setup>
import {
createResource,
Breadcrumbs,
Badge,
Tooltip,
usePageMeta,
} from 'frappe-ui'
import { computed, inject, watch } from 'vue'
import { Users, Star } from 'lucide-vue-next'
import { sessionStore } from '@/stores/session'
import { useRouter } from 'vue-router'
import CourseCardOverlay from '@/components/CourseCardOverlay.vue'
import CourseOutline from '@/components/CourseOutline.vue'
import CourseReviews from '@/components/CourseReviews.vue'
import UserAvatar from '@/components/UserAvatar.vue'
import CourseInstructors from '@/components/CourseInstructors.vue'
import RelatedCourses from '@/components/RelatedCourses.vue'
const { brand } = sessionStore()
const router = useRouter()
const user = inject('$user')
const props = defineProps({
courseName: {
type: String,
required: true,
},
})
const course = createResource({
url: 'lms.lms.utils.get_course_details',
cache: ['course', props.courseName],
makeParams() {
return {
course: props.courseName,
}
},
auto: true,
})
watch(
() => props.courseName,
() => {
course.reload()
}
)
watch(course, () => {
if (
!isInstructor() &&
!user.data?.is_moderator &&
!course.data?.published &&
!course.data?.upcoming
) {
router.push({
name: 'Courses',
})
}
})
const isInstructor = () => {
let user_is_instructor = false
course.data?.instructors.forEach((instructor) => {
if (!user_is_instructor && instructor.name == user.data?.name) {
user_is_instructor = true
}
})
return user_is_instructor
}
const breadcrumbs = computed(() => {
let items = [{ label: 'Courses', route: { name: 'Courses' } }]
items.push({
label: course?.data?.title,
route: { name: 'CourseDetail', params: { courseName: course?.data?.name } },
})
return items
})
usePageMeta(() => {
return {
title: course?.data?.title,
icon: brand.favicon,
}
})
</script>
<style>
.avatar-group {
display: inline-flex;
align-items: center;
}
.avatar-group .avatar {
transition: margin 0.1s ease-in-out;
}
</style>
@@ -38,7 +38,7 @@
import { computed, inject, onMounted, ref } from 'vue'
import { Breadcrumbs, call, createResource, usePageMeta } from 'frappe-ui'
import { useRouter } from 'vue-router'
import { sessionStore } from '../stores/session'
import { sessionStore } from '../../stores/session'
import UpcomingEvaluations from '@/components/UpcomingEvaluations.vue'
const courseTitle = ref(null)
@@ -0,0 +1,397 @@
<template>
<div class="p-5">
<div class="grid grid-cols-4 gap-5 mb-5">
<NumberChartGraph
:title="__('Enrolled')"
:value="formatAmount(course.data?.enrollments)"
/>
<NumberChartGraph
:title="__('Average Completion Rate')"
:value="averageCompletionRate"
/>
<NumberChartGraph
:title="__('Average Rating')"
:value="course.data?.rating || 0"
>
<template #prefix>
<Star class="size-5 text-transparent fill-amber-500" />
</template>
</NumberChartGraph>
<NumberChartGraph :title="__('Lessons')" :value="course.data?.lessons" />
</div>
<div class="grid grid-cols-[2fr_1fr] gap-5 items-start">
<div v-if="course.data?.enrollments" class="border rounded-lg py-3 px-4">
<div class="flex items-center justify-between mb-3">
<div class="text-lg font-semibold">
{{ __('Students') }}
</div>
<div class="flex items-center space-x-2">
<FormControl
v-model="searchFilter"
:placeholder="__('Search by name')"
type="text"
/>
<Button @click="showEnrollmentModal = true">
<template #prefix>
<Plus class="size-4 stroke-1.5" />
</template>
{{ __('Enroll') }}
</Button>
</div>
</div>
<div
v-if="progressList.loading || progressList.data?.length"
class="max-h-[63vh] overflow-y-auto"
>
<ListView
:columns="progressColumns"
:rows="progressList.data"
rowKey="name"
:options="{
selectable: false,
showTooltip: false,
}"
>
<ListHeader
class="mb-2 grid items-center space-x-4 rounded bg-surface-white border-b rounded-none p-2"
>
<ListHeaderItem
:item="item"
v-for="item in progressColumns"
:key="item.key"
>
</ListHeaderItem>
</ListHeader>
<ListRows v-for="row in progressList.data" class="max-h-[500px]">
<router-link
:to="{
name: 'Profile',
params: { username: row.member_username },
}"
>
<ListRow :row="row">
<template #default="{ column, item }">
<ListRowItem
:item="row[column.key]"
:align="column.align"
class="w-full"
>
<template #prefix>
<div v-if="column.key == 'member_name'">
<Avatar
class="flex items-center"
:image="row['member_image']"
:label="item"
size="sm"
/>
</div>
<ProgressBar
v-else-if="column.key == 'progress'"
:progress="Math.ceil(row[column.key])"
class="!mx-0 !mr-4"
/>
</template>
<div v-if="column.key == 'creation'">
{{ dayjs(row[column.key]).format('DD MMM YYYY') }}
</div>
<div
v-else-if="column.key == 'progress'"
class="text-xs !mx-0 w-5"
>
{{ Math.ceil(row[column.key]) }}%
</div>
<div v-else>
{{ row[column.key].toString() }}
</div>
</ListRowItem>
</template>
</ListRow>
</router-link>
</ListRows>
</ListView>
<div
v-if="progressList.data && progressList.hasNextPage"
class="flex justify-center my-3"
>
<Button @click="progressList.next()">
{{ __('Load More') }}
</Button>
</div>
</div>
</div>
<div class="space-y-5">
<div v-if="chartDetails.data?.length" class="border rounded-lg p-4">
<div class="text-ink-gray-5 mb-4">
{{ __('Progress Summary') }}
</div>
<div class="grid grid-cols-[2fr_1fr] items-center justify-between">
<div class="flex flex-col space-y-4 flex-1 text-sm">
<div
class="flex items-center"
v-for="row in chartDetails.data?.progress_distribution"
>
<div
class="size-2 rounded"
:style="{
backgroundColor:
colors[theme][
row.name.startsWith('Just')
? 'red'
: row.name.startsWith('In')
? 'amber'
: 'green'
][400],
}"
></div>
<Tooltip :text="row.name.split('(')[1].replace(')', '')">
<div class="ml-2">
{{ row.name.split('(')[0] }}
</div>
</Tooltip>
<div class="ml-auto">
{{
Math.round((row.value / course.data?.enrollments) * 100)
}}%
</div>
</div>
</div>
<ECharts
class="w-40 h-20"
:options="{
color: progressColors,
series: [
{
type: 'pie',
radius: ['50%', '70%'],
center: ['50%', '50%'],
label: {
show: false,
},
labelLine: {
show: false,
},
emphasis: {
label: {
show: false,
},
scale: false,
},
legend: {
show: false,
},
data: chartDetails.data?.progress_distribution || [],
},
],
showInlineLabels: false,
}"
/>
</div>
</div>
<div
v-if="lessonProgress.data?.length"
class="border rounded-lg pt-4 px-4"
>
<div class="flex items-center justify-between mb-4">
<div class="text-ink-gray-5">
{{ __('Lesson Completion') }}
</div>
<Select
:options="lessonProgressSortingOptions"
@update:modelValue="(value: string) => updateLessonProgress(value)"
:placeholder="__('Sort by')"
class="!w-32"
/>
</div>
<div class="divide-y max-h-[43vh] overflow-y-auto">
<div
v-for="progress in lessonProgress.data"
class="flex justify-between text-sm py-2 my-1"
>
<div class="">
<span class="mr-3 text-xs">
{{ progress.chapter_idx }}.{{ progress.idx }}
</span>
<span>
{{ progress.title }}
</span>
</div>
<Tooltip :text="progress.completion_count">
<div>
{{
Math.ceil(
(progress.completion_count / course.data?.enrollments) *
100
)
}}%
</div>
</Tooltip>
</div>
</div>
</div>
</div>
</div>
</div>
<CourseEnrollmentModal
v-if="showEnrollmentModal"
v-model="showEnrollmentModal"
:course="course"
/>
</template>
<script setup lang="ts">
import {
Avatar,
Button,
createListResource,
createResource,
dayjs,
Dropdown,
ECharts,
FormControl,
ListView,
ListHeader,
ListHeaderItem,
ListRows,
ListRow,
ListRowItem,
Select,
Tooltip,
} from 'frappe-ui'
import { computed, ref, watch } from 'vue'
import { ChevronDown, Plus, Star } from 'lucide-vue-next'
import { formatAmount } from '@/utils'
import colors from '@/utils/frappe-ui-colors.json'
import CourseEnrollmentModal from '@/pages/Courses/CourseEnrollmentModal.vue'
import NumberChartGraph from '@/components/NumberChartGraph.vue'
import ProgressBar from '@/components/ProgressBar.vue'
const props = defineProps<{
course: any
}>()
const showEnrollmentModal = ref(false)
const searchFilter = ref<string | null>(null)
const theme = ref<'darkMode' | 'lightMode'>(
localStorage.getItem('theme') == 'dark' ? 'darkMode' : 'lightMode'
)
type Filters = {
course: string | undefined
member_name?: string[]
}
const chartDetails = createResource({
url: 'lms.lms.api.get_course_progress_distribution',
makeParams() {
return {
course: props.course.data?.name,
}
},
auto: true,
})
const progressList = createListResource({
doctype: 'LMS Enrollment',
filters: {
course: props.course.data?.name,
},
fields: [
'name',
'member',
'member_name',
'member_image',
'member_username',
'progress',
'creation',
],
pageLength: 100,
auto: true,
})
const lessonProgress = createResource({
url: 'lms.lms.api.get_lesson_completion_stats',
params: {
course: props.course.data?.name,
},
auto: true,
})
const updateLessonProgress = (value: string) => {
if (value == 'completion_rate') {
lessonProgress.data?.sort((a: any, b: any) => {
const rateA = a.completion_count / (props.course.data?.enrollments || 1)
const rateB = b.completion_count / (props.course.data?.enrollments || 1)
return rateB - rateA
})
} else if (value == 'index') {
lessonProgress.data?.sort((a: any, b: any) => {
return a.chapter_idx - b.chapter_idx || a.idx - b.idx
})
}
}
watch([searchFilter], () => {
let filterApplied = false
let filters: Filters = {
course: props.course.data?.name,
}
if (searchFilter.value) {
filters.member_name = ['like', `%${searchFilter.value}%`]
filterApplied = true
}
progressList.update({
filters: filters,
})
progressList.reload()
})
const averageCompletionRate = computed(() => {
let value = Math.ceil(chartDetails.data?.average_progress) || 0
return value + '%'
})
const progressColors = computed(() => {
let colorList = []
colorList.push(colors[theme.value]['red'][400])
colorList.push(colors[theme.value]['amber'][400])
colorList.push(colors[theme.value]['green'][400])
return colorList
})
const progressColumns = computed(() => {
return [
{
label: __('Name'),
key: 'member_name',
width: '40%',
},
{
label: __('Progress'),
key: 'progress',
width: '30%',
},
{
label: __('Start Date'),
key: 'creation',
align: 'right',
},
]
})
const lessonProgressSortingOptions = [
{
label: __('Lesson Index'),
value: 'index',
onClick() {
updateLessonProgress('index')
},
},
{
label: __('Completion Rate'),
value: 'completion_rate',
onClick() {
updateLessonProgress('completion_rate')
},
},
]
</script>
+167
View File
@@ -0,0 +1,167 @@
<template>
<div v-if="course.data">
<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 class="h-7" :items="breadcrumbs" />
<div v-if="tabIndex == 2" class="flex items-center space-x-2">
<Badge v-if="childRef?.isDirty" theme="orange">
{{ __('Not Saved') }}
</Badge>
<Button @click="childRef.trashCourse()">
<template #icon>
<Trash2 class="w-4 h-4 stroke-1.5" />
</template>
</Button>
<Button variant="solid" @click="childRef.submitCourse()">
{{ __('Save') }}
</Button>
</div>
</header>
<CourseOverview v-if="!isAdmin" :course="course" />
<div v-else>
<Tabs :tabs="tabs" v-model="tabIndex">
<template #tab-panel="{ tab }">
<component :is="tab.component" :course="course" ref="childRef" />
</template>
</Tabs>
</div>
</div>
</template>
<script setup>
import {
Badge,
Button,
createResource,
Breadcrumbs,
Tabs,
usePageMeta,
} from 'frappe-ui'
import { computed, inject, markRaw, onMounted, ref, watch } from 'vue'
import { sessionStore } from '@/stores/session'
import { useRouter, useRoute } from 'vue-router'
import { List, Settings2, Trash2, TrendingUp } from 'lucide-vue-next'
import CourseOverview from '@/pages/Courses/CourseOverview.vue'
import CourseDashboard from '@/pages/Courses/CourseDashboard.vue'
import CourseForm from '@/pages/Courses/CourseForm.vue'
const { brand } = sessionStore()
const router = useRouter()
const route = useRoute()
const user = inject('$user')
const tabIndex = ref(0)
const childRef = ref(null)
const props = defineProps({
courseName: {
type: String,
required: true,
},
})
onMounted(() => {
updateTabIndex()
})
const updateTabIndex = () => {
const hash = route.hash
if (hash) {
tabs.value.forEach((tab, index) => {
if (tab.label?.toLowerCase() === hash.replace('#', '')) {
tabIndex.value = index
}
})
}
}
watch(tabIndex, () => {
const tab = tabs.value[tabIndex.value]
if (tab.label != route.hash.replace('#', '')) {
router.push({ ...route, hash: `#${tab.label.toLowerCase()}` })
}
})
const course = createResource({
url: 'lms.lms.utils.get_course_details',
cache: ['course', props.courseName],
makeParams() {
return {
course: props.courseName,
}
},
auto: true,
})
const tabs = ref([
{
label: __('Overview'),
component: markRaw(CourseOverview),
icon: List,
},
{
label: __('Dashboard'),
component: markRaw(CourseDashboard),
icon: TrendingUp,
},
{
label: __('Settings'),
component: markRaw(CourseForm),
icon: Settings2,
},
])
watch(
() => props.courseName,
() => {
course.reload()
}
)
watch(course, () => {
if (!isAdmin.value && !course.data?.published && !course.data?.upcoming) {
router.push({
name: 'Courses',
})
}
})
const isInstructor = () => {
let user_is_instructor = false
course.data?.instructors.forEach((instructor) => {
if (!user_is_instructor && instructor.name == user.data?.name) {
user_is_instructor = true
}
})
return user_is_instructor
}
const isAdmin = computed(() => {
return user.data?.is_moderator || isInstructor()
})
const breadcrumbs = computed(() => {
let items = [{ label: 'Courses', route: { name: 'Courses' } }]
items.push({
label: course?.data?.title,
route: { name: 'CourseDetail', params: { courseName: course?.data?.name } },
})
return items
})
usePageMeta(() => {
return {
title: course?.data?.title,
icon: brand.favicon,
}
})
</script>
<style>
.avatar-group {
display: inline-flex;
align-items: center;
}
.avatar-group .avatar {
transition: margin 0.1s ease-in-out;
}
</style>
@@ -0,0 +1,104 @@
<template>
<Dialog
v-model="show"
:options="{
title: __('Enroll a Student'),
size: 'xl',
}"
>
<template #body-content>
<div class="space-y-4">
<FormControl
type="checkbox"
:label="__('Purchased Certificate')"
v-model="purchasedCertificate"
/>
<Link
doctype="User"
:label="__('Student')"
placeholder=" "
v-model="student"
:required="true"
:allowCreate="true"
@create="
() => {
openSettings('Members')
show = false
}
"
/>
<Link
v-if="purchasedCertificate"
doctype="LMS Payment"
:label="__('Payment')"
placeholder=" "
v-model="payment"
:allowCreate="true"
@create="
() => {
openSettings('Transactions')
show = false
}
"
/>
</div>
</template>
<template #actions="{ close }">
<div class="text-right">
<Button variant="solid" @click="enrollStudent(close)">
{{ __('Enroll') }}
</Button>
</div>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { Button, call, Dialog, FormControl, toast } from 'frappe-ui'
import { Link } from 'frappe-ui/frappe'
import { ref } from 'vue'
import { openSettings } from '@/utils'
const show = defineModel<boolean>({ required: true, default: false })
const student = ref<string | null>(null)
const payment = ref<string | null>(null)
const purchasedCertificate = ref<boolean>(false)
const props = defineProps<{
course: any
}>()
const enrollStudent = (close: () => void) => {
let validationPassed = validateData()
if (!validationPassed) return
call('frappe.client.insert', {
doc: {
doctype: 'LMS Enrollment',
course: props.course.data?.name,
member: student.value,
payment: purchasedCertificate.value ? payment.value : null,
purchased_certificate: purchasedCertificate.value,
},
})
.then(() => {
toast.success(__('Student enrolled successfully'))
close()
})
.catch((err: any) => {
toast.error(__(err.messages?.[0] || err))
console.error(err)
})
}
const validateData = (): boolean => {
if (!student.value) {
toast.error(__('Please select a student to enroll.'))
return false
}
if (purchasedCertificate.value && !payment.value) {
toast.error(__('Please select a payment for the purchased certificate.'))
return false
}
return true
}
</script>
@@ -1,40 +1,25 @@
<template>
<div class="h-full">
<div class="grid grid-cols-1 md:grid-cols-[70%,30%] h-full">
<div>
<header
class="sticky top-0 z-10 flex flex-col md:flex-row md:items-center justify-between border-b bg-surface-white px-3 py-2.5 sm:px-5"
>
<Breadcrumbs class="h-7" :items="breadcrumbs" />
<div class="flex items-center mt-3 md:mt-0">
<Button v-if="courseResource.data?.name" @click="trashCourse()">
<template #icon>
<Trash2 class="w-4 h-4 stroke-1.5" />
</template>
</Button>
<Button variant="solid" @click="submitCourse()" class="ml-2">
<span>
{{ __('Save') }}
</span>
</Button>
</div>
</header>
<div class="mt-5 mb-5">
<div class="px-5 md:px-10 pb-5 mb-5 space-y-5 border-b">
<div class="pl-5">
<div class="grid grid-cols-1 md:grid-cols-[70%,30%] overflow-hidden">
<div v-if="courseResource.doc" class="h-[88vh] overflow-y-auto">
<div class="my-5">
<div class="pr-5 md:pr-10 pb-5 mb-5 space-y-5 border-b">
<div class="text-lg font-semibold mb-4 text-ink-gray-9">
{{ __('Details') }}
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-5">
<FormControl
v-model="course.title"
v-model="courseResource.doc.title"
:label="__('Title')"
:required="true"
@input="makeFormDirty()"
/>
<Link
doctype="LMS Category"
v-model="course.category"
v-model="courseResource.doc.category"
:label="__('Category')"
:onCreate="(value, close) => openSettings('Categories', close)"
@update:modelValue="makeFormDirty()"
/>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-5">
@@ -45,6 +30,7 @@
:filters="{ ignore_user_type: 1 }"
:onCreate="(close) => openSettings('Members', close)"
:required="true"
@update:modelValue="makeFormDirty()"
/>
<div>
<div class="text-xs text-ink-gray-5">
@@ -60,8 +46,8 @@
<div>
<div class="flex items-center flex-wrap gap-2">
<div
v-if="course.tags"
v-for="tag in course.tags?.split(', ')"
v-if="courseResource.doc.tags"
v-for="tag in courseResource.doc.tags?.split(', ')"
class="flex items-center bg-surface-gray-2 text-ink-gray-7 p-2 rounded-md"
>
{{ tag }}
@@ -76,21 +62,23 @@
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-5">
<Uploader
v-model="course.image"
v-model="courseResource.doc.image"
:label="__('Course Image')"
:required="false"
@update:modelValue="makeFormDirty()"
/>
<ColorSwatches
v-model="course.card_gradient"
v-model="courseResource.doc.card_gradient"
:label="__('Color')"
:description="__('Choose a color for the course card')"
class="w-full"
@update:modelValue="makeFormDirty()"
/>
</div>
</div>
<div class="px-5 md:px-10 pb-5 mb-5 space-y-5 border-b">
<div class="pr-5 md:pr-10 pb-5 mb-5 space-y-5 border-b">
<div class="text-lg font-semibold text-ink-gray-9">
{{ __('Settings') }}
</div>
@@ -101,41 +89,46 @@
>
<FormControl
type="checkbox"
v-model="course.published"
v-model="courseResource.doc.published"
:label="__('Published')"
@change="makeFormDirty()"
/>
<FormControl
v-model="course.published_on"
v-model="courseResource.doc.published_on"
:label="__('Published On')"
type="date"
@change="makeFormDirty()"
/>
</div>
<div class="flex flex-col space-y-5">
<FormControl
type="checkbox"
v-model="course.upcoming"
v-model="courseResource.doc.upcoming"
:label="__('Upcoming')"
@change="makeFormDirty()"
/>
<FormControl
type="checkbox"
v-model="course.featured"
v-model="courseResource.doc.featured"
:label="__('Featured')"
@change="makeFormDirty()"
/>
<FormControl
type="checkbox"
v-model="course.disable_self_learning"
v-model="courseResource.doc.disable_self_learning"
:label="__('Disable Self Enrollment')"
@change="makeFormDirty()"
/>
</div>
</div>
</div>
<div class="px-5 md:px-10 pb-5 mb-5 space-y-5 border-b">
<div class="pr-5 md:pr-10 pb-5 mb-5 space-y-5 border-b">
<div class="text-lg font-semibold text-ink-gray-9">
{{ __('About the Course') }}
</div>
<FormControl
v-model="course.short_introduction"
v-model="courseResource.doc.short_introduction"
type="textarea"
:rows="5"
:label="__('Short Introduction')"
@@ -145,6 +138,7 @@
)
"
:required="true"
@change="makeFormDirty()"
/>
<div class="">
<div class="mb-1.5 text-sm text-ink-gray-5">
@@ -152,8 +146,13 @@
<span class="text-ink-red-3">*</span>
</div>
<TextEditor
:content="course.description"
@change="(val) => (course.description = val)"
:content="courseResource.doc.description"
@change="
(val) => {
courseResource.doc.description = val
makeFormDirty()
}
"
:editable="true"
:fixedMenu="true"
editorClass="prose-sm max-w-none border-b border-x bg-surface-gray-2 rounded-b-md py-1 px-2 min-h-[7rem]"
@@ -161,92 +160,113 @@
</div>
<FormControl
v-model="course.video_link"
v-model="courseResource.doc.video_link"
:label="__('Preview Video')"
:placeholder="
__(
'Paste the youtube link of a short video introducing the course'
)
"
@input="makeFormDirty()"
/>
<MultiSelect
v-model="related_courses"
doctype="LMS Course"
:label="__('Related Courses')"
:filters="{ name: ['!=', courseResource.data?.name] }"
:filters="{ name: ['!=', courseResource.doc?.name] }"
:onCreate="
(close) => {
router.push({
name: 'CourseForm',
params: { courseName: 'new' },
name: 'Courses',
query: { newCourse: '1' },
})
}
"
@update:modelValue="makeFormDirty()"
/>
</div>
<div class="px-5 md:px-10 pb-5 space-y-5 border-b">
<div class="pr-5 md:pr-10 pb-5 space-y-5 border-b">
<div class="text-lg font-semibold mt-5 text-ink-gray-9">
{{ __('Pricing and Certification') }}
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-5">
<FormControl
type="checkbox"
v-model="course.paid_course"
v-model="courseResource.doc.paid_course"
:label="__('Paid Course')"
@change="makeFormDirty()"
/>
<FormControl
type="checkbox"
v-model="course.enable_certification"
v-model="courseResource.doc.enable_certification"
:label="__('Completion Certificate')"
@change="makeFormDirty()"
/>
<FormControl
type="checkbox"
v-model="course.paid_certificate"
v-model="courseResource.doc.paid_certificate"
:label="__('Paid Certificate')"
@change="makeFormDirty()"
/>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-5">
<div class="space-y-5">
<FormControl
v-if="course.paid_course || course.paid_certificate"
v-model="course.course_price"
v-if="
courseResource.doc.paid_course ||
courseResource.doc.paid_certificate
"
v-model="courseResource.doc.course_price"
:label="__('Amount')"
:required="course.paid_course || course.paid_certificate"
:required="
courseResource.doc.paid_course ||
courseResource.doc.paid_certificate
"
@input="makeFormDirty()"
/>
<Link
v-if="course.paid_certificate"
v-if="courseResource.doc.paid_certificate"
doctype="Course Evaluator"
v-model="course.evaluator"
v-model="courseResource.doc.evaluator"
:label="__('Evaluator')"
:required="course.paid_certificate"
:required="courseResource.doc.paid_certificate"
:onCreate="
(value, close) => openSettings('Evaluators', close)
"
@update:modelValue="makeFormDirty()"
/>
</div>
<div class="space-y-5">
<Link
v-if="course.paid_course || course.paid_certificate"
v-if="
courseResource.doc.paid_course ||
courseResource.doc.paid_certificate
"
doctype="Currency"
v-model="course.currency"
v-model="courseResource.doc.currency"
:filters="{ enabled: 1 }"
:label="__('Currency')"
:required="course.paid_course || course.paid_certificate"
:required="
courseResource.doc.paid_course ||
courseResource.doc.paid_certificate
"
@update:modelValue="makeFormDirty()"
/>
<FormControl
v-if="course.paid_certificate"
v-model="course.timezone"
v-if="courseResource.doc.paid_certificate"
v-model="courseResource.doc.timezone"
:label="__('Timezone')"
:required="course.paid_certificate"
:required="courseResource.doc.paid_certificate"
:placeholder="__('e.g. IST, UTC, GMT...')"
@input="makeFormDirty()"
/>
</div>
</div>
</div>
<div class="px-5 md:px-10 pb-5 space-y-5">
<div class="pr-5 md:pr-10 pb-5 space-y-5">
<div class="text-lg font-semibold mt-5 text-ink-gray-9">
{{ __('Meta Tags') }}
</div>
@@ -256,6 +276,7 @@
:label="__('Meta Description')"
type="textarea"
:rows="7"
@input="makeFormDirty()"
/>
<FormControl
v-model="meta.keywords"
@@ -263,16 +284,17 @@
type="textarea"
:rows="7"
:placeholder="__('Comma separated keywords for SEO')"
@input="makeFormDirty()"
/>
</div>
</div>
</div>
</div>
<div class="border-l">
<div class="border-l h-[88vh] overflow-y-auto">
<CourseOutline
v-if="courseResource.data"
:courseName="courseResource.data.name"
:title="__('Course Outline')"
v-if="courseResource.doc"
:courseName="courseResource.doc.name"
:title="__('Chapters')"
:allowEdit="true"
/>
</div>
@@ -281,10 +303,10 @@
</template>
<script setup>
import {
Breadcrumbs,
TextEditor,
Button,
createResource,
createDocumentResource,
FormControl,
usePageMeta,
toast,
@@ -293,7 +315,6 @@ import {
inject,
onMounted,
onBeforeUnmount,
computed,
ref,
reactive,
watch,
@@ -308,8 +329,7 @@ import {
} from '@/utils'
import { Trash2, X } from 'lucide-vue-next'
import { useRouter } from 'vue-router'
import { useOnboarding, useTelemetry } from 'frappe-ui/frappe'
import { sessionStore } from '../stores/session'
import { sessionStore } from '../../stores/session'
import Link from '@/components/Controls/Link.vue'
import CourseOutline from '@/components/CourseOutline.vue'
import MultiSelect from '@/components/Controls/MultiSelect.vue'
@@ -323,39 +343,15 @@ const router = useRouter()
const instructors = ref([])
const related_courses = ref([])
const app = getCurrentInstance()
const { capture } = useTelemetry()
const { updateOnboardingStep } = useOnboarding('learning')
const { $dialog } = app.appContext.config.globalProperties
const isDirty = ref(false)
const props = defineProps({
courseName: {
type: String,
course: {
type: Object,
},
})
const course = reactive({
title: '',
short_introduction: '',
description: '',
video_link: '',
image: null,
card_gradient: '',
tags: '',
category: '',
published: false,
published_on: '',
featured: false,
upcoming: false,
disable_self_learning: false,
enable_certification: false,
paid_course: false,
paid_certificate: false,
course_price: '',
currency: '',
evaluator: '',
timezone: '',
})
const meta = reactive({
description: '',
keywords: '',
@@ -365,18 +361,92 @@ onMounted(() => {
if (!user.data?.is_moderator && !user.data?.is_instructor) {
router.push({ name: 'Courses' })
}
if (props.courseName !== 'new') {
fetchCourseInfo()
} else {
capture('course_form_opened')
}
window.addEventListener('keydown', keyboardShortcut)
})
const fetchCourseInfo = () => {
courseResource.reload()
getMetaInfo('courses', props.courseName, meta)
const courseResource = createDocumentResource({
doctype: 'LMS Course',
name: props.course.data?.name,
auto: true,
})
watch(
() => courseResource.doc,
() => {
check_permission()
getMetaInfo('courses', courseResource.doc?.name, meta)
updateCourseData()
}
)
const updateCourseData = () => {
Object.keys(courseResource.doc).forEach((key) => {
if (key == 'instructors') {
instructors.value = []
courseResource.doc.instructors.forEach((instructor) => {
instructors.value.push(instructor.instructor)
})
} else if (key == 'related_courses') {
related_courses.value = []
courseResource.doc.related_courses.forEach((course) => {
related_courses.value.push(course.course)
})
}
})
let checkboxes = [
'published',
'upcoming',
'disable_self_learning',
'paid_course',
'featured',
'enable_certification',
'paid_certificate',
]
for (let idx in checkboxes) {
let key = checkboxes[idx]
courseResource.doc[key] = courseResource.doc[key] ? true : false
}
}
const submitCourse = () => {
validateFields()
updateCourse()
}
const validateFields = () => {
courseResource.doc.description = sanitizeHTML(courseResource.doc.description)
Object.keys(courseResource.doc).forEach((key) => {
if (key != 'description' && typeof courseResource.doc[key] === 'string') {
courseResource.doc[key] = escapeHTML(courseResource.doc[key])
}
})
}
const updateCourse = () => {
courseResource.setValue.submit(
{
...courseResource.doc,
instructors: instructors.value.map((instructor) => ({
instructor: instructor,
})),
related_courses: related_courses.value.map((course) => ({
course: course,
})),
},
{
onSuccess() {
updateMetaInfo('courses', courseResource.doc?.name, meta)
toast.success(__('Course updated successfully'))
isDirty.value = false
courseResource.reload()
},
onError(err) {
toast.error(err.messages?.[0] || err)
console.error(err)
},
}
)
}
const keyboardShortcut = (e) => {
@@ -394,151 +464,11 @@ onBeforeUnmount(() => {
window.removeEventListener('keydown', keyboardShortcut)
})
const courseCreationResource = createResource({
url: 'frappe.client.insert',
makeParams(values) {
return {
doc: {
doctype: 'LMS Course',
image: course.image,
instructors: instructors.value.map((instructor) => ({
instructor: instructor,
})),
related_courses: related_courses.value.map((course) => ({
course: course,
})),
...values,
},
}
},
})
const courseEditResource = createResource({
url: 'frappe.client.set_value',
auto: false,
makeParams(values) {
return {
doctype: 'LMS Course',
name: values.course,
fieldname: {
image: course.image,
instructors: instructors.value.map((instructor) => ({
instructor: instructor,
})),
related_courses: related_courses.value.map((course) => ({
course: course,
})),
...course,
},
}
},
})
const courseResource = createResource({
url: 'frappe.client.get',
makeParams(values) {
return {
doctype: 'LMS Course',
name: props.courseName,
}
},
auto: false,
onSuccess(data) {
Object.keys(data).forEach((key) => {
if (key == 'instructors') {
instructors.value = []
data.instructors.forEach((instructor) => {
instructors.value.push(instructor.instructor)
})
} else if (key == 'related_courses') {
related_courses.value = []
data.related_courses.forEach((course) => {
related_courses.value.push(course.course)
})
} else if (Object.hasOwn(course, key)) course[key] = data[key]
})
let checkboxes = [
'published',
'upcoming',
'disable_self_learning',
'paid_course',
'featured',
'enable_certification',
'paid_certificate',
]
for (let idx in checkboxes) {
let key = checkboxes[idx]
course[key] = course[key] ? true : false
}
check_permission()
},
})
const validateFields = () => {
course.description = sanitizeHTML(course.description)
Object.keys(course).forEach((key) => {
if (key != 'description' && typeof course[key] === 'string') {
course[key] = escapeHTML(course[key])
}
})
}
const submitCourse = () => {
validateFields()
if (courseResource.data) {
editCourse()
} else {
createCourse()
}
}
const createCourse = () => {
courseCreationResource.submit(course, {
onSuccess(data) {
updateMetaInfo('courses', data.name, meta)
if (user.data?.is_system_manager) {
updateOnboardingStep('create_first_course', true, false, () => {
localStorage.setItem('firstCourse', data.name)
})
}
capture('course_created')
toast.success(__('Course created successfully'))
router.push({
name: 'CourseForm',
params: { courseName: data.name },
})
},
onError(err) {
toast.error(err.messages?.[0] || err)
},
})
}
const editCourse = () => {
courseEditResource.submit(
{
course: courseResource.data.name,
},
{
onSuccess() {
updateMetaInfo('courses', props.courseName, meta)
toast.success(__('Course updated successfully'))
},
onError(err) {
toast.error(err.messages?.[0] || err)
},
}
)
}
const deleteCourse = createResource({
url: 'lms.lms.api.delete_course',
makeParams(values) {
return {
course: props.courseName,
course: courseResource.doc?.name,
}
},
onSuccess() {
@@ -567,28 +497,23 @@ const trashCourse = () => {
})
}
watch(
() => props.courseName !== 'new',
(newVal) => {
if (newVal) {
fetchCourseInfo()
}
}
)
const updateTags = () => {
if (newTag.value) {
course.tags = course.tags ? `${course.tags}, ${newTag.value}` : newTag.value
courseResource.doc.tags = courseResource.doc.tags
? `${courseResource.doc.tags}, ${newTag.value}`
: newTag.value
newTag.value = ''
makeFormDirty()
}
}
const removeTag = (tag) => {
course.tags = course.tags
courseResource.doc.tags = courseResource.doc.tags
?.split(', ')
.filter((t) => t !== tag)
.join(', ')
newTag.value = ''
makeFormDirty()
}
const check_permission = () => {
@@ -606,30 +531,20 @@ const check_permission = () => {
}
}
const breadcrumbs = computed(() => {
let crumbs = [
{
label: 'Courses',
route: { name: 'Courses' },
},
]
if (courseResource.data) {
crumbs.push({
label: course.title,
route: { name: 'CourseDetail', params: { courseName: props.courseName } },
})
}
crumbs.push({
label: props.courseName == 'new' ? 'New Course' : 'Edit Course',
route: { name: 'CourseForm', params: { courseName: props.courseName } },
})
return crumbs
})
const makeFormDirty = () => {
isDirty.value = true
}
usePageMeta(() => {
return {
title: courseResource.data?.title || __('New Course'),
title: courseResource.doc?.title,
icon: brand.favicon,
}
})
defineExpose({
submitCourse,
trashCourse,
isDirty,
})
</script>
@@ -0,0 +1,102 @@
<template>
<div class="p-5">
<div class="flex justify-between w-full space-x-5">
<div class="md:w-2/3">
<div class="text-3xl font-semibold text-ink-gray-9">
{{ course.data.title }}
</div>
<div class="my-3 leading-6 text-ink-gray-7">
{{ course.data.short_introduction }}
</div>
<div class="flex items-center">
<Tooltip
v-if="parseInt(course.data.rating) > 0"
:text="__('Average Rating')"
class="flex items-center"
>
<Star class="size-4 text-transparent fill-yellow-500" />
<span class="ml-1 text-ink-gray-7">
{{ course.data.rating }}
</span>
</Tooltip>
<span v-if="parseInt(course.data.rating) > 0" class="mx-3"
>&middot;</span
>
<Tooltip
v-if="course.data.enrollment_count"
:text="__('Enrolled Students')"
class="flex items-center"
>
<Users class="h-4 w-4 text-ink-gray-7" />
<span class="ml-1">
{{ course.data.enrollment_count_formatted }}
</span>
</Tooltip>
<span v-if="course.data.enrollment_count" class="mx-3">&middot;</span>
<div class="flex items-center">
<span
class="h-6 mr-1"
:class="{
'avatar-group overlap': course.data.instructors.length > 1,
}"
>
<UserAvatar
v-for="instructor in course.data.instructors"
:user="instructor"
/>
</span>
<CourseInstructors :instructors="course.data.instructors" />
</div>
</div>
<div v-if="course.data.tags" class="flex my-4 w-fit">
<Badge
theme="gray"
size="lg"
class="mr-2 text-ink-gray-9"
v-for="tag in course.data.tags.split(', ')"
>
{{ tag }}
</Badge>
</div>
<div class="md:hidden my-4">
<CourseCardOverlay :course="course" />
</div>
<div
v-html="course.data.description"
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"
></div>
<div class="mt-10">
<CourseOutline
:title="__('Course Outline')"
:courseName="course.data.name"
:showOutline="true"
:getProgress="course.data.membership ? true : false"
/>
</div>
<CourseReviews
:courseName="course.data.name"
:avg_rating="course.data.rating"
:membership="course.data.membership"
/>
</div>
<div class="hidden md:block">
<CourseCardOverlay :course="course" />
</div>
</div>
<RelatedCourses :courseName="course.data.name" />
</div>
</template>
<script setup lang="ts">
import { Star, Users } from 'lucide-vue-next'
import { Badge, Tooltip } from 'frappe-ui'
import CourseCardOverlay from '@/components/CourseCardOverlay.vue'
import CourseOutline from '@/components/CourseOutline.vue'
import CourseReviews from '@/components/CourseReviews.vue'
import UserAvatar from '@/components/UserAvatar.vue'
import CourseInstructors from '@/components/CourseInstructors.vue'
import RelatedCourses from '@/components/RelatedCourses.vue'
const props = defineProps<{
course: any
}>()
</script>
@@ -5,7 +5,7 @@
<Breadcrumbs :items="breadcrumbs" />
<Dropdown
placement="start"
placement="right"
side="bottom"
v-if="canCreateCourse()"
:options="[
@@ -13,10 +13,7 @@
label: __('New Course'),
icon: 'book-open',
onClick() {
router.push({
name: 'CourseForm',
params: { courseName: 'new' },
})
showCourseModal = true
},
},
{
@@ -109,6 +106,11 @@
</Button>
</div>
</div>
<NewCourseModal
v-if="showCourseModal"
v-model="showCourseModal"
:courses="courses"
/>
</template>
<script setup>
import {
@@ -128,13 +130,19 @@ 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'
import { useRouter } from 'vue-router'
import NewCourseModal from '@/pages/Courses/NewCourseModal.vue'
const user = inject('$user')
const dayjs = inject('$dayjs')
const start = ref(0)
const pageLength = ref(30)
const categories = ref([])
const categories = ref([
{
label: '',
value: null,
},
])
const currentCategory = ref(null)
const title = ref('')
const certification = ref(false)
@@ -142,17 +150,13 @@ const filters = ref({})
const currentTab = ref('Live')
const { brand } = sessionStore()
const courseCount = ref(0)
const router = useRouter()
const showCourseModal = ref(false)
onMounted(() => {
setFiltersFromQuery()
updateCourses()
getCourseCount()
categories.value = [
{
label: '',
value: null,
},
]
})
const setFiltersFromQuery = () => {
@@ -160,6 +164,9 @@ const setFiltersFromQuery = () => {
title.value = queries.get('title') || ''
currentCategory.value = queries.get('category') || null
certification.value = queries.get('certification') || false
if (queries.get('newCourse') == '1') {
showCourseModal.value = true
}
}
const courses = createListResource({
@@ -0,0 +1,156 @@
<template>
<Dialog
v-model="show"
:options="{
title: __('Create Course'),
size: '3xl',
}"
>
<template #body-content>
<div class="text-base">
<div class="grid grid-cols-2 gap-5 border-b mb-5">
<FormControl
v-model="course.title"
:label="__('Title')"
:required="true"
/>
<Link
doctype="LMS Category"
v-model="course.category"
:label="__('Category')"
:allowCreate="true"
@create="
() => {
openSettings('Categories')
show = false
}
"
/>
<MultiSelect
v-model="course.instructors"
doctype="User"
:label="__('Instructors')"
:filters="{ ignore_user_type: 1 }"
:onCreate="(close: () => void) => openSettings('Members', close)"
:required="true"
/>
<Uploader
v-model="course.image"
:label="__('Course Image')"
:required="false"
/>
</div>
<div class="space-y-4">
<FormControl
v-model="course.short_introduction"
:label="__('Short Introduction')"
type="textarea"
:required="true"
:rows="4"
/>
<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: string) => (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-[10rem]"
/>
</div>
</div>
</div>
</template>
<template #actions="{ close }">
<div class="text-right">
<Button variant="solid" @click="saveCourse(close)">
{{ __('Create') }}
</Button>
</div>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { Button, Dialog, FormControl, TextEditor, toast } from 'frappe-ui'
import { Link, useOnboarding, useTelemetry } from 'frappe-ui/frappe'
import { inject, onMounted, onBeforeUnmount, ref, watch } from 'vue'
import { useRouter } from 'vue-router'
import { openSettings } from '@/utils'
import MultiSelect from '@/components/Controls/MultiSelect.vue'
import Uploader from '@/components/Controls/Uploader.vue'
const show = defineModel<boolean>({ required: true, default: false })
const router = useRouter()
const { capture } = useTelemetry()
const { updateOnboardingStep } = useOnboarding('learning')
const user = inject<any>('$user')
const props = defineProps<{
courses: any
}>()
const course = ref({
title: '',
short_introduction: '',
description: '',
instructors: [],
category: null,
image: null,
})
const saveCourse = (close: () => void = () => {}) => {
props.courses.insert.submit(
{
...course.value,
instructors: course.value.instructors.map((instructor) => ({
instructor: instructor,
})),
},
{
onSuccess(data: any) {
toast.success(__('Course created successfully'))
close()
capture('course_created')
router.push({
name: 'CourseDetail',
params: { courseName: data.name },
hash: '#settings',
})
if (user.data?.is_system_manager) {
updateOnboardingStep('create_first_course', true, false, () => {
localStorage.setItem('firstCourse', data.name)
})
}
},
}
)
}
const keyboardShortcut = (e: KeyboardEvent) => {
if (
e.key === 's' &&
(e.ctrlKey || e.metaKey) &&
e.target &&
e.target instanceof HTMLElement &&
!e.target.classList.contains('ProseMirror')
) {
saveCourse()
e.preventDefault()
}
}
onMounted(() => {
window.addEventListener('keydown', keyboardShortcut)
})
onBeforeUnmount(() => {
window.removeEventListener('keydown', keyboardShortcut)
})
watch(show, () => {
capture('course_form_opened')
})
</script>
+1 -1
View File
@@ -74,7 +74,7 @@
}}
</div>
<router-link
:to="{ name: 'CourseForm', params: { courseName: 'new' } }"
:to="{ name: 'Courses', query: { newCourse: '1' } }"
class="mt-4"
>
<Button>
+5 -1
View File
@@ -471,7 +471,11 @@ const breadcrumbs = computed(() => {
},
{
label: lessonDetails.data?.course_title,
route: { name: 'CourseForm', params: { courseName: props.courseName } },
route: {
name: 'CourseDetail',
params: { courseName: props.courseName },
hash: '#settings',
},
},
]
+3 -9
View File
@@ -12,12 +12,12 @@ const routes = [
{
path: '/courses',
name: 'Courses',
component: () => import('@/pages/Courses.vue'),
component: () => import('@/pages/Courses/Courses.vue'),
},
{
path: '/courses/:courseName',
name: 'CourseDetail',
component: () => import('@/pages/CourseDetail.vue'),
component: () => import('@/pages/Courses/CourseDetail.vue'),
props: true,
},
{
@@ -29,7 +29,7 @@ const routes = [
{
path: '/courses/:courseName/certification',
name: 'CourseCertification',
component: () => import('@/pages/CourseCertification.vue'),
component: () => import('@/pages/Courses/CourseCertification.vue'),
props: true,
},
{
@@ -118,12 +118,6 @@ const routes = [
component: () => import('@/pages/JobApplications.vue'),
props: true,
},
{
path: '/courses/:courseName/edit',
name: 'CourseForm',
component: () => import('@/pages/CourseForm.vue'),
props: true,
},
{
path: '/courses/:courseName/learn/:chapterNumber-:lessonNumber/edit',
name: 'LessonForm',
-1
View File
@@ -465,7 +465,6 @@ const getSidebarItems = () => {
'Courses',
'CourseDetail',
'Lesson',
'CourseForm',
'LessonForm',
],
},
+1
View File
@@ -5,6 +5,7 @@ import { VitePWA } from 'vite-plugin-pwa'
export default defineConfig(async ({ mode }) => {
const isDev = mode === 'development'
console.log(mode, isDev)
const frappeui = await importFrappeUIPlugin(isDev)
const config = {
+272 -295
View File
@@ -920,16 +920,16 @@
crelt "^1.0.5"
"@codemirror/state@6.x", "@codemirror/state@^6.0.0", "@codemirror/state@^6.4.0", "@codemirror/state@^6.5.0":
version "6.5.3"
resolved "https://registry.yarnpkg.com/@codemirror/state/-/state-6.5.3.tgz#256e256d466f49ed0879d462031de8bd541e1403"
integrity sha512-MerMzJzlXogk2fxWFU1nKp36bY5orBG59HnPiz0G9nLRebWa0zXuv2siH6PLIHBvv5TH8CkQRqjBs0MlxCZu+A==
version "6.5.4"
resolved "https://registry.yarnpkg.com/@codemirror/state/-/state-6.5.4.tgz#f5be4b8c0d2310180d5f15a9f641c21ca69faf19"
integrity sha512-8y7xqG/hpB53l25CIoit9/ngxdfoG+fx+V3SHBrinnhOtLvKHRyAJJuHzkWrR4YXXLX8eXBsejgAAxHUOdW1yw==
dependencies:
"@marijn/find-cluster-break" "^1.0.0"
"@codemirror/view@6.x", "@codemirror/view@^6.0.0", "@codemirror/view@^6.17.0", "@codemirror/view@^6.23.0", "@codemirror/view@^6.27.0", "@codemirror/view@^6.35.0", "@codemirror/view@^6.37.0":
version "6.39.10"
resolved "https://registry.yarnpkg.com/@codemirror/view/-/view-6.39.10.tgz#ae0dfcb635fd307aa3b800e305c9f46152503dba"
integrity sha512-QfT/PXhiiP76PxMnX0RQVPDQrqfRt9wr9QhInNHnEUu4PWoNS8QwwcIDEneXFChJv22y+Yu/Cz5lFMTPz+h16w==
version "6.39.11"
resolved "https://registry.yarnpkg.com/@codemirror/view/-/view-6.39.11.tgz#200aebef2074bfbbb7a3d5f0644c1b560d876b39"
integrity sha512-bWdeR8gWM87l4DB/kYSF9A+dVackzDb/V56Tq7QVrQ7rn86W0rgZFtlL3g3pem6AeGcb9NQNoy3ao4WpW4h5tQ==
dependencies:
"@codemirror/state" "^6.5.0"
crelt "^1.0.6"
@@ -1400,130 +1400,130 @@
estree-walker "^1.0.1"
picomatch "^2.2.2"
"@rollup/rollup-android-arm-eabi@4.55.1":
version "4.55.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.1.tgz#76e0fef6533b3ce313f969879e61e8f21f0eeb28"
integrity sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==
"@rollup/rollup-android-arm-eabi@4.56.0":
version "4.56.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.56.0.tgz#067cfcd81f1c1bfd92aefe3ad5ef1523549d5052"
integrity sha512-LNKIPA5k8PF1+jAFomGe3qN3bbIgJe/IlpDBwuVjrDKrJhVWywgnJvflMt/zkbVNLFtF1+94SljYQS6e99klnw==
"@rollup/rollup-android-arm64@4.55.1":
version "4.55.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.55.1.tgz#d3cfc675a40bbdec97bda6d7fe3b3b05f0e1cd93"
integrity sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==
"@rollup/rollup-android-arm64@4.56.0":
version "4.56.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.56.0.tgz#85e39a44034d7d4e4fee2a1616f0bddb85a80517"
integrity sha512-lfbVUbelYqXlYiU/HApNMJzT1E87UPGvzveGg2h0ktUNlOCxKlWuJ9jtfvs1sKHdwU4fzY7Pl8sAl49/XaEk6Q==
"@rollup/rollup-darwin-arm64@4.55.1":
version "4.55.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.55.1.tgz#eb912b8f59dd47c77b3c50a78489013b1d6772b4"
integrity sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==
"@rollup/rollup-darwin-arm64@4.56.0":
version "4.56.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.56.0.tgz#17d92fe98f2cc277b91101eb1528b7c0b6c00c54"
integrity sha512-EgxD1ocWfhoD6xSOeEEwyE7tDvwTgZc8Bss7wCWe+uc7wO8G34HHCUH+Q6cHqJubxIAnQzAsyUsClt0yFLu06w==
"@rollup/rollup-darwin-x64@4.55.1":
version "4.55.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.55.1.tgz#e7d0839fdfd1276a1d34bc5ebbbd0dfd7d0b81a0"
integrity sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==
"@rollup/rollup-darwin-x64@4.56.0":
version "4.56.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.56.0.tgz#89ae6c66b1451609bd1f297da9384463f628437d"
integrity sha512-1vXe1vcMOssb/hOF8iv52A7feWW2xnu+c8BV4t1F//m9QVLTfNVpEdja5ia762j/UEJe2Z1jAmEqZAK42tVW3g==
"@rollup/rollup-freebsd-arm64@4.55.1":
version "4.55.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.55.1.tgz#7ff8118760f7351e48fd0cd3717ff80543d6aac8"
integrity sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==
"@rollup/rollup-freebsd-arm64@4.56.0":
version "4.56.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.56.0.tgz#cdbdb9947b26e76c188a31238c10639347413628"
integrity sha512-bof7fbIlvqsyv/DtaXSck4VYQ9lPtoWNFCB/JY4snlFuJREXfZnm+Ej6yaCHfQvofJDXLDMTVxWscVSuQvVWUQ==
"@rollup/rollup-freebsd-x64@4.55.1":
version "4.55.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.55.1.tgz#49d330dadbda1d4e9b86b4a3951b59928a9489a9"
integrity sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==
"@rollup/rollup-freebsd-x64@4.56.0":
version "4.56.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.56.0.tgz#9b1458d07b6e040be16ee36d308a2c9520f7f7cc"
integrity sha512-KNa6lYHloW+7lTEkYGa37fpvPq+NKG/EHKM8+G/g9WDU7ls4sMqbVRV78J6LdNuVaeeK5WB9/9VAFbKxcbXKYg==
"@rollup/rollup-linux-arm-gnueabihf@4.55.1":
version "4.55.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.55.1.tgz#98c5f1f8b9776b4a36e466e2a1c9ed1ba52ef1b6"
integrity sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==
"@rollup/rollup-linux-arm-gnueabihf@4.56.0":
version "4.56.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.56.0.tgz#1d50ded7c965d5f125f5832c971ad5b287befef7"
integrity sha512-E8jKK87uOvLrrLN28jnAAAChNq5LeCd2mGgZF+fGF5D507WlG/Noct3lP/QzQ6MrqJ5BCKNwI9ipADB6jyiq2A==
"@rollup/rollup-linux-arm-musleabihf@4.55.1":
version "4.55.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.55.1.tgz#b9acecd3672e742f70b0c8a94075c816a91ff040"
integrity sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==
"@rollup/rollup-linux-arm-musleabihf@4.56.0":
version "4.56.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.56.0.tgz#53597e319b7e65990d3bc2a5048097384814c179"
integrity sha512-jQosa5FMYF5Z6prEpTCCmzCXz6eKr/tCBssSmQGEeozA9tkRUty/5Vx06ibaOP9RCrW1Pvb8yp3gvZhHwTDsJw==
"@rollup/rollup-linux-arm64-gnu@4.55.1":
version "4.55.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.55.1.tgz#7a6ab06651bc29e18b09a50ed1a02bc972977c9b"
integrity sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==
"@rollup/rollup-linux-arm64-gnu@4.56.0":
version "4.56.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.56.0.tgz#597002909dec198ca4bdccb25f043d32db3d6283"
integrity sha512-uQVoKkrC1KGEV6udrdVahASIsaF8h7iLG0U0W+Xn14ucFwi6uS539PsAr24IEF9/FoDtzMeeJXJIBo5RkbNWvQ==
"@rollup/rollup-linux-arm64-musl@4.55.1":
version "4.55.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.55.1.tgz#3c8c9072ba4a4d4ef1156b85ab9a2cbb57c1fad0"
integrity sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==
"@rollup/rollup-linux-arm64-musl@4.56.0":
version "4.56.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.56.0.tgz#286f0e0f799545ce288bdc5a7c777261fcba3d54"
integrity sha512-vLZ1yJKLxhQLFKTs42RwTwa6zkGln+bnXc8ueFGMYmBTLfNu58sl5/eXyxRa2RarTkJbXl8TKPgfS6V5ijNqEA==
"@rollup/rollup-linux-loong64-gnu@4.55.1":
version "4.55.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.55.1.tgz#17a7af13530f4e4a7b12cd26276c54307a84a8b0"
integrity sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==
"@rollup/rollup-linux-loong64-gnu@4.56.0":
version "4.56.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.56.0.tgz#1fab07fa1a4f8d3697735b996517f1bae0ba101b"
integrity sha512-FWfHOCub564kSE3xJQLLIC/hbKqHSVxy8vY75/YHHzWvbJL7aYJkdgwD/xGfUlL5UV2SB7otapLrcCj2xnF1dg==
"@rollup/rollup-linux-loong64-musl@4.55.1":
version "4.55.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.55.1.tgz#5cd7a900fd7b077ecd753e34a9b7ff1157fe70c1"
integrity sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==
"@rollup/rollup-linux-loong64-musl@4.56.0":
version "4.56.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.56.0.tgz#efc2cb143d6c067f95205482afb177f78ed9ea3d"
integrity sha512-z1EkujxIh7nbrKL1lmIpqFTc/sr0u8Uk0zK/qIEFldbt6EDKWFk/pxFq3gYj4Bjn3aa9eEhYRlL3H8ZbPT1xvA==
"@rollup/rollup-linux-ppc64-gnu@4.55.1":
version "4.55.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.55.1.tgz#03a097e70243ddf1c07b59d3c20f38e6f6800539"
integrity sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==
"@rollup/rollup-linux-ppc64-gnu@4.56.0":
version "4.56.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.56.0.tgz#e8de8bd3463f96b92b7dfb7f151fd80ffe8a937c"
integrity sha512-iNFTluqgdoQC7AIE8Q34R3AuPrJGJirj5wMUErxj22deOcY7XwZRaqYmB6ZKFHoVGqRcRd0mqO+845jAibKCkw==
"@rollup/rollup-linux-ppc64-musl@4.55.1":
version "4.55.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.55.1.tgz#a5389873039d4650f35b4fa060d286392eb21a94"
integrity sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==
"@rollup/rollup-linux-ppc64-musl@4.56.0":
version "4.56.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.56.0.tgz#8c508fe28a239da83b3a9da75bcf093186e064b4"
integrity sha512-MtMeFVlD2LIKjp2sE2xM2slq3Zxf9zwVuw0jemsxvh1QOpHSsSzfNOTH9uYW9i1MXFxUSMmLpeVeUzoNOKBaWg==
"@rollup/rollup-linux-riscv64-gnu@4.55.1":
version "4.55.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.55.1.tgz#789e60e7d6e2b76132d001ffb24ba80007fb17d0"
integrity sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==
"@rollup/rollup-linux-riscv64-gnu@4.56.0":
version "4.56.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.56.0.tgz#ff6d51976e0830732880770a9e18553136b8d92b"
integrity sha512-in+v6wiHdzzVhYKXIk5U74dEZHdKN9KH0Q4ANHOTvyXPG41bajYRsy7a8TPKbYPl34hU7PP7hMVHRvv/5aCSew==
"@rollup/rollup-linux-riscv64-musl@4.55.1":
version "4.55.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.55.1.tgz#3556fa88d139282e9a73c337c9a170f3c5fe7aa4"
integrity sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==
"@rollup/rollup-linux-riscv64-musl@4.56.0":
version "4.56.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.56.0.tgz#325fb35eefc7e81d75478318f0deee1e4a111493"
integrity sha512-yni2raKHB8m9NQpI9fPVwN754mn6dHQSbDTwxdr9SE0ks38DTjLMMBjrwvB5+mXrX+C0npX0CVeCUcvvvD8CNQ==
"@rollup/rollup-linux-s390x-gnu@4.55.1":
version "4.55.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.55.1.tgz#c085995b10143c16747a67f1a5487512b2ff04b2"
integrity sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==
"@rollup/rollup-linux-s390x-gnu@4.56.0":
version "4.56.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.56.0.tgz#37410fabb5d3ba4ad34abcfbe9ba9b6288413f30"
integrity sha512-zhLLJx9nQPu7wezbxt2ut+CI4YlXi68ndEve16tPc/iwoylWS9B3FxpLS2PkmfYgDQtosah07Mj9E0khc3Y+vQ==
"@rollup/rollup-linux-x64-gnu@4.55.1":
version "4.55.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.55.1.tgz#9563a5419dd2604841bad31a39ccfdd2891690fb"
integrity sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==
"@rollup/rollup-linux-x64-gnu@4.56.0":
version "4.56.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.56.0.tgz#8ef907a53b2042068fc03fcc6a641e2b02276eca"
integrity sha512-MVC6UDp16ZSH7x4rtuJPAEoE1RwS8N4oK9DLHy3FTEdFoUTCFVzMfJl/BVJ330C+hx8FfprA5Wqx4FhZXkj2Kw==
"@rollup/rollup-linux-x64-musl@4.55.1":
version "4.55.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.55.1.tgz#691bb06e6269a8959c13476b0cd2aa7458facb31"
integrity sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==
"@rollup/rollup-linux-x64-musl@4.56.0":
version "4.56.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.56.0.tgz#61b9ba09ea219e0174b3f35a6ad2afc94bdd5662"
integrity sha512-ZhGH1eA4Qv0lxaV00azCIS1ChedK0V32952Md3FtnxSqZTBTd6tgil4nZT5cU8B+SIw3PFYkvyR4FKo2oyZIHA==
"@rollup/rollup-openbsd-x64@4.55.1":
version "4.55.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.55.1.tgz#223e71224746a59ce6d955bbc403577bb5a8be9d"
integrity sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==
"@rollup/rollup-openbsd-x64@4.56.0":
version "4.56.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.56.0.tgz#fc4e54133134c1787d0b016ffdd5aeb22a5effd3"
integrity sha512-O16XcmyDeFI9879pEcmtWvD/2nyxR9mF7Gs44lf1vGGx8Vg2DRNx11aVXBEqOQhWb92WN4z7fW/q4+2NYzCbBA==
"@rollup/rollup-openharmony-arm64@4.55.1":
version "4.55.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.55.1.tgz#0817e5d8ecbfeb8b7939bf58f8ce3c9dd67fce77"
integrity sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw==
"@rollup/rollup-openharmony-arm64@4.56.0":
version "4.56.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.56.0.tgz#959ae225b1eeea0cc5b7c9f88e4834330fb6cd09"
integrity sha512-LhN/Reh+7F3RCgQIRbgw8ZMwUwyqJM+8pXNT6IIJAqm2IdKkzpCh/V9EdgOMBKuebIrzswqy4ATlrDgiOwbRcQ==
"@rollup/rollup-win32-arm64-msvc@4.55.1":
version "4.55.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.55.1.tgz#de56d8f2013c84570ef5fb917aae034abda93e4a"
integrity sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==
"@rollup/rollup-win32-arm64-msvc@4.56.0":
version "4.56.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.56.0.tgz#842acd38869fa1cbdbc240c76c67a86f93444c27"
integrity sha512-kbFsOObXp3LBULg1d3JIUQMa9Kv4UitDmpS+k0tinPBz3watcUiV2/LUDMMucA6pZO3WGE27P7DsfaN54l9ing==
"@rollup/rollup-win32-ia32-msvc@4.55.1":
version "4.55.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.55.1.tgz#659aff5244312475aeea2c9479a6c7d397b517bf"
integrity sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==
"@rollup/rollup-win32-ia32-msvc@4.56.0":
version "4.56.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.56.0.tgz#7ab654def4042df44cb29f8ed9d5044e850c66d5"
integrity sha512-vSSgny54D6P4vf2izbtFm/TcWYedw7f8eBrOiGGecyHyQB9q4Kqentjaj8hToe+995nob/Wv48pDqL5a62EWtg==
"@rollup/rollup-win32-x64-gnu@4.55.1":
version "4.55.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.55.1.tgz#2cb09549cbb66c1b979f9238db6dd454cac14a88"
integrity sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==
"@rollup/rollup-win32-x64-gnu@4.56.0":
version "4.56.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.56.0.tgz#7426cdec1b01d2382ffd5cda83cbdd1c8efb3ca6"
integrity sha512-FeCnkPCTHQJFbiGG49KjV5YGW/8b9rrXAM2Mz2kiIoktq2qsJxRD5giEMEOD2lPdgs72upzefaUvS+nc8E3UzQ==
"@rollup/rollup-win32-x64-msvc@4.55.1":
version "4.55.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.55.1.tgz#f79437939020b83057faf07e98365b1fa51c458b"
integrity sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==
"@rollup/rollup-win32-x64-msvc@4.56.0":
version "4.56.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.56.0.tgz#9eec0212732a432c71bde0350bc40b673d15b2db"
integrity sha512-H8AE9Ur/t0+1VXujj90w0HrSOuv0Nq9r1vSZF2t5km20NTfosQsGGUXDaKdQZzwuLts7IyL1fYT4hM95TI9c4g==
"@socket.io/component-emitter@~3.1.0":
version "3.1.2"
@@ -1896,9 +1896,9 @@
integrity sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==
"@types/node@*":
version "25.0.8"
resolved "https://registry.yarnpkg.com/@types/node/-/node-25.0.8.tgz#e54e00f94fe1db2497b3e42d292b8376a2678c8d"
integrity sha512-powIePYMmC3ibL0UJ2i2s0WIbq6cg6UyVFQxSCpaPxxzAaziRfimGivjdF943sSGV6RADVbk0Nvlm5P/FB44Zg==
version "25.0.10"
resolved "https://registry.yarnpkg.com/@types/node/-/node-25.0.10.tgz#4864459c3c9459376b8b75fd051315071c8213e7"
integrity sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg==
dependencies:
undici-types "~7.16.0"
@@ -1919,11 +1919,6 @@
resolved "https://registry.yarnpkg.com/@types/unist/-/unist-3.0.3.tgz#acaab0f919ce69cce629c2d4ed2eb4adc1b6c20c"
integrity sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==
"@types/web-bluetooth@^0.0.17":
version "0.0.17"
resolved "https://registry.yarnpkg.com/@types/web-bluetooth/-/web-bluetooth-0.0.17.tgz#5c9f3c617f64a9735d7b72a7cc671e166d900c40"
integrity sha512-4p9vcSmxAayx72yn70joFoL44c9MO/0+iVEBIQXe3v2h2SiAsEIo/G5v6ObFWvNKRFjbrVadNf9LqEEZeQPzdA==
"@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"
@@ -1953,100 +1948,90 @@
resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-5.0.3.tgz#164b36653910d27c130cf6c945b4bd9bde5bcbee"
integrity sha512-b8S5dVS40rgHdDrw+DQi/xOM9ed+kSRZzfm1T74bMmBDCd8XO87NKlFYInzCtwvtWwXZvo1QxE2OSspTATWrbA==
"@vue/compiler-core@3.5.26":
version "3.5.26"
resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.5.26.tgz#1a91ea90980528bedff7b1c292690bfb30612485"
integrity sha512-vXyI5GMfuoBCnv5ucIT7jhHKl55Y477yxP6fc4eUswjP8FG3FFVFd41eNDArR+Uk3QKn2Z85NavjaxLxOC19/w==
"@vue/compiler-core@3.5.27":
version "3.5.27"
resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.5.27.tgz#ce4402428e26095586eb889c41f6e172eb3960bd"
integrity sha512-gnSBQjZA+//qDZen+6a2EdHqJ68Z7uybrMf3SPjEGgG4dicklwDVmMC1AeIHxtLVPT7sn6sH1KOO+tS6gwOUeQ==
dependencies:
"@babel/parser" "^7.28.5"
"@vue/shared" "3.5.26"
"@vue/shared" "3.5.27"
entities "^7.0.0"
estree-walker "^2.0.2"
source-map-js "^1.2.1"
"@vue/compiler-dom@3.5.26":
version "3.5.26"
resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.5.26.tgz#66c36b6ed8bdf43236d7188ea332bc9d078eb286"
integrity sha512-y1Tcd3eXs834QjswshSilCBnKGeQjQXB6PqFn/1nxcQw4pmG42G8lwz+FZPAZAby6gZeHSt/8LMPfZ4Rb+Bd/A==
"@vue/compiler-dom@3.5.27":
version "3.5.27"
resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.5.27.tgz#32b2bc87f0a652c253986796ace0ed6213093af8"
integrity sha512-oAFea8dZgCtVVVTEC7fv3T5CbZW9BxpFzGGxC79xakTr6ooeEqmRuvQydIiDAkglZEAd09LgVf1RoDnL54fu5w==
dependencies:
"@vue/compiler-core" "3.5.26"
"@vue/shared" "3.5.26"
"@vue/compiler-core" "3.5.27"
"@vue/shared" "3.5.27"
"@vue/compiler-sfc@3.5.26":
version "3.5.26"
resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.5.26.tgz#fb1c6c4bf9a9e22bb169e039e19437cb6995917a"
integrity sha512-egp69qDTSEZcf4bGOSsprUr4xI73wfrY5oRs6GSgXFTiHrWj4Y3X5Ydtip9QMqiCMCPVwLglB9GBxXtTadJ3mA==
"@vue/compiler-sfc@3.5.27":
version "3.5.27"
resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.5.27.tgz#84651b8816bf8e7d6e62fddd14db86efd6d6f1b6"
integrity sha512-sHZu9QyDPeDmN/MRoshhggVOWE5WlGFStKFwu8G52swATgSny27hJRWteKDSUUzUH+wp+bmeNbhJnEAel/auUQ==
dependencies:
"@babel/parser" "^7.28.5"
"@vue/compiler-core" "3.5.26"
"@vue/compiler-dom" "3.5.26"
"@vue/compiler-ssr" "3.5.26"
"@vue/shared" "3.5.26"
"@vue/compiler-core" "3.5.27"
"@vue/compiler-dom" "3.5.27"
"@vue/compiler-ssr" "3.5.27"
"@vue/shared" "3.5.27"
estree-walker "^2.0.2"
magic-string "^0.30.21"
postcss "^8.5.6"
source-map-js "^1.2.1"
"@vue/compiler-ssr@3.5.26":
version "3.5.26"
resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.5.26.tgz#f6e94bccbb5339180779036ddfb614f998a197ea"
integrity sha512-lZT9/Y0nSIRUPVvapFJEVDbEXruZh2IYHMk2zTtEgJSlP5gVOqeWXH54xDKAaFS4rTnDeDBQUYDtxKyoW9FwDw==
"@vue/compiler-ssr@3.5.27":
version "3.5.27"
resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.5.27.tgz#b480cad09dacf8f3d9c82b9843402f1a803baee7"
integrity sha512-Sj7h+JHt512fV1cTxKlYhg7qxBvack+BGncSpH+8vnN+KN95iPIcqB5rsbblX40XorP+ilO7VIKlkuu3Xq2vjw==
dependencies:
"@vue/compiler-dom" "3.5.26"
"@vue/shared" "3.5.26"
"@vue/compiler-dom" "3.5.27"
"@vue/shared" "3.5.27"
"@vue/devtools-api@^6.5.0":
"@vue/devtools-api@^6.5.0", "@vue/devtools-api@^6.6.4":
version "6.6.4"
resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz#cbe97fe0162b365edc1dba80e173f90492535343"
integrity sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==
"@vue/reactivity@3.5.26":
version "3.5.26"
resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.5.26.tgz#59a1edf566dc80133c1c26c93711c877e8602c48"
integrity sha512-9EnYB1/DIiUYYnzlnUBgwU32NNvLp/nhxLXeWRhHUEeWNTn1ECxX8aGO7RTXeX6PPcxe3LLuNBFoJbV4QZ+CFQ==
"@vue/reactivity@3.5.27":
version "3.5.27"
resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.5.27.tgz#d870557de1389a27b8abcb7cbfa30978dc69a000"
integrity sha512-vvorxn2KXfJ0nBEnj4GYshSgsyMNFnIQah/wczXlsNXt+ijhugmW+PpJ2cNPe4V6jpnBcs0MhCODKllWG+nvoQ==
dependencies:
"@vue/shared" "3.5.26"
"@vue/shared" "3.5.27"
"@vue/runtime-core@3.5.26":
version "3.5.26"
resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.5.26.tgz#3f2c040bcf8018c03a1ab5adb0d788c13c986f0e"
integrity sha512-xJWM9KH1kd201w5DvMDOwDHYhrdPTrAatn56oB/LRG4plEQeZRQLw0Bpwih9KYoqmzaxF0OKSn6swzYi84e1/Q==
"@vue/runtime-core@3.5.27":
version "3.5.27"
resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.5.27.tgz#bb43744ed070166c7d581b849ac22b71a9ccf127"
integrity sha512-fxVuX/fzgzeMPn/CLQecWeDIFNt3gQVhxM0rW02Tvp/YmZfXQgcTXlakq7IMutuZ/+Ogbn+K0oct9J3JZfyk3A==
dependencies:
"@vue/reactivity" "3.5.26"
"@vue/shared" "3.5.26"
"@vue/reactivity" "3.5.27"
"@vue/shared" "3.5.27"
"@vue/runtime-dom@3.5.26":
version "3.5.26"
resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.5.26.tgz#5954848614883948ecc1f631a67b32cc32f81936"
integrity sha512-XLLd/+4sPC2ZkN/6+V4O4gjJu6kSDbHAChvsyWgm1oGbdSO3efvGYnm25yCjtFm/K7rrSDvSfPDgN1pHgS4VNQ==
"@vue/runtime-dom@3.5.27":
version "3.5.27"
resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.5.27.tgz#392513252c7ca7e5277240fdc70b8093449127f5"
integrity sha512-/QnLslQgYqSJ5aUmb5F0z0caZPGHRB8LEAQ1s81vHFM5CBfnun63rxhvE/scVb/j3TbBuoZwkJyiLCkBluMpeg==
dependencies:
"@vue/reactivity" "3.5.26"
"@vue/runtime-core" "3.5.26"
"@vue/shared" "3.5.26"
"@vue/reactivity" "3.5.27"
"@vue/runtime-core" "3.5.27"
"@vue/shared" "3.5.27"
csstype "^3.2.3"
"@vue/server-renderer@3.5.26":
version "3.5.26"
resolved "https://registry.yarnpkg.com/@vue/server-renderer/-/server-renderer-3.5.26.tgz#269055497fcc75b3984063f866f17c748b565ef4"
integrity sha512-TYKLXmrwWKSodyVuO1WAubucd+1XlLg4set0YoV+Hu8Lo79mp/YMwWV5mC5FgtsDxX3qo1ONrxFaTP1OQgy1uA==
"@vue/server-renderer@3.5.27":
version "3.5.27"
resolved "https://registry.yarnpkg.com/@vue/server-renderer/-/server-renderer-3.5.27.tgz#8137d0d7ec3b59d5992bb04c553775d209dddba7"
integrity sha512-qOz/5thjeP1vAFc4+BY3Nr6wxyLhpeQgAE/8dDtKo6a6xdk+L4W46HDZgNmLOBUDEkFXV3G7pRiUqxjX0/2zWA==
dependencies:
"@vue/compiler-ssr" "3.5.26"
"@vue/shared" "3.5.26"
"@vue/compiler-ssr" "3.5.27"
"@vue/shared" "3.5.27"
"@vue/shared@3.5.26":
version "3.5.26"
resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.5.26.tgz#1e02ef2d64aced818cd31d81ce5175711dc90a9f"
integrity sha512-7Z6/y3uFI5PRoKeorTOSXKcDj0MSasfNNltcslbFrPpcw6aXRUALq4IfJlaTRspiWIUOEZbrpM+iQGmCOiWe4A==
"@vueuse/core@10.4.1":
version "10.4.1"
resolved "https://registry.yarnpkg.com/@vueuse/core/-/core-10.4.1.tgz#fc2c8a83a571c207aaedbe393b22daa6d35123f2"
integrity sha512-DkHIfMIoSIBjMgRRvdIvxsyboRZQmImofLyOHADqiVbQVilP8VVHDhBX2ZqoItOgu7dWa8oXiNnScOdPLhdEXg==
dependencies:
"@types/web-bluetooth" "^0.0.17"
"@vueuse/metadata" "10.4.1"
"@vueuse/shared" "10.4.1"
vue-demi ">=0.14.5"
"@vue/shared@3.5.27":
version "3.5.27"
resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.5.27.tgz#33a63143d8fb9ca1b3efbc7ecf9bd0ab05f7e06e"
integrity sha512-dXr/3CgqXsJkZ0n9F3I4elY8wM9jMJpP3pvRG52r6m0tu/MsAFIe6JpXVGeNMd/D9F4hQynWT8Rfuj0bdm9kFQ==
"@vueuse/core@^10.11.0", "@vueuse/core@^10.4.1":
version "10.11.1"
@@ -2068,28 +2053,29 @@
"@vueuse/shared" "12.8.2"
vue "^3.5.13"
"@vueuse/core@^14.1.0":
version "14.1.0"
resolved "https://registry.yarnpkg.com/@vueuse/core/-/core-14.1.0.tgz#274e98e591a505333b7dfb2bcaf7b4530a10b9c9"
integrity sha512-rgBinKs07hAYyPF834mDTigH7BtPqvZ3Pryuzt1SD/lg5wEcWqvwzXXYGEDb2/cP0Sj5zSvHl3WkmMELr5kfWw==
dependencies:
"@types/web-bluetooth" "^0.0.21"
"@vueuse/metadata" "14.1.0"
"@vueuse/shared" "14.1.0"
"@vueuse/metadata@10.11.1":
version "10.11.1"
resolved "https://registry.yarnpkg.com/@vueuse/metadata/-/metadata-10.11.1.tgz#209db7bb5915aa172a87510b6de2ca01cadbd2a7"
integrity sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==
"@vueuse/metadata@10.4.1":
version "10.4.1"
resolved "https://registry.yarnpkg.com/@vueuse/metadata/-/metadata-10.4.1.tgz#9d2ff5c67abf17a8c07865c2413fbd0e92f7b7d7"
integrity sha512-2Sc8X+iVzeuMGHr6O2j4gv/zxvQGGOYETYXEc41h0iZXIRnRbJZGmY/QP8dvzqUelf8vg0p/yEA5VpCEu+WpZg==
"@vueuse/metadata@12.8.2":
version "12.8.2"
resolved "https://registry.yarnpkg.com/@vueuse/metadata/-/metadata-12.8.2.tgz#6cb3a4e97cdcf528329eebc1bda73cd7f64318d3"
integrity sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A==
"@vueuse/router@12.7.0":
version "12.7.0"
resolved "https://registry.yarnpkg.com/@vueuse/router/-/router-12.7.0.tgz#b349b66e337057bb489b6d64d2dab044d41ca74d"
integrity sha512-Jp6dIel54oc2nh++zqjY06ipCcTT6YWDCNQ8dSSnqRwx90wIl7w7MQP7Wpp1wrDwXEoqhelfeZf2gjfrkAhq3g==
dependencies:
"@vueuse/shared" "12.7.0"
vue "^3.5.13"
"@vueuse/metadata@14.1.0":
version "14.1.0"
resolved "https://registry.yarnpkg.com/@vueuse/metadata/-/metadata-14.1.0.tgz#70fc2e94775e4a07369f11f86f6f0a465b04a381"
integrity sha512-7hK4g015rWn2PhKcZ99NyT+ZD9sbwm7SGvp7k+k+rKGWnLjS/oQozoIZzWfCewSUeBmnJkIb+CNr7Zc/EyRnnA==
"@vueuse/shared@10.11.1", "@vueuse/shared@^10.11.0":
version "10.11.1"
@@ -2098,20 +2084,6 @@
dependencies:
vue-demi ">=0.14.8"
"@vueuse/shared@10.4.1":
version "10.4.1"
resolved "https://registry.yarnpkg.com/@vueuse/shared/-/shared-10.4.1.tgz#d5ce33033c156efb60664b5d6034d6cd4e2f530c"
integrity sha512-vz5hbAM4qA0lDKmcr2y3pPdU+2EVw/yzfRsBdu+6+USGa4PxqSQRYIUC9/NcT06y+ZgaTsyURw2I9qOFaaXHAg==
dependencies:
vue-demi ">=0.14.5"
"@vueuse/shared@12.7.0":
version "12.7.0"
resolved "https://registry.yarnpkg.com/@vueuse/shared/-/shared-12.7.0.tgz#0c573789069818a2e25ddae3ab64b536c614537b"
integrity sha512-coLlUw2HHKsm7rPN6WqHJQr18WymN4wkA/3ThFaJ4v4gWGWAQQGK+MJxLuJTBs4mojQiazlVWAKNJNpUWGRkNw==
dependencies:
vue "^3.5.13"
"@vueuse/shared@12.8.2", "@vueuse/shared@^12.5.0":
version "12.8.2"
resolved "https://registry.yarnpkg.com/@vueuse/shared/-/shared-12.8.2.tgz#b9e4611d0603629c8e151f982459da394e22f930"
@@ -2119,6 +2091,11 @@
dependencies:
vue "^3.5.13"
"@vueuse/shared@14.1.0":
version "14.1.0"
resolved "https://registry.yarnpkg.com/@vueuse/shared/-/shared-14.1.0.tgz#49b2face86a9c0c52e20eaf4c732a0223276c11f"
integrity sha512-EcKxtYvn6gx1F8z9J5/rsg3+lTQnvOruQd8fUecW99DCK04BkWD7z5KQ/wTAx+DazyoEE9dJt/zV8OIEQbM6kw==
"@yr/monotone-cubic-spline@^1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@yr/monotone-cubic-spline/-/monotone-cubic-spline-1.0.3.tgz#7272d89f8e4f6fb7a1600c28c378cc18d3b577b9"
@@ -2288,9 +2265,9 @@ base64-js@^1.3.1:
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
baseline-browser-mapping@^2.9.0:
version "2.9.14"
resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.9.14.tgz#3b6af0bc032445bca04de58caa9a87cfe921cbb3"
integrity sha512-B0xUquLkiGLgHhpPBqvl7GWegWBUNuujQ6kXd/r1U38ElPT6Ok8KZ8e+FpUGEc2ZoRQUzq/aUnaKFc/svWUGSg==
version "2.9.17"
resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.9.17.tgz#9d6019766cd7eba738cb5f32c84b9f937cc87780"
integrity sha512-agD0MgJFUP/4nvjqzIB29zRPUuCF7Ge6mEv9s8dHrtYD7QWXRcx75rOADE/d5ah1NI+0vkDl0yorDd5U852IQQ==
binary-extensions@^2.0.0:
version "2.3.0"
@@ -2328,7 +2305,7 @@ braces@^3.0.3, braces@~3.0.2:
dependencies:
fill-range "^7.1.1"
browserslist@^4.19.1, browserslist@^4.24.0, browserslist@^4.28.0:
browserslist@^4.19.1, browserslist@^4.24.0, browserslist@^4.28.1:
version "4.28.1"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.28.1.tgz#7f534594628c53c63101079e27e40de490456a95"
integrity sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==
@@ -2389,9 +2366,9 @@ camelcase-css@^2.0.1:
integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==
caniuse-lite@^1.0.30001297, caniuse-lite@^1.0.30001759:
version "1.0.30001764"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001764.tgz#03206c56469f236103b90f9ae10bcb8b9e1f6005"
integrity sha512-9JGuzl2M+vPL+pz70gtMF9sHdMFbY9FJaQBi186cHKH3pSzDvzoUJUPV6fqiKIMyXbud9ZLg4F3Yza1vJ1+93g==
version "1.0.30001765"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001765.tgz#4a78d8a797fd4124ebaab2043df942eb091648ee"
integrity sha512-LWcNtSyZrakjECqmpP4qdg0MMGdN368D7X8XvvAqOcqMv0RxnlqVKZl2V6/mBR68oYMxOZPLw/gO7DuisMHUvQ==
chalk@^4.1.0:
version "4.1.2"
@@ -2506,16 +2483,16 @@ convert-source-map@^2.0.0:
integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==
core-js-compat@^3.43.0:
version "3.47.0"
resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.47.0.tgz#698224bbdbb6f2e3f39decdda4147b161e3772a3"
integrity sha512-IGfuznZ/n7Kp9+nypamBhvwdwLsW6KC8IOaURw2doAK5e98AG3acVLdh0woOnEqCfUtS+Vu882JE4k/DAm3ItQ==
version "3.48.0"
resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.48.0.tgz#7efbe1fc1cbad44008190462217cc5558adaeaa6"
integrity sha512-OM4cAF3D6VtH/WkLtWvyNC56EZVXsZdU3iqaMG2B4WvYrlqU831pc4UtG5yp0sE9z8Y02wVN7PjW5Zf9Gt0f1Q==
dependencies:
browserslist "^4.28.0"
browserslist "^4.28.1"
core-js@^3.1.3, core-js@^3.26.1:
version "3.47.0"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.47.0.tgz#436ef07650e191afeb84c24481b298bd60eb4a17"
integrity sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg==
version "3.48.0"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.48.0.tgz#1f813220a47bbf0e667e3885c36cd6f0593bf14d"
integrity sha512-zpEHTy1fjTMZCKLHUZoVeylt9XrzaIN2rbPXEt0k+q7JE5CkCZdo6bNq55bn24a69CH7ErAVLKijxJja4fw+UQ==
crelt@^1.0.0, crelt@^1.0.5, crelt@^1.0.6:
version "1.0.6"
@@ -2689,9 +2666,9 @@ ejs@^3.1.6:
jake "^10.8.5"
electron-to-chromium@^1.5.263:
version "1.5.267"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz#5d84f2df8cdb6bfe7e873706bb21bd4bfb574dc7"
integrity sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==
version "1.5.277"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.277.tgz#7164191a07bf32a7e646e68334f402dd60629821"
integrity sha512-wKXFZw4erWmmOz5N/grBoJ2XrNJGDFMu2+W5ACHza5rHtvsqrK4gb6rnLC7XxKB9WlJ+RmyQatuEXmtm86xbnw==
engine.io-client@~6.5.2:
version "6.5.4"
@@ -2726,9 +2703,9 @@ entities@^4.4.0:
integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==
entities@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/entities/-/entities-7.0.0.tgz#2ae4e443f3f17d152d3f5b0f79b932c1e59deb7a"
integrity sha512-FDWG5cmEYf2Z00IkYRhbFrwIwvdFKH07uV8dvNy0omp/Qb1xcyCWp2UDtcwJF4QZZvk0sLudP6/hAu42TaqVhQ==
version "7.0.1"
resolved "https://registry.yarnpkg.com/entities/-/entities-7.0.1.tgz#26e8a88889db63417dcb9a1e79a3f1bc92b5976b"
integrity sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==
es-abstract@^1.23.5, es-abstract@^1.23.6, es-abstract@^1.23.9:
version "1.24.1"
@@ -2977,10 +2954,10 @@ fraction.js@^4.1.2:
resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7"
integrity sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==
frappe-ui@^0.1.256:
version "0.1.256"
resolved "https://registry.yarnpkg.com/frappe-ui/-/frappe-ui-0.1.256.tgz#c14756eda75ca01ada034559e8bd2f91bcfe6dff"
integrity sha512-zj8n6KXpMv/0h1NcaCsjFLP8QBnofDEBJgQa+xECU0/jbq4gSqNhFOkcx788qNL+vmBo9frywTeXwDpl7hUCZA==
frappe-ui@^0.1.261:
version "0.1.261"
resolved "https://registry.yarnpkg.com/frappe-ui/-/frappe-ui-0.1.261.tgz#d6919c713a37ed8a2bdb667707dba9ece4956c6d"
integrity sha512-sEdEAgjAkrTERYWk5HBOQuKa7/xuex/X8/Y/hCYFbEThwwy2ZWmQOCsTNyOCjXAn7lyV49Ues/TW01koIq/ysQ==
dependencies:
"@floating-ui/vue" "^1.1.6"
"@headlessui/vue" "^1.7.14"
@@ -3613,9 +3590,9 @@ lodash.sortby@^4.7.0:
integrity sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==
lodash@^4.17.20:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
version "4.17.23"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.23.tgz#f113b0378386103be4f6893388c73d0bde7f2c5a"
integrity sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==
log-symbols@^4.1.0:
version "4.1.0"
@@ -4032,9 +4009,9 @@ postcss@^8.4.32, postcss@^8.4.47, postcss@^8.5.6:
source-map-js "^1.2.1"
prettier@^3.3.2:
version "3.7.4"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.7.4.tgz#d2f8335d4b1cec47e1c8098645411b0c9dff9c0f"
integrity sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==
version "3.8.1"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.8.1.tgz#edf48977cf991558f4fcbd8a3ba6015ba2a3a173"
integrity sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==
pretty-bytes@^5.3.0:
version "5.6.0"
@@ -4115,9 +4092,9 @@ prosemirror-keymap@^1.0.0, prosemirror-keymap@^1.2.2, prosemirror-keymap@^1.2.3:
w3c-keyname "^2.2.0"
prosemirror-markdown@^1.13.1:
version "1.13.2"
resolved "https://registry.yarnpkg.com/prosemirror-markdown/-/prosemirror-markdown-1.13.2.tgz#863eb3fd5f57a444e4378174622b562735b1c503"
integrity sha512-FPD9rHPdA9fqzNmIIDhhnYQ6WgNoSWX9StUZ8LEKapaXU9i6XgykaHKhp6XMyXlOWetmaFgGDS/nu/w9/vUc5g==
version "1.13.3"
resolved "https://registry.yarnpkg.com/prosemirror-markdown/-/prosemirror-markdown-1.13.3.tgz#cf38e98f10c432b906bfcc7179c2e3ab58f49362"
integrity sha512-3E+Et6cdXIH0EgN2tGYQ+EBT7N4kMiZFsW+hzx+aPtOmADDHWCdd2uUQb7yklJrfUYUOjEEu22BiN6UFgPe4cQ==
dependencies:
"@types/markdown-it" "^14.0.0"
markdown-it "^14.0.0"
@@ -4185,16 +4162,16 @@ prosemirror-trailing-node@^3.0.0:
escape-string-regexp "^4.0.0"
prosemirror-transform@^1.0.0, prosemirror-transform@^1.1.0, prosemirror-transform@^1.10.2, prosemirror-transform@^1.10.5, prosemirror-transform@^1.7.3:
version "1.10.5"
resolved "https://registry.yarnpkg.com/prosemirror-transform/-/prosemirror-transform-1.10.5.tgz#4cf9fe5dcbdbfebd62499f24386e7cec9bc9979b"
integrity sha512-RPDQCxIDhIBb1o36xxwsaeAvivO8VLJcgBtzmOwQ64bMtsVFh5SSuJ6dWSxO1UsHTiTXPCgQm3PDJt7p6IOLbw==
version "1.11.0"
resolved "https://registry.yarnpkg.com/prosemirror-transform/-/prosemirror-transform-1.11.0.tgz#f5c5050354423dc83c6b083f6f1959ec86a3f9ba"
integrity sha512-4I7Ce4KpygXb9bkiPS3hTEk4dSHorfRw8uI0pE8IhxlK2GXsqv5tIA7JUSxtSu7u8APVOTtbUBxTmnHIxVkIJw==
dependencies:
prosemirror-model "^1.21.0"
prosemirror-view@^1.0.0, prosemirror-view@^1.1.0, prosemirror-view@^1.27.0, prosemirror-view@^1.31.0, prosemirror-view@^1.37.0, prosemirror-view@^1.41.4:
version "1.41.4"
resolved "https://registry.yarnpkg.com/prosemirror-view/-/prosemirror-view-1.41.4.tgz#4e1b3e90accc0eebe3bddb497a40ce54e4de722d"
integrity sha512-WkKgnyjNncri03Gjaz3IFWvCAE94XoiEgvtr0/r2Xw7R8/IjK3sKLSiDoCHWcsXSAinVaKlGRZDvMCsF1kbzjA==
version "1.41.5"
resolved "https://registry.yarnpkg.com/prosemirror-view/-/prosemirror-view-1.41.5.tgz#3e152d14af633f2f5a73aba24e6130c63f643b2b"
integrity sha512-UDQbIPnDrjE8tqUBbPmCOZgtd75htE6W3r0JCmY9bL6W1iemDM37MZEKC49d+tdQ0v/CKx4gjxLoLsfkD2NiZA==
dependencies:
prosemirror-model "^1.20.0"
prosemirror-state "^1.0.0"
@@ -4395,37 +4372,37 @@ rollup@^2.43.1:
fsevents "~2.3.2"
rollup@^4.2.0:
version "4.55.1"
resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.55.1.tgz#4ec182828be440648e7ee6520dc35e9f20e05144"
integrity sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==
version "4.56.0"
resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.56.0.tgz#65959d13cfbd7e48b8868c05165b1738f0143862"
integrity sha512-9FwVqlgUHzbXtDg9RCMgodF3Ua4Na6Gau+Sdt9vyCN4RhHfVKX2DCHy3BjMLTDd47ITDhYAnTwGulWTblJSDLg==
dependencies:
"@types/estree" "1.0.8"
optionalDependencies:
"@rollup/rollup-android-arm-eabi" "4.55.1"
"@rollup/rollup-android-arm64" "4.55.1"
"@rollup/rollup-darwin-arm64" "4.55.1"
"@rollup/rollup-darwin-x64" "4.55.1"
"@rollup/rollup-freebsd-arm64" "4.55.1"
"@rollup/rollup-freebsd-x64" "4.55.1"
"@rollup/rollup-linux-arm-gnueabihf" "4.55.1"
"@rollup/rollup-linux-arm-musleabihf" "4.55.1"
"@rollup/rollup-linux-arm64-gnu" "4.55.1"
"@rollup/rollup-linux-arm64-musl" "4.55.1"
"@rollup/rollup-linux-loong64-gnu" "4.55.1"
"@rollup/rollup-linux-loong64-musl" "4.55.1"
"@rollup/rollup-linux-ppc64-gnu" "4.55.1"
"@rollup/rollup-linux-ppc64-musl" "4.55.1"
"@rollup/rollup-linux-riscv64-gnu" "4.55.1"
"@rollup/rollup-linux-riscv64-musl" "4.55.1"
"@rollup/rollup-linux-s390x-gnu" "4.55.1"
"@rollup/rollup-linux-x64-gnu" "4.55.1"
"@rollup/rollup-linux-x64-musl" "4.55.1"
"@rollup/rollup-openbsd-x64" "4.55.1"
"@rollup/rollup-openharmony-arm64" "4.55.1"
"@rollup/rollup-win32-arm64-msvc" "4.55.1"
"@rollup/rollup-win32-ia32-msvc" "4.55.1"
"@rollup/rollup-win32-x64-gnu" "4.55.1"
"@rollup/rollup-win32-x64-msvc" "4.55.1"
"@rollup/rollup-android-arm-eabi" "4.56.0"
"@rollup/rollup-android-arm64" "4.56.0"
"@rollup/rollup-darwin-arm64" "4.56.0"
"@rollup/rollup-darwin-x64" "4.56.0"
"@rollup/rollup-freebsd-arm64" "4.56.0"
"@rollup/rollup-freebsd-x64" "4.56.0"
"@rollup/rollup-linux-arm-gnueabihf" "4.56.0"
"@rollup/rollup-linux-arm-musleabihf" "4.56.0"
"@rollup/rollup-linux-arm64-gnu" "4.56.0"
"@rollup/rollup-linux-arm64-musl" "4.56.0"
"@rollup/rollup-linux-loong64-gnu" "4.56.0"
"@rollup/rollup-linux-loong64-musl" "4.56.0"
"@rollup/rollup-linux-ppc64-gnu" "4.56.0"
"@rollup/rollup-linux-ppc64-musl" "4.56.0"
"@rollup/rollup-linux-riscv64-gnu" "4.56.0"
"@rollup/rollup-linux-riscv64-musl" "4.56.0"
"@rollup/rollup-linux-s390x-gnu" "4.56.0"
"@rollup/rollup-linux-x64-gnu" "4.56.0"
"@rollup/rollup-linux-x64-musl" "4.56.0"
"@rollup/rollup-openbsd-x64" "4.56.0"
"@rollup/rollup-openharmony-arm64" "4.56.0"
"@rollup/rollup-win32-arm64-msvc" "4.56.0"
"@rollup/rollup-win32-ia32-msvc" "4.56.0"
"@rollup/rollup-win32-x64-gnu" "4.56.0"
"@rollup/rollup-win32-x64-msvc" "4.56.0"
fsevents "~2.3.2"
rope-sequence@^1.3.0:
@@ -4797,9 +4774,9 @@ tempy@^0.6.0:
unique-string "^2.0.0"
terser@^5.0.0:
version "5.44.1"
resolved "https://registry.yarnpkg.com/terser/-/terser-5.44.1.tgz#e391e92175c299b8c284ad6ded609e37303b0a9c"
integrity sha512-t/R3R/n0MSwnnazuPpPNVO60LX0SKL45pyl9YlvxIdkH0Of7D5qM2EVe+yASRIlY5pZ73nclYJfNANGWPwFDZw==
version "5.46.0"
resolved "https://registry.yarnpkg.com/terser/-/terser-5.46.0.tgz#1b81e560d584bbdd74a8ede87b4d9477b0ff9695"
integrity sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==
dependencies:
"@jridgewell/source-map" "^0.3.3"
acorn "^8.15.0"
@@ -4940,9 +4917,9 @@ uc.micro@^2.0.0, uc.micro@^2.1.0:
integrity sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==
ufo@^1.6.1:
version "1.6.2"
resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.6.2.tgz#aaf4d46b98425b2fb5031abe8d65ca069e93e755"
integrity sha512-heMioaxBcG9+Znsda5Q8sQbWnLJSl98AFDXTO80wELWEzX3hordXsTdxrIfMQoO9IY1MEnoGoPjpoKpMj+Yx0Q==
version "1.6.3"
resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.6.3.tgz#799666e4e88c122a9659805e30b9dc071c3aed4f"
integrity sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==
unbox-primitive@^1.1.0:
version "1.1.0"
@@ -5169,7 +5146,7 @@ vue-codemirror@6.1.1:
"@codemirror/state" "6.x"
"@codemirror/view" "6.x"
vue-demi@*, vue-demi@>=0.13.0, vue-demi@>=0.14.5, vue-demi@>=0.14.8:
vue-demi@*, vue-demi@>=0.13.0, vue-demi@>=0.14.8:
version "0.14.10"
resolved "https://registry.yarnpkg.com/vue-demi/-/vue-demi-0.14.10.tgz#afc78de3d6f9e11bf78c55e8510ee12814522f04"
integrity sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==
@@ -5179,28 +5156,28 @@ vue-draggable-next@2.2.1:
resolved "https://registry.yarnpkg.com/vue-draggable-next/-/vue-draggable-next-2.2.1.tgz#adbe98c74610cca8f4eb63f92042681f96920451"
integrity sha512-EAMS1IRHF0kZO0o5PMOinsQsXIqsrKT1hKmbICxG3UEtn7zLFkLxlAtajcCcUTisNvQ6TtCB5COjD9a1raNADw==
vue-router@4.2.2:
version "4.2.2"
resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-4.2.2.tgz#b0097b66d89ca81c0986be03da244c7b32a4fd81"
integrity sha512-cChBPPmAflgBGmy3tBsjeoe3f3VOSG6naKyY5pjtrqLGbNEXdzCigFUHgBvp9e3ysAtFtEx7OLqcSDh/1Cq2TQ==
vue-router@^4.6.4:
version "4.6.4"
resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-4.6.4.tgz#a0a9cb9ef811a106d249e4bb9313d286718020d8"
integrity sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==
dependencies:
"@vue/devtools-api" "^6.5.0"
"@vue/devtools-api" "^6.6.4"
vue3-apexcharts@1.8.0:
version "1.8.0"
resolved "https://registry.yarnpkg.com/vue3-apexcharts/-/vue3-apexcharts-1.8.0.tgz#1984648d966aa91bc4dc3e87fa847f5289f7f1cf"
integrity sha512-5tSD4mXTBbIJ9ir+58qHE6oNtIe0RNgqIRYMKpcsIaxkKtwUww4JhvPkpUFlmiW4OJbbdklgjleXq1lfcM4gdA==
vue@^3.5.0, vue@^3.5.13:
version "3.5.26"
resolved "https://registry.yarnpkg.com/vue/-/vue-3.5.26.tgz#03a0b17311e0e593d34b9358fa249b85e3a6d9fb"
integrity sha512-SJ/NTccVyAoNUJmkM9KUqPcYlY+u8OVL1X5EW9RIs3ch5H2uERxyyIUI4MRxVCSOiEcupX9xNGde1tL9ZKpimA==
vue@^3.5.13, vue@^3.5.27:
version "3.5.27"
resolved "https://registry.yarnpkg.com/vue/-/vue-3.5.27.tgz#e55fd941b614459ab2228489bc19d1692e05876c"
integrity sha512-aJ/UtoEyFySPBGarREmN4z6qNKpbEguYHMmXSiOGk69czc+zhs0NF6tEFrY8TZKAl8N/LYAkd4JHVd5E/AsSmw==
dependencies:
"@vue/compiler-dom" "3.5.26"
"@vue/compiler-sfc" "3.5.26"
"@vue/runtime-dom" "3.5.26"
"@vue/server-renderer" "3.5.26"
"@vue/shared" "3.5.26"
"@vue/compiler-dom" "3.5.27"
"@vue/compiler-sfc" "3.5.27"
"@vue/runtime-dom" "3.5.27"
"@vue/server-renderer" "3.5.27"
"@vue/shared" "3.5.27"
vuedraggable@4.1.0:
version "4.1.0"
@@ -5281,9 +5258,9 @@ which-collection@^1.0.2:
is-weakset "^2.0.3"
which-typed-array@^1.1.16, which-typed-array@^1.1.19:
version "1.1.19"
resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.19.tgz#df03842e870b6b88e117524a4b364b6fc689f956"
integrity sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==
version "1.1.20"
resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.20.tgz#3fdb7adfafe0ea69157b1509f3a1cd892bd1d122"
integrity sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==
dependencies:
available-typed-arrays "^1.0.7"
call-bind "^1.0.8"
+48 -15
View File
@@ -27,6 +27,7 @@ from frappe.utils import (
now,
)
from frappe.utils.response import Response
from pypika import functions as fn
from lms.lms.doctype.course_lesson.course_lesson import save_progress
from lms.lms.utils import (
@@ -1619,7 +1620,11 @@ def track_new_watch_time(lesson, video):
@frappe.whitelist()
def get_course_progress_distribution(course):
all_progress = frappe.get_all(
roles = frappe.get_roles()
if "Course Creator" not in roles and "Moderator" not in roles:
frappe.throw(_("You do not have permission to access course progress data."))
all_progress = frappe.get_list(
"LMS Enrollment",
{
"course": course,
@@ -1646,24 +1651,16 @@ def get_average_course_progress(progress_list):
def get_progress_distribution(progressList):
distribution = [
{
"category": "0-20%",
"count": len([p for p in progressList if 0 <= p < 20]),
"name": "Just Started (0-30%)",
"value": len([p for p in progressList if 0 <= p < 30]),
},
{
"category": "20-40%",
"count": len([p for p in progressList if 20 <= p < 40]),
"name": "In Progress (30-60%)",
"value": len([p for p in progressList if 30 <= p < 60]),
},
{
"category": "40-60%",
"count": len([p for p in progressList if 40 <= p < 60]),
},
{
"category": "60-80%",
"count": len([p for p in progressList if 60 <= p < 80]),
},
{
"category": "80-100%",
"count": len([p for p in progressList if 80 <= p <= 100]),
"name": "Advanced (60-100%)",
"value": len([p for p in progressList if 60 <= p <= 100]),
},
]
@@ -2062,3 +2059,39 @@ def get_upcoming_batches():
def delete_programming_exercise(exercise):
frappe.db.delete("LMS Programming Exercise Submission", {"exercise": exercise})
frappe.db.delete("LMS Programming Exercise", exercise)
@frappe.whitelist()
def get_lesson_completion_stats(course):
roles = frappe.get_roles()
if "Course Creator" not in roles and "Moderator" not in roles:
frappe.throw(_("You do not have permission to access lesson completion stats."))
CourseProgress = frappe.qb.DocType("LMS Course Progress")
LessonReference = frappe.qb.DocType("Lesson Reference")
ChapterReference = frappe.qb.DocType("Chapter Reference")
Lesson = frappe.qb.DocType("Course Lesson")
rows = (
frappe.qb.from_(CourseProgress)
.join(LessonReference)
.on(CourseProgress.lesson == LessonReference.lesson)
.join(ChapterReference)
.on(LessonReference.parent == ChapterReference.chapter)
.join(Lesson)
.on(CourseProgress.lesson == Lesson.name)
.select(
LessonReference.idx,
ChapterReference.idx.as_("chapter_idx"),
CourseProgress.lesson,
Lesson.title,
Lesson.name.as_("lesson_name"),
fn.Count(CourseProgress.name).as_("completion_count"),
)
.where((CourseProgress.course == course) & (CourseProgress.status == "Complete"))
.groupby(CourseProgress.lesson)
.orderby(ChapterReference.idx, LessonReference.idx)
.run(as_dict=True)
)
return rows
@@ -101,8 +101,8 @@
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2025-01-20 22:25:36.829929",
"modified_by": "Administrator",
"modified": "2026-01-28 18:10:51.370989",
"modified_by": "sayali@frappe.io",
"module": "LMS",
"name": "LMS Course Progress",
"owner": "Administrator",
@@ -123,13 +123,39 @@
"create": 1,
"email": 1,
"export": 1,
"if_owner": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "LMS Student",
"share": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Moderator",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Course Creator",
"share": 1,
"write": 1
}
],
"row_format": "Dynamic",
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
@@ -38,10 +38,11 @@
"fieldtype": "Column Break"
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2023-12-21 15:25:16.744558",
"modified_by": "Administrator",
"modified": "2026-01-29 16:10:47.787285",
"modified_by": "sayali@frappe.io",
"module": "LMS",
"name": "LMS Course Review",
"owner": "Administrator",
@@ -60,7 +61,6 @@
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
@@ -69,12 +69,49 @@
"role": "LMS Student",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Moderator",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Course Creator",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Batch Evaluator",
"share": 1,
"write": 1
}
],
"row_format": "Dynamic",
"search_fields": "course",
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"title_field": "course",
"track_changes": 1
}
}
@@ -141,7 +141,7 @@
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2025-12-23 11:11:23.908797",
"modified": "2026-01-28 18:07:58.529033",
"modified_by": "sayali@frappe.io",
"module": "LMS",
"name": "LMS Enrollment",
@@ -163,6 +163,7 @@
"create": 1,
"email": 1,
"export": 1,
"if_owner": 1,
"print": 1,
"read": 1,
"report": 1,
@@ -183,6 +184,18 @@
"select": 1,
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Course Creator",
"share": 1,
"write": 1
}
],
"quick_entry": 1,
@@ -9,11 +9,24 @@ from frappe.utils import ceil
class LMSEnrollment(Document):
def before_insert(self):
self.validate_duplicate_enrollment()
self.validate_course_enrollment_eligibility()
def on_update(self):
update_program_progress(self.member)
def validate_duplicate_enrollment(self):
existing_enrollment = frappe.db.exists(
"LMS Enrollment",
{
"course": self.course,
"member": self.member,
},
)
if existing_enrollment:
frappe.throw(_("Student is already enrolled in this course."))
def validate_course_enrollment_eligibility(self):
course_details = frappe.db.get_value(
"LMS Course",
+2
View File
@@ -25,6 +25,8 @@
},
"homepage": "https://github.com/frappe/lms#readme",
"devDependencies": {
"@semantic-release/exec": "^7.1.0",
"@semantic-release/git": "^10.0.1",
"cypress": "^14.5.4",
"cypress-file-upload": "^5.0.8",
"cypress-real-events": "^1.14.0"
+293 -20
View File
@@ -2,10 +2,24 @@
# yarn lockfile v1
"@babel/code-frame@^7.26.2":
version "7.28.6"
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.28.6.tgz#72499312ec58b1e2245ba4a4f550c132be4982f7"
integrity sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==
dependencies:
"@babel/helper-validator-identifier" "^7.28.5"
js-tokens "^4.0.0"
picocolors "^1.1.1"
"@babel/helper-validator-identifier@^7.28.5":
version "7.28.5"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz#010b6938fab7cb7df74aa2bbc06aa503b8fe5fb4"
integrity sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==
"@cypress/request@^3.0.9":
version "3.0.9"
resolved "https://registry.yarnpkg.com/@cypress/request/-/request-3.0.9.tgz#8ed6e08fea0c62998b5552301023af7268f11625"
integrity sha512-I3l7FdGRXluAS44/0NguwWlO83J18p0vlr2FYHrJkWdNYhgVoiYo61IXPqaOsL+vNxU1ZqMACzItGK3/KKDsdw==
version "3.0.10"
resolved "https://registry.yarnpkg.com/@cypress/request/-/request-3.0.10.tgz#e09c695e8460a82acafe6cfaf089cf2ca06dc054"
integrity sha512-hauBrOdvu08vOsagkZ/Aju5XuiZx6ldsLfByg1htFeldhex+PeMrYauANzFsMJeAA0+dyPLbDoX2OYuvVoLDkQ==
dependencies:
aws-sign2 "~0.7.0"
aws4 "^1.8.0"
@@ -20,7 +34,7 @@
json-stringify-safe "~5.0.1"
mime-types "~2.1.19"
performance-now "^2.1.0"
qs "6.14.0"
qs "~6.14.1"
safe-buffer "^5.1.2"
tough-cookie "^5.0.0"
tunnel-agent "^0.6.0"
@@ -34,10 +48,56 @@
debug "^3.1.0"
lodash.once "^4.1.1"
"@sec-ant/readable-stream@^0.4.1":
version "0.4.1"
resolved "https://registry.yarnpkg.com/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz#60de891bb126abfdc5410fdc6166aca065f10a0c"
integrity sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==
"@semantic-release/error@^3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@semantic-release/error/-/error-3.0.0.tgz#30a3b97bbb5844d695eb22f9d3aa40f6a92770c2"
integrity sha512-5hiM4Un+tpl4cKw3lV4UgzJj+SmfNIDCLLw0TepzQxz9ZGV5ixnqkzIVF+3tp0ZHgcMKE+VNGHJjEeyFG2dcSw==
"@semantic-release/error@^4.0.0":
version "4.0.0"
resolved "https://registry.yarnpkg.com/@semantic-release/error/-/error-4.0.0.tgz#692810288239637f74396976a9340fbc0aa9f6f9"
integrity sha512-mgdxrHTLOjOddRVYIYDo0fR3/v61GNN1YGkfbrjuIKg/uMgCd+Qzo3UAXJ+woLQQpos4pl5Esuw5A7AoNlzjUQ==
"@semantic-release/exec@^7.1.0":
version "7.1.0"
resolved "https://registry.yarnpkg.com/@semantic-release/exec/-/exec-7.1.0.tgz#cfaffe85a589aa0f385eede256904ce70acb829f"
integrity sha512-4ycZ2atgEUutspPZ2hxO6z8JoQt4+y/kkHvfZ1cZxgl9WKJId1xPj+UadwInj+gMn2Gsv+fLnbrZ4s+6tK2TFQ==
dependencies:
"@semantic-release/error" "^4.0.0"
aggregate-error "^3.0.0"
debug "^4.0.0"
execa "^9.0.0"
lodash-es "^4.17.21"
parse-json "^8.0.0"
"@semantic-release/git@^10.0.1":
version "10.0.1"
resolved "https://registry.yarnpkg.com/@semantic-release/git/-/git-10.0.1.tgz#c646e55d67fae623875bf3a06a634dd434904498"
integrity sha512-eWrx5KguUcU2wUPaO6sfvZI0wPafUKAMNC18aXY4EnNcrZL86dEmpNVnC9uMpGZkmZJ9EfCVJBQx4pV4EMGT1w==
dependencies:
"@semantic-release/error" "^3.0.0"
aggregate-error "^3.0.0"
debug "^4.0.0"
dir-glob "^3.0.0"
execa "^5.0.0"
lodash "^4.17.4"
micromatch "^4.0.0"
p-reduce "^2.0.0"
"@sindresorhus/merge-streams@^4.0.0":
version "4.0.0"
resolved "https://registry.yarnpkg.com/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz#abb11d99aeb6d27f1b563c38147a72d50058e339"
integrity sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==
"@types/node@*":
version "25.0.3"
resolved "https://registry.yarnpkg.com/@types/node/-/node-25.0.3.tgz#79b9ac8318f373fbfaaf6e2784893efa9701f269"
integrity sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==
version "25.0.10"
resolved "https://registry.yarnpkg.com/@types/node/-/node-25.0.10.tgz#4864459c3c9459376b8b75fd051315071c8213e7"
integrity sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg==
dependencies:
undici-types "~7.16.0"
@@ -159,6 +219,13 @@ bluebird@^3.7.2:
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
braces@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789"
integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==
dependencies:
fill-range "^7.1.1"
buffer-crc32@~0.2.3:
version "0.2.13"
resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242"
@@ -318,7 +385,7 @@ cross-spawn@^5.0.1:
shebang-command "^1.2.0"
which "^1.2.9"
cross-spawn@^7.0.0:
cross-spawn@^7.0.0, cross-spawn@^7.0.3, 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==
@@ -406,7 +473,7 @@ debug@^3.1.0:
dependencies:
ms "^2.1.1"
debug@^4.1.1, debug@^4.3.4:
debug@^4.0.0, debug@^4.1.1, debug@^4.3.4:
version "4.4.3"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a"
integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==
@@ -418,6 +485,13 @@ delayed-stream@~1.0.0:
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
dir-glob@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f"
integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==
dependencies:
path-type "^4.0.0"
dunder-proto@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a"
@@ -507,6 +581,39 @@ execa@4.1.0:
signal-exit "^3.0.2"
strip-final-newline "^2.0.0"
execa@^5.0.0:
version "5.1.1"
resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd"
integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==
dependencies:
cross-spawn "^7.0.3"
get-stream "^6.0.0"
human-signals "^2.1.0"
is-stream "^2.0.0"
merge-stream "^2.0.0"
npm-run-path "^4.0.1"
onetime "^5.1.2"
signal-exit "^3.0.3"
strip-final-newline "^2.0.0"
execa@^9.0.0:
version "9.6.1"
resolved "https://registry.yarnpkg.com/execa/-/execa-9.6.1.tgz#5b90acedc6bdc0fa9b9a6ddf8f9cbb0c75a7c471"
integrity sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==
dependencies:
"@sindresorhus/merge-streams" "^4.0.0"
cross-spawn "^7.0.6"
figures "^6.1.0"
get-stream "^9.0.0"
human-signals "^8.0.1"
is-plain-obj "^4.1.0"
is-stream "^4.0.1"
npm-run-path "^6.0.0"
pretty-ms "^9.2.0"
signal-exit "^4.1.0"
strip-final-newline "^4.0.0"
yoctocolors "^2.1.1"
executable@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/executable/-/executable-4.1.1.tgz#41532bff361d3e57af4d763b70582db18f5d133c"
@@ -554,6 +661,20 @@ figures@^3.2.0:
dependencies:
escape-string-regexp "^1.0.5"
figures@^6.1.0:
version "6.1.0"
resolved "https://registry.yarnpkg.com/figures/-/figures-6.1.0.tgz#935479f51865fa7479f6fa94fc6fc7ac14e62c4a"
integrity sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==
dependencies:
is-unicode-supported "^2.0.0"
fill-range@^7.1.1:
version "7.1.1"
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292"
integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==
dependencies:
to-regex-range "^5.0.1"
forever-agent@~0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
@@ -616,6 +737,19 @@ get-stream@^5.0.0, get-stream@^5.1.0:
dependencies:
pump "^3.0.0"
get-stream@^6.0.0:
version "6.0.1"
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7"
integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==
get-stream@^9.0.0:
version "9.0.1"
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-9.0.1.tgz#95157d21df8eb90d1647102b63039b1df60ebd27"
integrity sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==
dependencies:
"@sec-ant/readable-stream" "^0.4.1"
is-stream "^4.0.1"
getos@^3.2.1:
version "3.2.1"
resolved "https://registry.yarnpkg.com/getos/-/getos-3.2.1.tgz#0134d1f4e00eb46144c5a9c0ac4dc087cbb27dc5"
@@ -693,6 +827,16 @@ 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@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0"
integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==
human-signals@^8.0.1:
version "8.0.1"
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-8.0.1.tgz#f08bb593b6d1db353933d06156cedec90abe51fb"
integrity sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==
ieee754@^1.1.13:
version "1.2.1"
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
@@ -703,6 +847,11 @@ indent-string@^4.0.0:
resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251"
integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==
index-to-position@^1.1.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/index-to-position/-/index-to-position-1.2.0.tgz#c800eb34dacf4dbf96b9b06c7eb78d5f704138b4"
integrity sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==
inherits@^2.0.3, inherits@~2.0.3:
version "2.0.4"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
@@ -726,16 +875,31 @@ is-installed-globally@~0.4.0:
global-dirs "^3.0.0"
is-path-inside "^3.0.2"
is-number@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
is-path-inside@^3.0.2:
version "3.0.3"
resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283"
integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==
is-plain-obj@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-4.1.0.tgz#d65025edec3657ce032fd7db63c97883eaed71f0"
integrity sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==
is-stream@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077"
integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==
is-stream@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-4.0.1.tgz#375cf891e16d2e4baec250b85926cffc14720d9b"
integrity sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==
is-typedarray@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
@@ -746,6 +910,11 @@ is-unicode-supported@^0.1.0:
resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7"
integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==
is-unicode-supported@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz#09f0ab0de6d3744d48d265ebb98f65d11f2a9b3a"
integrity sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==
isarray@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
@@ -761,6 +930,11 @@ isstream@~0.1.2:
resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
integrity sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==
js-tokens@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
jsbn@~0.1.0:
version "0.1.1"
resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
@@ -814,15 +988,20 @@ listr2@^3.8.3:
through "^2.3.8"
wrap-ansi "^7.0.0"
lodash-es@^4.17.21:
version "4.17.23"
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.23.tgz#58c4360fd1b5d33afc6c0bbd3d1149349b1138e0"
integrity sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==
lodash.once@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac"
integrity sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==
lodash@^4.17.21:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
lodash@^4.17.21, lodash@^4.17.4:
version "4.17.23"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.23.tgz#f113b0378386103be4f6893388c73d0bde7f2c5a"
integrity sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==
log-symbols@^4.0.0:
version "4.1.0"
@@ -860,6 +1039,14 @@ merge-stream@^2.0.0:
resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==
micromatch@^4.0.0:
version "4.0.8"
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202"
integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==
dependencies:
braces "^3.0.3"
picomatch "^2.3.1"
mime-db@1.52.0:
version "1.52.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
@@ -887,13 +1074,21 @@ ms@^2.1.1, ms@^2.1.3:
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
npm-run-path@^4.0.0:
npm-run-path@^4.0.0, npm-run-path@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea"
integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==
dependencies:
path-key "^3.0.0"
npm-run-path@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-6.0.0.tgz#25cfdc4eae04976f3349c0b1afc089052c362537"
integrity sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==
dependencies:
path-key "^4.0.0"
unicorn-magic "^0.3.0"
object-inspect@^1.13.3:
version "1.13.4"
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.4.tgz#8375265e21bc20d0fa582c22e1b13485d6e00213"
@@ -906,7 +1101,7 @@ once@^1.3.1, once@^1.4.0:
dependencies:
wrappy "1"
onetime@^5.1.0:
onetime@^5.1.0, onetime@^5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e"
integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==
@@ -930,11 +1125,40 @@ p-map@^4.0.0:
dependencies:
aggregate-error "^3.0.0"
p-reduce@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/p-reduce/-/p-reduce-2.1.0.tgz#09408da49507c6c274faa31f28df334bc712b64a"
integrity sha512-2USApvnsutq8uoxZBGbbWM0JIYLiEMJ9RlaN7fAzVNb9OZN0SHjjTTfIcb667XynS5Y1VhwDJVDa72TnPzAYWw==
parse-json@^8.0.0:
version "8.3.0"
resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-8.3.0.tgz#88a195a2157025139a2317a4f2f9252b61304ed5"
integrity sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==
dependencies:
"@babel/code-frame" "^7.26.2"
index-to-position "^1.1.0"
type-fest "^4.39.1"
parse-ms@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/parse-ms/-/parse-ms-4.0.0.tgz#c0c058edd47c2a590151a718990533fd62803df4"
integrity sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==
path-key@^3.0.0, path-key@^3.1.0:
version "3.1.1"
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-type@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
pend@~1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50"
@@ -945,6 +1169,16 @@ performance-now@^2.1.0:
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==
picocolors@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"
integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
picomatch@^2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
pify@^2.2.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
@@ -964,6 +1198,13 @@ pretty-bytes@^5.6.0:
resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb"
integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==
pretty-ms@^9.2.0:
version "9.3.0"
resolved "https://registry.yarnpkg.com/pretty-ms/-/pretty-ms-9.3.0.tgz#dd2524fcb3c326b4931b2272dfd1e1a8ed9a9f5a"
integrity sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==
dependencies:
parse-ms "^4.0.0"
process-nextick-args@~2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
@@ -992,10 +1233,10 @@ pump@^3.0.0:
end-of-stream "^1.1.0"
once "^1.3.1"
qs@6.14.0:
version "6.14.0"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.14.0.tgz#c63fa40680d2c5c941412a0e899c89af60c0a930"
integrity sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==
qs@~6.14.1:
version "6.14.1"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.14.1.tgz#a41d85b9d3902f31d27861790506294881871159"
integrity sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==
dependencies:
side-channel "^1.1.0"
@@ -1123,11 +1364,16 @@ side-channel@^1.1.0:
side-channel-map "^1.0.1"
side-channel-weakmap "^1.0.2"
signal-exit@^3.0.2:
signal-exit@^3.0.2, signal-exit@^3.0.3:
version "3.0.7"
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9"
integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==
signal-exit@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04"
integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==
slice-ansi@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-3.0.0.tgz#31ddc10930a1b7e0b67b08c96c2f49b77a789787"
@@ -1197,6 +1443,11 @@ 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@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-4.0.0.tgz#35a369ec2ac43df356e3edd5dcebb6429aa1fa5c"
integrity sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==
supports-color@^7.1.0:
version "7.2.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da"
@@ -1238,6 +1489,13 @@ tmp@~0.2.3:
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.5.tgz#b06bcd23f0f3c8357b426891726d16015abfd8f8"
integrity sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==
to-regex-range@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
dependencies:
is-number "^7.0.0"
tough-cookie@^5.0.0:
version "5.1.2"
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-5.1.2.tgz#66d774b4a1d9e12dc75089725af3ac75ec31bed7"
@@ -1277,6 +1535,11 @@ type-fest@^0.8.0:
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d"
integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==
type-fest@^4.39.1:
version "4.41.0"
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-4.41.0.tgz#6ae1c8e5731273c2bf1f58ad39cbae2c91a46c58"
integrity sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==
typedarray@^0.0.6:
version "0.0.6"
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
@@ -1287,6 +1550,11 @@ undici-types@~7.16.0:
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.16.0.tgz#ffccdff36aea4884cbfce9a750a0580224f58a46"
integrity sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==
unicorn-magic@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/unicorn-magic/-/unicorn-magic-0.3.0.tgz#4efd45c85a69e0dd576d25532fbfa22aa5c8a104"
integrity sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==
universalify@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d"
@@ -1372,3 +1640,8 @@ yauzl@^2.10.0:
dependencies:
buffer-crc32 "~0.2.3"
fd-slicer "~1.1.0"
yoctocolors@^2.1.1:
version "2.1.2"
resolved "https://registry.yarnpkg.com/yoctocolors/-/yoctocolors-2.1.2.tgz#d795f54d173494e7d8db93150cec0ed7f678c83a"
integrity sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==