feat: discussions edit and delete

This commit is contained in:
Jannat Patel
2024-01-16 17:24:35 +05:30
parent 3a5977a718
commit 3313db844c
5 changed files with 185 additions and 60 deletions

View File

@@ -1,9 +1,12 @@
<template> <template>
<div <div
class="flex h-full flex-col justify-between transition-all duration-300 ease-in-out bg-gray-50" class="flex h-full flex-col justify-between transition-all duration-300 ease-in-out bg-gray-50"
:class="isSidebarCollapsed ? 'w-12' : 'w-56'" :class="isSidebarCollapsed ? 'w-14' : 'w-56'"
> >
<div class="flex flex-col overflow-hidden"> <div
class="flex flex-col overflow-hidden"
:class="isSidebarCollapsed ? 'items-center' : ''"
>
<UserDropdown class="p-2" :isCollapsed="isSidebarCollapsed" /> <UserDropdown class="p-2" :isCollapsed="isSidebarCollapsed" />
<div class="flex flex-col overflow-y-auto"> <div class="flex flex-col overflow-y-auto">
<SidebarLink <SidebarLink

View File

@@ -1,8 +1,10 @@
<template> <template>
<div class="mt-6"> <div class="mt-6">
<div class="flex items-center mb-5"> <div class="flex items-center mb-5">
<Button variant="subtle" @click="showTopics = true"> <Button variant="outline" @click="showTopics = true">
<ChevronLeft class="w-4 h-4 stroke-1.5" /> <template #icon>
<ChevronLeft class="w-5 h-5 stroke-1.5 text-gray-700" />
</template>
</Button> </Button>
<span class="text-lg font-semibold ml-2"> <span class="text-lg font-semibold ml-2">
{{ topic.title }} {{ topic.title }}
@@ -14,23 +16,65 @@
class="py-3" class="py-3"
:class="{ 'border-b': index + 1 != replies.data.length }" :class="{ 'border-b': index + 1 != replies.data.length }"
> >
<div class="flex items-center mb-2"> <div class="flex items-center justify-between mb-2">
<UserAvatar :user="reply.user" class="mr-2" /> <div class="flex items-center">
<span> <UserAvatar :user="reply.user" class="mr-2" />
{{ reply.user.full_name }} <span>
</span> {{ reply.user.full_name }}
<span class="text-sm ml-2"> </span>
{{ timeAgo(reply.creation) }} <span class="text-sm ml-2">
</span> {{ timeAgo(reply.creation) }}
</span>
</div>
<Dropdown
v-if="user.data.name == reply.owner && !reply.editable"
:options="[
{
label: 'Edit',
onClick() {
reply.editable = true
},
},
{
label: 'Delete',
onClick() {
deleteReply(reply)
},
},
]"
>
<template v-slot="{ open }">
<MoreHorizontal class="w-4 h-4 stroke-1.5 cursor-pointer" />
</template>
</Dropdown>
<div v-if="reply.editable">
<Button variant="ghost" @click="postEdited(reply)">
{{ __('Post') }}
</Button>
<Button variant="ghost" @click="reply.editable = false">
{{ __('Discard') }}
</Button>
</div>
</div> </div>
<div v-html="reply.reply"></div> <TextEditor
:content="reply.reply"
@change="(val) => (reply.reply = val)"
:editable="reply.editable || false"
:fixedMenu="reply.editable || false"
:editorClass="
reply.editable
? 'prose-sm max-w-none border-b border-x rounded-b-md py-1 px-2 min-h-[4rem]'
: 'prose-sm'
"
/>
</div> </div>
</div> </div>
<TextEditor <TextEditor
:content="newReply" :content="newReply"
@change="(val) => (newReply = val)" @change="(val) => (newReply = val)"
placeholder="Type your reply here..." placeholder="Type your reply here..."
editorClass="prose-sm py-2 px-2 min-h-[100px] border-gray-300 hover:border-gray-400 rounded-md bg-gray-200 w-full mt-5" :fixedMenu="true"
editorClass="prose-sm max-w-none min-h-[7rem] border-b border-x rounded-b-md py-1 px-2"
/> />
<div class="flex justify-between mt-2"> <div class="flex justify-between mt-2">
<span> </span> <span> </span>
@@ -43,15 +87,16 @@
</div> </div>
</template> </template>
<script setup> <script setup>
import { createResource, TextEditor, Button } from 'frappe-ui' import { createResource, TextEditor, Button, Dropdown } 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 } from 'lucide-vue-next' import { ChevronLeft, MoreHorizontal } from 'lucide-vue-next'
import { ref, inject, onMounted } from 'vue' import { ref, inject, onMounted } from 'vue'
const showTopics = defineModel('showTopics') const showTopics = defineModel('showTopics')
const newReply = ref('') const newReply = ref('')
const socket = inject('$socket') const socket = inject('$socket')
const user = inject('$user')
const props = defineProps({ const props = defineProps({
topic: { topic: {
@@ -60,11 +105,16 @@ const props = defineProps({
}, },
}) })
console.log(socket) onMounted(() => {
socket.on('publish_message', (data) => { socket.on('publish_message', (data) => {
console.log('publish') replies.reload()
console.log(data) })
replies.reload() socket.on('update_message', (data) => {
replies.reload()
})
socket.on('delete_message', (data) => {
replies.reload()
})
}) })
const replies = createResource({ const replies = createResource({
@@ -107,4 +157,59 @@ const postReply = () => {
} }
) )
} }
const editReplyResource = createResource({
url: 'frappe.client.set_value',
makeParams(values) {
return {
doctype: 'Discussion Reply',
name: values.name,
fieldname: 'reply',
value: values.reply,
}
},
})
const postEdited = (reply) => {
editReplyResource.submit(
{
name: reply.name,
reply: reply.reply,
},
{
validate() {
if (!reply.reply) {
return 'Reply cannot be empty'
}
},
onSuccess() {
reply.editable = false
replies.reload()
},
}
)
}
const deleteReplyResource = createResource({
url: 'frappe.client.delete',
makeParams(values) {
return {
doctype: 'Discussion Reply',
name: values.name,
}
},
})
const deleteReply = (reply) => {
deleteReplyResource.submit(
{
name: reply.name,
},
{
onSuccess() {
replies.reload()
},
}
)
}
</script> </script>

View File

@@ -1,6 +1,9 @@
<template> <template>
<div v-if="topics.data"> <div v-if="topics.data">
<div> <div>
<Button class="float-right" @click="openQuestionModal()">
{{ __('Ask a Question') }}
</Button>
<div class="text-xl font-semibold"> <div class="text-xl font-semibold">
{{ __(title) }} {{ __(title) }}
</div> </div>
@@ -36,7 +39,7 @@
</div> </div>
</template> </template>
<script setup> <script setup>
import { createResource } from 'frappe-ui' import { createResource, Button } from 'frappe-ui'
import UserAvatar from '@/components/UserAvatar.vue' 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'
@@ -45,6 +48,7 @@ import DiscussionReplies from '@/components/DiscussionReplies.vue'
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 props = defineProps({ const props = defineProps({
title: { title: {
@@ -81,4 +85,8 @@ const showReplies = (topic) => {
showTopics.value = false showTopics.value = false
currentTopic.value = topic currentTopic.value = topic
} }
const openQuestionModal = () => {
showQuestionModal.value = true
}
</script> </script>

View File

@@ -1,23 +1,32 @@
<template> <template>
<button <button
class="flex h-7 cursor-pointer items-center rounded text-gray-800 duration-300 ease-in-out focus:outline-none focus:transition-none focus-visible:rounded focus-visible:ring-2 focus-visible:ring-gray-400" class="flex h-7 cursor-pointer items-center rounded text-gray-800 duration-300 ease-in-out focus:outline-none focus:transition-none focus-visible:rounded focus-visible:ring-2 focus-visible:ring-gray-400"
:class="isActive ? 'bg-white shadow-sm' : 'hover:bg-gray-100'" @click="handleClick"> :class="isActive ? 'bg-white shadow-sm' : 'hover:bg-gray-100'"
<div class="flex items-center duration-300 ease-in-out" :class="isCollapsed ? 'p-1' : 'px-2 py-1'"> @click="handleClick"
<Tooltip :text="label" placement="right"> >
<slot name="icon"> <div
<span class="grid h-5 w-6 flex-shrink-0 place-items-center"> class="flex items-center duration-300 ease-in-out"
<component :is="icon" class="h-4 w-4 stroke-1.5 text-gray-700" /> :class="isCollapsed ? 'p-1' : 'px-2 py-1'"
</span> >
</slot> <Tooltip :text="label" placement="right">
</Tooltip> <slot name="icon">
<span class="flex-shrink-0 text-base duration-300 ease-in-out" :class="isCollapsed <span class="grid h-5 w-6 flex-shrink-0 place-items-center">
? 'ml-0 w-0 overflow-hidden opacity-0' <component :is="icon" class="h-5 w-5 stroke-1.5 text-gray-800" />
: 'ml-2 w-auto opacity-100' </span>
"> </slot>
{{ label }} </Tooltip>
</span> <span
</div> class="flex-shrink-0 text-base duration-300 ease-in-out"
</button> :class="
isCollapsed
? 'ml-0 w-0 overflow-hidden opacity-0'
: 'ml-2 w-auto opacity-100'
"
>
{{ label }}
</span>
</div>
</button>
</template> </template>
<script setup> <script setup>
@@ -28,28 +37,28 @@ import { useRouter } from 'vue-router'
const router = useRouter() const router = useRouter()
const props = defineProps({ const props = defineProps({
icon: { icon: {
type: Function, type: Function,
}, },
label: { label: {
type: String, type: String,
default: '', default: '',
}, },
to: { to: {
type: String, type: String,
default: '', default: '',
}, },
isCollapsed: { isCollapsed: {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
}) })
function handleClick() { function handleClick() {
router.push({ name: props.to }) router.push({ name: props.to })
} }
let isActive = computed(() => { let isActive = computed(() => {
return router.currentRoute.value.name === props.to return router.currentRoute.value.name === props.to
}) })
</script> </script>

View File

@@ -5,11 +5,11 @@ import { getCachedResource } from 'frappe-ui/src/resources/resources'
export function initSocket() { export function initSocket() {
let host = window.location.hostname let host = window.location.hostname
let siteName = window.site_name let siteName = window.site_name || host
let port = window.location.port ? `:${socketio_port}` : '' let port = window.location.port ? `:${socketio_port}` : ''
let protocol = port ? 'http' : 'https' let protocol = port ? 'http' : 'https'
let url = `${protocol}://${host}${port}/${siteName}` let url = `${protocol}://${host}${port}/${siteName}`
console.log(protocol, host, port, siteName)
let socket = io(url, { let socket = io(url, {
withCredentials: true, withCredentials: true,
reconnectionAttempts: 5, reconnectionAttempts: 5,