feat: discussions edit and delete
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user