fix: notification on quiz update and mention

This commit is contained in:
Jannat Patel
2026-01-07 14:28:56 +05:30
parent ea94813d94
commit 45e98b9ddc
12 changed files with 384 additions and 152 deletions

View File

@@ -2,18 +2,21 @@
<div class="mb-4">
<div v-if="label" class="text-xs text-ink-gray-5 mb-2">
{{ __(label) }}
<span class="text-ink-red-3">*</span>
<span v-if="required" class="text-ink-red-3">*</span>
</div>
<FileUploader
v-if="!modelValue"
:fileTypes="['image/*']"
:validateFile="validateFile"
@success="(file: File) => saveImage(file)"
:fileTypes="[fileType]"
:validateFile="(file: File) => validateFile(file, true, type)"
@success="(file: File) => saveFile(file)"
>
<template v-slot="{ file, progress, uploading, openFileSelector }">
<div class="flex items-center">
<div class="border rounded-md w-fit py-7 px-20">
<Image class="size-5 stroke-1 text-ink-gray-7" />
<component
:is="props.type === 'image' ? Image : Video"
class="size-5 stroke-1 text-ink-gray-7"
/>
</div>
<div class="ml-4">
<Button @click="openFileSelector">
@@ -28,7 +31,15 @@
</FileUploader>
<div v-else class="mb-4">
<div class="flex items-center">
<img :src="modelValue" class="border rounded-md w-44 h-auto" />
<img
v-if="type == 'image'"
:src="modelValue"
class="border rounded-md w-44 h-auto"
/>
<video v-else controls class="border rounded-md w-44 h-auto">
<source :src="modelValue" />
{{ __('Your browser does not support the video tag.') }}
</video>
<div class="ml-4">
<Button @click="removeImage()">
{{ __('Remove') }}
@@ -47,7 +58,8 @@
<script setup lang="ts">
import { validateFile } from '@/utils'
import { Button, FileUploader } from 'frappe-ui'
import { Image } from 'lucide-vue-next'
import { Image, Video } from 'lucide-vue-next'
import { computed } from 'vue'
const emit = defineEmits<{
(e: 'update:modelValue', value: string): void
@@ -58,15 +70,23 @@ const props = withDefaults(
modelValue: string
label?: string
description?: string
type: 'image' | 'video'
required?: boolean
}>(),
{
modelValue: '',
label: '',
description: '',
type: 'image',
required: true,
}
)
const saveImage = (file: any) => {
const fileType = computed(() => {
return props.type === 'image' ? 'image/*' : 'video/*'
})
const saveFile = (file: any) => {
emit('update:modelValue', file.file_url)
}

View File

@@ -55,7 +55,7 @@
</div>
</div>
</div>
<div v-else-if class="text-ink-red-3">
<div v-else class="text-ink-red-3">
{{ __('No slots available for the selected course.') }}
</div>
</div>

View File

@@ -202,60 +202,12 @@
/>
</div>
<div class="space-y-5">
<div>
<div class="text-xs text-ink-gray-5">
{{ __('Meta Image') }}
</div>
<FileUploader
v-if="!batch.image"
:fileTypes="['image/*']"
:validateFile="validateFile"
@success="(file) => saveImage(file)"
>
<template
v-slot="{ file, progress, uploading, openFileSelector }"
>
<div class="flex items-center">
<div
class="border rounded-md w-fit py-5 px-5 md:px-20 cursor-pointer"
@click="openFileSelector"
>
<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 when the batch URL is shared on socials')
}}
</div>
</div>
</div>
</template>
</FileUploader>
<div v-else class="mb-4">
<div class="flex items-center">
<img
:src="batch.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 when the batch URL is shared on any online platform'
)
}}
</div>
</div>
</div>
</div>
</div>
<Uploader
v-model="batch.video_link"
:label="__('Preview Video')"
type="video"
:required="false"
/>
</div>
</div>
</div>
@@ -292,6 +244,12 @@
{{ __('Meta Tags') }}
</div>
<div class="space-y-5">
<Uploader
v-model="batch.meta_image"
:label="__('Meta Image')"
type="image"
:required="false"
/>
<FormControl
v-model="meta.description"
:label="__('Meta Description')"
@@ -345,8 +303,8 @@ import {
openSettings,
sanitizeHTML,
updateMetaInfo,
validateFile,
} from '@/utils'
import Uploader from '@/components/Controls/Uploader.vue'
const router = useRouter()
const user = inject('$user')
@@ -380,11 +338,12 @@ const batch = reactive({
category: '',
allow_self_enrollment: false,
certification: false,
image: null,
meta_image: null,
paid_batch: false,
currency: '',
amount: 0,
zoom_account: '',
video_link: '',
})
const meta = reactive({
@@ -428,7 +387,8 @@ const newBatch = createResource({
return {
doc: {
doctype: 'LMS Batch',
meta_image: batch.image?.file_url,
meta_image: batch.image,
video_link: batch.video_link,
instructors: instructors.value.map((instructor) => ({
instructor: instructor,
})),
@@ -447,39 +407,48 @@ const batchDetail = createResource({
}
},
onSuccess(data) {
Object.keys(data).forEach((key) => {
if (key == 'instructors') {
data.instructors.forEach((instructor) => {
instructors.value.push(instructor.instructor)
})
} else if (['start_time', 'end_time'].includes(key)) {
let [hours, minutes, seconds] = data[key].split(':')
hours = hours.length == 1 ? '0' + hours : hours
batch[key] = `${hours}:${minutes}`
} else if (Object.hasOwn(batch, key)) batch[key] = data[key]
})
let checkboxes = [
'published',
'paid_batch',
'allow_self_enrollment',
'certification',
]
for (let idx in checkboxes) {
let key = checkboxes[idx]
batch[key] = batch[key] ? true : false
}
if (data.meta_image) imageResource.reload({ image: data.meta_image })
updateBatchData(data)
},
})
const updateBatchData = (data) => {
Object.keys(data).forEach((key) => {
if (key == 'instructors') {
data.instructors.forEach((instructor) => {
instructors.value.push(instructor.instructor)
})
} else if (['start_time', 'end_time'].includes(key)) {
batch[key] = formatTime(data[key])
} else if (Object.hasOwn(batch, key)) batch[key] = data[key]
})
let checkboxes = [
'published',
'paid_batch',
'allow_self_enrollment',
'certification',
]
for (let idx in checkboxes) {
let key = checkboxes[idx]
batch[key] = batch[key] ? true : false
}
}
const formatTime = (timeStr) => {
let [hours, minutes, seconds] = timeStr.split(':')
hours = hours.length == 1 ? '0' + hours : hours
return `${hours}:${minutes}`
}
const editBatch = createResource({
url: 'frappe.client.set_value',
makeParams(values) {
console.log(batch.meta_image, batch.video_link)
return {
doctype: 'LMS Batch',
name: props.batchName,
fieldname: {
meta_image: batch.image?.file_url,
meta_image: batch.meta_image,
video_link: batch.video_link,
instructors: instructors.value.map((instructor) => ({
instructor: instructor,
})),
@@ -489,19 +458,6 @@ const editBatch = createResource({
},
})
const imageResource = createResource({
url: 'lms.lms.api.get_file_info',
makeParams(values) {
return {
file_url: values.image,
}
},
auto: false,
onSuccess(data) {
batch.image = data
},
})
const validateFields = () => {
batch.description = sanitizeHTML(batch.description)
batch.batch_details = sanitizeHTML(batch.batch_details)
@@ -603,14 +559,6 @@ const trashBatch = (close) => {
})
}
const saveImage = (file) => {
batch.image = file
}
const removeImage = () => {
batch.image = null
}
const breadcrumbs = computed(() => {
let crumbs = [
{

View File

@@ -23,14 +23,19 @@
v-if="notifications?.length"
v-for="log in notifications"
:key="log.name"
class="flex space-x-2 p-2 rounded-md"
class="flex space-x-2 px-2 py-4"
:class="{
'cursor-pointer': log.link,
'items-center': !showDetails(log) && !isMention(log),
}"
@click="navigateToPage(log)"
>
<Avatar :image="log.user_image" size="2xl" :label="log.full_name" />
<div class="space-y-2">
<Avatar
:image="log.from_user_details.user_image"
size="xl"
:label="log.from_user_details.full_name"
/>
<div class="space-y-2 w-full">
<div class="flex items-center justify-between">
<div class="flex items-center">
<div class="text-ink-gray-9" v-html="log.subject"></div>
@@ -42,7 +47,7 @@
<Button
variant="ghost"
v-if="!log.read"
@click.stop="(e) => handleMarkAsRead(e, log.name)"
@click.stop="(e) => handleMarkAsRead(log.name)"
>
<template #icon>
<X class="h-4 w-4 text-ink-gray-7 stroke-1.5" />
@@ -51,19 +56,39 @@
</div>
</div>
<div
v-if="log.document_type == 'LMS Course' && log.document_details"
class="flex space-x-5 border border-outline-gray-2 p-2 rounded-md"
v-if="isMention(log)"
v-html="log.email_content"
class="bg-surface-gray-2 rounded-md px-3 py-2"
></div>
<div
v-else-if="showDetails(log)"
class="flex items-stretch border border-outline-gray-2 space-x-2 rounded-md"
>
<iframe
v-if="log.document_details.video_link"
v-if="
log.document_type == 'LMS Course' &&
log.document_details.video_link
"
:src="`https://www.youtube.com/embed/${log.document_details.video_link}`"
class="rounded-md w-64"
class="rounded-l-md w-72"
/>
<div class="">
<video
v-else-if="
log.document_type == 'LMS Batch' &&
log.document_details.video_link
"
:src="log.document_details.video_link"
class="rounded-l-md w-72"
/>
<div class="p-3">
<div
class="bg-surface-violet-1 w-fit py-1 px-1.5 rounded-full text-ink-violet-1 text-sm mb-2"
>
{{ __('New Course') }}
{{
log.document_type === 'LMS Course'
? __('New Course')
: __('New Batch')
}}
</div>
<div class="font-semibold mb-1">
{{ __(log.document_details.title) }}
@@ -71,7 +96,31 @@
<div class="leading-5">
{{ __(log.document_details.short_introduction) }}
</div>
<div class="mt-5 space-y-2">
<div
v-if="log.document_details.start_date"
class="flex items-center space-x-2 text-sm mt-5"
>
<Calendar class="size-3 stroke-1.5" />
<span>
{{
dayjs(log.document_details.start_date).format('DD MMM YYYY')
}}
</span>
</div>
<div
v-if="log.document_details.start_time"
class="flex items-center space-x-2 text-sm mt-2"
>
<Clock class="size-3 stroke-1.5" />
<span>
{{ formatTime(log.document_details.start_time) }}
{{ log.document_details.timezone }}
</span>
</div>
<div
v-if="log.document_details.instructors.length > 1"
class="space-y-2 mt-5"
>
<div
v-for="instructor in log.document_details.instructors"
class="flex items-center space-x-2"
@@ -81,7 +130,7 @@
:image="instructor.user_image"
:label="instructor.full_name"
/>
<span class="text-ink-gray-7 text-sm">
<span class="font-medium text-sm">
{{ instructor.full_name }}
</span>
</div>
@@ -109,7 +158,8 @@ import {
import { sessionStore } from '../stores/session'
import { computed, inject, ref, onMounted, onUnmounted } from 'vue'
import { useRouter } from 'vue-router'
import { X } from 'lucide-vue-next'
import { Calendar, Clock, X } from 'lucide-vue-next'
import { formatTime } from '@/utils/'
const { brand } = sessionStore()
const user = inject('$user')
@@ -189,13 +239,37 @@ const navigateToPage = (log) => {
params: { courseName: link[3] },
})
} else if (link[2] == 'batches') {
router.push({
name: 'Batch',
params: { batchName: link[3] },
})
if (link[3] == 'details') {
router.push({
name: 'BatchDetail',
params: { batchName: link[4] },
})
} else {
router.push({
name: 'Batch',
params: { batchName: link[3] },
})
}
}
}
const isMention = (log) => {
if (log.type == 'Mention') {
return true
}
if (log.subject.includes('mentioned you')) {
return true
}
return false
}
const showDetails = (log) => {
return (
['LMS Course', 'LMS Batch'].includes(log.document_type) &&
log.document_details
)
}
onUnmounted(() => {
socket.off('publish_lms_notifications')
})

View File

@@ -618,15 +618,19 @@ export function singularize(word) {
)
}
export const validateFile = async (file, showToast = true) => {
export const validateFile = async (
file,
showToast = true,
fileType = 'image'
) => {
const error = (msg) => {
if (showToast) toast.error(msg)
console.error(msg)
return msg
}
if (!file.type.startsWith('image/')) {
return error(__('Only image file is allowed.'))
console.log(file.type, fileType)
if (!file.type.startsWith(`${fileType}/`)) {
return error(__(`Only ${fileType} file is allowed.`))
}
if (file.type === 'image/svg+xml') {