feat: notifications

This commit is contained in:
Jannat Patel
2024-05-22 20:40:02 +05:30
parent 8e1b871f87
commit f38aebbc9c
18 changed files with 4683 additions and 2102 deletions

View File

@@ -44,9 +44,21 @@ import SidebarLink from '@/components/SidebarLink.vue'
import { useStorage } from '@vueuse/core'
import { ref } from 'vue'
import { getSidebarLinks } from '../utils'
import { sessionStore } from '@/stores/session'
import { Bell } from 'lucide-vue-next'
const { user } = sessionStore()
const links = getSidebarLinks()
if (user) {
links.push({
label: 'Notifications',
icon: Bell,
to: 'Notifications',
activeFor: ['Notifications'],
})
}
const getSidebarFromStorage = () => {
return useStorage('sidebar_is_collapsed', false)
}

View File

@@ -5,7 +5,6 @@
<div
class="relative block min-h-0 flex-shrink-0 overflow-hidden hover:overflow-auto"
>
<slot name="sidebar" />
<AppSidebar />
</div>
<div class="w-full overflow-auto" id="scrollContainer">

View File

@@ -69,9 +69,11 @@
/>
</div>
</div>
<TextEditor
class="mt-5"
:content="newReply"
:mentions="mentionUsers"
@change="(val) => (newReply = val)"
placeholder="Type your reply here..."
:fixedMenu="true"
@@ -92,13 +94,14 @@ import { createResource, TextEditor, Button, Dropdown } from 'frappe-ui'
import { timeAgo } from '../utils'
import UserAvatar from '@/components/UserAvatar.vue'
import { ChevronLeft, MoreHorizontal } from 'lucide-vue-next'
import { ref, inject, onMounted } from 'vue'
import { ref, inject, onMounted, computed } from 'vue'
import { createToast } from '../utils'
const showTopics = defineModel('showTopics')
const newReply = ref('')
const socket = inject('$socket')
const user = inject('$user')
const allUsers = inject('$allUsers')
const props = defineProps({
topic: {
@@ -147,6 +150,16 @@ const newReplyResource = createResource({
},
})
const mentionUsers = computed(() => {
return allUsers.data /* [{
label: "jannat",
value: "jannat"
}, {
label: "samreen",
value: "samreen"
}] */
})
const postReply = () => {
newReplyResource.submit(
{},

View File

@@ -42,14 +42,14 @@
</div>
<div
v-else
class="flex items-center justify-center border mt-5 p-5 rounded-md"
class="flex flex-col items-center justify-center border-2 border-dashed mt-5 py-8 rounded-md"
>
<MessageSquareIcon class="w-5 h-5 stroke-1.5 mr-2" />
<div>
<MessageSquareText class="w-7 h-7 text-gray-500 stroke-1.5 mr-2" />
<div class="">
<div v-if="emptyStateTitle" class="font-medium mb-2">
{{ __(emptyStateTitle) }}
</div>
<div class="">
<div class="text-gray-600">
{{ __(emptyStateText) }}
</div>
</div>
@@ -69,7 +69,7 @@ import { timeAgo } from '../utils'
import { ref, onMounted, inject } from 'vue'
import DiscussionReplies from '@/components/DiscussionReplies.vue'
import DiscussionModal from '@/components/Modals/DiscussionModal.vue'
import { MessageSquareIcon } from 'lucide-vue-next'
import { MessageSquareText } from 'lucide-vue-next'
const showTopics = ref(true)
const currentTopic = ref(null)
@@ -96,7 +96,7 @@ const props = defineProps({
},
emptyStateText: {
type: String,
default: 'Be the first to start a discussion',
default: 'Start a discussion',
},
singleThread: {
type: Boolean,

View File

@@ -34,7 +34,7 @@ import { sessionStore } from '@/stores/session'
import { usersStore } from '@/stores/user'
import { LogOut, LogIn, UserRound } from 'lucide-vue-next'
const { logout, user, username } = sessionStore()
const { logout, user } = sessionStore()
let { isLoggedIn } = sessionStore()
const router = useRouter()
let { userResource } = usersStore()

View File

@@ -30,8 +30,9 @@ app.provide('$dayjs', dayjs)
app.provide('$socket', initSocket())
app.mount('#app')
const { userResource } = usersStore()
const { userResource, allUsers } = usersStore()
let { isLoggedIn } = sessionStore()
app.provide('$user', userResource)
app.provide('$allUsers', allUsers)
app.config.globalProperties.$user = userResource

View File

@@ -0,0 +1,69 @@
<template>
<header
class="sticky top-0 z-10 flex flex-col md:flex-row md:items-center justify-between border-b bg-white px-3 py-2.5 sm:px-5"
>
<Breadcrumbs :items="breadcrumbs" />
</header>
<div class="w-3/4 mx-auto">
<div
v-for="log in notifications.data"
class="flex items-center border-b py-2 justify-between"
>
<div class="flex items-center">
<UserAvatar :user="allUsers.data[log.from_user]" class="mr-2" />
<div class="notification" v-html="log.subject"></div>
</div>
<Link
v-if="log.link"
:to="log.link"
class="text-gray-600 font-medium text-sm hover:text-gray-700"
>
{{ __('View') }}
</Link>
</div>
</div>
</template>
<script setup>
import { createResource, Breadcrumbs, Link } from 'frappe-ui'
import { computed, inject } from 'vue'
import UserAvatar from '@/components/UserAvatar.vue'
const user = inject('$user')
const allUsers = inject('$allUsers')
const notifications = createResource({
url: 'frappe.client.get_list',
makeParams: (values) => {
return {
doctype: 'Notification Log',
fields: ['subject', 'from_user', 'link'],
filters: {
for_user: user.data?.name,
},
order_by: 'creation desc',
}
},
auto: true,
cache: user.data?.name,
})
const breadcrumbs = computed(() => {
let crumbs = [
{
label: 'Notifications',
route: {
name: 'Notifications',
},
},
]
return crumbs
})
</script>
<style>
.notification strong {
font-weight: 400;
}
.notification b {
font-weight: 400;
}
</style>

View File

@@ -130,6 +130,11 @@ const routes = [
name: 'CertifiedParticipants',
component: () => import('@/pages/CertifiedParticipants.vue'),
},
{
path: '/notifications',
name: 'Notifications',
component: () => import('@/pages/Notifications.vue'),
},
]
let router = createRouter({
@@ -138,13 +143,21 @@ let router = createRouter({
})
router.beforeEach(async (to, from, next) => {
const { userResource } = usersStore()
const { userResource, allUsers } = usersStore()
let { isLoggedIn } = sessionStore()
try {
if (isLoggedIn) {
await userResource.reload()
}
if (
isLoggedIn &&
(to.name == 'Lesson' ||
to.name == 'Batch' ||
to.name == 'Notifications')
) {
await allUsers.reload()
}
} catch (error) {
isLoggedIn = false
}

View File

@@ -11,7 +11,21 @@ export const usersStore = defineStore('lms-users', () => {
},
})
const allUsers = createResource({
url: 'lms.lms.api.get_all_users',
cache: ['allUsers'],
/* transform(data) {
return data.map((user) => {
return {
value: user.name,
label: user.full_name.trimEnd(),
}
})
}, */
})
return {
userResource,
allUsers,
}
})

View File

@@ -6,6 +6,7 @@ import {
TrendingUp,
Briefcase,
GraduationCap,
Bell,
} from 'lucide-vue-next'
import { Quiz } from '@/utils/quiz'
import { Upload } from '@/utils/upload'
@@ -350,6 +351,7 @@ export function getSidebarLinks() {
label: 'Statistics',
icon: TrendingUp,
to: 'Statistics',
activeFor: ['Statistics'],
},
]
}

File diff suppressed because it is too large Load Diff

View File

@@ -385,3 +385,16 @@ def get_certificates(member):
fields=["name", "course", "course_title", "issue_date", "template"],
order_by="creation desc",
)
@frappe.whitelist()
def get_all_users():
users = frappe.get_all(
"User",
{
"enabled": 1,
},
["name", "full_name", "user_image"],
)
return {user.name: user for user in users}

View File

@@ -281,21 +281,21 @@ def get_lesson_index(lesson_name):
"Lesson Reference", {"lesson": lesson_name}, ["idx", "parent"], as_dict=True
)
if not lesson:
return "1.1"
return "1-1"
chapter = frappe.db.get_value(
"Chapter Reference", {"chapter": lesson.parent}, ["idx"], as_dict=True
)
if not chapter:
return "1.1"
return "1-1"
return f"{chapter.idx}.{lesson.idx}"
return f"{chapter.idx}-{lesson.idx}"
def get_lesson_url(course, lesson_number):
if not lesson_number:
return
return f"/lms/courses/{course}/learn/{lesson_number}"
return f"/courses/{course}/learn/{lesson_number}"
def get_batch(course, batch_name):
@@ -645,19 +645,23 @@ def handle_notifications(doc, method):
def create_notification_log(doc, topic):
course = frappe.db.get_value("Course Lesson", topic.reference_docname, "course")
course_title = frappe.db.get_value("LMS Course", course, "title")
instructors = frappe.db.get_all(
"Course Instructor", {"parent": course}, pluck="instructor"
)
notification = frappe._dict(
{
"subject": _("New reply on the topic {0}").format(topic.title),
"subject": _("New reply on the topic {0} in course {1}").format(
topic.title, course_title
),
"email_content": doc.reply,
"document_type": topic.reference_doctype,
"document_name": topic.reference_docname,
"for_user": topic.owner,
"from_user": doc.owner,
"type": "Alert",
"link": get_lesson_url(course, get_lesson_index(topic.reference_docname)),
}
)
@@ -671,6 +675,12 @@ def create_notification_log(doc, topic):
def notify_mentions(doc, topic):
outgoing_email_account = frappe.get_cached_value(
"Email Account", {"default_outgoing": 1, "enable_outgoing": 1}, "name"
)
if not outgoing_email_account or not frappe.conf.get("mail_login"):
return
mentions = extract_mentions(doc.reply)
if not mentions:
return
@@ -1708,6 +1718,7 @@ def create_discussion_topic(doctype, docname):
doc = frappe.new_doc("Discussion Topic")
doc.update(
{
"title": docname,
"reference_doctype": doctype,
"reference_docname": docname,
}

View File

@@ -87,4 +87,5 @@ lms.patches.v1_0.custom_perm_for_discussions #14-01-2024
lms.patches.v1_0.rename_evaluator_role
lms.patches.v1_0.change_navbar_urls
lms.patches.v1_0.set_published_on
lms.patches.v2_0.fix_progress_percentage
lms.patches.v2_0.fix_progress_percentage
lms.patches.v2_0.add_discussion_topic_titles

View File

@@ -0,0 +1,13 @@
import frappe
def execute():
topics = frappe.get_all(
"Discussion Topic",
{"title": ["is", "not set"]},
["name", "reference_docname", "title"],
)
for topic in topics:
if not topic.title:
frappe.db.set_value("Discussion Topic", topic.name, "title", topic.reference_docname)

View File

@@ -15,9 +15,9 @@
<meta name="twitter:title" content="{{ meta.title }}" />
<meta name="twitter:image" content="{{ meta.image }}" />
<meta name="twitter:description" content="{{ meta.description }}" />
<script type="module" crossorigin src="/assets/lms/frontend/assets/index-CdhjdjEj.js"></script>
<link rel="modulepreload" crossorigin href="/assets/lms/frontend/assets/frappe-ui-CgFK8870.js">
<link rel="stylesheet" crossorigin href="/assets/lms/frontend/assets/frappe-ui-DzKBfka9.css">
<script type="module" crossorigin src="/assets/lms/frontend/assets/index-C-DogOtg.js"></script>
<link rel="modulepreload" crossorigin href="/assets/lms/frontend/assets/frappe-ui-CGsuCsfq.js">
<link rel="stylesheet" crossorigin href="/assets/lms/frontend/assets/frappe-ui-B1gEXx4C.css">
<link rel="stylesheet" crossorigin href="/assets/lms/frontend/assets/index-C1pDkvO9.css">
</head>
<body>

View File

@@ -2,7 +2,7 @@
"name": "frappe_lms",
"version": "1.0.0",
"description": "Easy to use, open-source, Learning Management System",
"workspaces1": [
"workspaces": [
"frappe-ui",
"frontend"
],

4501
yarn.lock Normal file

File diff suppressed because it is too large Load Diff