feat(phase-4): frontend data layer — API client (13 new functions), folders store, documents store extensions, routes

- Extended listDocuments to accept folderId, q, sort, order query params
- Added 6 folder API functions: listFolders, createFolder, getFolder, renameFolder, deleteFolder, moveDocument
- Added 4 share API functions: createShare, listShares, deleteShare, getSharedWithMe
- Added 2 preference API functions: getMyPreferences, updateMyPreferences
- Added getDocumentContentUrl helper (returns URL string, no fetch)
- Created useFoldersStore with full CRUD, navigation state, and breadcrumb support
- Extended useDocumentsStore with currentFolderId, searchQuery, sortField, sortOrder refs
- Added debounced searchQuery watcher (300ms, 2-char minimum, T-04-08-03)
- Added shareDocument, revokeShare, listShares actions to documents store
- Added /folders/:folderId and /shared routes with requiresAuth guard
This commit is contained in:
curo1305
2026-05-25 21:58:38 +02:00
parent f9141b85b9
commit 5417f26b93
4 changed files with 220 additions and 5 deletions
+33 -4
View File
@@ -1,5 +1,5 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { ref, watch } from 'vue'
import * as api from '../api/client.js'
import { useAuthStore } from './auth.js'
@@ -34,12 +34,16 @@ export const useDocumentsStore = defineStore('documents', () => {
const error = ref(null)
// uploadProgress: keyed by "${file.name}__${Date.now()}" to prevent collision (T-03-25)
const uploadProgress = ref({})
const currentFolderId = ref(null)
const searchQuery = ref('')
const sortField = ref('date')
const sortOrder = ref('desc')
async function fetchDocuments({ topic, page = 1, perPage = 20 } = {}) {
async function fetchDocuments({ topic, page = 1, perPage = 20, folderId = null, q = null, sort = null, order = null } = {}) {
loading.value = true
error.value = null
try {
const data = await api.listDocuments({ topic, page, perPage })
const data = await api.listDocuments({ topic, page, perPage, folderId, q, sort, order })
documents.value = data.items
total.value = data.total
} catch (e) {
@@ -125,5 +129,30 @@ export const useDocumentsStore = defineStore('documents', () => {
return result.topics
}
return { documents, total, loading, error, uploadProgress, fetchDocuments, upload, remove, reclassify }
// Debounced search watcher: fires after 300 ms; ignores inputs shorter than 2 chars (T-04-08-03)
let _searchTimer = null
watch(searchQuery, (newVal) => {
clearTimeout(_searchTimer)
if (newVal.length < 2) {
fetchDocuments({ folderId: currentFolderId.value })
return
}
_searchTimer = setTimeout(() => {
fetchDocuments({ q: newVal, folderId: currentFolderId.value, sort: sortField.value, order: sortOrder.value })
}, 300)
})
async function shareDocument(docId, recipientHandle) {
try { return await api.createShare(docId, recipientHandle) } catch (e) { throw e }
}
async function revokeShare(shareId) {
try { await api.deleteShare(shareId) } catch (e) { throw e }
}
async function listShares(docId) {
try { return await api.listShares(docId) } catch (e) { throw e }
}
return { documents, total, loading, error, uploadProgress, currentFolderId, searchQuery, sortField, sortOrder, fetchDocuments, upload, remove, reclassify, shareDocument, revokeShare, listShares }
})
+85
View File
@@ -0,0 +1,85 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import * as api from '../api/client.js'
export const useFoldersStore = defineStore('folders', () => {
const folders = ref([])
const currentFolderId = ref(null)
const breadcrumb = ref([])
const loading = ref(false)
const error = ref(null)
async function fetchFolders(parentId = null) {
loading.value = true
error.value = null
try {
const data = await api.listFolders(parentId)
folders.value = data
} catch (e) {
error.value = e.message || 'Failed to load folders'
} finally {
loading.value = false
}
}
async function createFolder(name, parentId = null) {
loading.value = true
error.value = null
try {
const folder = await api.createFolder(name, parentId)
folders.value.push(folder)
return folder
} catch (e) {
error.value = e.message || 'Failed to create folder'
throw e
} finally {
loading.value = false
}
}
async function renameFolder(folderId, name) {
loading.value = true
error.value = null
try {
const updated = await api.renameFolder(folderId, name)
const idx = folders.value.findIndex(f => f.id === folderId)
if (idx !== -1) folders.value[idx] = updated
return updated
} catch (e) {
error.value = e.message || 'Failed to rename folder'
throw e
} finally {
loading.value = false
}
}
async function deleteFolder(folderId) {
loading.value = true
error.value = null
try {
await api.deleteFolder(folderId)
folders.value = folders.value.filter(f => f.id !== folderId)
} catch (e) {
error.value = e.message || 'Failed to delete folder'
throw e
} finally {
loading.value = false
}
}
async function navigateTo(folderId) {
currentFolderId.value = folderId
if (folderId != null) {
try {
const data = await api.getFolder(folderId)
breadcrumb.value = data.breadcrumb || []
} catch (e) {
breadcrumb.value = []
}
} else {
breadcrumb.value = []
}
}
return { folders, currentFolderId, breadcrumb, loading, error, fetchFolders, createFolder, renameFolder, deleteFolder, navigateTo }
})