mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-06-26 21:39:43 +02:00
feat: implement local folder synchronization and versioning with new metadata handling and document_versions table
This commit is contained in:
parent
53df393cf7
commit
25358fddcf
11 changed files with 205 additions and 17 deletions
|
|
@ -1,4 +1,4 @@
|
||||||
"""Add LOCAL_FOLDER_FILE document type and document_versions table
|
"""Add LOCAL_FOLDER_FILE document type, folder metadata, and document_versions table
|
||||||
|
|
||||||
Revision ID: 117
|
Revision ID: 117
|
||||||
Revises: 116
|
Revises: 116
|
||||||
|
|
@ -38,6 +38,19 @@ def upgrade() -> None:
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Add JSONB metadata column to folders table
|
||||||
|
col_exists = conn.execute(
|
||||||
|
sa.text(
|
||||||
|
"SELECT 1 FROM information_schema.columns "
|
||||||
|
"WHERE table_name = 'folders' AND column_name = 'metadata'"
|
||||||
|
)
|
||||||
|
).fetchone()
|
||||||
|
if not col_exists:
|
||||||
|
op.add_column(
|
||||||
|
"folders",
|
||||||
|
sa.Column("metadata", sa.dialects.postgresql.JSONB, nullable=True),
|
||||||
|
)
|
||||||
|
|
||||||
# Create document_versions table
|
# Create document_versions table
|
||||||
table_exists = conn.execute(
|
table_exists = conn.execute(
|
||||||
sa.text(
|
sa.text(
|
||||||
|
|
@ -124,3 +137,13 @@ def downgrade() -> None:
|
||||||
op.execute("DROP INDEX IF EXISTS ix_document_versions_created_at")
|
op.execute("DROP INDEX IF EXISTS ix_document_versions_created_at")
|
||||||
op.execute("DROP INDEX IF EXISTS ix_document_versions_document_id")
|
op.execute("DROP INDEX IF EXISTS ix_document_versions_document_id")
|
||||||
op.execute("DROP TABLE IF EXISTS document_versions")
|
op.execute("DROP TABLE IF EXISTS document_versions")
|
||||||
|
|
||||||
|
# Drop metadata column from folders
|
||||||
|
col_exists = conn.execute(
|
||||||
|
sa.text(
|
||||||
|
"SELECT 1 FROM information_schema.columns "
|
||||||
|
"WHERE table_name = 'folders' AND column_name = 'metadata'"
|
||||||
|
)
|
||||||
|
).fetchone()
|
||||||
|
if col_exists:
|
||||||
|
op.drop_column("folders", "metadata")
|
||||||
|
|
@ -956,6 +956,7 @@ class Folder(BaseModel, TimestampMixin):
|
||||||
onupdate=lambda: datetime.now(UTC),
|
onupdate=lambda: datetime.now(UTC),
|
||||||
index=True,
|
index=True,
|
||||||
)
|
)
|
||||||
|
folder_metadata = Column("metadata", JSONB, nullable=True)
|
||||||
|
|
||||||
parent = relationship("Folder", remote_side="Folder.id", backref="children")
|
parent = relationship("Folder", remote_side="Folder.id", backref="children")
|
||||||
search_space = relationship("SearchSpace", back_populates="folders")
|
search_space = relationship("SearchSpace", back_populates="folders")
|
||||||
|
|
|
||||||
|
|
@ -1310,6 +1310,13 @@ async def folder_index(
|
||||||
"You don't have permission to create documents in this search space",
|
"You don't have permission to create documents in this search space",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
watched_metadata = {
|
||||||
|
"watched": True,
|
||||||
|
"folder_path": request.folder_path,
|
||||||
|
"exclude_patterns": request.exclude_patterns,
|
||||||
|
"file_extensions": request.file_extensions,
|
||||||
|
}
|
||||||
|
|
||||||
root_folder_id = request.root_folder_id
|
root_folder_id = request.root_folder_id
|
||||||
if root_folder_id:
|
if root_folder_id:
|
||||||
existing = (
|
existing = (
|
||||||
|
|
@ -1319,6 +1326,9 @@ async def folder_index(
|
||||||
).scalar_one_or_none()
|
).scalar_one_or_none()
|
||||||
if not existing:
|
if not existing:
|
||||||
root_folder_id = None
|
root_folder_id = None
|
||||||
|
else:
|
||||||
|
existing.folder_metadata = watched_metadata
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
if not root_folder_id:
|
if not root_folder_id:
|
||||||
root_folder = Folder(
|
root_folder = Folder(
|
||||||
|
|
@ -1326,6 +1336,7 @@ async def folder_index(
|
||||||
search_space_id=request.search_space_id,
|
search_space_id=request.search_space_id,
|
||||||
created_by_id=str(user.id),
|
created_by_id=str(user.id),
|
||||||
position="a0",
|
position="a0",
|
||||||
|
folder_metadata=watched_metadata,
|
||||||
)
|
)
|
||||||
session.add(root_folder)
|
session.add(root_folder)
|
||||||
await session.flush()
|
await session.flush()
|
||||||
|
|
@ -1403,3 +1414,34 @@ async def folder_index_file(
|
||||||
"message": "File indexing started",
|
"message": "File indexing started",
|
||||||
"status": "processing",
|
"status": "processing",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/documents/watched-folders", response_model=list["FolderRead"])
|
||||||
|
async def get_watched_folders(
|
||||||
|
search_space_id: int,
|
||||||
|
session: AsyncSession = Depends(get_async_session),
|
||||||
|
user: User = Depends(current_active_user),
|
||||||
|
):
|
||||||
|
"""Return root folders that are marked as watched (metadata->>'watched' = 'true')."""
|
||||||
|
from app.schemas import FolderRead # noqa: F811
|
||||||
|
|
||||||
|
await check_permission(
|
||||||
|
session,
|
||||||
|
user,
|
||||||
|
search_space_id,
|
||||||
|
Permission.DOCUMENTS_READ.value,
|
||||||
|
"You don't have permission to read documents in this search space",
|
||||||
|
)
|
||||||
|
|
||||||
|
folders = (
|
||||||
|
await session.execute(
|
||||||
|
select(Folder).where(
|
||||||
|
Folder.search_space_id == search_space_id,
|
||||||
|
Folder.parent_id.is_(None),
|
||||||
|
Folder.folder_metadata.isnot(None),
|
||||||
|
Folder.folder_metadata["watched"].astext == "true",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
).scalars().all()
|
||||||
|
|
||||||
|
return folders
|
||||||
|
|
|
||||||
|
|
@ -192,6 +192,33 @@ async def get_folder_breadcrumb(
|
||||||
) from e
|
) from e
|
||||||
|
|
||||||
|
|
||||||
|
@router.patch("/folders/{folder_id}/watched")
|
||||||
|
async def stop_watching_folder(
|
||||||
|
folder_id: int,
|
||||||
|
session: AsyncSession = Depends(get_async_session),
|
||||||
|
user: User = Depends(current_active_user),
|
||||||
|
):
|
||||||
|
"""Clear the watched flag from a folder's metadata."""
|
||||||
|
folder = await session.get(Folder, folder_id)
|
||||||
|
if not folder:
|
||||||
|
raise HTTPException(status_code=404, detail="Folder not found")
|
||||||
|
|
||||||
|
await check_permission(
|
||||||
|
session,
|
||||||
|
user,
|
||||||
|
folder.search_space_id,
|
||||||
|
Permission.DOCUMENTS_UPDATE.value,
|
||||||
|
"You don't have permission to update folders in this search space",
|
||||||
|
)
|
||||||
|
|
||||||
|
if folder.folder_metadata and isinstance(folder.folder_metadata, dict):
|
||||||
|
updated = {**folder.folder_metadata, "watched": False}
|
||||||
|
folder.folder_metadata = updated
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
return {"message": "Folder watch status updated"}
|
||||||
|
|
||||||
|
|
||||||
@router.put("/folders/{folder_id}", response_model=FolderRead)
|
@router.put("/folders/{folder_id}", response_model=FolderRead)
|
||||||
async def update_folder(
|
async def update_folder(
|
||||||
folder_id: int,
|
folder_id: int,
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from pydantic import BaseModel, ConfigDict, Field
|
from pydantic import BaseModel, ConfigDict, Field
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -34,6 +36,7 @@ class FolderRead(BaseModel):
|
||||||
created_by_id: UUID | None
|
created_by_id: UUID | None
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
updated_at: datetime
|
updated_at: datetime
|
||||||
|
metadata: dict[str, Any] | None = Field(default=None, validation_alias="folder_metadata")
|
||||||
|
|
||||||
model_config = ConfigDict(from_attributes=True)
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,7 @@ interface FolderNodeProps {
|
||||||
isWatched?: boolean;
|
isWatched?: boolean;
|
||||||
onRescan?: (folder: FolderDisplay) => void;
|
onRescan?: (folder: FolderDisplay) => void;
|
||||||
onStopWatching?: (folder: FolderDisplay) => void;
|
onStopWatching?: (folder: FolderDisplay) => void;
|
||||||
|
onViewMetadata?: (folder: FolderDisplay) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDropZone(
|
function getDropZone(
|
||||||
|
|
@ -116,6 +117,7 @@ export const FolderNode = React.memo(function FolderNode({
|
||||||
isWatched,
|
isWatched,
|
||||||
onRescan,
|
onRescan,
|
||||||
onStopWatching,
|
onStopWatching,
|
||||||
|
onViewMetadata,
|
||||||
}: FolderNodeProps) {
|
}: FolderNodeProps) {
|
||||||
const [renameValue, setRenameValue] = useState(folder.name);
|
const [renameValue, setRenameValue] = useState(folder.name);
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
@ -251,13 +253,21 @@ export const FolderNode = React.memo(function FolderNode({
|
||||||
isOver && !canDrop && "cursor-not-allowed"
|
isOver && !canDrop && "cursor-not-allowed"
|
||||||
)}
|
)}
|
||||||
style={{ paddingLeft: `${depth * 16 + 4}px` }}
|
style={{ paddingLeft: `${depth * 16 + 4}px` }}
|
||||||
onClick={() => onToggleExpand(folder.id)}
|
onClick={(e) => {
|
||||||
onKeyDown={(e) => {
|
if ((e.ctrlKey || e.metaKey) && onViewMetadata) {
|
||||||
if (e.key === "Enter" || e.key === " ") {
|
e.preventDefault();
|
||||||
e.preventDefault();
|
e.stopPropagation();
|
||||||
onToggleExpand(folder.id);
|
onViewMetadata(folder);
|
||||||
}
|
return;
|
||||||
}}
|
}
|
||||||
|
onToggleExpand(folder.id);
|
||||||
|
}}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter" || e.key === " ") {
|
||||||
|
e.preventDefault();
|
||||||
|
onToggleExpand(folder.id);
|
||||||
|
}
|
||||||
|
}}
|
||||||
onDoubleClick={(e) => {
|
onDoubleClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
startRename();
|
startRename();
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ interface FolderTreeViewProps {
|
||||||
watchedFolderIds?: Set<number>;
|
watchedFolderIds?: Set<number>;
|
||||||
onRescanFolder?: (folder: FolderDisplay) => void;
|
onRescanFolder?: (folder: FolderDisplay) => void;
|
||||||
onStopWatchingFolder?: (folder: FolderDisplay) => void;
|
onStopWatchingFolder?: (folder: FolderDisplay) => void;
|
||||||
|
onViewFolderMetadata?: (folder: FolderDisplay) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function groupBy<T>(items: T[], keyFn: (item: T) => string | number): Record<string | number, T[]> {
|
function groupBy<T>(items: T[], keyFn: (item: T) => string | number): Record<string | number, T[]> {
|
||||||
|
|
@ -79,6 +80,7 @@ export function FolderTreeView({
|
||||||
watchedFolderIds,
|
watchedFolderIds,
|
||||||
onRescanFolder,
|
onRescanFolder,
|
||||||
onStopWatchingFolder,
|
onStopWatchingFolder,
|
||||||
|
onViewFolderMetadata,
|
||||||
}: FolderTreeViewProps) {
|
}: FolderTreeViewProps) {
|
||||||
const foldersByParent = useMemo(() => groupBy(folders, (f) => f.parentId ?? "root"), [folders]);
|
const foldersByParent = useMemo(() => groupBy(folders, (f) => f.parentId ?? "root"), [folders]);
|
||||||
|
|
||||||
|
|
@ -210,10 +212,11 @@ export function FolderTreeView({
|
||||||
siblingPositions={siblingPositions}
|
siblingPositions={siblingPositions}
|
||||||
contextMenuOpen={openContextMenuId === `folder-${f.id}`}
|
contextMenuOpen={openContextMenuId === `folder-${f.id}`}
|
||||||
onContextMenuOpenChange={(open) => setOpenContextMenuId(open ? `folder-${f.id}` : null)}
|
onContextMenuOpenChange={(open) => setOpenContextMenuId(open ? `folder-${f.id}` : null)}
|
||||||
isWatched={watchedFolderIds?.has(f.id)}
|
isWatched={watchedFolderIds?.has(f.id)}
|
||||||
onRescan={onRescanFolder}
|
onRescan={onRescanFolder}
|
||||||
onStopWatching={onStopWatchingFolder}
|
onStopWatching={onStopWatchingFolder}
|
||||||
/>
|
onViewMetadata={onViewFolderMetadata}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isExpanded) {
|
if (isExpanded) {
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import type { DocumentNodeDoc } from "@/components/documents/DocumentNode";
|
||||||
import type { FolderDisplay } from "@/components/documents/FolderNode";
|
import type { FolderDisplay } from "@/components/documents/FolderNode";
|
||||||
import { FolderPickerDialog } from "@/components/documents/FolderPickerDialog";
|
import { FolderPickerDialog } from "@/components/documents/FolderPickerDialog";
|
||||||
import { FolderTreeView } from "@/components/documents/FolderTreeView";
|
import { FolderTreeView } from "@/components/documents/FolderTreeView";
|
||||||
|
import { JsonMetadataViewer } from "@/components/json-metadata-viewer";
|
||||||
import { EXPORT_FILE_EXTENSIONS } from "@/components/shared/ExportMenuItems";
|
import { EXPORT_FILE_EXTENSIONS } from "@/components/shared/ExportMenuItems";
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
|
|
@ -95,12 +96,46 @@ export function DocumentsSidebar({
|
||||||
const [activeTypes, setActiveTypes] = useState<DocumentTypeEnum[]>([]);
|
const [activeTypes, setActiveTypes] = useState<DocumentTypeEnum[]>([]);
|
||||||
const [watchedFolderIds, setWatchedFolderIds] = useState<Set<number>>(new Set());
|
const [watchedFolderIds, setWatchedFolderIds] = useState<Set<number>>(new Set());
|
||||||
|
|
||||||
|
const [metadataFolder, setMetadataFolder] = useState<FolderDisplay | null>(null);
|
||||||
|
const [metadataJson, setMetadataJson] = useState<Record<string, unknown> | null>(null);
|
||||||
|
const [metadataLoading, setMetadataLoading] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const api = typeof window !== "undefined" ? window.electronAPI : null;
|
const api = typeof window !== "undefined" ? window.electronAPI : null;
|
||||||
if (!api?.getWatchedFolders) return;
|
if (!api?.getWatchedFolders) return;
|
||||||
|
|
||||||
async function loadWatchedIds() {
|
async function loadWatchedIds() {
|
||||||
const folders = await api!.getWatchedFolders();
|
const folders = await api!.getWatchedFolders();
|
||||||
|
|
||||||
|
if (folders.length === 0) {
|
||||||
|
try {
|
||||||
|
const backendFolders = await documentsApiService.getWatchedFolders(searchSpaceId);
|
||||||
|
for (const bf of backendFolders) {
|
||||||
|
const meta = bf.metadata as Record<string, unknown> | null;
|
||||||
|
if (!meta?.watched || !meta.folder_path) continue;
|
||||||
|
await api!.addWatchedFolder({
|
||||||
|
path: meta.folder_path as string,
|
||||||
|
name: bf.name,
|
||||||
|
rootFolderId: bf.id,
|
||||||
|
searchSpaceId: bf.search_space_id,
|
||||||
|
excludePatterns: (meta.exclude_patterns as string[]) ?? [],
|
||||||
|
fileExtensions: (meta.file_extensions as string[] | null) ?? null,
|
||||||
|
active: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const recovered = await api!.getWatchedFolders();
|
||||||
|
const ids = new Set(
|
||||||
|
recovered
|
||||||
|
.filter((f) => f.rootFolderId != null)
|
||||||
|
.map((f) => f.rootFolderId as number)
|
||||||
|
);
|
||||||
|
setWatchedFolderIds(ids);
|
||||||
|
return;
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[DocumentsSidebar] Recovery from backend failed:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const ids = new Set(
|
const ids = new Set(
|
||||||
folders
|
folders
|
||||||
.filter((f) => f.rootFolderId != null)
|
.filter((f) => f.rootFolderId != null)
|
||||||
|
|
@ -110,7 +145,7 @@ export function DocumentsSidebar({
|
||||||
}
|
}
|
||||||
|
|
||||||
loadWatchedIds();
|
loadWatchedIds();
|
||||||
}, []);
|
}, [searchSpaceId]);
|
||||||
const { mutateAsync: deleteDocumentMutation } = useAtomValue(deleteDocumentMutationAtom);
|
const { mutateAsync: deleteDocumentMutation } = useAtomValue(deleteDocumentMutationAtom);
|
||||||
|
|
||||||
const [sidebarDocs, setSidebarDocs] = useAtom(sidebarSelectedDocumentsAtom);
|
const [sidebarDocs, setSidebarDocs] = useAtom(sidebarSelectedDocumentsAtom);
|
||||||
|
|
@ -318,11 +353,30 @@ export function DocumentsSidebar({
|
||||||
}
|
}
|
||||||
|
|
||||||
await api.removeWatchedFolder(matched.path);
|
await api.removeWatchedFolder(matched.path);
|
||||||
|
try {
|
||||||
|
await foldersApiService.stopWatching(folder.id);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[DocumentsSidebar] Failed to clear watched metadata:", err);
|
||||||
|
}
|
||||||
toast.success(`Stopped watching: ${matched.name}`);
|
toast.success(`Stopped watching: ${matched.name}`);
|
||||||
},
|
},
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleViewFolderMetadata = useCallback(async (folder: FolderDisplay) => {
|
||||||
|
setMetadataFolder(folder);
|
||||||
|
setMetadataLoading(true);
|
||||||
|
try {
|
||||||
|
const fullFolder = await foldersApiService.getFolder(folder.id);
|
||||||
|
setMetadataJson((fullFolder.metadata as Record<string, unknown>) ?? {});
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[DocumentsSidebar] Failed to fetch folder metadata:", err);
|
||||||
|
setMetadataJson({ error: "Failed to load folder metadata" });
|
||||||
|
} finally {
|
||||||
|
setMetadataLoading(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleRenameFolder = useCallback(async (folder: FolderDisplay, newName: string) => {
|
const handleRenameFolder = useCallback(async (folder: FolderDisplay, newName: string) => {
|
||||||
try {
|
try {
|
||||||
await foldersApiService.updateFolder(folder.id, { name: newName });
|
await foldersApiService.updateFolder(folder.id, { name: newName });
|
||||||
|
|
@ -801,11 +855,26 @@ export function DocumentsSidebar({
|
||||||
onReorderFolder={handleReorderFolder}
|
onReorderFolder={handleReorderFolder}
|
||||||
watchedFolderIds={watchedFolderIds}
|
watchedFolderIds={watchedFolderIds}
|
||||||
onRescanFolder={handleRescanFolder}
|
onRescanFolder={handleRescanFolder}
|
||||||
onStopWatchingFolder={handleStopWatching}
|
onStopWatchingFolder={handleStopWatching}
|
||||||
/>
|
onViewFolderMetadata={handleViewFolderMetadata}
|
||||||
</div>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<FolderPickerDialog
|
<JsonMetadataViewer
|
||||||
|
title={metadataFolder?.name ?? "Folder"}
|
||||||
|
metadata={metadataJson}
|
||||||
|
loading={metadataLoading}
|
||||||
|
open={!!metadataFolder}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) {
|
||||||
|
setMetadataFolder(null);
|
||||||
|
setMetadataJson(null);
|
||||||
|
setMetadataLoading(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FolderPickerDialog
|
||||||
open={folderPickerOpen}
|
open={folderPickerOpen}
|
||||||
onOpenChange={setFolderPickerOpen}
|
onOpenChange={setFolderPickerOpen}
|
||||||
folders={treeFolders}
|
folders={treeFolders}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ export const folder = z.object({
|
||||||
created_by_id: z.string().nullable().optional(),
|
created_by_id: z.string().nullable().optional(),
|
||||||
created_at: z.string(),
|
created_at: z.string(),
|
||||||
updated_at: z.string(),
|
updated_at: z.string(),
|
||||||
|
metadata: z.record(z.unknown()).nullable().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const folderCreateRequest = z.object({
|
export const folderCreateRequest = z.object({
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ import {
|
||||||
uploadDocumentRequest,
|
uploadDocumentRequest,
|
||||||
uploadDocumentResponse,
|
uploadDocumentResponse,
|
||||||
} from "@/contracts/types/document.types";
|
} from "@/contracts/types/document.types";
|
||||||
|
import { folderListResponse } from "@/contracts/types/folder.types";
|
||||||
import { ValidationError } from "../error";
|
import { ValidationError } from "../error";
|
||||||
import { baseApiService } from "./base-api.service";
|
import { baseApiService } from "./base-api.service";
|
||||||
|
|
||||||
|
|
@ -403,6 +404,10 @@ class DocumentsApiService {
|
||||||
return baseApiService.post(`/api/v1/documents/folder-index-file`, undefined, { body });
|
return baseApiService.post(`/api/v1/documents/folder-index-file`, undefined, { body });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
getWatchedFolders = async (searchSpaceId: number) => {
|
||||||
|
return baseApiService.get(`/api/v1/documents/watched-folders?search_space_id=${searchSpaceId}`, folderListResponse);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete a document
|
* Delete a document
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -85,6 +85,10 @@ class FoldersApiService {
|
||||||
return baseApiService.delete(`/api/v1/folders/${folderId}`, folderDeleteResponse);
|
return baseApiService.delete(`/api/v1/folders/${folderId}`, folderDeleteResponse);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
stopWatching = async (folderId: number) => {
|
||||||
|
return baseApiService.patch(`/api/v1/folders/${folderId}/watched`, undefined);
|
||||||
|
};
|
||||||
|
|
||||||
moveDocument = async (documentId: number, request: DocumentMoveRequest) => {
|
moveDocument = async (documentId: number, request: DocumentMoveRequest) => {
|
||||||
const parsed = documentMoveRequest.safeParse(request);
|
const parsed = documentMoveRequest.safeParse(request);
|
||||||
if (!parsed.success) {
|
if (!parsed.success) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue