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:
@@ -57,9 +57,13 @@ async function request(path, options = {}) {
|
|||||||
|
|
||||||
// ── Documents ────────────────────────────────────────────────────────────────
|
// ── Documents ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export function listDocuments({ topic, page = 1, perPage = 20 } = {}) {
|
export function listDocuments({ topic, page = 1, perPage = 20, folderId = null, q = null, sort = null, order = null } = {}) {
|
||||||
const params = new URLSearchParams({ page, per_page: perPage })
|
const params = new URLSearchParams({ page, per_page: perPage })
|
||||||
if (topic) params.set('topic', topic)
|
if (topic) params.set('topic', topic)
|
||||||
|
if (folderId != null) params.set('folder_id', folderId)
|
||||||
|
if (q) params.set('q', q)
|
||||||
|
if (sort) params.set('sort', sort)
|
||||||
|
if (order) params.set('order', order)
|
||||||
return request(`/api/documents?${params}`)
|
return request(`/api/documents?${params}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -263,3 +267,86 @@ export function adminUpdateAiConfig(id, provider, model) {
|
|||||||
body: JSON.stringify({ ai_provider: provider, ai_model: model }),
|
body: JSON.stringify({ ai_provider: provider, ai_model: model }),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Folders ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export function listFolders(parentId = null) {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (parentId != null) params.set('parent_id', parentId)
|
||||||
|
const qs = params.toString()
|
||||||
|
return request(`/api/folders${qs ? `?${qs}` : ''}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createFolder(name, parentId = null) {
|
||||||
|
return request('/api/folders', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ name, parent_id: parentId || null }),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFolder(folderId) {
|
||||||
|
return request(`/api/folders/${folderId}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renameFolder(folderId, name) {
|
||||||
|
return request(`/api/folders/${folderId}`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ name }),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteFolder(folderId) {
|
||||||
|
return request(`/api/folders/${folderId}`, { method: 'DELETE' })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function moveDocument(docId, folderId) {
|
||||||
|
return request(`/api/documents/${docId}/folder`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ folder_id: folderId || null }),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Shares ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export function createShare(docId, recipientHandle) {
|
||||||
|
return request('/api/shares', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ document_id: docId, recipient_handle: recipientHandle }),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listShares(docId) {
|
||||||
|
return request(`/api/shares?document_id=${docId}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteShare(shareId) {
|
||||||
|
return request(`/api/shares/${shareId}`, { method: 'DELETE' })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSharedWithMe() {
|
||||||
|
return request('/api/shares/received')
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Preferences ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export function getMyPreferences() {
|
||||||
|
return request('/api/auth/me/preferences')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateMyPreferences(payload) {
|
||||||
|
return request('/api/auth/me/preferences', {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Document content proxy URL ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export function getDocumentContentUrl(docId) {
|
||||||
|
return `/api/documents/${docId}/content`
|
||||||
|
}
|
||||||
|
|||||||
@@ -38,6 +38,20 @@ const routes = [
|
|||||||
// Phase 2 — authenticated routes
|
// Phase 2 — authenticated routes
|
||||||
{ path: '/account', component: () => import('../views/AccountView.vue') },
|
{ path: '/account', component: () => import('../views/AccountView.vue') },
|
||||||
{ path: '/admin', component: () => import('../views/AdminView.vue') },
|
{ path: '/admin', component: () => import('../views/AdminView.vue') },
|
||||||
|
|
||||||
|
// Phase 4 — folder and sharing routes
|
||||||
|
{
|
||||||
|
path: '/folders/:folderId',
|
||||||
|
name: 'folder',
|
||||||
|
component: () => import('../views/FolderView.vue'),
|
||||||
|
meta: { requiresAuth: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/shared',
|
||||||
|
name: 'shared',
|
||||||
|
component: () => import('../views/SharedView.vue'),
|
||||||
|
meta: { requiresAuth: true },
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { ref } from 'vue'
|
import { ref, watch } from 'vue'
|
||||||
import * as api from '../api/client.js'
|
import * as api from '../api/client.js'
|
||||||
import { useAuthStore } from './auth.js'
|
import { useAuthStore } from './auth.js'
|
||||||
|
|
||||||
@@ -34,12 +34,16 @@ export const useDocumentsStore = defineStore('documents', () => {
|
|||||||
const error = ref(null)
|
const error = ref(null)
|
||||||
// uploadProgress: keyed by "${file.name}__${Date.now()}" to prevent collision (T-03-25)
|
// uploadProgress: keyed by "${file.name}__${Date.now()}" to prevent collision (T-03-25)
|
||||||
const uploadProgress = ref({})
|
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
|
loading.value = true
|
||||||
error.value = null
|
error.value = null
|
||||||
try {
|
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
|
documents.value = data.items
|
||||||
total.value = data.total
|
total.value = data.total
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -125,5 +129,30 @@ export const useDocumentsStore = defineStore('documents', () => {
|
|||||||
return result.topics
|
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 }
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user