chore: fixed conflicts

This commit is contained in:
Jannat Patel
2025-05-20 19:09:19 +05:30
97 changed files with 8874 additions and 10099 deletions
+1 -1
View File
@@ -105,7 +105,7 @@ jobs:
- name: cypress pre-requisites - name: cypress pre-requisites
run: | run: |
cd ~/frappe-bench/apps/lms cd ~/frappe-bench/apps/lms
yarn add cypress@^10 --no-lockfile yarn add cypress@^10 --no-lockfile -W
- name: UI Tests - name: UI Tests
run: cd ~/frappe-bench/ && bench --site lms.test run-ui-tests lms --headless run: cd ~/frappe-bench/ && bench --site lms.test run-ui-tests lms --headless
+1 -1
View File
@@ -1,4 +1,4 @@
module.exports = { export default {
parserPreset: "conventional-changelog-conventionalcommits", parserPreset: "conventional-changelog-conventionalcommits",
rules: { rules: {
"subject-empty": [2, "never"], "subject-empty": [2, "never"],
+2 -2
View File
@@ -1,6 +1,6 @@
const { defineConfig } = require("cypress"); import { defineConfig } from "cypress";
module.exports = defineConfig({ export default defineConfig({
projectId: "vandxn", projectId: "vandxn",
adminPassword: "admin", adminPassword: "admin",
testUser: "frappe@example.com", testUser: "frappe@example.com",
+3 -3
View File
@@ -16,9 +16,9 @@ cd frappe-bench
# Use containers instead of localhost # Use containers instead of localhost
bench set-mariadb-host mariadb bench set-mariadb-host mariadb
bench set-redis-cache-host redis:6379 bench set-redis-cache-host redis://redis:6379
bench set-redis-queue-host redis:6379 bench set-redis-queue-host redis://redis:6379
bench set-redis-socketio-host redis:6379 bench set-redis-socketio-host redis://redis:6379
# Remove redis, watch from Procfile # Remove redis, watch from Procfile
sed -i '/redis/d' ./Procfile sed -i '/redis/d' ./Procfile
+1
View File
@@ -47,6 +47,7 @@ declare module 'vue' {
Discussions: typeof import('./src/components/Discussions.vue')['default'] Discussions: typeof import('./src/components/Discussions.vue')['default']
EditCoverImage: typeof import('./src/components/Modals/EditCoverImage.vue')['default'] EditCoverImage: typeof import('./src/components/Modals/EditCoverImage.vue')['default']
EditProfile: typeof import('./src/components/Modals/EditProfile.vue')['default'] EditProfile: typeof import('./src/components/Modals/EditProfile.vue')['default']
EmptyState: typeof import('./src/components/EmptyState.vue')['default']
EvaluationModal: typeof import('./src/components/Modals/EvaluationModal.vue')['default'] EvaluationModal: typeof import('./src/components/Modals/EvaluationModal.vue')['default']
Evaluators: typeof import('./src/components/Evaluators.vue')['default'] Evaluators: typeof import('./src/components/Evaluators.vue')['default']
Event: typeof import('./src/components/Modals/Event.vue')['default'] Event: typeof import('./src/components/Modals/Event.vue')['default']
+1 -1
View File
@@ -27,7 +27,7 @@
"codemirror-editor-vue3": "^2.8.0", "codemirror-editor-vue3": "^2.8.0",
"dayjs": "^1.11.6", "dayjs": "^1.11.6",
"feather-icons": "^4.28.0", "feather-icons": "^4.28.0",
"frappe-ui": "^0.1.141", "frappe-ui": "^0.1.143",
"highlight.js": "^11.11.1", "highlight.js": "^11.11.1",
"lucide-vue-next": "^0.383.0", "lucide-vue-next": "^0.383.0",
"markdown-it": "^14.0.0", "markdown-it": "^14.0.0",
+7 -6
View File
@@ -1,12 +1,13 @@
<template> <template>
<Layout> <FrappeUIProvider>
<router-view /> <Layout>
</Layout> <router-view />
<Dialogs /> </Layout>
<Toasts /> <Dialogs />
</FrappeUIProvider>
</template> </template>
<script setup> <script setup>
import { Toasts } from 'frappe-ui' import { FrappeUIProvider } from 'frappe-ui'
import { Dialogs } from '@/utils/dialogs' import { Dialogs } from '@/utils/dialogs'
import { computed, onMounted, onUnmounted, ref } from 'vue' import { computed, onMounted, onUnmounted, ref } from 'vue'
import { useScreenSize } from './utils/composables' import { useScreenSize } from './utils/composables'
+1 -1
View File
@@ -125,7 +125,7 @@
@click="redirectToWebsite()" @click="redirectToWebsite()"
/> />
</Tooltip> </Tooltip>
<Tooltip :text="__('Help')"> <Tooltip v-if="showOnboarding" :text="__('Help')">
<CircleHelp <CircleHelp
class="size-4 stroke-1.5 text-ink-gray-7 cursor-pointer" class="size-4 stroke-1.5 text-ink-gray-7 cursor-pointer"
@click=" @click="
+6 -5
View File
@@ -191,10 +191,11 @@ import {
FileUploader, FileUploader,
FormControl, FormControl,
TextEditor, TextEditor,
toast,
} from 'frappe-ui' } from 'frappe-ui'
import { computed, inject, onMounted, onBeforeUnmount, ref, watch } from 'vue' import { computed, inject, onMounted, onBeforeUnmount, ref, watch } from 'vue'
import { FileText, X } from 'lucide-vue-next' import { FileText, X } from 'lucide-vue-next'
import { showToast, getFileSize } from '@/utils' import { getFileSize } from '@/utils'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
const submissionFile = ref(null) const submissionFile = ref(null)
@@ -284,7 +285,7 @@ const submissionResource = createDocumentResource({
doctype: 'LMS Assignment Submission', doctype: 'LMS Assignment Submission',
name: props.submissionName, name: props.submissionName,
onError(err) { onError(err) {
showToast(__('Error'), __(err.messages?.[0] || err), 'x') toast.error(err.messages?.[0] || err)
}, },
auto: false, auto: false,
cache: [user.data?.name, props.assignmentID], cache: [user.data?.name, props.assignmentID],
@@ -338,7 +339,7 @@ const submitAssignment = () => {
}, },
{ {
onSuccess(data) { onSuccess(data) {
showToast(__('Success'), __('Changes saved successfully'), 'check') toast.success(__('Changes saved successfully'))
}, },
} }
) )
@@ -352,7 +353,7 @@ const addNewSubmission = () => {
{}, {},
{ {
onSuccess(data) { onSuccess(data) {
showToast('Success', 'Assignment submitted successfully.', 'check') toast.success(__('Assignment submitted successfully'))
if (router.currentRoute.value.name == 'AssignmentSubmission') { if (router.currentRoute.value.name == 'AssignmentSubmission') {
router.push({ router.push({
name: 'AssignmentSubmission', name: 'AssignmentSubmission',
@@ -370,7 +371,7 @@ const addNewSubmission = () => {
submissionResource.reload() submissionResource.reload()
}, },
onError(err) { onError(err) {
showToast('Error', err.messages?.[0] || err, 'x') toast.error(err.messages?.[0] || err)
}, },
} }
) )
+2 -2
View File
@@ -86,9 +86,9 @@ import {
ListRows, ListRows,
ListView, ListView,
ListRowItem, ListRowItem,
toast,
} from 'frappe-ui' } from 'frappe-ui'
import { Plus, Trash2 } from 'lucide-vue-next' import { Plus, Trash2 } from 'lucide-vue-next'
import { showToast } from '@/utils'
const readOnlyMode = window.read_only_mode const readOnlyMode = window.read_only_mode
const showCourseModal = ref(false) const showCourseModal = ref(false)
@@ -152,7 +152,7 @@ const removeCourses = (selections, unselectAll) => {
{ {
onSuccess(data) { onSuccess(data) {
courses.reload() courses.reload()
showToast(__('Success'), __('Courses deleted successfully'), 'check') toast.success(__('Courses deleted successfully'))
unselectAll() unselectAll()
}, },
} }
+9 -8
View File
@@ -2,7 +2,12 @@
<div v-if="batch.data" class="border-2 rounded-md p-5 lg:w-72"> <div v-if="batch.data" class="border-2 rounded-md p-5 lg:w-72">
<div <div
v-if="batch.data.seat_count && seats_left > 0" v-if="batch.data.seat_count && seats_left > 0"
class="text-xs bg-green-100 text-green-700 float-right px-2 py-0.5 rounded-md" class="text-sm bg-green-100 text-green-700 px-2 py-1 rounded-md"
:class="
batch.data.amount || batch.data.courses.length
? 'float-right'
: 'w-fit mb-4'
"
> >
{{ seats_left }} {{ seats_left }}
<span v-if="seats_left > 1"> <span v-if="seats_left > 1">
@@ -117,9 +122,9 @@
</template> </template>
<script setup> <script setup>
import { inject, computed } from 'vue' import { inject, computed } from 'vue'
import { Badge, Button, createResource } from 'frappe-ui' import { Badge, Button, createResource, toast } from 'frappe-ui'
import { BookOpen, Clock, Globe } from 'lucide-vue-next' import { BookOpen, Clock, Globe } from 'lucide-vue-next'
import { formatNumberIntoCurrency, formatTime, showToast } from '@/utils' import { formatNumberIntoCurrency, formatTime } from '@/utils'
import DateRange from '@/components/Common/DateRange.vue' import DateRange from '@/components/Common/DateRange.vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
@@ -151,11 +156,7 @@ const enrollInBatch = () => {
{}, {},
{ {
onSuccess(data) { onSuccess(data) {
showToast( toast.success(__('You have been enrolled in this batch'))
__('Success'),
__('You have been enrolled in this batch'),
'check'
)
router.push({ router.push({
name: 'Batch', name: 'Batch',
params: { params: {
+35 -33
View File
@@ -1,12 +1,11 @@
<template> <template>
<div class=""> <div v-if="batch.data" class="">
<div class="w-full flex items-center justify-between pb-4"> <div class="w-full flex items-center justify-between pb-4">
<div class="font-medium text-ink-gray-7"> <div class="font-medium text-ink-gray-7">
{{ __('Statistics') }} {{ __('Statistics') }}
</div> </div>
</div> </div>
<div class="grid grid-cols-4 gap-5 mb-8"> <div class="grid grid-cols-4 gap-5 mb-8">
<NumberChart <NumberChart
class="border rounded-md" class="border rounded-md"
:config="{ title: __('Students'), value: students.data?.length || 0 }" :config="{ title: __('Students'), value: students.data?.length || 0 }"
@@ -14,7 +13,10 @@
<NumberChart <NumberChart
class="border rounded-md" class="border rounded-md"
:config="{ title: __('Certified'), value: certificationCount.data || 0 }" :config="{
title: __('Certified'),
value: certificationCount.data || 0,
}"
/> />
<NumberChart <NumberChart
@@ -79,26 +81,26 @@
product: 'HomePod', product: 'HomePod',
sales: 200, sales: 200,
}, },
], ],
title: __('Batch Summary'), title: __('Batch Summary'),
subtitle: __('Progress of students in courses and assessments'), subtitle: __('Progress of students in courses and assessments'),
xAxis: { xAxis: {
key: 'product', key: 'product',
title: 'Product', title: 'Product',
type: 'category', type: 'category',
},
yAxis: {
title: __('Number of Students'),
},
swapXY: true,
series: [
{
name: 'sales',
type: 'bar',
}, },
yAxis: { ],
title: __('Number of Students'), }"
}, />
swapXY: true,
series: [
{
name: 'sales',
type: 'bar',
},
],
}"
/>
<div v-if="showProgressChart" class="mb-8"> <div v-if="showProgressChart" class="mb-8">
<div class="text-ink-gray-7 font-medium"> <div class="text-ink-gray-7 font-medium">
@@ -231,9 +233,10 @@
</div> </div>
<StudentModal <StudentModal
:batch="props.batch.name" :batch="props.batch.data.name"
v-model="showStudentModal" v-model="showStudentModal"
v-model:reloadStudents="students" v-model:reloadStudents="students"
v-model:batchModal="props.batch"
/> />
<BatchStudentProgress <BatchStudentProgress
:student="selectedStudent" :student="selectedStudent"
@@ -255,6 +258,7 @@ import {
ListView, ListView,
ListRowItem, ListRowItem,
NumberChart, NumberChart,
toast,
} from 'frappe-ui' } from 'frappe-ui'
import { import {
BookOpen, BookOpen,
@@ -266,7 +270,6 @@ import {
} from 'lucide-vue-next' } from 'lucide-vue-next'
import { ref, watch } from 'vue' import { ref, watch } from 'vue'
import StudentModal from '@/components/Modals/StudentModal.vue' import StudentModal from '@/components/Modals/StudentModal.vue'
import { showToast } from '@/utils'
import ProgressBar from '@/components/ProgressBar.vue' import ProgressBar from '@/components/ProgressBar.vue'
import BatchStudentProgress from '@/components/Modals/BatchStudentProgress.vue' import BatchStudentProgress from '@/components/Modals/BatchStudentProgress.vue'
import ApexChart from 'vue3-apexcharts' import ApexChart from 'vue3-apexcharts'
@@ -290,15 +293,15 @@ const props = defineProps({
const students = createResource({ const students = createResource({
url: 'lms.lms.utils.get_batch_students', url: 'lms.lms.utils.get_batch_students',
cache: ['students', props.batch.name],
params: { params: {
batch: props.batch?.name, batch: props.batch?.data?.name,
}, },
auto: true, auto: true,
onSuccess(data) { onSuccess(data) {
chartData.value = getChartData() chartData.value = getChartData()
showProgressChart.value = showProgressChart.value =
data.length && (props.batch?.courses?.length || assessmentCount.value) data.length &&
(props.batch?.data?.courses?.length || assessmentCount.value)
}, },
}) })
@@ -355,7 +358,8 @@ const removeStudents = (selections, unselectAll) => {
{ {
onSuccess(data) { onSuccess(data) {
students.reload() students.reload()
showToast(__('Success'), __('Students deleted successfully'), 'check') props.batch.reload()
toast.success(__('Students deleted successfully'))
unselectAll() unselectAll()
}, },
} }
@@ -366,10 +370,8 @@ const getChartData = () => {
let data = [] let data = []
console.log(students.data) console.log(students.data)
students.data.forEach(row => { students.data.forEach((row) => {
row.assessments.forEach(assessment => { row.assessments.forEach((assessment) => {})
})
}) })
/* let categories = {} /* let categories = {}
@@ -476,7 +478,7 @@ const certificationCount = createResource({
params: { params: {
doctype: 'LMS Certificate', doctype: 'LMS Certificate',
filters: { filters: {
batch_name: props.batch.name, batch_name: props.batch?.data?.name,
}, },
}, },
auto: true, auto: true,
+3 -3
View File
@@ -5,10 +5,11 @@
{{ label }} {{ label }}
</div> </div>
<Button @click="() => showCategoryForm()"> <Button @click="() => showCategoryForm()">
<template #icon> <template #prefix>
<Plus v-if="!showForm" class="h-3 w-3 stroke-1.5" /> <Plus v-if="!showForm" class="h-3 w-3 stroke-1.5" />
<X v-else class="h-3 w-3 stroke-1.5" /> <X v-else class="h-3 w-3 stroke-1.5" />
</template> </template>
{{ showForm ? __('Close') : __('New') }}
</Button> </Button>
</div> </div>
@@ -28,12 +29,11 @@
</div> </div>
<div class="overflow-y-scroll"> <div class="overflow-y-scroll">
<div class="text-base divide-y space-y-2"> <div class="text-base space-y-2">
<FormControl <FormControl
:value="cat.category" :value="cat.category"
type="text" type="text"
v-for="cat in categories.data" v-for="cat in categories.data"
class=""
@change.stop="(e) => update(cat.name, e.target.value)" @change.stop="(e) => update(cat.name, e.target.value)"
/> />
</div> </div>
@@ -92,7 +92,10 @@
{{ option.label }} {{ option.label }}
</div> </div>
<div <div
v-if="option.description" v-if="
option.description &&
option.description != option.label
"
class="text-xs text-ink-gray-7" class="text-xs text-ink-gray-7"
v-html="option.description" v-html="option.description"
></div> ></div>
+1 -1
View File
@@ -34,7 +34,7 @@
<Button <Button
variant="ghost" variant="ghost"
class="w-full !justify-start" class="w-full !justify-start"
label="Create New" :label="__('Create New')"
@click="attrs.onCreate(value, close)" @click="attrs.onCreate(value, close)"
> >
<template #prefix> <template #prefix>
@@ -4,78 +4,91 @@
{{ label }} {{ label }}
<span class="text-ink-red-3" v-if="required">*</span> <span class="text-ink-red-3" v-if="required">*</span>
</label> </label>
<div class="grid grid-cols-3 gap-2"> <div class="w-full">
<Button <Combobox v-model="selectedValue" nullable>
ref="emails" <Popover class="w-full" v-model:show="showOptions">
v-for="value in values" <template #target="{ togglePopover }">
:key="value" <ComboboxInput
:label="value" ref="search"
theme="gray" class="search-input form-input w-full focus-visible:!ring-0"
variant="subtle" type="text"
class="rounded-md word-break-all" :value="query"
@keydown.delete.capture.stop="removeLastValue" @change="
> (e) => {
<template #suffix> query = e.target.value
<X @click="removeValue(value)" class="h-4 w-4 stroke-1.5" /> showOptions = true
</template> }
</Button> "
<div class=""> autocomplete="off"
<Combobox v-model="selectedValue" nullable> @focus="() => togglePopover()"
<Popover class="w-full" v-model:show="showOptions"> @keydown.delete.capture.stop="removeLastValue"
<template #target="{ togglePopover }"> />
<ComboboxInput </template>
ref="search" <template #body="{ isOpen, close }">
class="search-input form-input w-full focus-visible:!ring-0" <div v-show="isOpen">
type="text" <div
:value="query" class="mt-1 rounded-lg bg-surface-white py-1 text-base border-2"
@change=" >
(e) => { <ComboboxOptions
query = e.target.value class="my-1 min-h-[6rem] max-h-[12rem] overflow-y-auto px-1.5"
showOptions = true static
}
"
autocomplete="off"
@focus="() => togglePopover()"
@keydown.delete.capture.stop="removeLastValue"
/>
</template>
<template #body="{ isOpen }">
<div v-show="isOpen">
<div
class="mt-1 rounded-lg bg-surface-white py-1 text-base border-2"
> >
<ComboboxOptions <ComboboxOption
class="my-1 max-h-[12rem] overflow-y-auto px-1.5" v-for="option in options"
static :key="option.value"
:value="option"
v-slot="{ active }"
> >
<ComboboxOption <li
v-for="option in options" :class="[
:key="option.value" 'flex cursor-pointer items-center rounded px-2 py-1 text-base',
:value="option" { 'bg-surface-gray-2': active },
v-slot="{ active }" ]"
> >
<li <div class="flex flex-col gap-1 p-1">
:class="[ <div class="text-base font-medium text-ink-gray-8">
'flex cursor-pointer items-center rounded px-2 py-1 text-base', {{ option.description }}
{ 'bg-surface-gray-2': active },
]"
>
<div class="flex flex-col gap-1 p-1">
<div class="text-base font-medium text-ink-gray-8">
{{ option.description }}
</div>
<div class="text-sm text-ink-gray-5">
{{ option.value }}
</div>
</div> </div>
</li> <div class="text-sm text-ink-gray-5">
</ComboboxOption> {{ option.value }}
</ComboboxOptions> </div>
</div> </div>
</li>
</ComboboxOption>
<div
v-if="attrs.onCreate"
class="absolute bottom-2 left-1 w-[98%] pt-2 bg-white border-t"
>
<Button
variant="ghost"
class="w-full !justify-start"
:label="__('Create New')"
@click="attrs.onCreate(close)"
>
<template #prefix>
<Plus class="h-4 w-4 stroke-1.5" />
</template>
</Button>
</div>
</ComboboxOptions>
</div> </div>
</template> </div>
</Popover> </template>
</Combobox> </Popover>
</Combobox>
</div>
<div v-if="values.length" class="grid grid-cols-2 gap-2 mt-1">
<div
v-for="value in values"
class="flex items-center justify-between break-all bg-surface-gray-2 text-ink-gray-7 word-wrap p-2 rounded-md mr-2"
>
<span class="break-all">
{{ value }}
</span>
<X
class="size-4 stroke-1.5 cursor-pointer"
@click="removeValue(value)"
/>
</div> </div>
</div> </div>
<!-- <ErrorMessage class="mt-2 pl-2" v-if="error" :message="error" /> --> <!-- <ErrorMessage class="mt-2 pl-2" v-if="error" :message="error" /> -->
@@ -90,9 +103,9 @@ import {
ComboboxOption, ComboboxOption,
} from '@headlessui/vue' } from '@headlessui/vue'
import { createResource, Popover, Button } from 'frappe-ui' import { createResource, Popover, Button } from 'frappe-ui'
import { ref, computed, nextTick } from 'vue' import { ref, computed, nextTick, useAttrs } from 'vue'
import { watchDebounced } from '@vueuse/core' import { watchDebounced } from '@vueuse/core'
import { X } from 'lucide-vue-next' import { X, Plus } from 'lucide-vue-next'
const props = defineProps({ const props = defineProps({
label: { label: {
@@ -124,7 +137,7 @@ const props = defineProps({
}) })
const values = defineModel() const values = defineModel()
const attrs = useAttrs()
const emails = ref([]) const emails = ref([])
const search = ref(null) const search = ref(null)
const error = ref(null) const error = ref(null)
+4 -12
View File
@@ -146,8 +146,8 @@
<script setup> <script setup>
import { BookOpen, Users, Star, GraduationCap } from 'lucide-vue-next' import { BookOpen, Users, Star, GraduationCap } from 'lucide-vue-next'
import { computed, inject } from 'vue' import { computed, inject } from 'vue'
import { Badge, Button, createResource } from 'frappe-ui' import { Badge, Button, createResource, toast } from 'frappe-ui'
import { showToast, formatAmount } from '@/utils/' import { formatAmount } from '@/utils/'
import { capture } from '@/telemetry' import { capture } from '@/telemetry'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import CertificationLinks from '@/components/CertificationLinks.vue' import CertificationLinks from '@/components/CertificationLinks.vue'
@@ -172,11 +172,7 @@ const video_link = computed(() => {
function enrollStudent() { function enrollStudent() {
if (!user.data) { if (!user.data) {
showToast( toast.success(__('You need to login first to enroll for this course'))
__('Please Login'),
__('You need to login first to enroll for this course'),
'alert-circle'
)
setTimeout(() => { setTimeout(() => {
window.location.href = `/login?redirect-to=${window.location.pathname}` window.location.href = `/login?redirect-to=${window.location.pathname}`
}, 1000) }, 1000)
@@ -192,11 +188,7 @@ function enrollStudent() {
capture('enrolled_in_course', { capture('enrolled_in_course', {
course: props.course.data.name, course: props.course.data.name,
}) })
showToast( toast.success(__('You have been enrolled in this course'))
__('Success'),
__('You have been enrolled in this course'),
'check'
)
setTimeout(() => { setTimeout(() => {
router.push({ router.push({
name: 'Lesson', name: 'Lesson',
+5 -10
View File
@@ -147,7 +147,7 @@
/> />
</template> </template>
<script setup> <script setup>
import { Button, createResource, Tooltip } from 'frappe-ui' import { Button, createResource, Tooltip, toast } from 'frappe-ui'
import { getCurrentInstance, inject, ref } from 'vue' import { getCurrentInstance, inject, ref } from 'vue'
import Draggable from 'vuedraggable' import Draggable from 'vuedraggable'
import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/vue' import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/vue'
@@ -162,7 +162,6 @@ import {
} from 'lucide-vue-next' } from 'lucide-vue-next'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import ChapterModal from '@/components/Modals/ChapterModal.vue' import ChapterModal from '@/components/Modals/ChapterModal.vue'
import { showToast } from '@/utils'
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
@@ -215,7 +214,7 @@ const deleteLesson = createResource({
}, },
onSuccess() { onSuccess() {
outline.reload() outline.reload()
showToast('Success', 'Lesson deleted successfully', 'check') toast.success(__('Lesson deleted successfully'))
}, },
}) })
@@ -230,7 +229,7 @@ const updateLessonIndex = createResource({
} }
}, },
onSuccess() { onSuccess() {
showToast('Success', 'Lesson moved successfully', 'check') toast.success(__('Lesson moved successfully'))
}, },
}) })
@@ -288,7 +287,7 @@ const deleteChapter = createResource({
}, },
onSuccess() { onSuccess() {
outline.reload() outline.reload()
showToast('Success', 'Chapter deleted successfully', 'check') toast.success(__('Chapter deleted successfully'))
}, },
}) })
@@ -317,11 +316,7 @@ const redirectToChapter = (chapter) => {
event.preventDefault() event.preventDefault()
if (props.allowEdit) return if (props.allowEdit) return
if (!user.data) { if (!user.data) {
showToast( toast.success(__('Please enroll for this course to view this lesson'))
__('You are not enrolled'),
__('Please enroll for this course to view this lesson'),
'alert-circle'
)
return return
} }
+2 -10
View File
@@ -93,12 +93,11 @@
</div> </div>
</template> </template>
<script setup> <script setup>
import { createResource, TextEditor, Button, Dropdown } from 'frappe-ui' import { createResource, TextEditor, Button, Dropdown, toast } from 'frappe-ui'
import { timeAgo } from '../utils' import { timeAgo } from '../utils'
import UserAvatar from '@/components/UserAvatar.vue' import UserAvatar from '@/components/UserAvatar.vue'
import { ChevronLeft, MoreHorizontal } from 'lucide-vue-next' import { ChevronLeft, MoreHorizontal } from 'lucide-vue-next'
import { ref, inject, onMounted } from 'vue' import { ref, inject, onMounted } from 'vue'
import { createToast } from '../utils'
const showTopics = defineModel('showTopics') const showTopics = defineModel('showTopics')
const newReply = ref('') const newReply = ref('')
@@ -192,14 +191,7 @@ const postReply = () => {
replies.reload() replies.reload()
}, },
onError(err) { onError(err) {
createToast({ toast.error(err.messages?.[0] || err)
title: 'Error',
text: err.messages?.[0] || err,
icon: 'x',
iconClasses: 'bg-surface-red-5 text-ink-white rounded-md p-px',
position: 'top-center',
timeout: 10,
})
}, },
} }
) )
+24
View File
@@ -0,0 +1,24 @@
<template>
<div class="flex flex-col items-center justify-center mt-60">
<GraduationCap class="size-10 mx-auto stroke-1 text-ink-gray-5" />
<div class="text-lg font-semibold text-ink-gray-7 mb-2.5">
{{ __('No {0}').format(type?.toLowerCase()) }}
</div>
<div
class="leading-5 text-base w-2/5 text-base text-center text-ink-gray-7"
>
{{
__(
'There are no {0} currently. Keep an eye out, fresh learning experiences are on the way!'
).format(type?.toLowerCase())
}}
</div>
</div>
</template>
<script setup lang="ts">
import { BookOpen, GraduationCap } from 'lucide-vue-next'
const props = defineProps({
type: String,
})
</script>
+4 -3
View File
@@ -17,10 +17,11 @@
:debounce="300" :debounce="300"
/> />
<Button @click="() => (showForm = !showForm)"> <Button @click="() => (showForm = !showForm)">
<template #icon> <template #prefix>
<Plus v-if="!showForm" class="h-3 w-3 stroke-1.5" /> <Plus v-if="!showForm" class="size-4 stroke-1.5" />
<X v-else class="h-3 w-3 stroke-1.5" /> <X v-else class="size-4 stroke-1.5" />
</template> </template>
{{ showForm ? __('Close') : __('New') }}
</Button> </Button>
</div> </div>
</div> </div>
+4 -3
View File
@@ -17,10 +17,11 @@
:debounce="300" :debounce="300"
/> />
<Button @click="() => (showForm = !showForm)"> <Button @click="() => (showForm = !showForm)">
<template #icon> <template #prefix>
<Plus v-if="!showForm" class="h-3 w-3 stroke-1.5" /> <Plus v-if="!showForm" class="size-4 stroke-1.5" />
<X v-else class="h-3 w-3 stroke-1.5" /> <X v-else class="size-4 stroke-1.5" />
</template> </template>
{{ showForm ? __('Close') : __('New') }}
</Button> </Button>
</div> </div>
</div> </div>
@@ -31,6 +31,7 @@
<div class="mb-4"> <div class="mb-4">
<div class="mb-1.5 text-sm text-ink-gray-5"> <div class="mb-1.5 text-sm text-ink-gray-5">
{{ __('Announcement') }} {{ __('Announcement') }}
<span class="text-ink-red-3">*</span>
</div> </div>
<TextEditor <TextEditor
:fixedMenu="true" :fixedMenu="true"
@@ -43,9 +44,8 @@
</Dialog> </Dialog>
</template> </template>
<script setup> <script setup>
import { Dialog, Input, TextEditor, createResource } from 'frappe-ui' import { Dialog, Input, TextEditor, createResource, toast } from 'frappe-ui'
import { reactive } from 'vue' import { reactive } from 'vue'
import { showToast } from '@/utils/'
const show = defineModel() const show = defineModel()
@@ -87,22 +87,21 @@ const makeAnnouncement = (close) => {
{ {
validate() { validate() {
if (!props.students.length) { if (!props.students.length) {
return 'No students in this batch' return __('No students in this batch')
} }
if (!announcement.subject) { if (!announcement.subject) {
return 'Subject is required' return __('Subject is required')
}
if (!announcement.announcement) {
return __('Announcement is required')
} }
}, },
onSuccess() { onSuccess() {
close() close()
showToast( toast.success(__('Announcement has been sent successfully'))
__('Success'),
__('Announcement has been sent successfully'),
'check'
)
}, },
onError(err) { onError(err) {
showToast(__('Error'), __(err.messages?.[0] || err), 'alert-circle') toast.error(__(err.messages?.[0] || err))
}, },
} }
) )
@@ -25,21 +25,39 @@
v-model="assessment" v-model="assessment"
:doctype="assessmentType" :doctype="assessmentType"
:label="__('Assessment')" :label="__('Assessment')"
:onCreate="
(value, close) => {
close()
if (assessmentType === 'LMS Quiz') {
router.push({
name: 'QuizForm',
params: {
quizID: 'new',
},
})
} else if (assessmentType === 'LMS Assignment') {
router.push({
name: 'Assignments',
})
}
}
"
/> />
</div> </div>
</template> </template>
</Dialog> </Dialog>
</template> </template>
<script setup> <script setup>
import { Dialog, FormControl, createResource } from 'frappe-ui' import { Dialog, FormControl, createResource, toast } from 'frappe-ui'
import Link from '@/components/Controls/Link.vue' import Link from '@/components/Controls/Link.vue'
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import { showToast } from '@/utils' import { useRouter } from 'vue-router'
const show = defineModel() const show = defineModel()
const assessmentType = ref(null) const assessmentType = ref(null)
const assessment = ref(null) const assessment = ref(null)
const assessments = defineModel('assessments') const assessments = defineModel('assessments')
const router = useRouter()
const props = defineProps({ const props = defineProps({
batch: { batch: {
@@ -70,7 +88,7 @@ const addAssessment = (close) => {
{ {
onSuccess(data) { onSuccess(data) {
assessments.value.reload() assessments.value.reload()
showToast(__('Success'), __('Assessment added successfully'), 'check') toast.success(__('Assessment added successfully'))
close() close()
}, },
} }
@@ -37,7 +37,7 @@
@change="(val) => (assignment.question = val)" @change="(val) => (assignment.question = val)"
:editable="true" :editable="true"
:fixedMenu="true" :fixedMenu="true"
editorClass="prose-sm max-w-none border-b border-x bg-surface-gray-2 rounded-b-md py-1 px-2 min-h-[7rem]" editorClass="prose-sm max-w-none border-b border-x bg-surface-gray-2 rounded-b-md py-1 px-2 min-h-[7rem] max-h-[18rem] overflow-y-auto"
/> />
</div> </div>
</div> </div>
@@ -64,9 +64,8 @@
</Dialog> </Dialog>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { Button, Dialog, FormControl, TextEditor } from 'frappe-ui' import { Button, Dialog, FormControl, TextEditor, toast } from 'frappe-ui'
import { computed, reactive, watch } from 'vue' import { computed, reactive, watch } from 'vue'
import { showToast } from '@/utils'
const show = defineModel() const show = defineModel()
const assignments = defineModel<Assignments>('assignments') const assignments = defineModel<Assignments>('assignments')
@@ -123,11 +122,7 @@ const saveAssignment = () => {
{ {
onSuccess() { onSuccess() {
show.value = false show.value = false
showToast( toast.success(__('Assignment created successfully'))
__('Success'),
__('Assignment created successfully'),
'check'
)
}, },
} }
) )
@@ -140,11 +135,7 @@ const saveAssignment = () => {
{ {
onSuccess() { onSuccess() {
show.value = false show.value = false
showToast( toast.success(__('Assignment updated successfully'))
__('Success'),
__('Assignment updated successfully'),
'check'
)
}, },
} }
) )
@@ -19,32 +19,43 @@
v-model="course" v-model="course"
:label="__('Course')" :label="__('Course')"
:required="true" :required="true"
:onCreate="
(value, close) => {
close()
router.push({
name: 'CourseForm',
params: {
courseName: 'new',
},
})
}
"
/> />
<Link <Link
doctype="Course Evaluator" doctype="Course Evaluator"
v-model="evaluator" v-model="evaluator"
:label="__('Evaluator')" :label="__('Evaluator')"
:onCreate="(value, close) => openSettings(close)" :onCreate="(value, close) => openSettings('Evaluators', close)"
class="mt-4" class="mt-4"
/> />
</template> </template>
</Dialog> </Dialog>
</template> </template>
<script setup> <script setup>
import { Dialog, createResource } from 'frappe-ui' import { Dialog, createResource, toast } from 'frappe-ui'
import { ref, inject } from 'vue' import { ref, inject } from 'vue'
import Link from '@/components/Controls/Link.vue' import Link from '@/components/Controls/Link.vue'
import { showToast } from '@/utils'
import { useOnboarding } from 'frappe-ui/frappe' import { useOnboarding } from 'frappe-ui/frappe'
import { useSettings } from '@/stores/settings' import { openSettings } from '@/utils'
import { useRouter } from 'vue-router'
const show = defineModel() const show = defineModel()
const course = ref(null) const course = ref(null)
const evaluator = ref(null) const evaluator = ref(null)
const user = inject('$user') const user = inject('$user')
const courses = defineModel('courses') const courses = defineModel('courses')
const router = useRouter()
const { updateOnboardingStep } = useOnboarding('learning') const { updateOnboardingStep } = useOnboarding('learning')
const settingsStore = useSettings()
const props = defineProps({ const props = defineProps({
batch: { batch: {
@@ -83,15 +94,9 @@ const addCourse = (close) => {
evaluator.value = null evaluator.value = null
}, },
onError(err) { onError(err) {
showToast('Error', err.message[0] || err, 'x') toast.error(err.messages?.[0] || err)
}, },
} }
) )
} }
const openSettings = (close) => {
close()
settingsStore.activeTab = 'Evaluators'
settingsStore.isSettingsOpen = true
}
</script> </script>
@@ -14,7 +14,13 @@
<div class="text-xl font-semibold"> <div class="text-xl font-semibold">
{{ student.full_name }} {{ student.full_name }}
</div> </div>
<Badge :theme="student.progress === 100 ? 'green' : 'red'"> <Badge
v-if="
Object.keys(student.assessments).length ||
Object.keys(student.courses).length
"
:theme="student.progress === 100 ? 'green' : 'red'"
>
{{ student.progress }}% {{ __('Complete') }} {{ student.progress }}% {{ __('Complete') }}
</Badge> </Badge>
</div> </div>
@@ -26,7 +32,10 @@
<div class="space-y-8"> <div class="space-y-8">
<!-- Assessments --> <!-- Assessments -->
<div class="space-y-2 text-sm"> <div
v-if="Object.keys(student.assessments).length"
class="space-y-2 text-sm"
>
<div class="flex items-center border-b pb-1 font-medium"> <div class="flex items-center border-b pb-1 font-medium">
<span class="flex-1"> <span class="flex-1">
{{ __('Assessment') }} {{ __('Assessment') }}
@@ -73,7 +82,10 @@
</div> </div>
<!-- Courses --> <!-- Courses -->
<div class="space-y-2 text-sm"> <div
v-if="Object.keys(student.courses).length"
class="space-y-2 text-sm"
>
<div class="flex items-center border-b pb-1 font-medium"> <div class="flex items-center border-b pb-1 font-medium">
<span class="flex-1"> <span class="flex-1">
{{ __('Courses') }} {{ __('Courses') }}
@@ -62,9 +62,8 @@
</template> </template>
<script setup> <script setup>
import { inject, reactive } from 'vue' import { inject, reactive } from 'vue'
import { createResource, Dialog, FormControl, Switch } from 'frappe-ui' import { createResource, Dialog, FormControl, Switch, toast } from 'frappe-ui'
import Link from '@/components/Controls/Link.vue' import Link from '@/components/Controls/Link.vue'
import { showToast } from '@/utils'
const show = defineModel() const show = defineModel()
const dayjs = inject('$dayjs') const dayjs = inject('$dayjs')
@@ -112,13 +111,13 @@ const generateCertificates = (close) => {
}, },
{ {
onError(err) { onError(err) {
showToast(__('Error'), err.messages?.[0] || err, 'x') toast.error(err.messages?.[0] || err)
}, },
} }
) )
}) })
close() close()
showToast(__('Success'), __('Certificates generated successfully'), 'check') toast.success(__('Certificates generated successfully'))
} }
const getCourses = () => { const getCourses = () => {
@@ -76,9 +76,10 @@ import {
FileUploader, FileUploader,
FormControl, FormControl,
Switch, Switch,
toast,
} from 'frappe-ui' } from 'frappe-ui'
import { reactive, watch, inject } from 'vue' import { reactive, watch, inject } from 'vue'
import { showToast, getFileSize } from '@/utils/' import { getFileSize } from '@/utils/'
import { capture } from '@/telemetry' import { capture } from '@/telemetry'
import { FileText, X } from 'lucide-vue-next' import { FileText, X } from 'lucide-vue-next'
import { useOnboarding } from 'frappe-ui/frappe' import { useOnboarding } from 'frappe-ui/frappe'
@@ -150,21 +151,17 @@ const addChapter = async (close) => {
onSuccess(data) { onSuccess(data) {
cleanChapter() cleanChapter()
outline.value.reload() outline.value.reload()
showToast( toast.success(__('Chapter added successfully'))
__('Success'),
__('Chapter added successfully'),
'check'
)
}, },
onError(err) { onError(err) {
showToast(__('Error'), err.messages?.[0] || err, 'x') toast.error(err.messages?.[0] || err)
}, },
} }
) )
close() close()
}, },
onError(err) { onError(err) {
showToast(__('Error'), err.messages?.[0] || err, 'x') toast.error(err.messages?.[0] || err)
}, },
} }
) )
@@ -196,11 +193,11 @@ const editChapter = (close) => {
}, },
onSuccess() { onSuccess() {
outline.value.reload() outline.value.reload()
showToast(__('Success'), __('Chapter updated successfully'), 'check') toast.success(__('Chapter updated successfully'))
close() close()
}, },
onError(err) { onError(err) {
showToast(__('Error'), err.messages?.[0] || err, 'x') toast.error(err.messages?.[0] || err)
}, },
} }
) )
@@ -34,9 +34,15 @@
</Dialog> </Dialog>
</template> </template>
<script setup> <script setup>
import { Dialog, FormControl, TextEditor, createResource } from 'frappe-ui' import {
Dialog,
FormControl,
TextEditor,
createResource,
toast,
} from 'frappe-ui'
import { reactive } from 'vue' import { reactive } from 'vue'
import { showToast, singularize } from '@/utils' import { singularize } from '@/utils'
const topics = defineModel('reloadTopics') const topics = defineModel('reloadTopics')
@@ -115,7 +121,7 @@ const submitTopic = (close) => {
) )
}, },
onError(err) { onError(err) {
showToast('Error', err.message, 'x') toast.error(err.messages?.[0] || err)
}, },
} }
) )
@@ -93,10 +93,11 @@ import {
Button, Button,
createResource, createResource,
TextEditor, TextEditor,
toast,
} from 'frappe-ui' } from 'frappe-ui'
import { reactive, watch } from 'vue' import { reactive, watch } from 'vue'
import { FileText, X } from 'lucide-vue-next' import { FileText, X } from 'lucide-vue-next'
import { getFileSize, showToast, escapeHTML } from '@/utils' import { getFileSize, escapeHTML } from '@/utils'
const reloadProfile = defineModel('reloadProfile') const reloadProfile = defineModel('reloadProfile')
@@ -155,7 +156,7 @@ const saveProfile = (close) => {
reloadProfile.value.reload() reloadProfile.value.reload()
}, },
onError(err) { onError(err) {
showToast('Error', err.messages?.[0] || err, 'x') toast.error(err.messages?.[0] || err)
}, },
} }
) )
@@ -68,7 +68,7 @@
<script setup> <script setup>
import { Dialog, createResource, Select, FormControl } from 'frappe-ui' import { Dialog, createResource, Select, FormControl } from 'frappe-ui'
import { reactive, watch, inject } from 'vue' import { reactive, watch, inject } from 'vue'
import { createToast, formatTime } from '@/utils/' import { formatTime } from '@/utils/'
const user = inject('$user') const user = inject('$user')
const dayjs = inject('$dayjs') const dayjs = inject('$dayjs')
@@ -148,14 +148,7 @@ function submitEvaluation(close) {
unavailabilityMessage = false unavailabilityMessage = false
} }
createToast({ toast.warning(__('Evaluator is unavailable'))
title: unavailabilityMessage ? __('Evaluator is Unavailable') : '',
text: message,
icon: unavailabilityMessage ? 'alert-circle' : 'x',
iconClasses: 'bg-yellow-600 text-ink-white rounded-md p-px',
position: 'top-center',
timeout: 10,
})
}, },
}) })
} }
+4 -3
View File
@@ -144,6 +144,7 @@ import {
Tabs, Tabs,
Tooltip, Tooltip,
Textarea, Textarea,
toast,
} from 'frappe-ui' } from 'frappe-ui'
import { import {
User, User,
@@ -157,7 +158,7 @@ import {
ClipboardList, ClipboardList,
} from 'lucide-vue-next' } from 'lucide-vue-next'
import { inject, reactive, watch, ref, computed } from 'vue' import { inject, reactive, watch, ref, computed } from 'vue'
import { formatTime, showToast } from '@/utils' import { formatTime } from '@/utils'
import Rating from '@/components/Controls/Rating.vue' import Rating from '@/components/Controls/Rating.vue'
import Link from '@/components/Controls/Link.vue' import Link from '@/components/Controls/Link.vue'
@@ -252,7 +253,7 @@ const saveEvaluation = () => {
} else { } else {
show.value = false show.value = false
} }
showToast(__('Success'), __('Evaluation saved successfully'), 'check') toast.success(__('Evaluation saved successfully'))
}, },
} }
) )
@@ -307,7 +308,7 @@ const saveCertificate = () => {
{}, {},
{ {
onSuccess: () => { onSuccess: () => {
showToast(__('Success'), __('Certificate saved successfully'), 'check') toast.success(__('Certificate saved successfully'))
}, },
} }
) )
@@ -64,10 +64,10 @@
</Dialog> </Dialog>
</template> </template>
<script setup> <script setup>
import { Dialog, FileUploader, Button, createResource } from 'frappe-ui' import { Dialog, FileUploader, Button, createResource, toast } from 'frappe-ui'
import { FileText } from 'lucide-vue-next' import { FileText } from 'lucide-vue-next'
import { ref, inject } from 'vue' import { ref, inject } from 'vue'
import { createToast, getFileSize } from '@/utils/' import { getFileSize } from '@/utils/'
const resume = ref(null) const resume = ref(null)
const show = defineModel() const show = defineModel()
@@ -112,24 +112,12 @@ const submitResume = (close) => {
} }
}, },
onSuccess() { onSuccess() {
createToast({ toast.success('Your application has been submitted successfully')
title: 'Success',
text: 'Your application has been submitted',
icon: 'check',
iconClasses: 'bg-surface-green-3 text-ink-white rounded-md p-px',
})
application.value.reload() application.value.reload()
close() close()
}, },
onError(err) { onError(err) {
createToast({ toast.error(err.messages?.[0] || err)
title: 'Error',
text: err.messages?.[0] || err,
icon: 'x',
iconClasses: 'bg-surface-red-5 text-ink-white rounded-md p-px',
position: 'top-center',
timeout: 10,
})
}, },
} }
) )
@@ -94,9 +94,10 @@ import {
Tooltip, Tooltip,
FormControl, FormControl,
Autocomplete, Autocomplete,
toast,
} from 'frappe-ui' } from 'frappe-ui'
import { reactive, inject, onMounted } from 'vue' import { reactive, inject, onMounted } from 'vue'
import { getTimezones, createToast, getUserTimezone } from '@/utils/' import { getTimezones, getUserTimezone } from '@/utils/'
const liveClasses = defineModel('reloadLiveClasses') const liveClasses = defineModel('reloadLiveClasses')
const show = defineModel() const show = defineModel()
@@ -202,14 +203,7 @@ const submitLiveClass = (close) => {
close() close()
}, },
onError(err) { onError(err) {
createToast({ toast.error(err.messages?.[0] || err)
title: 'Error',
text: err.messages?.[0] || err,
icon: 'x',
iconClasses: 'bg-surface-red-5 text-ink-white rounded-md p-px',
position: 'top-center',
timeout: 10,
})
}, },
}) })
} }
+3 -4
View File
@@ -30,11 +30,10 @@
</Dialog> </Dialog>
</template> </template>
<script setup> <script setup>
import { Dialog, createResource } from 'frappe-ui' import { Dialog, createResource, toast } from 'frappe-ui'
import Link from '@/components/Controls/Link.vue' import Link from '@/components/Controls/Link.vue'
import { reactive, watch } from 'vue' import { reactive, watch } from 'vue'
import IconPicker from '@/components/Controls/IconPicker.vue' import IconPicker from '@/components/Controls/IconPicker.vue'
import { showToast } from '@/utils'
const sidebar = defineModel('reloadSidebar') const sidebar = defineModel('reloadSidebar')
const show = defineModel() const show = defineModel()
@@ -78,10 +77,10 @@ const addWebPage = (close) => {
onSuccess() { onSuccess() {
sidebar.value.reload() sidebar.value.reload()
close() close()
showToast('Success', 'Web page added to sidebar', 'check') toast.success(__('Web page added to sidebar'))
}, },
onError(err) { onError(err) {
showToast('Error', err.message[0] || err, 'x') toast.error(err.message[0] || err)
close() close()
}, },
} }
+6 -10
View File
@@ -121,10 +121,10 @@ import {
createResource, createResource,
Switch, Switch,
Button, Button,
toast,
} from 'frappe-ui' } from 'frappe-ui'
import { computed, watch, reactive, ref, inject } from 'vue' import { computed, watch, reactive, ref, inject } from 'vue'
import Link from '@/components/Controls/Link.vue' import Link from '@/components/Controls/Link.vue'
import { showToast } from '@/utils'
import { useOnboarding } from 'frappe-ui/frappe' import { useOnboarding } from 'frappe-ui/frappe'
const show = defineModel() const show = defineModel()
@@ -260,7 +260,7 @@ const addQuestion = () => {
}) })
}, },
onError(err) { onError(err) {
showToast(__('Error'), __(err.messages?.[0] || err), 'x') toast.error(err.messages?.[0] || err)
}, },
} }
) )
@@ -278,12 +278,12 @@ const addQuestionRow = (question) => {
updateOnboardingStep('create_first_quiz') updateOnboardingStep('create_first_quiz')
show.value = false show.value = false
showToast(__('Success'), __('Question added successfully'), 'check') toast.success(__('Question added successfully'))
quiz.value.reload() quiz.value.reload()
show.value = false show.value = false
}, },
onError(err) { onError(err) {
showToast(__('Error'), __(err.messages?.[0] || err), 'x') toast.error(err.messages?.[0] || err)
show.value = false show.value = false
}, },
} }
@@ -328,18 +328,14 @@ const updateQuestion = () => {
{ {
onSuccess() { onSuccess() {
show.value = false show.value = false
showToast( toast.success(__('Question updated successfully'))
__('Success'),
__('Question updated successfully'),
'check'
)
quiz.value.reload() quiz.value.reload()
}, },
} }
) )
}, },
onError(err) { onError(err) {
showToast(__('Error'), __(err.messages?.[0] || err), 'x') toast.error(err.messages?.[0] || err)
}, },
} }
) )
@@ -32,10 +32,9 @@
</Dialog> </Dialog>
</template> </template>
<script setup> <script setup>
import { Dialog, Textarea, createResource } from 'frappe-ui' import { Dialog, Textarea, createResource, toast } from 'frappe-ui'
import { reactive } from 'vue' import { reactive } from 'vue'
import Rating from '@/components/Controls/Rating.vue' import Rating from '@/components/Controls/Rating.vue'
import { createToast } from '@/utils/'
const show = defineModel() const show = defineModel()
const reviews = defineModel('reloadReviews') const reviews = defineModel('reloadReviews')
@@ -78,11 +77,7 @@ function submitReview(close) {
hasReviewed.value.reload() hasReviewed.value.reload()
}, },
onError(err) { onError(err) {
createToast({ toast.error(err.messages?.[0] || err)
text: err.messages?.[0] || err,
icon: 'x',
iconClasses: 'text-ink-red-4 bg-surface-red-4',
})
}, },
}) })
close() close()
+1 -1
View File
@@ -1,5 +1,5 @@
<template> <template>
<Dialog v-model="show" :options="{ size: '4xl' }"> <Dialog v-model="show" :options="{ size: '5xl' }">
<template #body> <template #body>
<div class="flex h-[calc(100vh_-_8rem)]"> <div class="flex h-[calc(100vh_-_8rem)]">
<div class="flex w-52 shrink-0 flex-col bg-surface-gray-2 p-2"> <div class="flex w-52 shrink-0 flex-col bg-surface-gray-2 p-2">
@@ -19,19 +19,25 @@
doctype="User" doctype="User"
v-model="student" v-model="student"
:filters="{ ignore_user_type: 1 }" :filters="{ ignore_user_type: 1 }"
:onCreate="
(value, close) => {
openSettings('Members', close)
}
"
/> />
</div> </div>
</template> </template>
</Dialog> </Dialog>
</template> </template>
<script setup> <script setup>
import { Dialog, createResource } from 'frappe-ui' import { Dialog, createResource, toast } from 'frappe-ui'
import { ref, inject } from 'vue' import { ref, inject } from 'vue'
import Link from '@/components/Controls/Link.vue' import Link from '@/components/Controls/Link.vue'
import { showToast } from '@/utils'
import { useOnboarding } from 'frappe-ui/frappe' import { useOnboarding } from 'frappe-ui/frappe'
import { openSettings } from '@/utils'
const students = defineModel('reloadStudents') const students = defineModel('reloadStudents')
const batchModal = defineModel('batchModal')
const student = ref() const student = ref()
const user = inject('$user') const user = inject('$user')
const { updateOnboardingStep } = useOnboarding('learning') const { updateOnboardingStep } = useOnboarding('learning')
@@ -66,11 +72,12 @@ const addStudent = (close) => {
updateOnboardingStep('add_batch_student') updateOnboardingStep('add_batch_student')
students.value.reload() students.value.reload()
batchModal.value.reload()
student.value = null student.value = null
close() close()
}, },
onError(err) { onError(err) {
showToast(__('Error'), __(err.messages?.[0] || err), 'x') toast.error(err.messages?.[0] || err)
}, },
} }
) )
+3 -8
View File
@@ -291,9 +291,9 @@ import {
ListView, ListView,
TextEditor, TextEditor,
FormControl, FormControl,
toast,
} from 'frappe-ui' } from 'frappe-ui'
import { ref, watch, reactive, inject, computed } from 'vue' import { ref, watch, reactive, inject, computed } from 'vue'
import { createToast, showToast } from '@/utils/'
import { CheckCircle, XCircle, MinusCircle } from 'lucide-vue-next' import { CheckCircle, XCircle, MinusCircle } from 'lucide-vue-next'
import { timeAgo } from '@/utils' import { timeAgo } from '@/utils'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
@@ -494,12 +494,7 @@ const getAnswers = () => {
const checkAnswer = () => { const checkAnswer = () => {
let answers = getAnswers() let answers = getAnswers()
if (!answers.length) { if (!answers.length) {
createToast({ toast.warning(__('Please select an option'))
title: 'Please select an option',
icon: 'alert-circle',
iconClasses: 'text-yellow-600 bg-yellow-100 rounded-full',
position: 'top-center',
})
return return
} }
@@ -589,7 +584,7 @@ const createSubmission = () => {
const errorTitle = err?.message || '' const errorTitle = err?.message || ''
if (errorTitle.includes('MaximumAttemptsExceededError')) { if (errorTitle.includes('MaximumAttemptsExceededError')) {
const errorMessage = err.messages?.[0] || err const errorMessage = err.messages?.[0] || err
showToast(__('Error'), __(errorMessage), 'x') toast.error(__(errorMessage))
setTimeout(() => { setTimeout(() => {
window.location.reload() window.location.reload()
}, 3000) }, 3000)
+2 -3
View File
@@ -27,9 +27,8 @@
</template> </template>
<script setup> <script setup>
import { Button, Badge } from 'frappe-ui' import { Button, Badge, toast } from 'frappe-ui'
import SettingFields from '@/components/SettingFields.vue' import SettingFields from '@/components/SettingFields.vue'
import { showToast } from '@/utils'
const props = defineProps({ const props = defineProps({
fields: { fields: {
@@ -61,7 +60,7 @@ const update = () => {
{}, {},
{ {
onError(err) { onError(err) {
showToast(__('Error'), err.messages?.[0] || err, 'x') toast.error(err.messages?.[0] || err)
}, },
} }
) )
+36 -26
View File
@@ -21,14 +21,28 @@
</header> </header>
<div class="md:w-3/4 md:mx-auto py-5 mx-5"> <div class="md:w-3/4 md:mx-auto py-5 mx-5">
<div class="grid grid-cols-3 gap-5 mb-5"> <div class="flex items-center justify-between mb-5">
<FormControl v-model="titleFilter" :placeholder="__('Search by title')" /> <div
<FormControl v-if="assignmentCount"
v-model="typeFilter" class="text-xl font-semibold text-ink-gray-7 mb-4"
type="select" >
:options="assignmentTypes" {{ __('{0} Assignments').format(assignmentCount) }}
:placeholder="__('Type')" </div>
/> <div
v-if="assignments.data?.length || assigmentCount > 0"
class="grid grid-cols-2 gap-5"
>
<FormControl
v-model="titleFilter"
:placeholder="__('Search by title')"
/>
<FormControl
v-model="typeFilter"
type="select"
:options="assignmentTypes"
:placeholder="__('Type')"
/>
</div>
</div> </div>
<ListView <ListView
v-if="assignments.data?.length" v-if="assignments.data?.length"
@@ -46,22 +60,7 @@
}" }"
> >
</ListView> </ListView>
<div <EmptyState v-else type="Assignments" />
v-else
class="text-center p-5 text-ink-gray-5 mt-52 w-3/4 md:w-1/2 mx-auto space-y-2"
>
<Pencil class="size-10 mx-auto stroke-1 text-ink-gray-4" />
<div class="text-xl font-medium">
{{ __('No assignments found') }}
</div>
<div class="leading-5">
{{
__(
'You have not created any assignments yet. To create a new assignment, click on the "New" button above.'
)
}}
</div>
</div>
<div <div
v-if="assignments.data && assignments.hasNextPage" v-if="assignments.data && assignments.hasNextPage"
class="flex justify-center my-5" class="flex justify-center my-5"
@@ -81,16 +80,18 @@
import { import {
Breadcrumbs, Breadcrumbs,
Button, Button,
call,
createListResource, createListResource,
FormControl, FormControl,
ListView, ListView,
usePageMeta, usePageMeta,
} from 'frappe-ui' } from 'frappe-ui'
import { computed, inject, onMounted, ref, watch } from 'vue' import { computed, inject, onMounted, ref, watch } from 'vue'
import { Plus, Pencil } from 'lucide-vue-next' import { Plus } from 'lucide-vue-next'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { sessionStore } from '../stores/session' import { sessionStore } from '../stores/session'
import AssignmentForm from '@/components/Modals/AssignmentForm.vue' import AssignmentForm from '@/components/Modals/AssignmentForm.vue'
import EmptyState from '@/components/EmptyState.vue'
const user = inject('$user') const user = inject('$user')
const dayjs = inject('$dayjs') const dayjs = inject('$dayjs')
@@ -98,6 +99,7 @@ const titleFilter = ref('')
const typeFilter = ref('') const typeFilter = ref('')
const showAssignmentForm = ref(false) const showAssignmentForm = ref(false)
const assignmentID = ref('new') const assignmentID = ref('new')
const assignmentCount = ref(0)
const { brand } = sessionStore() const { brand } = sessionStore()
const router = useRouter() const router = useRouter()
const readOnlyMode = window.read_only_mode const readOnlyMode = window.read_only_mode
@@ -106,7 +108,7 @@ onMounted(() => {
if (!user.data?.is_moderator && !user.data?.is_instructor) { if (!user.data?.is_moderator && !user.data?.is_instructor) {
router.push({ name: 'Courses' }) router.push({ name: 'Courses' })
} }
getAssignmentCount()
titleFilter.value = router.currentRoute.value.query.title titleFilter.value = router.currentRoute.value.query.title
typeFilter.value = router.currentRoute.value.query.type typeFilter.value = router.currentRoute.value.query.type
}) })
@@ -179,6 +181,14 @@ const assignmentColumns = computed(() => {
] ]
}) })
const getAssignmentCount = () => {
call('frappe.client.get_count', {
doctype: 'LMS Assignment',
}).then((data) => {
assignmentCount.value = data
})
}
const assignmentTypes = computed(() => { const assignmentTypes = computed(() => {
let types = ['', 'Document', 'Image', 'PDF', 'URL', 'Text'] let types = ['', 'Document', 'Image', 'PDF', 'URL', 'Text']
return types.map((type) => { return types.map((type) => {
+4 -1
View File
@@ -67,7 +67,7 @@
<BatchDashboard :batch="batch" :isStudent="isStudent" /> <BatchDashboard :batch="batch" :isStudent="isStudent" />
</div> </div>
<div v-else-if="tab.label == 'Dashboard'"> <div v-else-if="tab.label == 'Dashboard'">
<BatchStudents :batch="batch.data" /> <BatchStudents :batch="batch" />
</div> </div>
<div v-else-if="tab.label == 'Classes'"> <div v-else-if="tab.label == 'Classes'">
<LiveClass :batch="batch.data.name" /> <LiveClass :batch="batch.data.name" />
@@ -357,6 +357,9 @@ watch(tabIndex, () => {
const canMakeAnnouncement = () => { const canMakeAnnouncement = () => {
if (readOnlyMode) return false if (readOnlyMode) return false
if (!batch.data?.students?.length) return false
return user.data?.is_moderator || user.data?.is_evaluator return user.data?.is_moderator || user.data?.is_evaluator
} }
+29 -51
View File
@@ -6,67 +6,45 @@
<Breadcrumbs :items="breadcrumbs" /> <Breadcrumbs :items="breadcrumbs" />
</header> </header>
<div class="m-5 pb-10"> <div class="m-5 pb-10">
<div> <div class="flex justify-between w-full">
<div class="text-3xl font-semibold text-ink-gray-9"> <div class="md:w-2/3">
{{ batch.data.title }} <div class="text-3xl font-semibold text-ink-gray-9">
</div> {{ batch.data.title }}
<div class="my-3 leading-6 text-ink-gray-7">
{{ batch.data.description }}
</div>
<div
class="flex flex-col gap-2 lg:gap-0 lg:flex-row lg:items-center space-x-0 md:space-x-5 lg:w-1/2"
>
<div
v-if="batch.data?.courses?.length"
class="flex items-center text-ink-gray-7"
>
<BookOpen class="h-4 w-4 mr-2 stroke-1.5" />
<span> {{ batch.data?.courses?.length }} {{ __('Courses') }} </span>
</div> </div>
<span v-if="batch.data?.courses?.length" class="hidden lg:block" <div class="my-3 leading-6 text-ink-gray-7">
>&middot;</span {{ batch.data.description }}
>
<DateRange
:startDate="batch.data.start_date"
:endDate="batch.data.end_date"
/>
<span class="hidden lg:block" v-if="batch.data.start_date"
>&middot;</span
>
<div class="flex items-center text-ink-gray-7">
<Clock class="h-4 w-4 mr-2 stroke-1.5" />
<span>
{{ formatTime(batch.data.start_time) }} -
{{ formatTime(batch.data.end_time) }}
</span>
</div> </div>
</div> <div class="flex avatar-group overlap">
<div class="flex avatar-group overlap mt-3"> <div
<div class="h-6 mr-1"
class="h-6 mr-1" :class="{
:class="{ 'avatar-group overlap': batch.data.instructors.length > 1,
'avatar-group overlap': batch.data.instructors.length > 1, }"
}" >
> <UserAvatar
<UserAvatar v-for="instructor in batch.data.instructors"
v-for="instructor in batch.data.instructors" :user="instructor"
:user="instructor" />
/> </div>
<CourseInstructors :instructors="batch.data.instructors" />
</div> </div>
<CourseInstructors :instructors="batch.data.instructors" />
</div>
</div>
<div class="grid lg:grid-cols-[60%,20%] gap-4 lg:gap-20 mt-10">
<div class="order-2 lg:order-none">
<div <div
class="ProseMirror prose prose-table:table-fixed prose-td:p-2 prose-th:p-2 prose-td:border prose-th:border prose-td:border-outline-gray-2 prose-th:border-outline-gray-2 prose-td:relative prose-th:relative prose-th:bg-surface-gray-2 prose-sm max-w-none !whitespace-normal mt-6" class="ProseMirror prose prose-table:table-fixed prose-td:p-2 prose-th:p-2 prose-td:border prose-th:border prose-td:border-outline-gray-2 prose-th:border-outline-gray-2 prose-td:relative prose-th:relative prose-th:bg-surface-gray-2 prose-sm max-w-none !whitespace-normal mt-10"
v-html="batch.data.batch_details" v-html="batch.data.batch_details"
></div> ></div>
</div>
<div class="hidden md:block">
<BatchOverlay :batch="batch" />
</div>
</div>
<!-- <div class="grid lg:grid-cols-[60%,20%] gap-4 lg:gap-20 mt-10">
<div class="order-2 lg:order-none">
</div> </div>
<div class="order-1 lg:order-none"> <div class="order-1 lg:order-none">
<BatchOverlay :batch="batch" /> <BatchOverlay :batch="batch" />
</div> </div>
</div> </div> -->
<div v-if="batch.data.courses.length"> <div v-if="batch.data.courses.length">
<div class="flex items-center mt-10"> <div class="flex items-center mt-10">
<div class="text-2xl font-semibold"> <div class="text-2xl font-semibold">
+164 -164
View File
@@ -8,13 +8,13 @@
{{ __('Save') }} {{ __('Save') }}
</Button> </Button>
</header> </header>
<div class="w-3/4 mx-auto py-5"> <div class="py-5">
<div class=""> <div class="px-20 pb-5 space-y-5 border-b mb-5">
<div class="text-lg text-ink-gray-9 font-semibold mb-4"> <div class="text-lg text-ink-gray-9 font-semibold mb-4">
{{ __('Details') }} {{ __('Details') }}
</div> </div>
<div class="space-y-10 mb-4"> <div class="grid grid-cols-2 gap-5">
<div class="space-y-4"> <div class="space-y-5">
<FormControl <FormControl
v-model="batch.title" v-model="batch.title"
:label="__('Title')" :label="__('Title')"
@@ -26,31 +26,162 @@
doctype="User" doctype="User"
:label="__('Instructors')" :label="__('Instructors')"
:required="true" :required="true"
:onCreate="(close) => openSettings('Members', close)"
:filters="{ ignore_user_type: 1 }" :filters="{ ignore_user_type: 1 }"
/> />
</div> </div>
<FormControl
v-model="batch.description"
:label="__('Short Description')"
type="textarea"
:rows="8"
:placeholder="__('Short description of the batch')"
:required="true"
/>
</div>
</div>
<div class="grid grid-cols-2 gap-10"> <div class="px-20 pb-5 space-y-5 border-b mb-5">
<div class="flex flex-col space-y-5"> <div class="text-lg text-ink-gray-9 font-semibold mb-4">
<FormControl {{ __('Settings') }}
v-model="batch.published" </div>
type="checkbox" <div class="grid grid-cols-3 gap-5">
:label="__('Published')" <FormControl
/> v-model="batch.published"
<FormControl type="checkbox"
v-model="batch.allow_self_enrollment" :label="__('Published')"
type="checkbox" />
:label="__('Allow self enrollment')" <FormControl
/> v-model="batch.allow_self_enrollment"
<FormControl type="checkbox"
v-model="batch.certification" :label="__('Allow self enrollment')"
type="checkbox" />
:label="__('Certification')" <FormControl
/> v-model="batch.certification"
</div> type="checkbox"
:label="__('Certification')"
/>
</div>
</div>
<div class="px-20 pb-5 space-y-5 border-b mb-5">
<div class="text-lg text-ink-gray-9 font-semibold mb-4">
{{ __('Date and Time') }}
</div>
<div class="grid grid-cols-3 gap-10">
<div class="space-y-5">
<FormControl
v-model="batch.start_date"
:label="__('Start Date')"
type="date"
class="mb-4"
:required="true"
/>
<FormControl
v-model="batch.end_date"
:label="__('End Date')"
type="date"
class="mb-4"
:required="true"
/>
</div>
<div class="space-y-5">
<FormControl
v-model="batch.start_time"
:label="__('Start Time')"
type="time"
class="mb-4"
:required="true"
/>
<FormControl
v-model="batch.end_time"
:label="__('End Time')"
type="time"
class="mb-4"
:required="true"
/>
</div>
<div class="space-y-5">
<FormControl
v-model="batch.timezone"
:label="__('Timezone')"
type="text"
:placeholder="__('Example: IST (+5:30)')"
class="mb-4"
:required="true"
/>
<FormControl
v-model="batch.evaluation_end_date"
:label="__('Evaluation End Date')"
type="date"
class="mb-4"
/>
</div>
</div>
</div>
<div class="px-20 pb-5 space-y-5 border-b mb-5">
<div>
<label class="block text-sm text-ink-gray-5 mb-1">
{{ __('Batch Details') }}
<span class="text-ink-red-3">*</span>
</label>
<TextEditor
:content="batch.batch_details"
@change="(val) => (batch.batch_details = val)"
:editable="true"
:fixedMenu="true"
editorClass="prose-sm max-w-none border-b border-x bg-surface-gray-2 rounded-b-md py-1 px-2 min-h-[7rem] max-h-[20rem] overflow-y-scroll mb-4"
/>
</div>
</div>
<div class="px-20 pb-5 space-y-5 border-b mb-5">
<div class="text-lg text-ink-gray-9 font-semibold mb-4">
{{ __('Configurations') }}
</div>
<div class="grid grid-cols-3 gap-10">
<div class="space-y-5">
<FormControl
v-model="batch.seat_count"
:label="__('Seat Count')"
type="number"
class="mb-4"
:placeholder="__('Number of seats available')"
/>
<Link
doctype="Email Template"
:label="__('Email Template')"
v-model="batch.confirmation_email_template"
/>
</div>
<div class="space-y-5">
<FormControl
v-model="batch.medium"
type="select"
:options="[
{
label: 'Online',
value: 'Online',
},
{
label: 'Offline',
value: 'Offline',
},
]"
:label="__('Medium')"
class="mb-4"
/>
<Link
doctype="LMS Category"
:label="__('Category')"
v-model="batch.category"
:onCreate="(value, close) => openSettings('Categories', close)"
/>
</div>
<div class="space-y-5">
<div> <div>
<div class="text-xs text-ink-gray-5 mb-2"> <div class="text-xs text-ink-gray-5">
{{ __('Meta Image') }} {{ __('Meta Image') }}
</div> </div>
<FileUploader <FileUploader
@@ -70,11 +201,9 @@
<Button @click="openFileSelector"> <Button @click="openFileSelector">
{{ __('Upload') }} {{ __('Upload') }}
</Button> </Button>
<div class="mt-2 text-ink-gray-5 text-sm"> <div class="mt-1 text-ink-gray-5 text-sm leading-5">
{{ {{
__( __('Appears when the batch URL is shared on socials')
'Appears when the batch URL is shared on any online platform'
)
}} }}
</div> </div>
</div> </div>
@@ -106,119 +235,16 @@
</div> </div>
</div> </div>
<div class="my-10"> <div class="px-20 pb-5 space-y-5">
<div class="text-lg text-ink-gray-9 font-semibold mb-4"> <div class="text-lg text-ink-gray-9 font-semibold">
{{ __('Date and Time') }} {{ __('Pricing') }}
</div>
<div class="grid grid-cols-3 gap-10">
<div>
<FormControl
v-model="batch.start_date"
:label="__('Start Date')"
type="date"
class="mb-4"
:required="true"
/>
<FormControl
v-model="batch.end_date"
:label="__('End Date')"
type="date"
class="mb-4"
:required="true"
/>
</div>
<div>
<FormControl
v-model="batch.start_time"
:label="__('Start Time')"
type="time"
class="mb-4"
:required="true"
/>
<FormControl
v-model="batch.end_time"
:label="__('End Time')"
type="time"
class="mb-4"
:required="true"
/>
</div>
<div>
<FormControl
v-model="batch.timezone"
:label="__('Timezone')"
type="text"
:placeholder="__('Example: IST (+5:30)')"
class="mb-4"
:required="true"
/>
</div>
</div>
</div>
<div class="mb-10">
<div class="text-lg text-ink-gray-9 font-semibold mb-4">
{{ __('Settings') }}
</div>
<div class="grid grid-cols-3 gap-10">
<div>
<FormControl
v-model="batch.seat_count"
:label="__('Seat Count')"
type="number"
class="mb-4"
:placeholder="__('Number of seats available')"
/>
<FormControl
v-model="batch.evaluation_end_date"
:label="__('Evaluation End Date')"
type="date"
class="mb-4"
/>
</div>
<div>
<FormControl
v-model="batch.medium"
type="select"
:options="[
{
label: 'Online',
value: 'Online',
},
{
label: 'Offline',
value: 'Offline',
},
]"
:label="__('Medium')"
class="mb-4"
/>
<Link
doctype="LMS Category"
:label="__('Category')"
v-model="batch.category"
/>
</div>
<div>
<Link
doctype="Email Template"
:label="__('Email Template')"
v-model="batch.confirmation_email_template"
/>
</div>
</div>
</div>
<div class="">
<div class="text-lg text-ink-gray-9 font-semibold mb-4">
{{ __('Payment') }}
</div> </div>
<FormControl <FormControl
v-model="batch.paid_batch" v-model="batch.paid_batch"
type="checkbox" type="checkbox"
:label="__('Paid Batch')" :label="__('Paid Batch')"
/> />
<div class="grid grid-cols-3 gap-10 mt-4"> <div v-if="batch.paid_batch" class="grid grid-cols-3 gap-5">
<FormControl <FormControl
v-model="batch.amount" v-model="batch.amount"
:label="__('Amount')" :label="__('Amount')"
@@ -232,33 +258,6 @@
/> />
</div> </div>
</div> </div>
<div class="my-10">
<div class="text-lg text-ink-gray-9 font-semibold mb-4">
{{ __('Description') }}
</div>
<FormControl
v-model="batch.description"
:label="__('Short Description')"
type="textarea"
class="my-4"
:placeholder="__('Short description of the batch')"
:required="true"
/>
<div>
<label class="block text-sm text-ink-gray-5 mb-1">
{{ __('Batch Details') }}
<span class="text-ink-red-3">*</span>
</label>
<TextEditor
:content="batch.batch_details"
@change="(val) => (batch.batch_details = val)"
:editable="true"
:fixedMenu="true"
editorClass="prose-sm max-w-none border-b border-x bg-surface-gray-2 rounded-b-md py-1 px-2 min-h-[7rem] mb-4"
/>
</div>
</div>
</div> </div>
</div> </div>
</template> </template>
@@ -279,15 +278,16 @@ import {
TextEditor, TextEditor,
createResource, createResource,
usePageMeta, usePageMeta,
toast,
} from 'frappe-ui' } from 'frappe-ui'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { showToast } from '@/utils'
import { Image } from 'lucide-vue-next' import { Image } from 'lucide-vue-next'
import { capture } from '@/telemetry' import { capture } from '@/telemetry'
import { useOnboarding } from 'frappe-ui/frappe' import { useOnboarding } from 'frappe-ui/frappe'
import { sessionStore } from '../stores/session' import { sessionStore } from '../stores/session'
import MultiSelect from '@/components/Controls/MultiSelect.vue' import MultiSelect from '@/components/Controls/MultiSelect.vue'
import Link from '@/components/Controls/Link.vue' import Link from '@/components/Controls/Link.vue'
import { openSettings } from '@/utils'
const router = useRouter() const router = useRouter()
const user = inject('$user') const user = inject('$user')
@@ -459,7 +459,7 @@ const createNewBatch = () => {
}) })
}, },
onError(err) { onError(err) {
showToast('Message', err.messages?.[0] || err, 'alert-circle') toast.error(err.messages?.[0] || err)
}, },
} }
) )
@@ -478,7 +478,7 @@ const editBatchDetails = () => {
}) })
}, },
onError(err) { onError(err) {
showToast('Message', err.messages?.[0] || err, 'alert-circle') toast.error(err.messages?.[0] || err)
}, },
} }
) )
+17 -17
View File
@@ -20,12 +20,14 @@
</header> </header>
<div class="p-5 pb-10"> <div class="p-5 pb-10">
<div <div
v-if="batchCount"
class="flex flex-col lg:flex-row space-y-4 lg:space-y-0 lg:items-center justify-between mb-5" class="flex flex-col lg:flex-row space-y-4 lg:space-y-0 lg:items-center justify-between mb-5"
> >
<div class="text-lg text-ink-gray-9 font-semibold"> <div class="text-lg text-ink-gray-9 font-semibold">
{{ __('All Batches') }} {{ __('All Batches') }}
</div> </div>
<div <div
v-if="batches.data?.length || batchCount"
class="flex flex-col space-y-2 lg:space-y-0 lg:flex-row lg:items-center lg:space-x-4" class="flex flex-col space-y-2 lg:space-y-0 lg:flex-row lg:items-center lg:space-x-4"
> >
<TabButtons <TabButtons
@@ -70,22 +72,8 @@
<BatchCard :batch="batch" /> <BatchCard :batch="batch" />
</router-link> </router-link>
</div> </div>
<div <EmptyState v-else-if="!batches.list.loading" type="Batches" />
v-else-if="!batches.list.loading"
class="flex flex-col items-center justify-center text-sm text-ink-gray-5 mt-48"
>
<BookOpen class="size-10 mx-auto stroke-1 text-ink-gray-4" />
<div class="text-lg font-medium mb-1">
{{ __('No batches found') }}
</div>
<div class="leading-5 w-2/5 text-center">
{{
__(
'There are no batches matching the criteria. Keep an eye out, fresh learning experiences are on the way soon!'
)
}}
</div>
</div>
<div <div
v-if="!batches.list.loading && batches.hasNextPage" v-if="!batches.list.loading && batches.hasNextPage"
class="flex justify-center mt-5" class="flex justify-center mt-5"
@@ -100,6 +88,7 @@
import { import {
Breadcrumbs, Breadcrumbs,
Button, Button,
call,
createListResource, createListResource,
FormControl, FormControl,
Select, Select,
@@ -107,9 +96,10 @@ import {
usePageMeta, usePageMeta,
} from 'frappe-ui' } from 'frappe-ui'
import { computed, inject, onMounted, ref, watch } from 'vue' import { computed, inject, onMounted, ref, watch } from 'vue'
import { BookOpen, Plus } from 'lucide-vue-next' import { Plus } from 'lucide-vue-next'
import { sessionStore } from '@/stores/session' import { sessionStore } from '@/stores/session'
import BatchCard from '@/components/BatchCard.vue' import BatchCard from '@/components/BatchCard.vue'
import EmptyState from '@/components/EmptyState.vue'
const user = inject('$user') const user = inject('$user')
const dayjs = inject('$dayjs') const dayjs = inject('$dayjs')
@@ -125,10 +115,12 @@ const is_student = computed(() => user.data?.is_student)
const currentTab = ref(is_student.value ? 'All' : 'Upcoming') const currentTab = ref(is_student.value ? 'All' : 'Upcoming')
const orderBy = ref('start_date') const orderBy = ref('start_date')
const readOnlyMode = window.read_only_mode const readOnlyMode = window.read_only_mode
const batchCount = ref(0)
onMounted(() => { onMounted(() => {
setFiltersFromQuery() setFiltersFromQuery()
updateBatches() updateBatches()
getBatchCount()
categories.value = [ categories.value = [
{ {
label: '', label: '',
@@ -306,6 +298,14 @@ const canCreateBatch = () => {
return false return false
} }
const getBatchCount = () => {
call('frappe.client.get_count', {
doctype: 'LMS Batch',
}).then((data) => {
batchCount.value = data
})
}
const breadcrumbs = computed(() => [ const breadcrumbs = computed(() => [
{ {
label: __('Batches'), label: __('Batches'),
+3 -10
View File
@@ -156,9 +156,9 @@ import {
FormControl, FormControl,
Breadcrumbs, Breadcrumbs,
usePageMeta, usePageMeta,
toast,
} from 'frappe-ui' } from 'frappe-ui'
import { reactive, inject, onMounted, computed } from 'vue' import { reactive, inject, onMounted, computed } from 'vue'
import { showToast } from '@/utils/'
import { sessionStore } from '../stores/session' import { sessionStore } from '../stores/session'
import Link from '@/components/Controls/Link.vue' import Link from '@/components/Controls/Link.vue'
import NotPermitted from '@/components/NotPermitted.vue' import NotPermitted from '@/components/NotPermitted.vue'
@@ -259,7 +259,7 @@ const generatePaymentLink = () => {
window.location.href = data window.location.href = data
}, },
onError(err) { onError(err) {
showToast(__('Error'), err.messages?.[0] || err, 'x') toast.error(err.messages?.[0] || err)
}, },
} }
) )
@@ -333,14 +333,7 @@ const validateAddress = () => {
} }
const showError = (err) => { const showError = (err) => {
createToast({ toast.error(err.messages?.[0] || err)
title: 'Error',
text: err.messages?.[0] || err,
icon: 'x',
iconClasses: 'bg-surface-red-5 text-ink-white rounded-md p-px',
position: 'top-center',
timeout: 10,
})
} }
const changeCurrency = (country) => { const changeCurrency = (country) => {
+4 -18
View File
@@ -3,7 +3,7 @@
class="sticky flex items-center justify-between top-0 z-10 border-b bg-surface-white px-3 py-2.5 sm:px-5" class="sticky flex items-center justify-between top-0 z-10 border-b bg-surface-white px-3 py-2.5 sm:px-5"
> >
<Breadcrumbs :items="breadcrumbs" /> <Breadcrumbs :items="breadcrumbs" />
<router-link :to="{ name: 'Batches' }"> <router-link :to="{ name: 'Batches', query: { certification: true } }">
<Button> <Button>
<template #prefix> <template #prefix>
<GraduationCap class="h-4 w-4 stroke-1.5" /> <GraduationCap class="h-4 w-4 stroke-1.5" />
@@ -101,22 +101,7 @@
</Button> </Button>
</div> </div>
</div> </div>
<div <EmptyState v-else type="Certified Members" />
v-else
class="flex flex-col items-center justify-center text-sm text-ink-gray-5 mt-48"
>
<BookOpen class="size-10 mx-auto stroke-1 text-ink-gray-4" />
<div class="text-lg font-medium mb-1">
{{ __('No certified members') }}
</div>
<div class="leading-5 w-2/5 text-center">
{{
__(
'No certified members found. Please check again later or get certified yourself.'
)
}}
</div>
</div>
</template> </template>
<script setup> <script setup>
import { import {
@@ -130,8 +115,9 @@ import {
usePageMeta, usePageMeta,
} from 'frappe-ui' } from 'frappe-ui'
import { computed, inject, onMounted, ref } from 'vue' import { computed, inject, onMounted, ref } from 'vue'
import { BookOpen, GraduationCap } from 'lucide-vue-next' import { GraduationCap } from 'lucide-vue-next'
import { sessionStore } from '../stores/session' import { sessionStore } from '../stores/session'
import EmptyState from '@/components/EmptyState.vue'
const currentCategory = ref('') const currentCategory = ref('')
const filters = ref({}) const filters = ref({})
+163 -150
View File
@@ -19,62 +19,112 @@
</Button> </Button>
</div> </div>
</header> </header>
<div class="mt-5 mb-10"> <div class="mt-5 mb-5">
<div class="container mb-5"> <div class="px-10 pb-5 mb-5 space-y-5 border-b">
<div class="text-lg font-semibold mb-4"> <div class="text-lg font-semibold mb-4">
{{ __('Details') }} {{ __('Details') }}
</div> </div>
<FormControl <div class="grid grid-cols-2 gap-5">
v-model="course.title" <FormControl
:label="__('Title')" v-model="course.title"
class="mb-4" :label="__('Title')"
:required="true" :required="true"
/> />
<FormControl <Link
v-model="course.short_introduction" doctype="LMS Category"
:label="__('Short Introduction')" v-model="course.category"
:placeholder=" :label="__('Category')"
__( :onCreate="(value, close) => openSettings('Categories', close)"
'A one line introduction to the course that appears on the course card'
)
"
class="mb-4"
:required="true"
/>
<div class="mb-4">
<div class="mb-1.5 text-sm text-ink-gray-5">
{{ __('Course Description') }}
<span class="text-ink-red-3">*</span>
</div>
<TextEditor
:content="course.description"
@change="(val) => (course.description = val)"
:editable="true"
:fixedMenu="true"
editorClass="prose-sm max-w-none border-b border-x bg-surface-gray-2 rounded-b-md py-1 px-2 min-h-[7rem]"
/> />
</div> </div>
<div class="mb-4"> <div class="grid grid-cols-2 gap-5">
<div class="text-xs text-ink-gray-5 mb-2"> <MultiSelect
{{ __('Course Image') }} v-model="instructors"
<span class="text-ink-red-3">*</span> doctype="User"
:label="__('Instructors')"
:filters="{ ignore_user_type: 1 }"
:onCreate="(close) => openSettings('Members', close)"
:required="true"
/>
<div>
<div class="mb-1.5 text-xs text-ink-gray-5">
{{ __('Tags') }}
</div>
<div class="flex items-center">
<div
v-if="course.tags"
v-for="tag in course.tags?.split(', ')"
class="flex items-center bg-surface-gray-2 text-ink-gray-7 p-2 rounded-md mr-2"
>
{{ tag }}
<X
class="stroke-1.5 w-3 h-3 ml-2 cursor-pointer"
@click="removeTag(tag)"
/>
</div>
<FormControl
v-model="newTag"
:placeholder="__('Add a keyword and then press enter')"
class="w-full"
@keyup.enter="updateTags()"
id="tags"
/>
</div>
</div> </div>
<FileUploader </div>
v-if="!course.course_image" <div class="grid grid-cols-2 gap-5">
:fileTypes="['image/*']" <FormControl
:validateFile="validateFile" v-model="course.short_introduction"
@success="(file) => saveImage(file)" type="textarea"
> :rows="4"
<template :label="__('Short Introduction')"
v-slot="{ file, progress, uploading, openFileSelector }" :placeholder="
__(
'A one line introduction to the course that appears on the course card'
)
"
:required="true"
/>
<div class="mb-4">
<div class="text-xs text-ink-gray-5 mb-2">
{{ __('Course Image') }}
<span class="text-ink-red-3">*</span>
</div>
<FileUploader
v-if="!course.course_image"
:fileTypes="['image/*']"
:validateFile="validateFile"
@success="(file) => saveImage(file)"
> >
<div class="flex items-center"> <template
<div class="border rounded-md w-fit py-5 px-20"> v-slot="{ file, progress, uploading, openFileSelector }"
<Image class="size-5 stroke-1 text-ink-gray-7" /> >
<div class="flex items-center">
<div class="border rounded-md w-fit py-5 px-20">
<Image class="size-5 stroke-1 text-ink-gray-7" />
</div>
<div class="ml-4">
<Button @click="openFileSelector">
{{ __('Upload') }}
</Button>
<div class="mt-1 text-ink-gray-5 text-sm leading-5">
{{
__('Appears on the course card in the course list')
}}
</div>
</div>
</div> </div>
</template>
</FileUploader>
<div v-else class="mb-4">
<div class="flex items-center">
<img
:src="course.course_image.file_url"
class="border rounded-md w-40"
/>
<div class="ml-4"> <div class="ml-4">
<Button @click="openFileSelector"> <Button @click="removeImage()">
{{ __('Upload') }} {{ __('Remove') }}
</Button> </Button>
<div class="mt-2 text-ink-gray-5 text-sm"> <div class="mt-2 text-ink-gray-5 text-sm">
{{ {{
@@ -83,85 +133,17 @@
</div> </div>
</div> </div>
</div> </div>
</template>
</FileUploader>
<div v-else class="mb-4">
<div class="flex items-center">
<img
:src="course.course_image.file_url"
class="border rounded-md w-40"
/>
<div class="ml-4">
<Button @click="removeImage()">
{{ __('Remove') }}
</Button>
<div class="mt-2 text-ink-gray-5 text-sm">
{{ __('Appears on the course card in the course list') }}
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
<FormControl
v-model="course.video_link"
:label="__('Preview Video')"
:placeholder="
__(
'Paste the youtube link of a short video introducing the course'
)
"
class="mb-4"
/>
<div class="mb-4">
<div class="mb-1.5 text-xs text-ink-gray-5">
{{ __('Tags') }}
</div>
<div class="flex items-center">
<div
v-if="course.tags"
v-for="tag in course.tags?.split(', ')"
class="flex items-center bg-surface-gray-2 text-ink-gray-7 p-2 rounded-md mr-2"
>
{{ tag }}
<X
class="stroke-1.5 w-3 h-3 ml-2 cursor-pointer"
@click="removeTag(tag)"
/>
</div>
<FormControl
v-model="newTag"
:placeholder="__('Add a keyword and then press enter')"
class="w-72"
@keyup.enter="updateTags()"
id="tags"
/>
</div>
</div>
<div class="w-1/2 mb-4">
<Link
doctype="LMS Category"
v-model="course.category"
:label="__('Category')"
:onCreate="(value, close) => openSettings(close)"
/>
</div>
<MultiSelect
v-model="instructors"
doctype="User"
:label="__('Instructors')"
:filters="{ ignore_user_type: 1 }"
:required="true"
/>
</div> </div>
<div class="container border-t">
<div class="text-lg font-semibold mt-5 mb-4"> <div class="px-10 pb-5 mb-5 space-y-5 border-b">
<div class="text-lg font-semibold">
{{ __('Settings') }} {{ __('Settings') }}
</div> </div>
<div class="grid grid-cols-2 gap-10 mb-4"> <div class="grid grid-cols-2 gap-5">
<div <div class="flex flex-col space-y-5">
v-if="user.data?.is_moderator"
class="flex flex-col space-y-4"
>
<FormControl <FormControl
type="checkbox" type="checkbox"
v-model="course.published" v-model="course.published"
@@ -171,10 +153,9 @@
v-model="course.published_on" v-model="course.published_on"
:label="__('Published On')" :label="__('Published On')"
type="date" type="date"
class="mb-5"
/> />
</div> </div>
<div class="flex flex-col space-y-3"> <div class="flex flex-col space-y-5">
<FormControl <FormControl
type="checkbox" type="checkbox"
v-model="course.upcoming" v-model="course.upcoming"
@@ -193,7 +174,34 @@
</div> </div>
</div> </div>
</div> </div>
<div class="container border-t space-y-4">
<div class="px-10 pb-5 mb-5 space-y-5 border-b">
<div class="">
<div class="mb-1.5 text-sm text-ink-gray-5">
{{ __('Course Description') }}
<span class="text-ink-red-3">*</span>
</div>
<TextEditor
:content="course.description"
@change="(val) => (course.description = val)"
:editable="true"
:fixedMenu="true"
editorClass="prose-sm max-w-none border-b border-x bg-surface-gray-2 rounded-b-md py-1 px-2 min-h-[7rem]"
/>
</div>
<FormControl
v-model="course.video_link"
:label="__('Preview Video')"
:placeholder="
__(
'Paste the youtube link of a short video introducing the course'
)
"
/>
</div>
<div class="px-10 pb-5 space-y-5">
<div class="text-lg font-semibold mt-5"> <div class="text-lg font-semibold mt-5">
{{ __('Pricing and Certification') }} {{ __('Pricing and Certification') }}
</div> </div>
@@ -214,19 +222,31 @@
:label="__('Paid Certificate')" :label="__('Paid Certificate')"
/> />
</div> </div>
<FormControl v-model="course.course_price" :label="__('Amount')" /> <div class="grid grid-cols-2 gap-5">
<Link <div class="space-y-5">
doctype="Currency" <FormControl
v-model="course.currency" v-if="course.paid_course || course.paid_certificate"
:filters="{ enabled: 1 }" v-model="course.course_price"
:label="__('Currency')" :label="__('Amount')"
/> />
<Link <Link
v-if="course.paid_certificate" v-if="course.paid_certificate"
doctype="Course Evaluator" doctype="Course Evaluator"
v-model="course.evaluator" v-model="course.evaluator"
:label="__('Evaluator')" :label="__('Evaluator')"
/> :onCreate="
(value, close) => openSettings('Evaluators', close)
"
/>
</div>
<Link
v-if="course.paid_course || course.paid_certificate"
doctype="Currency"
v-model="course.currency"
:filters="{ enabled: 1 }"
:label="__('Currency')"
/>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -250,6 +270,7 @@ import {
FormControl, FormControl,
FileUploader, FileUploader,
usePageMeta, usePageMeta,
toast,
} from 'frappe-ui' } from 'frappe-ui'
import { import {
inject, inject,
@@ -261,13 +282,12 @@ import {
watch, watch,
getCurrentInstance, getCurrentInstance,
} from 'vue' } from 'vue'
import { showToast } from '@/utils'
import { Image, Trash2, X } from 'lucide-vue-next' import { Image, Trash2, X } from 'lucide-vue-next'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { capture } from '@/telemetry' import { capture } from '@/telemetry'
import { useOnboarding } from 'frappe-ui/frappe' import { useOnboarding } from 'frappe-ui/frappe'
import { sessionStore } from '../stores/session' import { sessionStore } from '../stores/session'
import { useSettings } from '@/stores/settings' import { openSettings } from '@/utils'
import Link from '@/components/Controls/Link.vue' import Link from '@/components/Controls/Link.vue'
import CourseOutline from '@/components/CourseOutline.vue' import CourseOutline from '@/components/CourseOutline.vue'
import MultiSelect from '@/components/Controls/MultiSelect.vue' import MultiSelect from '@/components/Controls/MultiSelect.vue'
@@ -277,7 +297,6 @@ const newTag = ref('')
const { brand } = sessionStore() const { brand } = sessionStore()
const router = useRouter() const router = useRouter()
const instructors = ref([]) const instructors = ref([])
const settingsStore = useSettings()
const app = getCurrentInstance() const app = getCurrentInstance()
const { updateOnboardingStep } = useOnboarding('learning') const { updateOnboardingStep } = useOnboarding('learning')
const { $dialog } = app.appContext.config.globalProperties const { $dialog } = app.appContext.config.globalProperties
@@ -429,10 +448,10 @@ const submitCourse = () => {
}, },
{ {
onSuccess() { onSuccess() {
showToast('Success', 'Course updated successfully', 'check') toast.success(__('Course updated successfully'))
}, },
onError(err) { onError(err) {
showToast('Error', err.messages?.[0] || err, 'x') toast.error(err.messages?.[0] || err)
}, },
} }
) )
@@ -446,14 +465,14 @@ const submitCourse = () => {
} }
capture('course_created') capture('course_created')
showToast('Success', 'Course created successfully', 'check') toast.success(__('Course created successfully'))
router.push({ router.push({
name: 'CourseForm', name: 'CourseForm',
params: { courseName: data.name }, params: { courseName: data.name },
}) })
}, },
onError(err) { onError(err) {
showToast('Error', err.messages?.[0] || err, 'x') toast.error(err.messages?.[0] || err)
}, },
}) })
} }
@@ -467,7 +486,7 @@ const deleteCourse = createResource({
} }
}, },
onSuccess() { onSuccess() {
showToast(__('Success'), __('Course deleted successfully'), 'check') toast.success(__('Course deleted successfully'))
router.push({ name: 'Courses' }) router.push({ name: 'Courses' })
}, },
}) })
@@ -531,12 +550,6 @@ const removeImage = () => {
course.course_image = null course.course_image = null
} }
const openSettings = (close) => {
close()
settingsStore.activeTab = 'Categories'
settingsStore.isSettingsOpen = true
}
const check_permission = () => { const check_permission = () => {
let user_is_instructor = false let user_is_instructor = false
if (user.data?.is_moderator) return if (user.data?.is_moderator) return
+21 -29
View File
@@ -20,12 +20,14 @@
</header> </header>
<div class="p-5 pb-10"> <div class="p-5 pb-10">
<div <div
v-if="courseCount"
class="flex flex-col lg:flex-row space-y-4 lg:space-y-0 lg:items-center justify-between mb-5" class="flex flex-col lg:flex-row space-y-4 lg:space-y-0 lg:items-center justify-between mb-5"
> >
<div class="text-lg text-ink-gray-9 font-semibold"> <div class="text-lg text-ink-gray-9 font-semibold">
{{ __('All Courses') }} {{ __('All Courses') }}
</div> </div>
<div <div
v-if="courses.data?.length || courseCount"
class="flex flex-col space-y-2 lg:space-y-0 lg:flex-row lg:items-center lg:space-x-4" class="flex flex-col space-y-2 lg:space-y-0 lg:flex-row lg:items-center lg:space-x-4"
> >
<TabButtons :buttons="courseTabs" v-model="currentTab" /> <TabButtons :buttons="courseTabs" v-model="currentTab" />
@@ -66,22 +68,7 @@
<CourseCard :course="course" /> <CourseCard :course="course" />
</router-link> </router-link>
</div> </div>
<div <EmptyState v-else-if="!courses.list.loading" type="Courses" />
v-else-if="!courses.list.loading"
class="flex flex-col items-center justify-center text-sm text-ink-gray-5 italic mt-48"
>
<BookOpen class="size-10 mx-auto stroke-1 text-ink-gray-4" />
<div class="text-lg font-medium mb-1">
{{ __('No courses found') }}
</div>
<div class="leading-5 w-2/5 text-center">
{{
__(
'There are no courses matching the criteria. Keep an eye out, fresh learning experiences are on the way soon!'
)
}}
</div>
</div>
<div <div
v-if="!courses.list.loading && courses.hasNextPage" v-if="!courses.list.loading && courses.hasNextPage"
class="flex justify-center mt-5" class="flex justify-center mt-5"
@@ -104,10 +91,11 @@ import {
usePageMeta, usePageMeta,
} from 'frappe-ui' } from 'frappe-ui'
import { computed, inject, onMounted, ref, watch } from 'vue' import { computed, inject, onMounted, ref, watch } from 'vue'
import { BookOpen, Plus } from 'lucide-vue-next' import { Plus } from 'lucide-vue-next'
import { sessionStore } from '@/stores/session' import { sessionStore } from '@/stores/session'
import { canCreateCourse } from '@/utils' import { canCreateCourse } from '@/utils'
import CourseCard from '@/components/CourseCard.vue' import CourseCard from '@/components/CourseCard.vue'
import EmptyState from '@/components/EmptyState.vue'
import router from '../router' import router from '../router'
const user = inject('$user') const user = inject('$user')
@@ -121,12 +109,12 @@ const certification = ref(false)
const filters = ref({}) const filters = ref({})
const currentTab = ref('Live') const currentTab = ref('Live')
const { brand } = sessionStore() const { brand } = sessionStore()
const readOnlyMode = window.read_only_mode const courseCount = ref(0)
onMounted(() => { onMounted(() => {
identifyUserPersona()
setFiltersFromQuery() setFiltersFromQuery()
updateCourses() updateCourses()
getCourseCount()
categories.value = [ categories.value = [
{ {
label: '', label: '',
@@ -175,19 +163,23 @@ const identifyUserPersona = async () => {
if (user.data?.is_system_manager && !user.data?.developer_mode) { if (user.data?.is_system_manager && !user.data?.developer_mode) {
let personaCaptured = await isPersonaCaptured() let personaCaptured = await isPersonaCaptured()
if (personaCaptured) return if (personaCaptured) return
if (!courseCount.value) {
call('frappe.client.get_count', { router.push({
doctype: 'LMS Course', name: 'PersonaForm',
}).then((data) => { })
if (!data) { }
router.push({
name: 'PersonaForm',
})
}
})
} }
} }
const getCourseCount = () => {
call('frappe.client.get_count', {
doctype: 'LMS Course',
}).then((data) => {
courseCount.value = data
identifyUserPersona()
})
}
const updateCourses = () => { const updateCourses = () => {
updateFilters() updateFilters()
courses.update({ courses.update({
+6 -6
View File
@@ -94,6 +94,12 @@
{{ dayjs(job.data.creation).fromNow() }} {{ dayjs(job.data.creation).fromNow() }}
</Badge> </Badge>
<Badge size="lg"> <Badge size="lg">
<template #prefix>
<ClipboardType class="size-3 stroke-2 text-ink-gray-7" />
</template>
{{ job.data.type }}
</Badge>
<Badge v-if="applicationCount.data" size="lg">
<template #prefix> <template #prefix>
<SquareUserRound class="size-3 stroke-2 text-ink-gray-7" /> <SquareUserRound class="size-3 stroke-2 text-ink-gray-7" />
</template> </template>
@@ -102,12 +108,6 @@
applicationCount.data == 1 ? __('applicant') : __('applicants') applicationCount.data == 1 ? __('applicant') : __('applicants')
}} }}
</Badge> </Badge>
<Badge size="lg">
<template #prefix>
<ClipboardType class="size-3 stroke-2 text-ink-gray-7" />
</template>
{{ job.data.type }}
</Badge>
</div> </div>
</div> </div>
+16 -15
View File
@@ -9,7 +9,7 @@
</Button> </Button>
</header> </header>
<div class="py-5"> <div class="py-5">
<div class="container border-b mb-4 pb-4"> <div class="container border-b mb-4 pb-5">
<div class="text-lg font-semibold mb-4"> <div class="text-lg font-semibold mb-4">
{{ __('Job Details') }} {{ __('Job Details') }}
</div> </div>
@@ -20,6 +20,15 @@
:label="__('Title')" :label="__('Title')"
:required="true" :required="true"
/> />
<FormControl
v-model="job.type"
:label="__('Type')"
type="select"
:options="jobTypes"
:required="true"
/>
</div>
<div class="space-y-4">
<FormControl <FormControl
v-model="job.location" v-model="job.location"
:label="__('City')" :label="__('City')"
@@ -31,17 +40,8 @@
:label="__('Country')" :label="__('Country')"
:required="true" :required="true"
/> />
</div>
<div>
<FormControl
v-model="job.type"
:label="__('Type')"
type="select"
:options="jobTypes"
class="mb-4"
:required="true"
/>
<FormControl <FormControl
v-if="jobName != 'new'"
v-model="job.status" v-model="job.status"
:label="__('Status')" :label="__('Status')"
type="select" type="select"
@@ -51,7 +51,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="container border-b mb-4 pb-4"> <div class="container border-b mb-4 pb-5">
<div class="text-lg font-semibold mb-4"> <div class="text-lg font-semibold mb-4">
{{ __('Company Details') }} {{ __('Company Details') }}
</div> </div>
@@ -145,12 +145,13 @@ import {
TextEditor, TextEditor,
FileUploader, FileUploader,
usePageMeta, usePageMeta,
toast,
} from 'frappe-ui' } from 'frappe-ui'
import { computed, onMounted, reactive, inject } from 'vue' import { computed, onMounted, reactive, inject } from 'vue'
import { FileText, X } from 'lucide-vue-next' import { FileText, X } from 'lucide-vue-next'
import { sessionStore } from '@/stores/session' import { sessionStore } from '@/stores/session'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { getFileSize, showToast } from '../utils' import { getFileSize } from '@/utils'
const user = inject('$user') const user = inject('$user')
const router = useRouter() const router = useRouter()
@@ -259,7 +260,7 @@ const createNewJob = () => {
}) })
}, },
onError(err) { onError(err) {
showToast('Error', err.messages?.[0] || err, 'x') toast.error(err.messages?.[0] || err)
}, },
} }
) )
@@ -278,7 +279,7 @@ const editJobDetails = () => {
}) })
}, },
onError(err) { onError(err) {
showToast('Error', err.messages?.[0] || err, 'x') toast.error(err.messages?.[0] || err)
}, },
} }
) )
+10 -21
View File
@@ -26,15 +26,17 @@
</header> </header>
<div> <div>
<div <div
v-if="jobCount"
class="flex flex-col lg:flex-row space-y-4 lg:space-y-0 lg:items-center justify-between w-full md:w-4/5 mx-auto p-5" class="flex flex-col lg:flex-row space-y-4 lg:space-y-0 lg:items-center justify-between w-full md:w-4/5 mx-auto p-5"
> >
<div <div class="text-xl font-semibold text-ink-gray-7 mb-4 md:mb-0">
v-if="jobCount"
class="text-xl font-semibold text-ink-gray-7 mb-4 md:mb-0"
>
{{ __('{0} Open Jobs').format(jobCount) }} {{ __('{0} Open Jobs').format(jobCount) }}
</div> </div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-2">
<div
v-if="jobs.data?.length || jobCount > 0"
class="grid grid-cols-1 md:grid-cols-3 gap-2"
>
<FormControl <FormControl
type="text" type="text"
:placeholder="__('Search')" :placeholder="__('Search')"
@@ -79,21 +81,7 @@
</router-link> </router-link>
</div> </div>
</div> </div>
<div <EmptyState v-else type="Job Openings" />
v-else
class="flex flex-col items-center justify-center text-sm text-ink-gray-5 mt-56"
>
<Laptop class="size-10 mx-auto stroke-1 text-ink-gray-4" />
<div class="text-lg font-medium mb-1">
{{ __('No jobs found') }}
</div>
<div class="leading-5 w-2/5 text-center">
{{ __('There are no jobs available at the moment.') }}
</div>
<div class="leading-5 w-1/5 text-center">
{{ __('Post a new job or check again later.') }}
</div>
</div>
</div> </div>
</div> </div>
</template> </template>
@@ -106,11 +94,12 @@ import {
FormControl, FormControl,
usePageMeta, usePageMeta,
} from 'frappe-ui' } from 'frappe-ui'
import { Laptop, Plus, Search } from 'lucide-vue-next' import { Plus, Search } from 'lucide-vue-next'
import { sessionStore } from '../stores/session' import { sessionStore } from '../stores/session'
import { inject, computed, ref, onMounted, watch } from 'vue' import { inject, computed, ref, onMounted, watch } from 'vue'
import JobCard from '@/components/JobCard.vue' import JobCard from '@/components/JobCard.vue'
import Link from '@/components/Controls/Link.vue' import Link from '@/components/Controls/Link.vue'
import EmptyState from '@/components/EmptyState.vue'
const user = inject('$user') const user = inject('$user')
const jobType = ref(null) const jobType = ref(null)
+1 -1
View File
@@ -334,7 +334,6 @@ const props = defineProps({
onMounted(() => { onMounted(() => {
startTimer() startTimer()
enablePlyr()
document.addEventListener('fullscreenchange', attachFullscreenEvent) document.addEventListener('fullscreenchange', attachFullscreenEvent)
}) })
@@ -473,6 +472,7 @@ watch(
() => lesson.data, () => lesson.data,
(data) => { (data) => {
setupLesson(data) setupLesson(data)
enablePlyr()
} }
) )
+6 -19
View File
@@ -84,6 +84,7 @@ import {
createResource, createResource,
FormControl, FormControl,
usePageMeta, usePageMeta,
toast,
} from 'frappe-ui' } from 'frappe-ui'
import { import {
computed, computed,
@@ -97,7 +98,7 @@ import { sessionStore } from '../stores/session'
import EditorJS from '@editorjs/editorjs' import EditorJS from '@editorjs/editorjs'
import LessonHelp from '@/components/LessonHelp.vue' import LessonHelp from '@/components/LessonHelp.vue'
import { ChevronRight } from 'lucide-vue-next' import { ChevronRight } from 'lucide-vue-next'
import { createToast, getEditorTools, enablePlyr } from '@/utils' import { getEditorTools, enablePlyr } from '@/utils'
import { capture } from '@/telemetry' import { capture } from '@/telemetry'
import { useOnboarding } from 'frappe-ui/frappe' import { useOnboarding } from 'frappe-ui/frappe'
@@ -410,14 +411,14 @@ const createNewLesson = () => {
updateOnboardingStep('create_first_lesson') updateOnboardingStep('create_first_lesson')
capture('lesson_created') capture('lesson_created')
showToast('Success', 'Lesson created successfully', 'check') toast.success(__('Lesson created successfully'))
lessonDetails.reload() lessonDetails.reload()
}, },
} }
) )
}, },
onError(err) { onError(err) {
showToast('Error', err.message, 'x') toast.error(err.messages?.[0] || err)
}, },
} }
) )
@@ -434,11 +435,11 @@ const editCurrentLesson = () => {
}, },
onSuccess() { onSuccess() {
showSuccessMessage showSuccessMessage
? showToast('Success', 'Lesson updated successfully', 'check') ? toast.success(__('Lesson updated successfully'))
: '' : ''
}, },
onError(err) { onError(err) {
showToast('Error', err.message, 'x') toast.error(err.message)
}, },
} }
) )
@@ -453,20 +454,6 @@ const validateLesson = () => {
} }
} }
const showToast = (title, text, icon) => {
createToast({
title: title,
text: text,
icon: icon,
iconClasses:
icon == 'check'
? 'bg-surface-green-3 text-ink-white rounded-md p-px'
: 'bg-surface-red-5 text-ink-white rounded-md p-px',
position: icon == 'check' ? 'bottom-right' : 'top-center',
timeout: icon == 'check' ? 5 : 10,
})
}
const breadcrumbs = computed(() => { const breadcrumbs = computed(() => {
let crumbs = [ let crumbs = [
{ {
+24 -6
View File
@@ -1,6 +1,6 @@
<template> <template>
<div class="flex h-screen overflow-hidden sm:bg-gray-50"> <div class="flex h-screen overflow-hidden sm:bg-gray-50">
<div class="relative h-full z-10 mx-auto pt-8 sm:w-max sm:pt-32"> <div class="relative h-full z-10 mx-auto sm:w-max pt-40">
<div class="mx-auto flex items-center justify-center space-x-2"> <div class="mx-auto flex items-center justify-center space-x-2">
<LMSLogo class="size-7" /> <LMSLogo class="size-7" />
<span <span
@@ -18,7 +18,7 @@
<div class="mb-5"> <div class="mb-5">
<div class="text-sm text-gray-700 mb-2"> <div class="text-sm text-gray-700 mb-2">
{{ __('What is your main use case for Frappe Learning?') }} {{ __('What is your use case for Frappe Learning?') }}
</div> </div>
<FormControl <FormControl
v-model="persona.useCase" v-model="persona.useCase"
@@ -29,12 +29,12 @@
<div class="mb-5"> <div class="mb-5">
<div class="text-sm text-gray-700 mb-2"> <div class="text-sm text-gray-700 mb-2">
{{ __('How many students are you planning to teach?') }} {{ __('What best describes your role?') }}
</div> </div>
<FormControl <FormControl
v-model="persona.noOfStudents" v-model="persona.role"
type="select" type="select"
:options="noOfStudentsOptions" :options="roleOptions"
/> />
</div> </div>
@@ -65,7 +65,7 @@ const router = useRouter()
const { brand } = sessionStore() const { brand } = sessionStore()
const persona = reactive({ const persona = reactive({
noOfStudents: null, role: null,
useCase: null, useCase: null,
}) })
@@ -97,6 +97,24 @@ const skipPersonaForm = () => {
}) })
} }
const roleOptions = computed(() => {
const options = [
'Trainer / Instructor',
'Freelancer / Consultant',
'HR / L&D Professional',
'School / University Admin',
'Software Developer',
'Community Manager',
'Business Owner / Team Lead',
'Other',
]
return options.map((option) => ({
label: option,
value: option,
}))
})
const noOfStudentsOptions = computed(() => { const noOfStudentsOptions = computed(() => {
const options = [ const options = [
'Less than 50', 'Less than 50',
+10 -10
View File
@@ -141,9 +141,9 @@
</div> </div>
</template> </template>
<script setup> <script setup>
import { createResource, FormControl, Button, Badge } from 'frappe-ui' import { createResource, FormControl, Button, Badge, toast } from 'frappe-ui'
import { computed, reactive, ref, onMounted, inject } from 'vue' import { computed, reactive, ref, onMounted, inject } from 'vue'
import { showToast, convertToTitleCase } from '@/utils' import { convertToTitleCase } from '@/utils'
import { Plus, X, Check, CircleAlert } from 'lucide-vue-next' import { Plus, X, Check, CircleAlert } from 'lucide-vue-next'
const user = inject('$user') const user = inject('$user')
@@ -198,7 +198,7 @@ const createSlot = createResource({
} }
}, },
onSuccess() { onSuccess() {
showToast('Success', 'Slot added successfully', 'check') toast.success(__('Slot added successfully'))
evaluator.reload() evaluator.reload()
showSlotsTemplate.value = 0 showSlotsTemplate.value = 0
newSlot.day = '' newSlot.day = ''
@@ -206,7 +206,7 @@ const createSlot = createResource({
newSlot.end_time = '' newSlot.end_time = ''
}, },
onError(err) { onError(err) {
showToast('Error', err.messages?.[0] || err, 'x') toast.error(err.messages?.[0] || err)
}, },
}) })
@@ -221,10 +221,10 @@ const updateSlot = createResource({
} }
}, },
onSuccess() { onSuccess() {
showToast('Success', 'Availability updated successfully', 'check') toast.success(__('Availability updated successfully'))
}, },
onError(err) { onError(err) {
showToast('Error', err.messages?.[0] || err, 'x') toast.error(err.messages?.[0] || err)
}, },
}) })
@@ -237,11 +237,11 @@ const deleteSlot = createResource({
} }
}, },
onSuccess() { onSuccess() {
showToast('Success', 'Slot deleted successfully', 'check') toast.success(__('Slot deleted successfully'))
evaluator.reload() evaluator.reload()
}, },
onError(err) { onError(err) {
showToast('Error', err.messages?.[0] || err, 'x') toast.error(err.messages?.[0] || err)
}, },
}) })
@@ -256,10 +256,10 @@ const updateUnavailability = createResource({
} }
}, },
onSuccess() { onSuccess() {
showToast('Success', 'Unavailability updated successfully', 'check') toast.success(__('Unavailability updated successfully'))
}, },
onError(err) { onError(err) {
showToast('Error', err.messages?.[0] || err, 'x') toast.error(err.messages?.[0] || err)
}, },
}) })
+3 -3
View File
@@ -44,9 +44,9 @@
</div> </div>
</template> </template>
<script setup> <script setup>
import { FormControl, createResource } from 'frappe-ui' import { FormControl, createResource, toast } from 'frappe-ui'
import { ref } from 'vue' import { ref } from 'vue'
import { showToast, convertToTitleCase } from '@/utils' import { convertToTitleCase } from '@/utils'
import { CircleAlert } from 'lucide-vue-next' import { CircleAlert } from 'lucide-vue-next'
const moderator = ref(false) const moderator = ref(false)
@@ -102,7 +102,7 @@ const changeRole = (role) => {
}, },
{ {
onSuccess(data) { onSuccess(data) {
showToast('Success', 'Role updated successfully', 'check') toast.success(__('Role updated successfully'))
}, },
} }
) )
+12 -10
View File
@@ -168,6 +168,7 @@
ignore_user_type: 1, ignore_user_type: 1,
}" }"
:label="__('Program Member')" :label="__('Program Member')"
:onCreate="(value, close) => openSettings('Members', close)"
/> />
</template> </template>
</Dialog> </Dialog>
@@ -187,12 +188,13 @@ import {
ListHeaderItem, ListHeaderItem,
ListSelectBanner, ListSelectBanner,
usePageMeta, usePageMeta,
toast,
} from 'frappe-ui' } from 'frappe-ui'
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import { Plus, Trash2 } from 'lucide-vue-next' import { Plus, Trash2 } from 'lucide-vue-next'
import { showToast } from '@/utils/'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { sessionStore } from '../stores/session' import { sessionStore } from '@/stores/session'
import { openSettings } from '@/utils'
import Draggable from 'vuedraggable' import Draggable from 'vuedraggable'
import Link from '@/components/Controls/Link.vue' import Link from '@/components/Controls/Link.vue'
@@ -229,11 +231,11 @@ const addProgramCourse = () => {
onSuccess(data) { onSuccess(data) {
showDialog.value = false showDialog.value = false
course.value = null course.value = null
showToast(__('Success'), __('Course added to program'), 'check') toast.success(__('Course added to program'))
program.reload() program.reload()
}, },
onError(err) { onError(err) {
showToast('Error', err.messages?.[0] || err, 'x') toast.error(err.messages?.[0] || err)
}, },
} }
) )
@@ -251,11 +253,11 @@ const addProgramMember = () => {
onSuccess(data) { onSuccess(data) {
showDialog.value = false showDialog.value = false
member.value = null member.value = null
showToast(__('Success'), __('Member added to program'), 'check') toast.success(__('Member added to program'))
program.reload() program.reload()
}, },
onError(err) { onError(err) {
showToast('Error', err.messages?.[0] || err, 'x') toast.error(err.messages?.[0] || err)
}, },
} }
) )
@@ -272,11 +274,11 @@ const remove = (selections, unselectAll, doctype) => {
{ {
onSuccess(data) { onSuccess(data) {
unselectAll() unselectAll()
showToast(__('Success'), __('Items removed successfully'), 'check') toast.success(__('Items removed successfully'))
program.reload() program.reload()
}, },
onError(err) { onError(err) {
showToast('Error', err.messages?.[0] || err, 'x') toast.error(err.messages?.[0] || err)
}, },
} }
) )
@@ -298,11 +300,11 @@ const updateOrder = (e) => {
}, },
{ {
onSuccess(data) { onSuccess(data) {
showToast(__('Success'), __('Course moved successfully'), 'check') toast.success(__('Course moved successfully'))
program.reload() program.reload()
}, },
onError(err) { onError(err) {
showToast('Error', err.messages?.[0] || err, 'x') toast.error(err.messages?.[0] || err)
}, },
} }
) )
+5 -19
View File
@@ -82,22 +82,7 @@
</div> </div>
</div> </div>
</div> </div>
<div <EmptyState v-else type="Programs" />
v-else
class="text-center p-5 text-ink-gray-5 mt-52 w-3/4 md:w-1/2 mx-auto space-y-2"
>
<BookOpen class="size-10 mx-auto stroke-1 text-ink-gray-4" />
<div class="text-xl font-medium">
{{ __('No programs found') }}
</div>
<div class="leading-5">
{{
__(
'There are no programs available at the moment. Keep an eye out, fresh learning experiences are on the way soon!'
)
}}
</div>
</div>
<Dialog <Dialog
v-model="showDialog" v-model="showDialog"
@@ -127,13 +112,14 @@ import {
Dialog, Dialog,
FormControl, FormControl,
usePageMeta, usePageMeta,
toast,
} from 'frappe-ui' } from 'frappe-ui'
import { computed, inject, onMounted, ref } from 'vue' import { computed, inject, onMounted, ref } from 'vue'
import { BookOpen, Edit, Plus, LockKeyhole } from 'lucide-vue-next' import { Edit, Plus, LockKeyhole } from 'lucide-vue-next'
import CourseCard from '@/components/CourseCard.vue' import CourseCard from '@/components/CourseCard.vue'
import EmptyState from '@/components/EmptyState.vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { sessionStore } from '../stores/session' import { sessionStore } from '../stores/session'
import { showToast } from '@/utils'
import { useSettings } from '@/stores/settings' import { useSettings } from '@/stores/settings'
const { brand } = sessionStore() const { brand } = sessionStore()
@@ -198,7 +184,7 @@ const enrollMember = (program, course) => {
} }
}) })
.catch((err) => { .catch((err) => {
showToast('Error', err.messages?.[0] || err, 'x') toast.error(err.messages?.[0] || err)
}) })
} }
+7 -6
View File
@@ -198,6 +198,7 @@ import {
ListSelectBanner, ListSelectBanner,
Button, Button,
usePageMeta, usePageMeta,
toast,
} from 'frappe-ui' } from 'frappe-ui'
import { import {
computed, computed,
@@ -210,7 +211,7 @@ import {
} from 'vue' } from 'vue'
import { sessionStore } from '../stores/session' import { sessionStore } from '../stores/session'
import { Plus, Trash2 } from 'lucide-vue-next' import { Plus, Trash2 } from 'lucide-vue-next'
import { showToast, updateDocumentTitle } from '@/utils' import { updateDocumentTitle } from '@/utils'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import Question from '@/components/Modals/Question.vue' import Question from '@/components/Modals/Question.vue'
@@ -340,14 +341,14 @@ const createQuiz = () => {
{}, {},
{ {
onSuccess(data) { onSuccess(data) {
showToast(__('Success'), __('Quiz created successfully'), 'check') toast.success(__('Quiz created successfully'))
router.push({ router.push({
name: 'QuizForm', name: 'QuizForm',
params: { quizID: data.name }, params: { quizID: data.name },
}) })
}, },
onError(err) { onError(err) {
showToast(__('Error'), __(err.messages?.[0] || err), 'x') toast.error(err.messages?.[0] || err)
}, },
} }
) )
@@ -359,10 +360,10 @@ const updateQuiz = () => {
{ {
onSuccess(data) { onSuccess(data) {
quiz.total_marks = data.total_marks quiz.total_marks = data.total_marks
showToast(__('Success'), __('Quiz updated successfully'), 'check') toast.success(__('Quiz updated successfully'))
}, },
onError(err) { onError(err) {
showToast(__('Error'), __(err.messages?.[0] || err), 'x') toast.error(err.messages?.[0] || err)
}, },
} }
) )
@@ -428,7 +429,7 @@ const deleteQuestions = (selections, unselectAll) => {
}, },
{ {
onSuccess() { onSuccess() {
showToast(__('Success'), __('Questions deleted successfully'), 'check') toast.success(__('Questions deleted successfully'))
quizDetails.reload() quizDetails.reload()
unselectAll() unselectAll()
}, },
+38 -38
View File
@@ -2,10 +2,10 @@
<header <header
class="sticky top-0 z-10 flex items-center justify-between border-b bg-surface-white px-3 py-2.5 sm:px-5" class="sticky top-0 z-10 flex items-center justify-between border-b bg-surface-white px-3 py-2.5 sm:px-5"
> >
<Breadcrumbs v-if="submisisonDetails.doc" :items="breadcrumbs" /> <Breadcrumbs v-if="submissionDetails.doc" :items="breadcrumbs" />
<div class="space-x-2"> <div class="space-x-2">
<Badge <Badge
v-if="submisisonDetails.isDirty" v-if="submissionDetails.isDirty"
:label="__('Not Saved')" :label="__('Not Saved')"
variant="subtle" variant="subtle"
theme="orange" theme="orange"
@@ -15,19 +15,19 @@
</Button> </Button>
</div> </div>
</header> </header>
<div v-if="submisisonDetails.doc" class="w-1/2 mx-auto py-5 space-y-5"> <div v-if="submissionDetails.doc" class="w-2/3 border-x mx-auto py-5">
<div class="text-xl font-semibold text-ink-gray-9"> <div class="text-xl px-10 font-semibold text-ink-gray-9 mb-5">
{{ submisisonDetails.doc.member_name }} {{ submissionDetails.doc.member_name }}
</div> </div>
<div class="space-y-4 border p-5 rounded-md"> <div class="space-y-4 border-b pb-5 px-10">
<div class="grid grid-cols-2 gap-5"> <div class="grid grid-cols-2 gap-5">
<FormControl <FormControl
v-model="submisisonDetails.doc.quiz_title" v-model="submissionDetails.doc.quiz_title"
:label="__('Quiz')" :label="__('Quiz')"
:disabled="true" :disabled="true"
/> />
<FormControl <FormControl
v-model="submisisonDetails.doc.member_name" v-model="submissionDetails.doc.member_name"
:label="__('Member')" :label="__('Member')"
:disabled="true" :disabled="true"
/> />
@@ -35,39 +35,39 @@
<div class="grid grid-cols-2 gap-5"> <div class="grid grid-cols-2 gap-5">
<FormControl <FormControl
v-model="submisisonDetails.doc.score" v-model="submissionDetails.doc.score"
:label="__('Score')" :label="__('Score')"
:disabled="true" :disabled="true"
/> />
<FormControl <FormControl
v-model="submisisonDetails.doc.percentage" v-model="submissionDetails.doc.percentage"
:label="__('Percentage')" :label="__('Percentage')"
:disabled="true" :disabled="true"
/> />
</div> </div>
</div> </div>
<div <div class="divide-y">
v-for="(row, index) in submisisonDetails.doc.result" <div
class="border p-5 rounded-md space-y-4" v-for="(row, index) in submissionDetails.doc.result"
> class="py-5 px-10 space-y-4"
<div class="flex items-start space-x-1 font-semibold text-ink-gray-9"> >
<!-- <span> <div class="text-ink-gray-9">
{{ index + 1 }}. <span class="font-semibold"> {{ __('Question') }}: </span>
</span> --> <span class="leading-5" v-html="row.question"> </span>
<span class="leading-5" v-html="row.question"> </span> </div>
</div> <div class="">
<div class="leading-5 text-ink-gray-7 space-x-1"> <span class="font-semibold"> {{ __('Answer') }} </span>
<span> {{ __('Answer') }}: </span> <span class="leading-5" v-html="row.answer"></span>
<span v-html="row.answer"></span> </div>
</div> <div class="grid grid-cols-2 gap-5">
<div class="grid grid-cols-2 gap-5"> <FormControl v-model="row.marks" :label="__('Marks')" />
<FormControl v-model="row.marks" :label="__('Marks')" /> <FormControl
<FormControl v-model="row.marks_out_of"
v-model="row.marks_out_of" :label="__('Marks out of')"
:label="__('Marks out of')" :disabled="true"
:disabled="true" />
/> </div>
</div> </div>
</div> </div>
</div> </div>
@@ -80,10 +80,10 @@ import {
Button, Button,
Badge, Badge,
usePageMeta, usePageMeta,
toast,
} from 'frappe-ui' } from 'frappe-ui'
import { computed, onBeforeUnmount, onMounted, inject } from 'vue' import { computed, onBeforeUnmount, onMounted, inject } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { showToast } from '@/utils'
import { sessionStore } from '@/stores/session' import { sessionStore } from '@/stores/session'
const { brand } = sessionStore() const { brand } = sessionStore()
@@ -119,7 +119,7 @@ const props = defineProps({
}, },
}) })
const submisisonDetails = createDocumentResource({ const submissionDetails = createDocumentResource({
doctype: 'LMS Quiz Submission', doctype: 'LMS Quiz Submission',
name: props.submission, name: props.submission,
auto: true, auto: true,
@@ -132,22 +132,22 @@ const breadcrumbs = computed(() => {
route: { route: {
name: 'QuizSubmissionList', name: 'QuizSubmissionList',
params: { params: {
quizID: submisisonDetails.doc.quiz, quizID: submissionDetails.doc.quiz,
}, },
}, },
}, },
{ {
label: submisisonDetails.doc.quiz_title, label: submissionDetails.doc.quiz_title,
}, },
] ]
}) })
const saveSubmission = () => { const saveSubmission = () => {
submisisonDetails.save.submit( submissionDetails.save.submit(
{}, {},
{ {
onError(err) { onError(err) {
showToast(__('Error'), __(err.messages?.[0] || err), 'x') toast.error(err.messages?.[0] || err)
}, },
} }
) )
@@ -155,7 +155,7 @@ const saveSubmission = () => {
usePageMeta(() => { usePageMeta(() => {
return { return {
title: `${submisisonDetails.doc.quiz_title}`, title: `${submissionDetails.doc?.quiz_title}`,
icon: brand.favicon, icon: brand.favicon,
} }
}) })
+2 -13
View File
@@ -40,18 +40,7 @@
</Button> </Button>
</div> </div>
</div> </div>
<div <EmptyState v-else />
v-else
class="text-center p-5 text-ink-gray-5 mt-52 w-3/4 md:w-1/2 mx-auto space-y-2"
>
<BookOpen class="size-10 mx-auto stroke-1 text-ink-gray-4" />
<div class="text-xl font-medium">
{{ __('No submissions') }}
</div>
<div class="leading-5">
{{ __('No quiz submissions found. Please check again later.') }}
</div>
</div>
</template> </template>
<script setup> <script setup>
import { import {
@@ -65,10 +54,10 @@ import {
ListHeaderItem, ListHeaderItem,
usePageMeta, usePageMeta,
} from 'frappe-ui' } from 'frappe-ui'
import { BookOpen } from 'lucide-vue-next'
import { computed, onMounted, inject } from 'vue' import { computed, onMounted, inject } from 'vue'
import { sessionStore } from '../stores/session' import { sessionStore } from '../stores/session'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import EmptyState from '@/components/EmptyState.vue'
const { brand } = sessionStore() const { brand } = sessionStore()
const router = useRouter() const router = useRouter()
+18 -18
View File
@@ -21,6 +21,9 @@
</router-link> </router-link>
</header> </header>
<div v-if="quizzes.data?.length" class="md:w-3/4 md:mx-auto py-5 mx-5"> <div v-if="quizzes.data?.length" class="md:w-3/4 md:mx-auto py-5 mx-5">
<div v-if="quizCount" class="text-xl font-semibold text-ink-gray-7 mb-4">
{{ __('{0} Quizzes').format(quizCount) }}
</div>
<ListView <ListView
:columns="quizColumns" :columns="quizColumns"
:rows="quizzes.data" :rows="quizzes.data"
@@ -53,27 +56,13 @@
</Button> </Button>
</div> </div>
</div> </div>
<div <EmptyState v-else type="Quizzes" />
v-else
class="text-center p-5 text-ink-gray-5 mt-52 w-3/4 md:w-1/2 mx-auto space-y-2"
>
<BookOpen class="size-10 mx-auto stroke-1 text-ink-gray-4" />
<div class="text-xl font-medium">
{{ __('No quizzes found') }}
</div>
<div class="leading-5">
{{
__(
'You have not created any quizzes yet. To create a new quiz, click on the "New Quiz" button above.'
)
}}
</div>
</div>
</template> </template>
<script setup> <script setup>
import { import {
Breadcrumbs, Breadcrumbs,
Button, Button,
call,
createListResource, createListResource,
ListView, ListView,
ListRows, ListRows,
@@ -83,19 +72,22 @@ import {
usePageMeta, usePageMeta,
} from 'frappe-ui' } from 'frappe-ui'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { computed, inject, onMounted } from 'vue' import { computed, inject, onMounted, ref } from 'vue'
import { BookOpen, Plus } from 'lucide-vue-next' import { Plus } from 'lucide-vue-next'
import { sessionStore } from '@/stores/session' import { sessionStore } from '@/stores/session'
import EmptyState from '@/components/EmptyState.vue'
const { brand } = sessionStore() const { brand } = sessionStore()
const user = inject('$user') const user = inject('$user')
const router = useRouter() const router = useRouter()
const quizCount = ref(0)
const readOnlyMode = window.read_only_mode const readOnlyMode = window.read_only_mode
onMounted(() => { onMounted(() => {
if (!user.data?.is_moderator && !user.data?.is_instructor) { if (!user.data?.is_moderator && !user.data?.is_instructor) {
router.push({ name: 'Courses' }) router.push({ name: 'Courses' })
} }
getQuizCount()
}) })
const quizFilter = computed(() => { const quizFilter = computed(() => {
@@ -114,6 +106,14 @@ const quizzes = createListResource({
orderBy: 'modified desc', orderBy: 'modified desc',
}) })
const getQuizCount = () => {
call('frappe.client.get_count', {
doctype: 'LMS Quiz',
}).then((data) => {
quizCount.value = data
})
}
const quizColumns = computed(() => { const quizColumns = computed(() => {
return [ return [
{ {
+31 -49
View File
@@ -1,9 +1,10 @@
import { toast } from 'frappe-ui'
import { useTimeAgo } from '@vueuse/core' import { useTimeAgo } from '@vueuse/core'
import { Quiz } from '@/utils/quiz' import { Quiz } from '@/utils/quiz'
import { Assignment } from '@/utils/assignment' import { Assignment } from '@/utils/assignment'
import { Upload } from '@/utils/upload' import { Upload } from '@/utils/upload'
import { Markdown } from '@/utils/markdownParser' import { Markdown } from '@/utils/markdownParser'
import { useSettings } from '@/stores/settings'
import { usersStore } from '@/stores/user'
import Header from '@editorjs/header' import Header from '@editorjs/header'
import Paragraph from '@editorjs/paragraph' import Paragraph from '@editorjs/paragraph'
import { CodeBox } from '@/utils/code' import { CodeBox } from '@/utils/code'
@@ -14,19 +15,11 @@ import dayjs from '@/utils/dayjs'
import Embed from '@editorjs/embed' import Embed from '@editorjs/embed'
import SimpleImage from '@editorjs/simple-image' import SimpleImage from '@editorjs/simple-image'
import Table from '@editorjs/table' import Table from '@editorjs/table'
import { usersStore } from '../stores/user'
import Plyr from 'plyr' import Plyr from 'plyr'
import 'plyr/dist/plyr.css' import 'plyr/dist/plyr.css'
const readOnlyMode = window.read_only_mode const readOnlyMode = window.read_only_mode
export function createToast(options) {
toast({
position: 'bottom-right',
...options,
})
}
export function timeAgo(date) { export function timeAgo(date) {
return useTimeAgo(date).value return useTimeAgo(date).value
} }
@@ -97,26 +90,6 @@ export function getFileSize(file_size) {
return value return value
} }
export function showToast(title, text, icon, iconClasses = null) {
if (!iconClasses) {
if (icon == 'check') {
iconClasses = 'bg-surface-green-3 text-ink-white rounded-md p-px'
} else if (icon == 'alert-circle') {
iconClasses = 'bg-yellow-600 text-ink-white rounded-md p-px'
} else {
iconClasses = 'bg-surface-red-5 text-ink-white rounded-md p-px'
}
}
createToast({
title: title,
text: htmlToText(text),
icon: icon,
iconClasses: iconClasses,
position: icon == 'check' ? 'bottom-right' : 'top-center',
timeout: icon != 'check' ? 10 : 5,
})
}
export function getImgDimensions(imgSrc) { export function getImgDimensions(imgSrc) {
return new Promise((resolve) => { return new Promise((resolve) => {
let img = new Image() let img = new Image()
@@ -558,24 +531,33 @@ export const enablePlyr = () => {
const videoElement = document.getElementsByClassName('video-player') const videoElement = document.getElementsByClassName('video-player')
if (videoElement.length === 0) return if (videoElement.length === 0) return
const src = videoElement[0].getAttribute('src') Array.from(videoElement).forEach((video) => {
if (src) { const src = video.getAttribute('src')
let videoID = src.split('/').pop() if (src) {
videoElement[0].setAttribute('data-plyr-embed-id', videoID) let videoID = src.split('/').pop()
} video.setAttribute('data-plyr-embed-id', videoID)
new Plyr('.video-player', { }
youtube: { new Plyr(video, {
noCookie: true, youtube: {
}, noCookie: true,
controls: [ },
'play-large', controls: [
'play', 'play-large',
'progress', 'play',
'current-time', 'progress',
'mute', 'current-time',
'volume', 'mute',
'fullscreen', 'volume',
], 'fullscreen',
}) ],
}, 500) })
}, 500)
})
}
export const openSettings = (category, close) => {
const settingsStore = useSettings()
close()
settingsStore.activeTab = category
settingsStore.isSettingsOpen = true
} }
+1 -1
View File
@@ -25,7 +25,7 @@ export default defineConfig({
}), }),
], ],
server: { server: {
allowedHosts: ['fs', 'persona'], allowedHosts: ['fs', 'per2'],
}, },
resolve: { resolve: {
alias: { alias: {
+1 -1
View File
@@ -1 +1 @@
__version__ = "2.28.0" __version__ = "2.28.1"
+1 -1
View File
@@ -21,7 +21,7 @@ app_license = "AGPL"
# include js, css files in header of web template # include js, css files in header of web template
web_include_css = "lms.bundle.css" web_include_css = "lms.bundle.css"
# web_include_css = "/assets/lms/css/lms.css" # web_include_css = "/assets/lms/css/lms.css"
web_include_js = ["website.bundle.js"] web_include_js = []
# include custom scss in every website theme (without file extension ".scss") # include custom scss in every website theme (without file extension ".scss")
# website_theme_scss = "lms/public/scss/website" # website_theme_scss = "lms/public/scss/website"
+10 -82
View File
@@ -21,9 +21,9 @@ from lms.lms.utils import (
class LMSBatch(Document): class LMSBatch(Document):
def validate(self): def validate(self):
if self.seat_count: self.validate_seats_left()
self.validate_seats_left()
self.validate_batch_end_date() self.validate_batch_end_date()
self.validate_batch_time()
self.validate_duplicate_courses() self.validate_duplicate_courses()
self.validate_payments_app() self.validate_payments_app()
self.validate_amount_and_currency() self.validate_amount_and_currency()
@@ -40,6 +40,11 @@ class LMSBatch(Document):
if self.end_date < self.start_date: if self.end_date < self.start_date:
frappe.throw(_("Batch end date cannot be before the batch start date")) frappe.throw(_("Batch end date cannot be before the batch start date"))
def validate_batch_time(self):
if self.start_time and self.end_time:
if get_time(self.start_time) >= get_time(self.end_time):
frappe.throw(_("Batch start time cannot be greater than or equal to end time."))
def validate_duplicate_courses(self): def validate_duplicate_courses(self):
courses = [row.course for row in self.courses] courses = [row.course for row in self.courses]
duplicates = {course for course in courses if courses.count(course) > 1} duplicates = {course for course in courses if courses.count(course) > 1}
@@ -94,6 +99,9 @@ class LMSBatch(Document):
enrollment.save() enrollment.save()
def validate_seats_left(self): def validate_seats_left(self):
if cint(self.seat_count) < 0:
frappe.throw(_("Seat count cannot be negative."))
students = frappe.db.count("LMS Batch Enrollment", {"batch": self.name}) students = frappe.db.count("LMS Batch Enrollment", {"batch": self.name})
if cint(self.seat_count) < students: if cint(self.seat_count) < students:
frappe.throw(_("There are no seats available in this batch.")) frappe.throw(_("There are no seats available in this batch."))
@@ -208,86 +216,6 @@ def authenticate():
return response.json()["access_token"] return response.json()["access_token"]
@frappe.whitelist()
def create_batch(
title,
start_date,
end_date,
description=None,
batch_details=None,
batch_details_raw=None,
meta_image=None,
seat_count=0,
start_time=None,
end_time=None,
medium="Online",
category=None,
paid_batch=0,
amount=0,
currency=None,
amount_usd=0,
name=None,
published=0,
evaluation_end_date=None,
):
frappe.only_for("Moderator")
if name:
doc = frappe.get_doc("LMS Batch", name)
else:
doc = frappe.get_doc({"doctype": "LMS Batch"})
doc.update(
{
"title": title,
"start_date": start_date,
"end_date": end_date,
"description": description,
"batch_details": batch_details,
"batch_details_raw": batch_details_raw,
"meta_image": meta_image,
"seat_count": seat_count,
"start_time": start_time,
"end_time": end_time,
"medium": medium,
"category": category,
"paid_batch": paid_batch,
"amount": amount,
"currency": currency,
"amount_usd": amount_usd,
"published": published,
"evaluation_end_date": evaluation_end_date,
}
)
doc.save()
return doc
@frappe.whitelist()
def add_course(course, parent, name=None, evaluator=None):
frappe.only_for("Moderator")
if frappe.db.exists("Batch Course", {"course": course, "parent": parent}):
frappe.throw(_("Course already added to the batch."))
if name:
doc = frappe.get_doc("Batch Course", name)
else:
doc = frappe.new_doc("Batch Course")
doc.update(
{
"course": course,
"evaluator": evaluator,
"parent": parent,
"parentfield": "courses",
"parenttype": "LMS Batch",
}
)
doc.save()
return doc.name
@frappe.whitelist() @frappe.whitelist()
def get_batch_timetable(batch): def get_batch_timetable(batch):
timetable = frappe.get_all( timetable = frappe.get_all(
+67 -41
View File
@@ -96,9 +96,7 @@ def set_total_marks(questions):
@frappe.whitelist() @frappe.whitelist()
def quiz_summary(quiz, results): def quiz_summary(quiz, results):
score = 0
results = results and json.loads(results) results = results and json.loads(results)
is_open_ended = False
percentage = 0 percentage = 0
quiz_details = frappe.db.get_value( quiz_details = frappe.db.get_value(
@@ -108,7 +106,32 @@ def quiz_summary(quiz, results):
as_dict=1, as_dict=1,
) )
data = process_results(results, quiz)
results = data["results"]
score = data["score"]
is_open_ended = data["is_open_ended"]
score_out_of = quiz_details.total_marks score_out_of = quiz_details.total_marks
percentage = (score / score_out_of) * 100 if score_out_of else 0
submission = create_submission(
quiz, results, score_out_of, quiz_details.passing_percentage
)
save_progress_after_quiz(quiz_details, percentage)
return {
"score": score,
"score_out_of": score_out_of,
"submission": submission.name,
"pass": percentage == quiz_details.passing_percentage,
"percentage": percentage,
"is_open_ended": is_open_ended,
}
def process_results(results, quiz):
score = 0
is_open_ended = False
for result in results: for result in results:
question_details = frappe.db.get_value( question_details = frappe.db.get_value(
@@ -123,55 +146,28 @@ def quiz_summary(quiz, results):
result["marks_out_of"] = question_details.marks result["marks_out_of"] = question_details.marks
if question_details.type != "Open Ended": if question_details.type != "Open Ended":
correct = result["is_correct"][0] if len(result["is_correct"]) > 0:
for point in result["is_correct"]: correct = result["is_correct"][0]
correct = correct and point for point in result["is_correct"]:
result["is_correct"] = correct correct = correct and point
result["is_correct"] = correct
else:
result["is_correct"] = 0
marks = question_details.marks if correct else 0 marks = question_details.marks if correct else 0
result["marks"] = marks result["marks"] = marks
score += marks score += marks
else: else:
result["is_correct"] = 0
is_open_ended = True is_open_ended = True
result["is_correct"] = 0
percentage = (score / score_out_of) * 100 result["answer"] = re.sub(
result["answer"] = re.sub( r'<img[^>]*src\s*=\s*["\'](?=data:)(.*?)["\']', _save_file, result["answer"]
r'<img[^>]*src\s*=\s*["\'](?=data:)(.*?)["\']', _save_file, result["answer"] )
)
submission = frappe.new_doc("LMS Quiz Submission")
# Score and percentage are calculated by the controller function
submission.update(
{
"doctype": "LMS Quiz Submission",
"quiz": quiz,
"result": results,
"score": 0,
"score_out_of": score_out_of,
"member": frappe.session.user,
"percentage": 0,
"passing_percentage": quiz_details.passing_percentage,
}
)
submission.save(ignore_permissions=True)
if (
percentage >= quiz_details.passing_percentage
and quiz_details.lesson
and quiz_details.course
):
save_progress(quiz_details.lesson, quiz_details.course)
elif not quiz_details.passing_percentage:
save_progress(quiz_details.lesson, quiz_details.course)
return { return {
"results": results,
"score": score, "score": score,
"score_out_of": score_out_of,
"submission": submission.name,
"pass": percentage == quiz_details.passing_percentage,
"percentage": percentage,
"is_open_ended": is_open_ended, "is_open_ended": is_open_ended,
} }
@@ -219,6 +215,36 @@ def get_corrupted_image_msg():
return _("Image: Corrupted Data Stream") return _("Image: Corrupted Data Stream")
def create_submission(quiz, results, score_out_of, passing_percentage):
submission = frappe.new_doc("LMS Quiz Submission")
# Score and percentage are calculated by the controller function
submission.update(
{
"doctype": "LMS Quiz Submission",
"quiz": quiz,
"result": results,
"score": 0,
"score_out_of": score_out_of,
"member": frappe.session.user,
"percentage": 0,
"passing_percentage": passing_percentage,
}
)
submission.save(ignore_permissions=True)
return submission
def save_progress_after_quiz(quiz_details, percentage):
if (
percentage >= quiz_details.passing_percentage
and quiz_details.lesson
and quiz_details.course
):
save_progress(quiz_details.lesson, quiz_details.course)
elif not quiz_details.passing_percentage:
save_progress(quiz_details.lesson, quiz_details.course)
@frappe.whitelist() @frappe.whitelist()
def get_question_details(question): def get_question_details(question):
if frappe.db.exists("LMS Quiz Question", question): if frappe.db.exists("LMS Quiz Question", question):
+11 -1
View File
@@ -403,7 +403,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2025-04-22 16:05:27.914422", "modified": "2025-05-14 12:43:22.749850",
"modified_by": "sayali@frappe.io", "modified_by": "sayali@frappe.io",
"module": "LMS", "module": "LMS",
"name": "LMS Settings", "name": "LMS Settings",
@@ -425,6 +425,16 @@
"read": 1, "read": 1,
"role": "LMS Student", "role": "LMS Student",
"share": 1 "share": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"role": "Moderator",
"share": 1,
"write": 1
} }
], ],
"row_format": "Dynamic", "row_format": "Dynamic",
+403 -434
View File
File diff suppressed because it is too large Load Diff
+405 -436
View File
File diff suppressed because it is too large Load Diff
+404 -435
View File
File diff suppressed because it is too large Load Diff
+405 -436
View File
File diff suppressed because it is too large Load Diff
+405 -436
View File
File diff suppressed because it is too large Load Diff
+406 -437
View File
File diff suppressed because it is too large Load Diff
+405 -436
View File
File diff suppressed because it is too large Load Diff
+403 -434
View File
File diff suppressed because it is too large Load Diff
+403 -434
View File
File diff suppressed because it is too large Load Diff
+403 -434
View File
File diff suppressed because it is too large Load Diff
+403 -434
View File
File diff suppressed because it is too large Load Diff
+403 -434
View File
File diff suppressed because it is too large Load Diff
+403 -434
View File
File diff suppressed because it is too large Load Diff
+404 -435
View File
File diff suppressed because it is too large Load Diff
+494 -525
View File
File diff suppressed because it is too large Load Diff
+406 -437
View File
File diff suppressed because it is too large Load Diff
+403 -434
View File
File diff suppressed because it is too large Load Diff
+405 -436
View File
File diff suppressed because it is too large Load Diff
+405 -436
View File
File diff suppressed because it is too large Load Diff
-450
View File
@@ -1,450 +0,0 @@
frappe.ready(() => {
setup_file_size();
pin_header();
$(".enroll-in-course").click((e) => {
enroll_in_course(e);
});
$(".notify-me").click((e) => {
notify_user(e);
});
$(".nav-link").click((e) => {
change_hash(e);
});
if (window.location.hash) {
open_tab();
}
if (window.location.pathname == "/statistics") {
generate_graph("New Signups", "#new-signups");
generate_graph("Course Enrollments", "#course-enrollments");
generate_graph("Lesson Completion", "#lesson-completion");
generate_course_completion_graph();
}
expand_the_active_chapter();
$(".chapter-title")
.unbind()
.click((e) => {
rotate_chapter_icon(e);
});
$(".no-preview").click((e) => {
show_no_preview_dialog(e);
});
$("#create-batch").click((e) => {
open_batch_dialog(e);
});
$("#course-filter").change((e) => {
filter_courses(e);
});
});
const pin_header = () => {
const el = document.querySelector(".sticky");
if (el) {
const observer = new IntersectionObserver(
([e]) =>
e.target.classList.toggle("is-pinned", e.intersectionRatio < 1),
{ threshold: [1] }
);
observer.observe(el);
}
};
const setup_file_size = () => {
frappe.provide("frappe.form.formatters");
frappe.form.formatters.FileSize = file_size;
};
const file_size = (value) => {
if (value > 1048576) {
value = flt(flt(value) / 1048576, 1) + "M";
} else if (value > 1024) {
value = flt(flt(value) / 1024, 1) + "K";
}
return value;
};
const enroll_in_course = (e) => {
e.preventDefault();
let course = $(e.currentTarget).attr("data-course");
if (frappe.session.user == "Guest") {
window.location.href = `/login?redirect-to=/courses/${course}`;
return;
}
let batch = $(e.currentTarget).attr("data-batch");
batch = batch ? decodeURIComponent(batch) : "";
frappe.call({
method: "lms.lms.doctype.lms_enrollment.lms_enrollment.create_membership",
args: {
batch: batch ? batch : "",
course: course,
},
callback: (data) => {
if (data.message == "OK") {
$(".no-preview-modal").modal("hide");
frappe.show_alert(
{
message: __("Enrolled successfully"),
indicator: "green",
},
3
);
setTimeout(function () {
window.location.href = `/courses/${course}/learn/1.1`;
}, 1000);
}
},
});
};
const notify_user = (e) => {
e.preventDefault();
var course = decodeURIComponent($("#outline-heading").attr("data-course"));
if (frappe.session.user == "Guest") {
window.location.href = `/login?redirect-to=/courses/${course}`;
return;
}
frappe.call({
method: "lms.lms.doctype.lms_course_interest.lms_course_interest.capture_interest",
args: {
course: course,
},
callback: (data) => {
$(".no-preview-modal").modal("hide");
frappe.show_alert(
{
message: __(
"You have opted to be notified for this course. You will receive an email when the course becomes available."
),
indicator: "green",
},
3
);
setTimeout(() => {
window.location.reload();
}, 3000);
},
});
};
const generate_graph = (chart_name, element, type = "line") => {
let date = frappe.datetime;
frappe.call({
method: "lms.lms.utils.get_chart_data",
args: {
chart_name: chart_name,
timespan: "Select Date Range",
timegrain: "Daily",
from_date: date.add_days(date.get_today(), -30),
to_date: date.add_days(date.get_today(), +1),
},
callback: (data) => {
render_chart(data.message, chart_name, element, type);
},
});
};
const render_chart = (data, chart_name, element, type) => {
const chart = new frappe.Chart(element, {
title: chart_name,
data: data,
type: type,
height: 250,
colors: ["#4563f1"],
axisOptions: {
xIsSeries: 1,
},
lineOptions: {
regionFill: 1,
},
});
};
const generate_course_completion_graph = () => {
frappe.call({
method: "lms.lms.utils.get_course_completion_data",
callback: (data) => {
render_chart(
data.message,
"Course Completion",
"#course-completion",
"pie"
);
},
});
};
const change_hash = (e) => {
window.location.hash = $(e.currentTarget).attr("href");
};
const open_tab = () => {
$(`a[href="${window.location.hash}"]`).click();
};
const expand_the_first_chapter = () => {
let elements = $(".course-home-outline .collapse");
elements.each((i, element) => {
if (i < 1) {
show_section(element);
return false;
}
});
};
const expand_the_active_chapter = () => {
let selector = $(".course-home-headings.title");
if (selector.length && $(".course-details-page").length) {
expand_for_course_details(selector);
} else if ($(".active-lesson").length) {
/* For course home page */
selector = $(".active-lesson");
show_section(selector.parent().parent());
} else {
/* If no active chapter then exapand the first chapter */
expand_the_first_chapter();
}
};
const expand_for_course_details = (selector) => {
$(".lesson-info").removeClass("active-lesson");
$(".lesson-info").each((i, elem) => {
if ($(elem).data("lesson") == selector.data("lesson")) {
$(elem).addClass("active-lesson");
show_section($(elem).parent().parent());
}
});
};
const show_section = (element) => {
$(element).addClass("show");
$(element)
.siblings(".chapter-title")
.children(".chapter-icon")
.css("transform", "rotate(90deg)");
$(element).siblings(".chapter-title").attr("aria-expanded", true);
};
const rotate_chapter_icon = (e) => {
let icon = $(e.currentTarget).children(".chapter-icon");
if (icon.css("transform") == "none") {
icon.css("transform", "rotate(90deg)");
} else {
icon.css("transform", "none");
}
};
const show_no_preview_dialog = (e) => {
$("#no-preview-modal").modal("show");
};
const open_batch_dialog = () => {
this.batch_dialog = new frappe.ui.Dialog({
title: __("New Batch"),
fields: [
{
fieldtype: "Data",
label: __("Title"),
fieldname: "title",
reqd: 1,
default: batch_info && batch_info.title,
},
{
fieldtype: "Check",
label: __("Published"),
fieldname: "published",
default: batch_info && batch_info.published,
},
{
fieldtype: "Section Break",
},
{
fieldtype: "Date",
label: __("Start Date"),
fieldname: "start_date",
reqd: 1,
default: batch_info && batch_info.start_date,
},
{
fieldtype: "Date",
label: __("End Date"),
fieldname: "end_date",
reqd: 1,
default: batch_info && batch_info.end_date,
},
{
fieldtype: "Column Break",
},
{
fieldtype: "Time",
label: __("Start Time"),
fieldname: "start_time",
default: batch_info && batch_info.start_time,
reqd: 1,
},
{
fieldtype: "Time",
label: __("End Time"),
fieldname: "end_time",
default: batch_info && batch_info.end_time,
reqd: 1,
},
{
fieldtype: "Section Break",
},
{
fieldtype: "Select",
label: __("Medium"),
fieldname: "medium",
options: ["Online", "Offline"],
default: (batch_info && batch_info.medium) || "Online",
},
{
fieldtype: "Link",
label: __("Category"),
fieldname: "category",
options: "LMS Category",
only_select: 1,
default: batch_info && batch_info.category,
},
{
fieldtype: "Column Break",
},
{
fieldtype: "Int",
label: __("Seat Count"),
fieldname: "seat_count",
default: batch_info && batch_info.seat_count,
},
{
fieldtype: "Date",
label: __("Evaluation End Date"),
fieldname: "evaluation_end_date",
default: batch_info && batch_info.evaluation_end_date,
},
{
fieldtype: "Section Break",
},
{
fieldtype: "Small Text",
label: __("Description"),
fieldname: "description",
default: batch_info && batch_info.description,
reqd: 1,
},
{
fieldtype: "Text Editor",
label: __("Batch Details"),
fieldname: "batch_details",
default: batch_info && batch_info.batch_details,
reqd: 1,
},
{
fieldtype: "HTML Editor",
label: __("Batch Details Raw"),
fieldname: "batch_details_raw",
default: batch_info && batch_info.batch_details_raw,
},
{
fieldtype: "Attach Image",
label: __("Meta Image"),
fieldname: "meta_image",
default: batch_info && batch_info.meta_image,
},
{
fieldtype: "Section Break",
label: __("Pricing"),
fieldname: "pricing",
},
{
fieldtype: "Check",
label: __("Paid Batch"),
fieldname: "paid_batch",
default: batch_info && batch_info.paid_batch,
},
{
fieldtype: "Currency",
label: __("Amount"),
fieldname: "amount",
default: batch_info && batch_info.amount,
mandatory_depends_on: "paid_batch",
depends_on: "paid_batch",
},
{
fieldtype: "Link",
label: __("Currency"),
fieldname: "currency",
options: "Currency",
default: batch_info && batch_info.currency,
mandatory_depends_on: "paid_batch",
depends_on: "paid_batch",
only_select: 1,
},
{
fieldtype: "Currency",
label: __("Amount (USD)"),
fieldname: "amount_usd",
depends_on: "paid_batch",
description: __(
"If you set an amount here, then the USD equivalent setting will not get applied."
),
},
],
primary_action_label: __("Save"),
primary_action: (values) => {
save_batch(values);
},
});
this.batch_dialog.show();
};
const save_batch = (values) => {
let args = {};
if (batch_info) {
args = Object.assign(batch_info, values);
} else {
args = values;
}
frappe.call({
method: "lms.lms.doctype.lms_batch.lms_batch.create_batch",
args: args,
callback: (r) => {
if (r.message) {
frappe.show_alert({
message: batch_info
? __("Batch Updated")
: __("Batch Created"),
indicator: "green",
});
this.batch_dialog.hide();
window.location.href = `/batches/details/${r.message.name}`;
}
},
});
};
const filter_courses = (e) => {
const course_lists = $(".course-cards-parent");
const filter = $(e.currentTarget).val();
course_lists.each((i, list) => {
const course_cards = $(list).children(".course-card");
course_cards.sort((a, b) => {
var value1 = $(a).data(filter);
var value2 = $(b).data(filter);
return value1 > value2 ? -1 : value1 < value2 ? 1 : 0;
});
$(list).append(course_cards);
});
};
-14
View File
@@ -1,14 +0,0 @@
frappe.ready(() => {
hide_profile_and_dashboard_for_guest_users();
});
const hide_profile_and_dashboard_for_guest_users = () => {
if (frappe.session.user == "Guest") {
let links = $(".nav-link").filter(
(i, elem) =>
$(elem).text().trim() === "My Profile" ||
$(elem).text().trim() === "Dashboard"
);
links.length && links.each((i, elem) => $(elem).addClass("hide"));
}
};
-5
View File
@@ -1,5 +0,0 @@
import "./profile.js";
import "./common_functions.js";
import "../../../../frappe/frappe/public/js/frappe/ui/chart.js";
import "../../../../frappe/frappe/public/js/frappe/ui/keyboard.js";
import "../../../../frappe/frappe/public/js/frappe/event_emitter.js";
@@ -11,9 +11,11 @@
<b>{{ _("Batch Start Date:") }}</b> {{ frappe.utils.format_date(start_date, "medium") }} <b>{{ _("Batch Start Date:") }}</b> {{ frappe.utils.format_date(start_date, "medium") }}
</p> </p>
{% if medium %}
<p> <p>
<b>{{ _("Medium:") }}</b> {{ medium }} <b>{{ _("Medium:") }}</b> {{ medium }}
</p> </p>
{% endif %}
<p> <p>
<b>{{ _("Timings:") }}</b> {{ frappe.utils.format_time(start_time, "hh:mm a") }} <b>{{ _("Timings:") }}</b> {{ frappe.utils.format_time(start_time, "hh:mm a") }}
-31
View File
@@ -1,31 +0,0 @@
{% extends "templates/base.html" %}
{% block content %}
{% include "public/icons/symbol-defs.svg" %}
{% include "lms/templates/onboarding_header.html" %}
{% block page_content %}
Hello, world!
{% endblock %}
{% endblock %}
{%- block script -%}
{{ super() }}
{% if frappe.get_system_settings("enable_telemetry") %}
{% set telemetry_boot_info = get_telemetry_boot_info() %}
<script>
const telemetry_boot_info = {{ get_telemetry_boot_info() }}
if (telemetry_boot_info && Object.keys(telemetry_boot_info).length)
Object.assign(frappe.boot, telemetry_boot_info)
</script>
{% endif %}
<script>
frappe.router = {
slug(name) {
return name.toLowerCase().replace(/ /g, "-");
},
};
frappe.utils.make_event_emitter(frappe.router)
</script>
{{ include_script("telemetry.bundle.js") }}
{%- endblock -%}
+52 -106
View File
@@ -129,9 +129,9 @@
"@lezer/highlight" "^1.0.0" "@lezer/highlight" "^1.0.0"
"@codemirror/view@^6.0.0", "@codemirror/view@^6.22.0", "@codemirror/view@^6.23.0", "@codemirror/view@^6.27.0", "@codemirror/view@^6.35.0": "@codemirror/view@^6.0.0", "@codemirror/view@^6.22.0", "@codemirror/view@^6.23.0", "@codemirror/view@^6.27.0", "@codemirror/view@^6.35.0":
version "6.36.7" version "6.36.8"
resolved "https://registry.yarnpkg.com/@codemirror/view/-/view-6.36.7.tgz#f3711d3fea457a3eec1c09a76b8a132f98df17b1" resolved "https://registry.yarnpkg.com/@codemirror/view/-/view-6.36.8.tgz#d72e59d2b2d99bb5f177178f63e11cef6e605b94"
integrity sha512-kCWGW/chWGPgZqfZ36Um9Iz0X2IVpmCjg1P/qY6B6a2ecXtWRRAigmpJ6YgUQ5lTWXMyyVdfmpzhLZmsZQMbtg== integrity sha512-yoRo4f+FdnD01fFt4XpfpMCcCAo9QvZOtbrXExn4SqzH32YC6LgzqxfLZw/r6Ge65xyY03mK/UfUqrVw1gFiFg==
dependencies: dependencies:
"@codemirror/state" "^6.5.0" "@codemirror/state" "^6.5.0"
style-mod "^4.1.0" style-mod "^4.1.0"
@@ -496,17 +496,17 @@
mlly "^1.7.4" mlly "^1.7.4"
"@inquirer/confirm@^5.0.0": "@inquirer/confirm@^5.0.0":
version "5.1.9" version "5.1.10"
resolved "https://registry.yarnpkg.com/@inquirer/confirm/-/confirm-5.1.9.tgz#c858b6a3decb458241ec36ca9a9117477338076a" resolved "https://registry.yarnpkg.com/@inquirer/confirm/-/confirm-5.1.10.tgz#de3732cb7ae9333bd3e354afee6a6ef8cf28d951"
integrity sha512-NgQCnHqFTjF7Ys2fsqK2WtnA8X1kHyInyG+nMIuHowVTIgIuS10T4AznI/PvbqSpJqjCUqNBlKGh1v3bwLFL4w== integrity sha512-FxbQ9giWxUWKUk2O5XZ6PduVnH2CZ/fmMKMBkH71MHJvWr7WL5AHKevhzF1L5uYWB2P548o1RzVxrNd3dpmk6g==
dependencies: dependencies:
"@inquirer/core" "^10.1.10" "@inquirer/core" "^10.1.11"
"@inquirer/type" "^3.0.6" "@inquirer/type" "^3.0.6"
"@inquirer/core@^10.1.10": "@inquirer/core@^10.1.11":
version "10.1.10" version "10.1.11"
resolved "https://registry.yarnpkg.com/@inquirer/core/-/core-10.1.10.tgz#222a374e3768536a1eb0adf7516c436d5f4a291d" resolved "https://registry.yarnpkg.com/@inquirer/core/-/core-10.1.11.tgz#4022032b5b6b35970e1c3fcfc522bc250ef8810d"
integrity sha512-roDaKeY1PYY0aCqhRmXihrHjoSW2A00pV3Ke5fTpMCkzcGF64R8e0lw3dK+eLEHwS4vB5RnW1wuQmvzoRul8Mw== integrity sha512-BXwI/MCqdtAhzNQlBEFE7CEflhPkl/BqvAuV/aK6lW3DClIfYVDWPP/kXuXHtBWC7/EEbNqd/1BGq2BGBBnuxw==
dependencies: dependencies:
"@inquirer/figures" "^1.0.11" "@inquirer/figures" "^1.0.11"
"@inquirer/type" "^3.0.6" "@inquirer/type" "^3.0.6"
@@ -1170,9 +1170,9 @@
integrity sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg== integrity sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==
"@types/node@*": "@types/node@*":
version "22.15.16" version "22.15.17"
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.15.16.tgz#685cf0338ad9f5b14860f50a6ac2c3ebd58582cd" resolved "https://registry.yarnpkg.com/@types/node/-/node-22.15.17.tgz#355ccec95f705b664e4332bb64a7f07db30b7055"
integrity sha512-3pr+KjwpVujqWqOKT8mNR+rd09FqhBLwg+5L/4t0cNYBzm/yEiYGCxWttjaPBsLtAo+WFNoXzGJfolM1JuRXoA== integrity sha512-wIX2aSZL5FE+MR0JlvF87BNVrtFWf6AE6rxSE9X7OwnVvoyCQjpzSRJ+M87se/4QCkCiebQAqrJ0y6fwIyi7nw==
dependencies: dependencies:
undici-types "~6.21.0" undici-types "~6.21.0"
@@ -1224,9 +1224,9 @@
integrity sha512-kqf7SGFoG+80aZG6Pf+gsZIVvGSCKE98JbiWqcCV9cThtg91Jav0yvYFC9Zb+jKetNGF6ZKeoaxgZfND21fWKw== integrity sha512-kqf7SGFoG+80aZG6Pf+gsZIVvGSCKE98JbiWqcCV9cThtg91Jav0yvYFC9Zb+jKetNGF6ZKeoaxgZfND21fWKw==
"@vitejs/plugin-vue@^5.0.3": "@vitejs/plugin-vue@^5.0.3":
version "5.2.3" version "5.2.4"
resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-5.2.3.tgz#71a8fc82d4d2e425af304c35bf389506f674d89b" resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz#9e8a512eb174bfc2a333ba959bbf9de428d89ad8"
integrity sha512-IYSLEQj4LgZZuoVpdSUCw3dIynTWQgPlaRP6iAvMle4My0HdYwr5g5wQAfwOeHQBmYwEkqF70nRpSilr6PoUDg== integrity sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==
"@vitest/expect@2.1.9": "@vitest/expect@2.1.9":
version "2.1.9" version "2.1.9"
@@ -1756,9 +1756,9 @@ camelcase-css@^2.0.1:
integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA== integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==
caniuse-lite@^1.0.30001702, caniuse-lite@^1.0.30001716: caniuse-lite@^1.0.30001702, caniuse-lite@^1.0.30001716:
version "1.0.30001717" version "1.0.30001718"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001717.tgz#5d9fec5ce09796a1893013825510678928aca129" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001718.tgz#dae13a9c80d517c30c6197515a96131c194d8f82"
integrity sha512-auPpttCq6BDEG8ZAuHJIplGw6GODhjw+/11e7IjpnYCxZcW/ONgPs0KVBJ0d1bY3e2+7PRe5RCLyP+PfwVgkYw== integrity sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==
capital-case@^1.0.4: capital-case@^1.0.4:
version "1.0.4" version "1.0.4"
@@ -2061,7 +2061,7 @@ cross-spawn@^5.0.1:
shebang-command "^1.2.0" shebang-command "^1.2.0"
which "^1.2.9" which "^1.2.9"
cross-spawn@^7.0.0, cross-spawn@^7.0.3, cross-spawn@^7.0.6: cross-spawn@^7.0.0, cross-spawn@^7.0.6:
version "7.0.6" version "7.0.6"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f"
integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==
@@ -2322,9 +2322,9 @@ ee-first@1.1.1:
integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==
electron-to-chromium@^1.5.149: electron-to-chromium@^1.5.149:
version "1.5.151" version "1.5.152"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.151.tgz#5edd6c17e1b2f14b4662c41b9379f96cc8c2bb7c" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.152.tgz#bcdd39567e291b930ec26b930031137a05593695"
integrity sha512-Rl6uugut2l9sLojjS4H4SAr3A4IgACMLgpuEMPYCVcKydzfyPrn5absNRju38IhQOf/NwjJY8OGWjlteqYeBCA== integrity sha512-xBOfg/EBaIlVsHipHl2VdTPJRSvErNUaqW8ejTq5OlOlIYx1wOllCHsAvAIrr55jD1IYEfdR86miUEt8H5IeJg==
emoji-regex@^10.3.0: emoji-regex@^10.3.0:
version "10.4.0" version "10.4.0"
@@ -2541,21 +2541,6 @@ execa@4.1.0:
signal-exit "^3.0.2" signal-exit "^3.0.2"
strip-final-newline "^2.0.0" strip-final-newline "^2.0.0"
execa@^8.0.1:
version "8.0.1"
resolved "https://registry.yarnpkg.com/execa/-/execa-8.0.1.tgz#51f6a5943b580f963c3ca9c6321796db8cc39b8c"
integrity sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==
dependencies:
cross-spawn "^7.0.3"
get-stream "^8.0.1"
human-signals "^5.0.0"
is-stream "^3.0.0"
merge-stream "^2.0.0"
npm-run-path "^5.1.0"
onetime "^6.0.0"
signal-exit "^4.1.0"
strip-final-newline "^3.0.0"
executable@^4.1.1: executable@^4.1.1:
version "4.1.1" version "4.1.1"
resolved "https://registry.yarnpkg.com/executable/-/executable-4.1.1.tgz#41532bff361d3e57af4d763b70582db18f5d133c" resolved "https://registry.yarnpkg.com/executable/-/executable-4.1.1.tgz#41532bff361d3e57af4d763b70582db18f5d133c"
@@ -2677,9 +2662,9 @@ finalhandler@1.1.2:
unpipe "~1.0.0" unpipe "~1.0.0"
flexsearch@*: flexsearch@*:
version "0.8.161" version "0.8.164"
resolved "https://registry.yarnpkg.com/flexsearch/-/flexsearch-0.8.161.tgz#fe964fe1841a99b580ba19f853603d1747fb3bc8" resolved "https://registry.yarnpkg.com/flexsearch/-/flexsearch-0.8.164.tgz#2d1277249d6dec8eb745358fa64543ddbddc9e05"
integrity sha512-3dlwyJpl7PN4vRTp0pV8FkCULPzeuK6JzZYE3KhLiv5vbhUPGUqiOWgBbjlpCC4Rl1aW7B0lJeOS0Cr8G0N12g== integrity sha512-tauQG+NlwSWW6uL3BIJdHNEXYiei2xTR3H/mrjNadAOwvXgiLbLvLzQWSJvD31Yn0+1lAxG2NVRndywGnDZZiA==
flexsearch@0.7.21: flexsearch@0.7.21:
version "0.7.21" version "0.7.21"
@@ -2835,11 +2820,6 @@ get-stream@^5.0.0, get-stream@^5.1.0:
dependencies: dependencies:
pump "^3.0.0" pump "^3.0.0"
get-stream@^8.0.1:
version "8.0.1"
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-8.0.1.tgz#def9dfd71742cd7754a7761ed43749a27d02eca2"
integrity sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==
getos@^3.2.1: getos@^3.2.1:
version "3.2.1" version "3.2.1"
resolved "https://registry.yarnpkg.com/getos/-/getos-3.2.1.tgz#0134d1f4e00eb46144c5a9c0ac4dc087cbb27dc5" resolved "https://registry.yarnpkg.com/getos/-/getos-3.2.1.tgz#0134d1f4e00eb46144c5a9c0ac4dc087cbb27dc5"
@@ -3045,11 +3025,6 @@ human-signals@^1.1.1:
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3"
integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw== integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==
human-signals@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-5.0.0.tgz#42665a284f9ae0dade3ba41ebc37eb4b852f3a28"
integrity sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==
iconv-lite@0.6.3: iconv-lite@0.6.3:
version "0.6.3" version "0.6.3"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501"
@@ -3058,9 +3033,9 @@ iconv-lite@0.6.3:
safer-buffer ">= 2.1.2 < 3.0.0" safer-buffer ">= 2.1.2 < 3.0.0"
idb-keyval@^6.2.0: idb-keyval@^6.2.0:
version "6.2.1" version "6.2.2"
resolved "https://registry.yarnpkg.com/idb-keyval/-/idb-keyval-6.2.1.tgz#94516d625346d16f56f3b33855da11bfded2db33" resolved "https://registry.yarnpkg.com/idb-keyval/-/idb-keyval-6.2.2.tgz#b0171b5f73944854a3291a5cdba8e12768c4854a"
integrity sha512-8Sb3veuYCyrZL+VBt9LJfZjLUPWVvqn8tG28VqYNFCo43KHcKuq+b4EiXGeuaLAQWL2YmyDgMp2aSpH9JHsEQg== integrity sha512-yjD9nARJ/jb1g+CvD0tlhUHOrJ9Sy0P8T9MF3YaLlHnSRpwPfpTX0XIvpmw3gAJUmEu3FiICLBDPXVwyEvrleg==
ieee754@^1.1.13: ieee754@^1.1.13:
version "1.2.1" version "1.2.1"
@@ -3173,11 +3148,6 @@ is-stream@^2.0.0:
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077"
integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==
is-stream@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-3.0.0.tgz#e6bfd7aa6bef69f4f472ce9bb681e3e57b4319ac"
integrity sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==
is-typedarray@~1.0.0: is-typedarray@~1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
@@ -3349,20 +3319,20 @@ linkifyjs@^4.2.0:
integrity sha512-DRSlB9DKVW04c4SUdGvKK5FR6be45lTU9M76JnngqPeeGDqPwYc0zdUErtsNVMtxPXgUWV4HbXbnC4sNyBxkYg== integrity sha512-DRSlB9DKVW04c4SUdGvKK5FR6be45lTU9M76JnngqPeeGDqPwYc0zdUErtsNVMtxPXgUWV4HbXbnC4sNyBxkYg==
lint-staged@>=10: lint-staged@>=10:
version "15.5.2" version "16.0.0"
resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-15.5.2.tgz#beff028fd0681f7db26ffbb67050a21ed4d059a3" resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-16.0.0.tgz#31826709bde6a62542431da3055f038e386a20db"
integrity sha512-YUSOLq9VeRNAo/CTaVmhGDKG+LBtA8KF1X4K5+ykMSwWST1vDxJRB2kv2COgLb1fvpCo+A/y9A0G0znNVmdx4w== integrity sha512-sUCprePs6/rbx4vKC60Hez6X10HPkpDJaGcy3D1NdwR7g1RcNkWL8q9mJMreOqmHBTs+1sNFp+wOiX9fr+hoOQ==
dependencies: dependencies:
chalk "^5.4.1" chalk "^5.4.1"
commander "^13.1.0" commander "^13.1.0"
debug "^4.4.0" debug "^4.4.0"
execa "^8.0.1"
lilconfig "^3.1.3" lilconfig "^3.1.3"
listr2 "^8.2.5" listr2 "^8.3.3"
micromatch "^4.0.8" micromatch "^4.0.8"
nano-spawn "^1.0.0"
pidtree "^0.6.0" pidtree "^0.6.0"
string-argv "^0.3.2" string-argv "^0.3.2"
yaml "^2.7.0" yaml "^2.7.1"
listr2@^3.8.3: listr2@^3.8.3:
version "3.14.0" version "3.14.0"
@@ -3378,7 +3348,7 @@ listr2@^3.8.3:
through "^2.3.8" through "^2.3.8"
wrap-ansi "^7.0.0" wrap-ansi "^7.0.0"
listr2@^8.2.5: listr2@^8.3.3:
version "8.3.3" version "8.3.3"
resolved "https://registry.yarnpkg.com/listr2/-/listr2-8.3.3.tgz#815fc8f738260ff220981bf9e866b3e11e8121bf" resolved "https://registry.yarnpkg.com/listr2/-/listr2-8.3.3.tgz#815fc8f738260ff220981bf9e866b3e11e8121bf"
integrity sha512-LWzX2KsqcB1wqQ4AHgYb4RsDXauQiqhjLk+6hjbaeHG4zpjjVAB6wC/gz6X0l+Du1cN3pUB5ZlrvTbhGSNnUQQ== integrity sha512-LWzX2KsqcB1wqQ4AHgYb4RsDXauQiqhjLk+6hjbaeHG4zpjjVAB6wC/gz6X0l+Du1cN3pUB5ZlrvTbhGSNnUQQ==
@@ -3597,11 +3567,6 @@ mimic-fn@^2.1.0:
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
mimic-fn@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-4.0.0.tgz#60a90550d5cb0b239cca65d893b1a53b29871ecc"
integrity sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==
mimic-function@^5.0.0: mimic-function@^5.0.0:
version "5.0.1" version "5.0.1"
resolved "https://registry.yarnpkg.com/mimic-function/-/mimic-function-5.0.1.tgz#acbe2b3349f99b9deaca7fb70e48b83e94e67076" resolved "https://registry.yarnpkg.com/mimic-function/-/mimic-function-5.0.1.tgz#acbe2b3349f99b9deaca7fb70e48b83e94e67076"
@@ -3665,9 +3630,9 @@ ms@^2.1.1, ms@^2.1.3:
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
msw@^2.7.0: msw@^2.7.0:
version "2.7.6" version "2.8.2"
resolved "https://registry.yarnpkg.com/msw/-/msw-2.7.6.tgz#1471ce4311f4c173f287dced31dee211b6958deb" resolved "https://registry.yarnpkg.com/msw/-/msw-2.8.2.tgz#a35545275403da472f4ed2152cd3c77000db544c"
integrity sha512-P+rwn43ktxN8ghcl8q+hSAUlEi0PbJpDhGmDkw4zeUnRj3hBCVynWD+dTu38yLYKCE9ZF1OYcvpy7CTBRcqkZA== integrity sha512-ugu8RBgUj6//RD0utqDDPdS+QIs36BKYkDAM6u59hcMVtFM4PM0vW4l3G1R+1uCWP2EWFUG8reT/gPXVEtx7/w==
dependencies: dependencies:
"@bundled-es-modules/cookie" "^2.0.1" "@bundled-es-modules/cookie" "^2.0.1"
"@bundled-es-modules/statuses" "^1.0.1" "@bundled-es-modules/statuses" "^1.0.1"
@@ -3702,6 +3667,11 @@ mz@^2.7.0:
object-assign "^4.0.1" object-assign "^4.0.1"
thenify-all "^1.0.0" thenify-all "^1.0.0"
nano-spawn@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/nano-spawn/-/nano-spawn-1.0.1.tgz#c8e4c1e133e567e3efba44041dcfb12113d861b6"
integrity sha512-BfcvzBlUTxSDWfT+oH7vd6CbUV+rThLLHCIym/QO6GGLBsyVXleZs00fto2i2jzC/wPiBYk5jyOmpXWg4YopiA==
nanoid@^3.3.8: nanoid@^3.3.8:
version "3.3.11" version "3.3.11"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b"
@@ -3742,13 +3712,6 @@ npm-run-path@^4.0.0:
dependencies: dependencies:
path-key "^3.0.0" path-key "^3.0.0"
npm-run-path@^5.1.0:
version "5.3.0"
resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-5.3.0.tgz#e23353d0ebb9317f174e93417e4a4d82d0249e9f"
integrity sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==
dependencies:
path-key "^4.0.0"
nwsapi@^2.2.2: nwsapi@^2.2.2:
version "2.2.20" version "2.2.20"
resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.20.tgz#22e53253c61e7b0e7e93cef42c891154bcca11ef" resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.20.tgz#22e53253c61e7b0e7e93cef42c891154bcca11ef"
@@ -3795,13 +3758,6 @@ onetime@^5.1.0:
dependencies: dependencies:
mimic-fn "^2.1.0" mimic-fn "^2.1.0"
onetime@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/onetime/-/onetime-6.0.0.tgz#7c24c18ed1fd2e9bca4bd26806a33613c77d34b4"
integrity sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==
dependencies:
mimic-fn "^4.0.0"
onetime@^7.0.0: onetime@^7.0.0:
version "7.0.0" version "7.0.0"
resolved "https://registry.yarnpkg.com/onetime/-/onetime-7.0.0.tgz#9f16c92d8c9ef5120e3acd9dd9957cceecc1ab60" resolved "https://registry.yarnpkg.com/onetime/-/onetime-7.0.0.tgz#9f16c92d8c9ef5120e3acd9dd9957cceecc1ab60"
@@ -3902,11 +3858,6 @@ path-key@^3.0.0, path-key@^3.1.0:
resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==
path-key@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/path-key/-/path-key-4.0.0.tgz#295588dc3aee64154f877adb9d780b81c554bf18"
integrity sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==
path-parse@^1.0.7: path-parse@^1.0.7:
version "1.0.7" version "1.0.7"
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
@@ -4540,9 +4491,9 @@ section-matter@^1.0.0:
kind-of "^6.0.0" kind-of "^6.0.0"
semver@^7.5.3: semver@^7.5.3:
version "7.7.1" version "7.7.2"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.1.tgz#abd5098d82b18c6c81f6074ff2647fd3e7220c9f" resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.2.tgz#67d99fdcd35cec21e6f8b87a7fd515a33f982b58"
integrity sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA== integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==
sentence-case@^3.0.4: sentence-case@^3.0.4:
version "3.0.4" version "3.0.4"
@@ -4877,11 +4828,6 @@ strip-final-newline@^2.0.0:
resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad"
integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==
strip-final-newline@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-3.0.0.tgz#52894c313fbff318835280aed60ff71ebf12b8fd"
integrity sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==
style-mod@^4.0.0, style-mod@^4.1.0: style-mod@^4.0.0, style-mod@^4.1.0:
version "4.1.2" version "4.1.2"
resolved "https://registry.yarnpkg.com/style-mod/-/style-mod-4.1.2.tgz#ca238a1ad4786520f7515a8539d5a63691d7bf67" resolved "https://registry.yarnpkg.com/style-mod/-/style-mod-4.1.2.tgz#ca238a1ad4786520f7515a8539d5a63691d7bf67"
@@ -5223,9 +5169,9 @@ unplugin-vue-components@^28.4.1:
unplugin-utils "^0.2.4" unplugin-utils "^0.2.4"
unplugin@^2.2.0, unplugin@^2.3.2: unplugin@^2.2.0, unplugin@^2.3.2:
version "2.3.2" version "2.3.3"
resolved "https://registry.yarnpkg.com/unplugin/-/unplugin-2.3.2.tgz#36c93a1662b70c97a2e2fc45c0e78fa09f7a4984" resolved "https://registry.yarnpkg.com/unplugin/-/unplugin-2.3.3.tgz#f83507e4484008e400f3d831a628eaede22c954f"
integrity sha512-3n7YA46rROb3zSj8fFxtxC/PqoyvYQ0llwz9wtUPUutr9ig09C8gGo5CWCwHrUzlqC1LLR43kxp5vEIyH1ac1w== integrity sha512-DN4DgiS13HFrAapoRmmoa9B35QzmQVRH2k58HelO28htXVNEEFZ8CGlZL0aRHXOXtz9McwY6lqaZjcc15uWMow==
dependencies: dependencies:
acorn "^8.14.1" acorn "^8.14.1"
picomatch "^4.0.2" picomatch "^4.0.2"
@@ -5563,7 +5509,7 @@ yallist@^2.1.2:
resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"
integrity sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A== integrity sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==
yaml@^2.3.4, yaml@^2.7.0: yaml@^2.3.4, yaml@^2.7.1:
version "2.7.1" version "2.7.1"
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.7.1.tgz#44a247d1b88523855679ac7fa7cda6ed7e135cf6" resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.7.1.tgz#44a247d1b88523855679ac7fa7cda6ed7e135cf6"
integrity sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ== integrity sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==