feat: discussions edit and delete
This commit is contained in:
@@ -1,9 +1,12 @@
|
||||
<template>
|
||||
<div
|
||||
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" />
|
||||
<div class="flex flex-col overflow-y-auto">
|
||||
<SidebarLink
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
<template>
|
||||
<div class="mt-6">
|
||||
<div class="flex items-center mb-5">
|
||||
<Button variant="subtle" @click="showTopics = true">
|
||||
<ChevronLeft class="w-4 h-4 stroke-1.5" />
|
||||
<Button variant="outline" @click="showTopics = true">
|
||||
<template #icon>
|
||||
<ChevronLeft class="w-5 h-5 stroke-1.5 text-gray-700" />
|
||||
</template>
|
||||
</Button>
|
||||
<span class="text-lg font-semibold ml-2">
|
||||
{{ topic.title }}
|
||||
@@ -14,23 +16,65 @@
|
||||
class="py-3"
|
||||
:class="{ 'border-b': index + 1 != replies.data.length }"
|
||||
>
|
||||
<div class="flex items-center mb-2">
|
||||
<UserAvatar :user="reply.user" class="mr-2" />
|
||||
<span>
|
||||
{{ reply.user.full_name }}
|
||||
</span>
|
||||
<span class="text-sm ml-2">
|
||||
{{ timeAgo(reply.creation) }}
|
||||
</span>
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div class="flex items-center">
|
||||
<UserAvatar :user="reply.user" class="mr-2" />
|
||||
<span>
|
||||
{{ reply.user.full_name }}
|
||||
</span>
|
||||
<span class="text-sm ml-2">
|
||||
{{ 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 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>
|
||||
<TextEditor
|
||||
:content="newReply"
|
||||
@change="(val) => (newReply = val)"
|
||||
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">
|
||||
<span> </span>
|
||||
@@ -43,15 +87,16 @@
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { createResource, TextEditor, Button } from 'frappe-ui'
|
||||
import { createResource, TextEditor, Button, Dropdown } from 'frappe-ui'
|
||||
import { timeAgo } from '../utils'
|
||||
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'
|
||||
|
||||
const showTopics = defineModel('showTopics')
|
||||
const newReply = ref('')
|
||||
const socket = inject('$socket')
|
||||
const user = inject('$user')
|
||||
|
||||
const props = defineProps({
|
||||
topic: {
|
||||
@@ -60,11 +105,16 @@ const props = defineProps({
|
||||
},
|
||||
})
|
||||
|
||||
console.log(socket)
|
||||
socket.on('publish_message', (data) => {
|
||||
console.log('publish')
|
||||
console.log(data)
|
||||
replies.reload()
|
||||
onMounted(() => {
|
||||
socket.on('publish_message', (data) => {
|
||||
replies.reload()
|
||||
})
|
||||
socket.on('update_message', (data) => {
|
||||
replies.reload()
|
||||
})
|
||||
socket.on('delete_message', (data) => {
|
||||
replies.reload()
|
||||
})
|
||||
})
|
||||
|
||||
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>
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
<template>
|
||||
<div v-if="topics.data">
|
||||
<div>
|
||||
<Button class="float-right" @click="openQuestionModal()">
|
||||
{{ __('Ask a Question') }}
|
||||
</Button>
|
||||
<div class="text-xl font-semibold">
|
||||
{{ __(title) }}
|
||||
</div>
|
||||
@@ -36,7 +39,7 @@
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { createResource } from 'frappe-ui'
|
||||
import { createResource, Button } from 'frappe-ui'
|
||||
import UserAvatar from '@/components/UserAvatar.vue'
|
||||
import { timeAgo } from '../utils'
|
||||
import { ref, onMounted, inject } from 'vue'
|
||||
@@ -45,6 +48,7 @@ import DiscussionReplies from '@/components/DiscussionReplies.vue'
|
||||
const showTopics = ref(true)
|
||||
const currentTopic = ref(null)
|
||||
const socket = inject('$socket')
|
||||
const showQuestionModal = ref(false)
|
||||
|
||||
const props = defineProps({
|
||||
title: {
|
||||
@@ -81,4 +85,8 @@ const showReplies = (topic) => {
|
||||
showTopics.value = false
|
||||
currentTopic.value = topic
|
||||
}
|
||||
|
||||
const openQuestionModal = () => {
|
||||
showQuestionModal.value = true
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,23 +1,32 @@
|
||||
<template>
|
||||
<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="isActive ? 'bg-white shadow-sm' : 'hover:bg-gray-100'" @click="handleClick">
|
||||
<div class="flex items-center duration-300 ease-in-out" :class="isCollapsed ? 'p-1' : 'px-2 py-1'">
|
||||
<Tooltip :text="label" placement="right">
|
||||
<slot name="icon">
|
||||
<span class="grid h-5 w-6 flex-shrink-0 place-items-center">
|
||||
<component :is="icon" class="h-4 w-4 stroke-1.5 text-gray-700" />
|
||||
</span>
|
||||
</slot>
|
||||
</Tooltip>
|
||||
<span class="flex-shrink-0 text-base duration-300 ease-in-out" :class="isCollapsed
|
||||
? 'ml-0 w-0 overflow-hidden opacity-0'
|
||||
: 'ml-2 w-auto opacity-100'
|
||||
">
|
||||
{{ label }}
|
||||
</span>
|
||||
</div>
|
||||
</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="isActive ? 'bg-white shadow-sm' : 'hover:bg-gray-100'"
|
||||
@click="handleClick"
|
||||
>
|
||||
<div
|
||||
class="flex items-center duration-300 ease-in-out"
|
||||
:class="isCollapsed ? 'p-1' : 'px-2 py-1'"
|
||||
>
|
||||
<Tooltip :text="label" placement="right">
|
||||
<slot name="icon">
|
||||
<span class="grid h-5 w-6 flex-shrink-0 place-items-center">
|
||||
<component :is="icon" class="h-5 w-5 stroke-1.5 text-gray-800" />
|
||||
</span>
|
||||
</slot>
|
||||
</Tooltip>
|
||||
<span
|
||||
class="flex-shrink-0 text-base duration-300 ease-in-out"
|
||||
:class="
|
||||
isCollapsed
|
||||
? 'ml-0 w-0 overflow-hidden opacity-0'
|
||||
: 'ml-2 w-auto opacity-100'
|
||||
"
|
||||
>
|
||||
{{ label }}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@@ -28,28 +37,28 @@ import { useRouter } from 'vue-router'
|
||||
const router = useRouter()
|
||||
|
||||
const props = defineProps({
|
||||
icon: {
|
||||
type: Function,
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
to: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
isCollapsed: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
icon: {
|
||||
type: Function,
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
to: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
isCollapsed: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
function handleClick() {
|
||||
router.push({ name: props.to })
|
||||
router.push({ name: props.to })
|
||||
}
|
||||
|
||||
let isActive = computed(() => {
|
||||
return router.currentRoute.value.name === props.to
|
||||
return router.currentRoute.value.name === props.to
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -5,11 +5,11 @@ import { getCachedResource } from 'frappe-ui/src/resources/resources'
|
||||
|
||||
export function initSocket() {
|
||||
let host = window.location.hostname
|
||||
let siteName = window.site_name
|
||||
let siteName = window.site_name || host
|
||||
let port = window.location.port ? `:${socketio_port}` : ''
|
||||
let protocol = port ? 'http' : 'https'
|
||||
let url = `${protocol}://${host}${port}/${siteName}`
|
||||
console.log(protocol, host, port, siteName)
|
||||
|
||||
let socket = io(url, {
|
||||
withCredentials: true,
|
||||
reconnectionAttempts: 5,
|
||||
|
||||
Reference in New Issue
Block a user