feat: update UI
This commit is contained in:
@@ -1,60 +1,36 @@
|
||||
<template>
|
||||
<Splitter v-if="isDesktop" class="h-[calc(100vh-170px)]">
|
||||
<SplitterPanel :size="30" :minSize="20">
|
||||
<DocumentTreePanel
|
||||
:tree="tree"
|
||||
:loading="loading"
|
||||
:selected-id="selectedId"
|
||||
v-model:search-term="searchTerm"
|
||||
:creating-label="creatingLabel"
|
||||
:can-create="auth.can('documents', 'create')"
|
||||
@select="selectDocument"
|
||||
@create="openCreate"
|
||||
/>
|
||||
</SplitterPanel>
|
||||
|
||||
<SplitterPanel>
|
||||
<DocumentEditorPanel
|
||||
:doc="doc"
|
||||
:model-value="contentModel"
|
||||
:save-state="saveState"
|
||||
:can-edit="auth.can('documents', 'edit')"
|
||||
:can-delete="auth.can('documents', 'delete')"
|
||||
@update:model-value="onContentChange"
|
||||
@rename="openRename"
|
||||
@move="openMove"
|
||||
@delete="confirmDelete"
|
||||
/>
|
||||
</SplitterPanel>
|
||||
</Splitter>
|
||||
|
||||
<div v-else>
|
||||
<div class="h-full min-h-[420px]">
|
||||
<DocumentTreePanel
|
||||
v-if="!doc"
|
||||
:tree="tree"
|
||||
:loading="loading"
|
||||
:selected-id="selectedId"
|
||||
v-model:search-term="searchTerm"
|
||||
:creating-label="creatingLabel"
|
||||
:can-create="auth.can('documents', 'create')"
|
||||
:can-create="canCreate"
|
||||
:members="members"
|
||||
@select="selectDocument"
|
||||
@create="openCreate"
|
||||
/>
|
||||
<DocumentEditorPanel
|
||||
v-else
|
||||
:doc="doc"
|
||||
:model-value="contentModel"
|
||||
:save-state="saveState"
|
||||
:can-edit="auth.can('documents', 'edit')"
|
||||
:can-delete="auth.can('documents', 'delete')"
|
||||
@update:model-value="onContentChange"
|
||||
@rename="openRename"
|
||||
@move="openMove"
|
||||
@delete="confirmDelete"
|
||||
@back="closeDocument"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<DocumentViewerModal
|
||||
:visible="viewerVisible"
|
||||
:doc="doc"
|
||||
:model-value="contentModel"
|
||||
:editing="editing"
|
||||
:save-state="saveState"
|
||||
:can-edit="canEdit"
|
||||
:can-delete="canDelete"
|
||||
@update:visible="onViewerClose"
|
||||
@update:model-value="onContentChange"
|
||||
@edit="startEdit"
|
||||
@cancel-edit="cancelEdit"
|
||||
@save="onSave"
|
||||
@rename="openRename"
|
||||
@move="openMove"
|
||||
@delete="confirmDelete"
|
||||
/>
|
||||
|
||||
<Dialog v-model:visible="createDialog" :header="`New ${createType}`" :modal="true" style="width: min(420px, 92vw)">
|
||||
<div class="field">
|
||||
<label>Title</label>
|
||||
@@ -95,7 +71,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import DocumentTreePanel from '../../components/DocumentTreePanel.vue'
|
||||
import DocumentEditorPanel from '../../components/DocumentEditorPanel.vue'
|
||||
import DocumentViewerModal from '../../components/DocumentViewerModal.vue'
|
||||
import {
|
||||
getDocumentTree,
|
||||
getDocument,
|
||||
@@ -105,9 +81,10 @@ import {
|
||||
deleteDocument,
|
||||
searchDocuments,
|
||||
} from '../../services/modules'
|
||||
import { getMembers } from '../../services/backend'
|
||||
import { errorMessage } from '../../services/api'
|
||||
import { useAuthStore } from '../../stores/auth'
|
||||
import type { DocumentItem, DocumentNode, DocumentType } from '../../types'
|
||||
import type { DocumentItem, DocumentNode, DocumentType, ProjectMember } from '../../types'
|
||||
|
||||
const route = useRoute()
|
||||
const toast = useToast()
|
||||
@@ -121,6 +98,10 @@ const selectedId = ref<string | null>(null)
|
||||
const doc = ref<DocumentItem | null>(null)
|
||||
const contentModel = ref('')
|
||||
const saveState = ref<'idle' | 'saving' | 'saved'>('idle')
|
||||
const editing = ref(false)
|
||||
const viewerVisible = ref(false)
|
||||
|
||||
const members = ref<ProjectMember[]>([])
|
||||
|
||||
const searchTerm = ref('')
|
||||
const searchMode = ref(false)
|
||||
@@ -133,14 +114,15 @@ const renameTitle = ref('')
|
||||
const moveDialog = ref(false)
|
||||
const moveTarget = ref<string | null>(null)
|
||||
|
||||
const isDesktop = ref(false)
|
||||
let mediaQuery: MediaQueryList | null = null
|
||||
|
||||
let saveTimer: ReturnType<typeof setTimeout> | undefined
|
||||
let searchTimer: ReturnType<typeof setTimeout> | undefined
|
||||
|
||||
const currentMember = computed(() => members.value.find((m) => m.userId === auth.user?.id))
|
||||
const canCreate = computed(() => currentMember.value?.canCreateDocuments ?? false)
|
||||
const canEdit = computed(() => currentMember.value?.canEditDocuments ?? false)
|
||||
const canDelete = computed(() => currentMember.value?.canDeleteDocuments ?? false)
|
||||
|
||||
const creatingLabel = computed(() =>
|
||||
doc.value?.type === 'Folder' ? `New items go inside: ${doc.value.title}` : 'New items are created at root',
|
||||
doc.value?.type === 'Folder' ? `New items go inside "${doc.value.title}"` : 'New items are created at the root',
|
||||
)
|
||||
|
||||
const folderOptions = computed(() => {
|
||||
@@ -163,8 +145,15 @@ async function loadTree() {
|
||||
tree.value = searchMode.value ? await searchDocuments(searchTerm.value) : await getDocumentTree(projectId)
|
||||
} catch (e) {
|
||||
toast.add({ severity: 'error', summary: 'Error', detail: errorMessage(e), life: 4000 })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadMembers() {
|
||||
try {
|
||||
const res = await getMembers(projectId, 1, 100)
|
||||
members.value = res.items
|
||||
} catch (e) {
|
||||
toast.add({ severity: 'error', summary: 'Error', detail: errorMessage(e), life: 4000 })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,19 +167,55 @@ watch(searchTerm, () => {
|
||||
|
||||
async function selectDocument(id: string) {
|
||||
selectedId.value = id
|
||||
clearTimeout(saveTimer)
|
||||
try {
|
||||
doc.value = await getDocument(id)
|
||||
contentModel.value = doc.value.content ?? ''
|
||||
editing.value = false
|
||||
saveState.value = 'idle'
|
||||
viewerVisible.value = true
|
||||
} catch (e) {
|
||||
toast.add({ severity: 'error', summary: 'Error', detail: errorMessage(e), life: 4000 })
|
||||
}
|
||||
}
|
||||
|
||||
function closeDocument() {
|
||||
doc.value = null
|
||||
selectedId.value = null
|
||||
function onViewerClose(value: boolean) {
|
||||
viewerVisible.value = value
|
||||
if (!value) {
|
||||
editing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function startEdit() {
|
||||
editing.value = true
|
||||
saveState.value = 'idle'
|
||||
}
|
||||
|
||||
function cancelEdit() {
|
||||
editing.value = false
|
||||
contentModel.value = doc.value?.content ?? ''
|
||||
saveState.value = 'idle'
|
||||
}
|
||||
|
||||
function onContentChange(value: string) {
|
||||
contentModel.value = value
|
||||
saveState.value = 'idle'
|
||||
}
|
||||
|
||||
async function onSave() {
|
||||
if (!doc.value) return
|
||||
saveState.value = 'saving'
|
||||
try {
|
||||
const updated = await updateDocument(doc.value.id, { title: doc.value.title, content: contentModel.value })
|
||||
doc.value = updated
|
||||
contentModel.value = updated.content ?? ''
|
||||
editing.value = false
|
||||
saveState.value = 'saved'
|
||||
toast.add({ severity: 'success', summary: 'Saved', life: 2000 })
|
||||
await loadTree()
|
||||
} catch (e) {
|
||||
saveState.value = 'idle'
|
||||
toast.add({ severity: 'error', summary: 'Save failed', detail: errorMessage(e), life: 4000 })
|
||||
}
|
||||
}
|
||||
|
||||
function openCreate(type: DocumentType) {
|
||||
@@ -264,11 +289,14 @@ function confirmDelete() {
|
||||
confirm.require({
|
||||
message: `Delete "${doc.value.title}"?`,
|
||||
header: 'Delete',
|
||||
acceptProps: { severity: 'danger' },
|
||||
rejectProps: { severity: 'secondary', outlined: true },
|
||||
accept: async () => {
|
||||
try {
|
||||
await deleteDocument(doc.value!.id)
|
||||
doc.value = null
|
||||
selectedId.value = null
|
||||
viewerVisible.value = false
|
||||
toast.add({ severity: 'success', summary: 'Deleted', life: 2000 })
|
||||
await loadTree()
|
||||
} catch (e) {
|
||||
@@ -278,36 +306,10 @@ function confirmDelete() {
|
||||
})
|
||||
}
|
||||
|
||||
function onContentChange(value: string) {
|
||||
if (!doc.value) return
|
||||
contentModel.value = value
|
||||
saveState.value = 'saving'
|
||||
clearTimeout(saveTimer)
|
||||
saveTimer = setTimeout(async () => {
|
||||
try {
|
||||
const updated = await updateDocument(doc.value!.id, { title: doc.value!.title, content: contentModel.value })
|
||||
doc.value = updated
|
||||
saveState.value = 'saved'
|
||||
} catch (e) {
|
||||
saveState.value = 'idle'
|
||||
toast.add({ severity: 'error', summary: 'Save failed', detail: errorMessage(e), life: 5000 })
|
||||
}
|
||||
}, 800)
|
||||
}
|
||||
|
||||
function syncDesktop() {
|
||||
isDesktop.value = mediaQuery?.matches ?? false
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
mediaQuery = window.matchMedia('(min-width: 1024px)')
|
||||
syncDesktop()
|
||||
mediaQuery.addEventListener('change', syncDesktop)
|
||||
loading.value = true
|
||||
void loadTree()
|
||||
Promise.all([loadTree(), loadMembers()]).finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
mediaQuery?.removeEventListener('change', syncDesktop)
|
||||
})
|
||||
</script>
|
||||
</script>
|
||||
Reference in New Issue
Block a user