feat: discussions new topic
This commit is contained in:
@@ -11,7 +11,7 @@
|
|||||||
"dayjs": "^1.11.6",
|
"dayjs": "^1.11.6",
|
||||||
"feather-icons": "^4.28.0",
|
"feather-icons": "^4.28.0",
|
||||||
"frappe-ui": "^0.1.22",
|
"frappe-ui": "^0.1.22",
|
||||||
"lucide-vue-next": "^0.259.0",
|
"lucide-vue-next": "^0.309.0",
|
||||||
"markdown-it": "^14.0.0",
|
"markdown-it": "^14.0.0",
|
||||||
"pinia": "^2.0.33",
|
"pinia": "^2.0.33",
|
||||||
"tailwindcss": "^3.2.7",
|
"tailwindcss": "^3.2.7",
|
||||||
|
|||||||
@@ -70,6 +70,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<TextEditor
|
<TextEditor
|
||||||
|
class="mt-5"
|
||||||
:content="newReply"
|
:content="newReply"
|
||||||
@change="(val) => (newReply = val)"
|
@change="(val) => (newReply = val)"
|
||||||
placeholder="Type your reply here..."
|
placeholder="Type your reply here..."
|
||||||
@@ -92,6 +93,7 @@ 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('')
|
||||||
@@ -147,13 +149,23 @@ const postReply = () => {
|
|||||||
{
|
{
|
||||||
validate() {
|
validate() {
|
||||||
if (!newReply.value) {
|
if (!newReply.value) {
|
||||||
return __('Reply cannot be empty')
|
return 'Reply cannot be empty'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSuccess() {
|
onSuccess() {
|
||||||
newReply.value = ''
|
newReply.value = ''
|
||||||
replies.reload()
|
replies.reload()
|
||||||
},
|
},
|
||||||
|
onError() {
|
||||||
|
createToast({
|
||||||
|
title: 'Error',
|
||||||
|
text: err.messages?.[0] || err,
|
||||||
|
icon: 'x',
|
||||||
|
iconClasses: 'bg-red-600 text-white rounded-md p-px',
|
||||||
|
position: 'top-center',
|
||||||
|
timeout: 10,
|
||||||
|
})
|
||||||
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="topics.data">
|
<div>
|
||||||
<div>
|
<Button class="float-right" @click="openTopicModal()">
|
||||||
<Button class="float-right" @click="openQuestionModal()">
|
{{ __('New {0}').format(title) }}
|
||||||
{{ __('Ask a Question') }}
|
</Button>
|
||||||
</Button>
|
<div class="text-xl font-semibold">
|
||||||
<div class="text-xl font-semibold">
|
{{ __(title) }}
|
||||||
{{ __(title) }}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="topics.data?.length">
|
||||||
<div v-if="showTopics" v-for="(topic, index) in topics.data">
|
<div v-if="showTopics" v-for="(topic, index) in topics.data">
|
||||||
<div
|
<div
|
||||||
@click="showReplies(topic)"
|
@click="showReplies(topic)"
|
||||||
@@ -37,6 +37,24 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else class="flex justify-center border mt-5 p-5 rounded-md">
|
||||||
|
<MessageSquareIcon class="w-10 h-10 stroke-1.5 text-gray-800 mr-2" />
|
||||||
|
<div>
|
||||||
|
<div class="text-xl font-semibold mb-2">
|
||||||
|
{{ __(emptyStateTitle) }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{{ __(emptyStateText) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DiscussionModal
|
||||||
|
v-model="showTopicModal"
|
||||||
|
:title="__('New {0}').format(title)"
|
||||||
|
:doctype="props.doctype"
|
||||||
|
:docname="props.docname"
|
||||||
|
v-model:reloadTopics="topics"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { createResource, Button } from 'frappe-ui'
|
import { createResource, Button } from 'frappe-ui'
|
||||||
@@ -44,11 +62,13 @@ import UserAvatar from '@/components/UserAvatar.vue'
|
|||||||
import { timeAgo } from '../utils'
|
import { timeAgo } from '../utils'
|
||||||
import { ref, onMounted, inject } from 'vue'
|
import { ref, onMounted, inject } from 'vue'
|
||||||
import DiscussionReplies from '@/components/DiscussionReplies.vue'
|
import DiscussionReplies from '@/components/DiscussionReplies.vue'
|
||||||
|
import DiscussionModal from '@/components/Modals/DiscussionModal.vue'
|
||||||
|
import { MessageSquareIcon, MessagesSquare } from 'lucide-vue-next'
|
||||||
|
|
||||||
const showTopics = ref(true)
|
const showTopics = ref(true)
|
||||||
const currentTopic = ref(null)
|
const currentTopic = ref(null)
|
||||||
const socket = inject('$socket')
|
const socket = inject('$socket')
|
||||||
const showQuestionModal = ref(false)
|
const showTopicModal = ref(false)
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
title: {
|
title: {
|
||||||
@@ -63,6 +83,14 @@ const props = defineProps({
|
|||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
emptyStateTitle: {
|
||||||
|
type: String,
|
||||||
|
default: 'No topics yet',
|
||||||
|
},
|
||||||
|
emptyStateText: {
|
||||||
|
type: String,
|
||||||
|
default: 'Be the first to start a discussion',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
@@ -74,9 +102,11 @@ onMounted(() => {
|
|||||||
const topics = createResource({
|
const topics = createResource({
|
||||||
url: 'lms.lms.utils.get_discussion_topics',
|
url: 'lms.lms.utils.get_discussion_topics',
|
||||||
cache: ['topics', props.doctype, props.docname],
|
cache: ['topics', props.doctype, props.docname],
|
||||||
params: {
|
makeParams() {
|
||||||
doctype: props.doctype,
|
return {
|
||||||
docname: props.docname,
|
doctype: props.doctype,
|
||||||
|
docname: props.docname,
|
||||||
|
}
|
||||||
},
|
},
|
||||||
auto: true,
|
auto: true,
|
||||||
})
|
})
|
||||||
@@ -86,7 +116,7 @@ const showReplies = (topic) => {
|
|||||||
currentTopic.value = topic
|
currentTopic.value = topic
|
||||||
}
|
}
|
||||||
|
|
||||||
const openQuestionModal = () => {
|
const openTopicModal = () => {
|
||||||
showQuestionModal.value = true
|
showTopicModal.value = true
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
114
frontend/src/components/Modals/DiscussionModal.vue
Normal file
114
frontend/src/components/Modals/DiscussionModal.vue
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
<template>
|
||||||
|
<Dialog
|
||||||
|
:options="{
|
||||||
|
title: props.title,
|
||||||
|
size: '2xl',
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
label: 'Submit',
|
||||||
|
variant: 'solid',
|
||||||
|
onClick: (close) => submitTopic(close),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<template #body-content>
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<div>
|
||||||
|
<div class="mb-1.5 text-sm text-gray-600">
|
||||||
|
{{ __('Title') }}
|
||||||
|
</div>
|
||||||
|
<Input type="text" v-model="topic.title" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="mb-1.5 text-sm text-gray-600">
|
||||||
|
{{ __('Details') }}
|
||||||
|
</div>
|
||||||
|
<TextEditor
|
||||||
|
:content="topic.reply"
|
||||||
|
@change="(val) => (topic.reply = val)"
|
||||||
|
:editable="true"
|
||||||
|
:fixedMenu="true"
|
||||||
|
editorClass="prose-sm max-w-none border-b border-x bg-gray-100 rounded-b-md py-1 px-2 min-h-[7rem]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import { Dialog, Input, TextEditor, createResource } from 'frappe-ui'
|
||||||
|
import { reactive, defineModel } from 'vue'
|
||||||
|
|
||||||
|
const topics = defineModel('reloadTopics')
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
doctype: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
docname: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const topic = reactive({
|
||||||
|
title: '',
|
||||||
|
reply: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
const topicResource = createResource({
|
||||||
|
url: 'frappe.client.insert',
|
||||||
|
makeParams(values) {
|
||||||
|
return {
|
||||||
|
doc: {
|
||||||
|
doctype: 'Discussion Topic',
|
||||||
|
reference_doctype: props.doctype,
|
||||||
|
reference_docname: props.docname,
|
||||||
|
title: topic.title,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const replyResource = createResource({
|
||||||
|
url: 'frappe.client.insert',
|
||||||
|
makeParams(values) {
|
||||||
|
return {
|
||||||
|
doc: {
|
||||||
|
doctype: 'Discussion Reply',
|
||||||
|
topic: values.topic,
|
||||||
|
reply: topic.reply,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const submitTopic = (close) => {
|
||||||
|
topicResource.submit(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
onSuccess(data) {
|
||||||
|
replyResource.submit(
|
||||||
|
{
|
||||||
|
topic: data.name,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess() {
|
||||||
|
topic.title = ''
|
||||||
|
topic.reply = ''
|
||||||
|
topics.value.reload()
|
||||||
|
close()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
<template #tab="{ tab, selected }">
|
<template #tab="{ tab, selected }">
|
||||||
<div>
|
<div>
|
||||||
<button
|
<button
|
||||||
class="group -mb-px flex items-center gap-2 border-b border-transparent py-2.5 text-base text-gray-600 duration-300 ease-in-out hover:border-gray-400 hover:text-gray-900"
|
class="group -mb-px flex items-center gap-2 overflow-hidden border-b border-transparent py-2.5 text-base text-gray-600 duration-300 ease-in-out hover:border-gray-400 hover:text-gray-900"
|
||||||
:class="{ 'text-gray-900': selected }"
|
:class="{ 'text-gray-900': selected }"
|
||||||
>
|
>
|
||||||
<component v-if="tab.icon" :is="tab.icon" class="h-5" />
|
<component v-if="tab.icon" :is="tab.icon" class="h-5" />
|
||||||
|
|||||||
@@ -171,6 +171,7 @@
|
|||||||
:title="'Questions'"
|
:title="'Questions'"
|
||||||
:doctype="'Course Lesson'"
|
:doctype="'Course Lesson'"
|
||||||
:docname="lesson.data.name"
|
:docname="lesson.data.name"
|
||||||
|
:key="lesson.data.name"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -343,6 +344,10 @@ const allowDiscussions = () => {
|
|||||||
user.data?.is_instructor
|
user.data?.is_instructor
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hideLesson = () => {
|
||||||
|
return false
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
<style>
|
<style>
|
||||||
.avatar-group {
|
.avatar-group {
|
||||||
|
|||||||
@@ -1259,10 +1259,10 @@ lodash.merge@^4.6.2:
|
|||||||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.1.0.tgz#2098d41c2dc56500e6c88584aa656c84de7d0484"
|
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.1.0.tgz#2098d41c2dc56500e6c88584aa656c84de7d0484"
|
||||||
integrity sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==
|
integrity sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==
|
||||||
|
|
||||||
lucide-vue-next@^0.259.0:
|
lucide-vue-next@^0.309.0:
|
||||||
version "0.259.0"
|
version "0.309.0"
|
||||||
resolved "https://registry.yarnpkg.com/lucide-vue-next/-/lucide-vue-next-0.259.0.tgz#3e1bbe57ccbb0704dc854efd5e2221c65099d0f6"
|
resolved "https://registry.yarnpkg.com/lucide-vue-next/-/lucide-vue-next-0.309.0.tgz#53a1a347764260adc23fe0163ca94d98bb1d5cf6"
|
||||||
integrity sha512-LOUmg73Sw7v1si8y/JpYv6KJFc8Rp3d8OwCO0jE/qixiOcJpg5PvKs7xvvtRSO3E4sWwJAQQaL1re5zGJirONg==
|
integrity sha512-MRrJOV1uyAxHaCqqgwRcLN/8CwlKCwuz7MNdWo2OG/jjrGI+7wLJeJOYJ8lp/61Fps3Mat/k7+0WIskwbGhqTA==
|
||||||
|
|
||||||
magic-string@^0.30.5:
|
magic-string@^0.30.5:
|
||||||
version "0.30.5"
|
version "0.30.5"
|
||||||
@@ -1767,6 +1767,7 @@ source-map-js@^1.0.2:
|
|||||||
integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==
|
integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==
|
||||||
|
|
||||||
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0:
|
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0:
|
||||||
|
name string-width-cjs
|
||||||
version "4.2.3"
|
version "4.2.3"
|
||||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||||
@@ -1785,6 +1786,7 @@ string-width@^5.0.1, string-width@^5.1.2:
|
|||||||
strip-ansi "^7.0.1"
|
strip-ansi "^7.0.1"
|
||||||
|
|
||||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||||
|
name strip-ansi-cjs
|
||||||
version "6.0.1"
|
version "6.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||||
|
|||||||
@@ -1296,7 +1296,12 @@ def get_lesson(course, chapter, lesson):
|
|||||||
"Course Lesson", lesson_name, ["include_in_preview", "title"], as_dict=1
|
"Course Lesson", lesson_name, ["include_in_preview", "title"], as_dict=1
|
||||||
)
|
)
|
||||||
membership = get_membership(course)
|
membership = get_membership(course)
|
||||||
if not lesson_details.include_in_preview and not membership:
|
if (
|
||||||
|
not lesson_details.include_in_preview
|
||||||
|
and not membership
|
||||||
|
and not has_course_moderator_role()
|
||||||
|
and not is_instructor(course.name)
|
||||||
|
):
|
||||||
return {
|
return {
|
||||||
"no_preview": 1,
|
"no_preview": 1,
|
||||||
"title": lesson_details.title,
|
"title": lesson_details.title,
|
||||||
|
|||||||
Reference in New Issue
Block a user