feat(webui): complete files quick access
This commit is contained in:
@@ -686,10 +686,11 @@ manifest = ModuleManifest(
|
|||||||
summary="Use managed files in the Records and documents area and keep a compact file surface beside current work.",
|
summary="Use managed files in the Records and documents area and keep a compact file surface beside current work.",
|
||||||
body=(
|
body=(
|
||||||
"Files contributes its authorized workspace to Records and documents. When Quick Access is enabled, the owner-rendered "
|
"Files contributes its authorized workspace to Records and documents. When Quick Access is enabled, the owner-rendered "
|
||||||
"recent-file selector appears in Files on the right rail and can return one exact file-version reference to the current task. "
|
"selector shows at most seven recently changed files and can return one exact file-version reference to the current task. "
|
||||||
"Opening the selector causes Files to re-run its own authorization; completion and cancellation are explicit, and the full Files "
|
"Accounts with upload permission can launch the full Files upload dialog into a freshly reauthorized managed space. Opening "
|
||||||
"workspace remains available as a deep-link fallback. View and rail settings may recommend or focus this tool, but do not bypass "
|
"either path causes Files to re-run its own provider, space, folder, object, and scope checks; completion and cancellation are "
|
||||||
"file ownership, shares, connector policy, integrity gates, or purpose-aware access."
|
"explicit, and the full Files workspace remains available as a deep-link fallback. View and rail settings may recommend or "
|
||||||
|
"focus this tool, but do not bypass file ownership, shares, connector policy, integrity gates, or purpose-aware access."
|
||||||
),
|
),
|
||||||
layer="configured",
|
layer="configured",
|
||||||
documentation_types=("user", "admin"),
|
documentation_types=("user", "admin"),
|
||||||
@@ -715,11 +716,11 @@ manifest = ModuleManifest(
|
|||||||
"summary": "Verwaltete Dateien im Produktbereich Akten und Dokumente und optional neben der aktuellen Arbeit verwenden.",
|
"summary": "Verwaltete Dateien im Produktbereich Akten und Dokumente und optional neben der aktuellen Arbeit verwenden.",
|
||||||
"body": (
|
"body": (
|
||||||
"Files ordnet seinen berechtigten Arbeitsbereich Akten und Dokumente zu. Ist der Schnellzugriff aktiviert, erscheint "
|
"Files ordnet seinen berechtigten Arbeitsbereich Akten und Dokumente zu. Ist der Schnellzugriff aktiviert, erscheint "
|
||||||
"die vom Modul gerenderte Auswahl zuletzt berechtigter Dateien rechts neben der aktuellen Seite und kann einen exakten "
|
"die vom Modul gerenderte Auswahl von höchstens sieben zuletzt geänderten berechtigten Dateien rechts neben der aktuellen "
|
||||||
"Dateiversionsverweis an die aktuelle Aufgabe zurückgeben. Files prüft die Berechtigung beim Öffnen erneut; Abschluss und "
|
"Seite und kann einen exakten Dateiversionsverweis zurückgeben. Mit Upload-Berechtigung lässt sich der vollständige "
|
||||||
"Abbruch sind ausdrücklich, und der vollständige Files-Arbeitsbereich bleibt als Ausweichziel erreichbar. Ansichts- und "
|
"Upload-Dialog in einem erneut berechtigungsgeprüften verwalteten Bereich öffnen. Files prüft Anbieter, Bereich, Ordner, "
|
||||||
"Leistenkonfiguration dürfen das Werkzeug empfehlen oder fokussieren, umgehen aber weder Eigentum, Freigaben, Connector-"
|
"Objekt und Berechtigung bei jedem Pfad erneut. Abschluss und Abbruch sind ausdrücklich; Eigentum, Freigaben, Connector-"
|
||||||
"Richtlinien, Integritätsprüfungen noch zweckgebundenen Zugriff."
|
"Richtlinien, Integritätsprüfungen und zweckgebundener Zugriff bleiben maßgeblich."
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Literal
|
from typing import Literal
|
||||||
from fastapi import APIRouter, Depends, Query
|
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from govoplan_core.auth import ApiPrincipal, require_scope
|
from govoplan_core.auth import ApiPrincipal, require_scope
|
||||||
@@ -14,6 +14,7 @@ from govoplan_files.backend.storage.files import (
|
|||||||
count_assets_for_user,
|
count_assets_for_user,
|
||||||
list_assets_for_user,
|
list_assets_for_user,
|
||||||
list_assets_for_user_window,
|
list_assets_for_user_window,
|
||||||
|
list_recent_assets_for_user,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -79,6 +80,7 @@ def list_files(
|
|||||||
path_prefix: str | None = None,
|
path_prefix: str | None = None,
|
||||||
campaign_usage: Literal["linked", "unlinked"] | None = None,
|
campaign_usage: Literal["linked", "unlinked"] | None = None,
|
||||||
audit_relevant: bool | None = None,
|
audit_relevant: bool | None = None,
|
||||||
|
sort: Literal["path", "recent"] = "path",
|
||||||
page_size: int | None = Query(default=None, ge=1, le=1000),
|
page_size: int | None = Query(default=None, ge=1, le=1000),
|
||||||
cursor: str | None = None,
|
cursor: str | None = None,
|
||||||
session: Session = Depends(get_session),
|
session: Session = Depends(get_session),
|
||||||
@@ -99,6 +101,31 @@ def list_files(
|
|||||||
audit_relevant=audit_relevant,
|
audit_relevant=audit_relevant,
|
||||||
is_admin=_is_admin(principal),
|
is_admin=_is_admin(principal),
|
||||||
)
|
)
|
||||||
|
if sort == "recent":
|
||||||
|
if cursor:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
|
||||||
|
detail="Recent file projections do not accept a path-order cursor.",
|
||||||
|
)
|
||||||
|
recent_limit = page_size or 25
|
||||||
|
assets = list_recent_assets_for_user(
|
||||||
|
session,
|
||||||
|
tenant_id=principal.tenant_id,
|
||||||
|
user_id=principal.user.id,
|
||||||
|
limit=recent_limit,
|
||||||
|
owner_type=owner_type,
|
||||||
|
owner_id=owner_id,
|
||||||
|
campaign_id=campaign_id,
|
||||||
|
path_prefix=path_prefix,
|
||||||
|
campaign_usage=campaign_usage,
|
||||||
|
audit_relevant=audit_relevant,
|
||||||
|
is_admin=_is_admin(principal),
|
||||||
|
)
|
||||||
|
return FileListResponse(
|
||||||
|
files=_asset_list_response(session, assets, include_shares=True),
|
||||||
|
total=total,
|
||||||
|
watermark=watermark,
|
||||||
|
)
|
||||||
effective_page_size = _cursor_page_size(FILES_LIST_CURSOR_SCOPE, cursor, page_size)
|
effective_page_size = _cursor_page_size(FILES_LIST_CURSOR_SCOPE, cursor, page_size)
|
||||||
if effective_page_size is not None:
|
if effective_page_size is not None:
|
||||||
fingerprint = _files_list_fingerprint(
|
fingerprint = _files_list_fingerprint(
|
||||||
|
|||||||
@@ -529,6 +529,41 @@ def list_assets_for_user(
|
|||||||
return query.order_by(FileAsset.display_path.asc(), FileAsset.updated_at.desc(), FileAsset.id.asc()).all()
|
return query.order_by(FileAsset.display_path.asc(), FileAsset.updated_at.desc(), FileAsset.id.asc()).all()
|
||||||
|
|
||||||
|
|
||||||
|
def list_recent_assets_for_user(
|
||||||
|
session: Session,
|
||||||
|
*,
|
||||||
|
tenant_id: str,
|
||||||
|
user_id: str,
|
||||||
|
limit: int,
|
||||||
|
owner_type: str | None = None,
|
||||||
|
owner_id: str | None = None,
|
||||||
|
campaign_id: str | None = None,
|
||||||
|
path_prefix: str | None = None,
|
||||||
|
campaign_usage: str | None = None,
|
||||||
|
audit_relevant: bool | None = None,
|
||||||
|
is_admin: bool = False,
|
||||||
|
) -> list[FileAsset]:
|
||||||
|
"""Return a bounded recent projection through the normal access query."""
|
||||||
|
|
||||||
|
query = _asset_visibility_query_for_user(
|
||||||
|
session,
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
user_id=user_id,
|
||||||
|
owner_type=owner_type,
|
||||||
|
owner_id=owner_id,
|
||||||
|
campaign_id=campaign_id,
|
||||||
|
path_prefix=path_prefix,
|
||||||
|
campaign_usage=campaign_usage,
|
||||||
|
audit_relevant=audit_relevant,
|
||||||
|
is_admin=is_admin,
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
query.order_by(FileAsset.updated_at.desc(), FileAsset.id.asc())
|
||||||
|
.limit(max(1, limit))
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def list_assets_for_user_window(
|
def list_assets_for_user_window(
|
||||||
session: Session,
|
session: Session,
|
||||||
*,
|
*,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
from sqlalchemy import create_engine, text
|
from sqlalchemy import create_engine, text
|
||||||
from sqlalchemy.orm import sessionmaker
|
from sqlalchemy.orm import sessionmaker
|
||||||
@@ -12,7 +13,11 @@ from govoplan_core.core.change_sequence import ChangeSequenceEntry
|
|||||||
from govoplan_core.db.base import Base
|
from govoplan_core.db.base import Base
|
||||||
from govoplan_files.backend.capabilities import FilesAccessService, virtual_folder_resource_id
|
from govoplan_files.backend.capabilities import FilesAccessService, virtual_folder_resource_id
|
||||||
from govoplan_files.backend.db.models import FileAsset, FileFolder, FileShare
|
from govoplan_files.backend.db.models import FileAsset, FileFolder, FileShare
|
||||||
from govoplan_files.backend.storage.files import count_assets_for_user, list_assets_for_user
|
from govoplan_files.backend.storage.files import (
|
||||||
|
count_assets_for_user,
|
||||||
|
list_assets_for_user,
|
||||||
|
list_recent_assets_for_user,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
TENANT_ID = "tenant-1"
|
TENANT_ID = "tenant-1"
|
||||||
@@ -22,6 +27,54 @@ GROUP_ID = "group-1"
|
|||||||
|
|
||||||
|
|
||||||
class FilesAccessProviderTests(unittest.TestCase):
|
class FilesAccessProviderTests(unittest.TestCase):
|
||||||
|
def test_recent_projection_is_bounded_ordered_and_access_filtered(self) -> None:
|
||||||
|
session = _session()
|
||||||
|
self.addCleanup(_close_session, session)
|
||||||
|
_seed_access_subjects(session)
|
||||||
|
now = datetime.now(timezone.utc)
|
||||||
|
session.add_all(
|
||||||
|
[
|
||||||
|
FileAsset(
|
||||||
|
id="owned-older",
|
||||||
|
tenant_id=TENANT_ID,
|
||||||
|
owner_type="user",
|
||||||
|
owner_user_id=USER_ID,
|
||||||
|
display_path="older.pdf",
|
||||||
|
filename="older.pdf",
|
||||||
|
updated_at=now - timedelta(hours=2),
|
||||||
|
),
|
||||||
|
FileAsset(
|
||||||
|
id="owned-newer",
|
||||||
|
tenant_id=TENANT_ID,
|
||||||
|
owner_type="user",
|
||||||
|
owner_user_id=USER_ID,
|
||||||
|
display_path="newer.pdf",
|
||||||
|
filename="newer.pdf",
|
||||||
|
updated_at=now - timedelta(hours=1),
|
||||||
|
),
|
||||||
|
FileAsset(
|
||||||
|
id="other-newest",
|
||||||
|
tenant_id=TENANT_ID,
|
||||||
|
owner_type="user",
|
||||||
|
owner_user_id=OTHER_USER_ID,
|
||||||
|
display_path="private.pdf",
|
||||||
|
filename="private.pdf",
|
||||||
|
updated_at=now,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
with patch("govoplan_files.backend.storage.files.user_group_ids", return_value=[]):
|
||||||
|
recent = list_recent_assets_for_user(
|
||||||
|
session,
|
||||||
|
tenant_id=TENANT_ID,
|
||||||
|
user_id=USER_ID,
|
||||||
|
limit=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(["owned-newer"], [asset.id for asset in recent])
|
||||||
|
|
||||||
def test_file_access_provider_explains_owner_share_admin_and_missing_resources(self) -> None:
|
def test_file_access_provider_explains_owner_share_admin_and_missing_resources(self) -> None:
|
||||||
session = _session()
|
session = _session()
|
||||||
self.addCleanup(_close_session, session)
|
self.addCleanup(_close_session, session)
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from govoplan_files.backend.manifest import manifest
|
||||||
|
|
||||||
|
|
||||||
|
REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
|
||||||
|
|
||||||
|
class FilesQuickAccessContractTests(unittest.TestCase):
|
||||||
|
def test_manifest_declares_exact_version_result_and_fallback(self) -> None:
|
||||||
|
tool = manifest.frontend.quick_access_tools[0]
|
||||||
|
|
||||||
|
self.assertEqual("files.recent", tool.id)
|
||||||
|
self.assertEqual(("files.file-version",), tool.returned_reference_kinds)
|
||||||
|
self.assertEqual("files.quick_access.files", tool.help_context_id)
|
||||||
|
self.assertEqual("/files", tool.full_page_path)
|
||||||
|
|
||||||
|
def test_renderer_and_page_keep_recent_selection_and_upload_bounded(self) -> None:
|
||||||
|
renderer = (
|
||||||
|
REPOSITORY_ROOT / "webui/src/features/files/FileQuickAccess.tsx"
|
||||||
|
).read_text(encoding="utf-8")
|
||||||
|
page = (
|
||||||
|
REPOSITORY_ROOT / "webui/src/features/files/FilesPage.tsx"
|
||||||
|
).read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
self.assertIn('sort: "recent", page_size: 7', renderer)
|
||||||
|
self.assertIn('kind: "file-version"', renderer)
|
||||||
|
self.assertIn("quickAccessLaunchState(launchContext)", renderer)
|
||||||
|
self.assertIn('to="/files?quickAction=upload"', renderer)
|
||||||
|
self.assertIn('parameters.get("quickAction") !== "upload"', page)
|
||||||
|
self.assertIn('openDialog("upload"', page)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -127,6 +127,7 @@ class FilesRouterContractTests(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertIn("campaign_usage", parameters)
|
self.assertIn("campaign_usage", parameters)
|
||||||
self.assertIn("audit_relevant", parameters)
|
self.assertIn("audit_relevant", parameters)
|
||||||
|
self.assertIn("sort", parameters)
|
||||||
self.assertIn("total", route.response_model.model_fields)
|
self.assertIn("total", route.response_model.model_fields)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -470,7 +470,7 @@ payload: {owner_type: "user" | "group";owner_id: string;path: string;recursive?:
|
|||||||
return apiFetch<FolderDeleteResponse>(settings, "/api/v1/files/folders/delete", { method: "POST", body: JSON.stringify({ recursive: true, ...payload }) });
|
return apiFetch<FolderDeleteResponse>(settings, "/api/v1/files/folders/delete", { method: "POST", body: JSON.stringify({ recursive: true, ...payload }) });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function listFiles(settings: ApiSettings, params: {owner_type?: string;owner_id?: string;campaign_id?: string;path_prefix?: string;campaign_usage?: FileCampaignUsageFilter;audit_relevant?: boolean;page_size?: number;cursor?: string | null;} = {}): Promise<FileListResponse> {
|
export function listFiles(settings: ApiSettings, params: {owner_type?: string;owner_id?: string;campaign_id?: string;path_prefix?: string;campaign_usage?: FileCampaignUsageFilter;audit_relevant?: boolean;sort?: "path" | "recent";page_size?: number;cursor?: string | null;} = {}): Promise<FileListResponse> {
|
||||||
const search = new URLSearchParams();
|
const search = new URLSearchParams();
|
||||||
for (const [key, value] of Object.entries(params)) {
|
for (const [key, value] of Object.entries(params)) {
|
||||||
if (value !== undefined && value !== null && value !== "") search.set(key, String(value));
|
if (value !== undefined && value !== null && value !== "") search.set(key, String(value));
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { FileText, X } from "lucide-react";
|
import { FileText, UploadCloud, X } from "lucide-react";
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
|
import { Link } from "react-router";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
DismissibleAlert,
|
DismissibleAlert,
|
||||||
@@ -7,6 +8,8 @@ import {
|
|||||||
SelectionList,
|
SelectionList,
|
||||||
SelectionListItem,
|
SelectionListItem,
|
||||||
SelectionListItemContent,
|
SelectionListItemContent,
|
||||||
|
hasScope,
|
||||||
|
quickAccessLaunchState,
|
||||||
useDashboardWidgetData,
|
useDashboardWidgetData,
|
||||||
type QuickAccessToolRenderContext
|
type QuickAccessToolRenderContext
|
||||||
} from "@govoplan/core-webui";
|
} from "@govoplan/core-webui";
|
||||||
@@ -14,7 +17,7 @@ import { listFiles } from "../../api/files";
|
|||||||
|
|
||||||
type Props = Pick<
|
type Props = Pick<
|
||||||
QuickAccessToolRenderContext,
|
QuickAccessToolRenderContext,
|
||||||
"settings" | "launchContext" | "complete" | "cancel"
|
"settings" | "auth" | "launchContext" | "complete" | "cancel" | "close"
|
||||||
>;
|
>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -23,15 +26,18 @@ type Props = Pick<
|
|||||||
*/
|
*/
|
||||||
export default function FileQuickAccess({
|
export default function FileQuickAccess({
|
||||||
settings,
|
settings,
|
||||||
|
auth,
|
||||||
launchContext,
|
launchContext,
|
||||||
complete,
|
complete,
|
||||||
cancel
|
cancel,
|
||||||
|
close
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const load = useCallback(
|
const load = useCallback(
|
||||||
async () => (await listFiles(settings, { page_size: 7 })).files,
|
async () => (await listFiles(settings, { sort: "recent", page_size: 7 })).files,
|
||||||
[settings]
|
[settings]
|
||||||
);
|
);
|
||||||
const { data: files, loading, error } = useDashboardWidgetData(load, 0);
|
const { data: files, loading, error } = useDashboardWidgetData(load, 0);
|
||||||
|
const canUpload = hasScope(auth, "files:upload");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LoadingFrame loading={loading} label="Loading recent files">
|
<LoadingFrame loading={loading} label="Loading recent files">
|
||||||
@@ -74,7 +80,17 @@ export default function FileQuickAccess({
|
|||||||
<p className="muted">No authorized files are available.</p>
|
<p className="muted">No authorized files are available.</p>
|
||||||
) : null}
|
) : null}
|
||||||
<div className="button-row compact-actions">
|
<div className="button-row compact-actions">
|
||||||
<Button onClick={() => cancel("user")}><X size={15} aria-hidden="true" /> Cancel selection</Button>
|
{canUpload ? (
|
||||||
|
<Link
|
||||||
|
className="btn btn-secondary"
|
||||||
|
to="/files?quickAction=upload"
|
||||||
|
state={quickAccessLaunchState(launchContext)}
|
||||||
|
onClick={close}
|
||||||
|
>
|
||||||
|
<UploadCloud size={15} aria-hidden="true" /> i18n:govoplan-files.upload.8bdf057f
|
||||||
|
</Link>
|
||||||
|
) : null}
|
||||||
|
<Button onClick={() => cancel("user")}><X size={15} aria-hidden="true" /> i18n:govoplan-files.cancel.77dfd213</Button>
|
||||||
</div>
|
</div>
|
||||||
</LoadingFrame>
|
</LoadingFrame>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { FormGrid, ActionToolbar,
|
|||||||
type ApiSettings,
|
type ApiSettings,
|
||||||
type AuthInfo, i18nMessage } from
|
type AuthInfo, i18nMessage } from
|
||||||
"@govoplan/core-webui";
|
"@govoplan/core-webui";
|
||||||
|
import { useLocation, useNavigate } from "react-router";
|
||||||
import {
|
import {
|
||||||
bulkDeleteFiles,
|
bulkDeleteFiles,
|
||||||
bulkRenameFiles,
|
bulkRenameFiles,
|
||||||
@@ -114,12 +115,15 @@ const FILES_WORKFLOW_DOCUMENTATION = {
|
|||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export default function FilesPage({ settings, auth }: {settings: ApiSettings;auth: AuthInfo;}) {
|
export default function FilesPage({ settings, auth }: {settings: ApiSettings;auth: AuthInfo;}) {
|
||||||
|
const location = useLocation();
|
||||||
|
const navigate = useNavigate();
|
||||||
const canDownload = hasScope(auth, "files:download");
|
const canDownload = hasScope(auth, "files:download");
|
||||||
const canUpload = hasScope(auth, "files:upload");
|
const canUpload = hasScope(auth, "files:upload");
|
||||||
const canOrganize = hasScope(auth, "files:organize");
|
const canOrganize = hasScope(auth, "files:organize");
|
||||||
const canDelete = hasScope(auth, "files:delete");
|
const canDelete = hasScope(auth, "files:delete");
|
||||||
const canShare = hasScope(auth, "files:file:share");
|
const canShare = hasScope(auth, "files:file:share");
|
||||||
const [spaces, setSpaces] = useState<FileSpace[]>(EMPTY_SPACES);
|
const [spaces, setSpaces] = useState<FileSpace[]>(EMPTY_SPACES);
|
||||||
|
const [spacesLoaded, setSpacesLoaded] = useState(false);
|
||||||
const [activeSpaceId, setActiveSpaceId] = useState("");
|
const [activeSpaceId, setActiveSpaceId] = useState("");
|
||||||
const activeSpace = spaces.find((space) => space.id === activeSpaceId) ?? spaces[0] ?? null;
|
const activeSpace = spaces.find((space) => space.id === activeSpaceId) ?? spaces[0] ?? null;
|
||||||
const activeSpaceIsConnector = activeSpace?.space_type === "connector";
|
const activeSpaceIsConnector = activeSpace?.space_type === "connector";
|
||||||
@@ -330,6 +334,7 @@ export default function FilesPage({ settings, auth }: {settings: ApiSettings;aut
|
|||||||
}, [activeSpaceId, currentFolder, propertyFiltersActive, propertyFilterResults, searchActive, searchResults, sortColumn, sortDirection]);
|
}, [activeSpaceId, currentFolder, propertyFiltersActive, propertyFilterResults, searchActive, searchResults, sortColumn, sortDirection]);
|
||||||
|
|
||||||
async function loadSpaces() {
|
async function loadSpaces() {
|
||||||
|
setSpacesLoaded(false);
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
setError("");
|
setError("");
|
||||||
try {
|
try {
|
||||||
@@ -341,6 +346,7 @@ export default function FilesPage({ settings, auth }: {settings: ApiSettings;aut
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : String(err));
|
setError(err instanceof Error ? err.message : String(err));
|
||||||
} finally {
|
} finally {
|
||||||
|
setSpacesLoaded(true);
|
||||||
setBusy(false);
|
setBusy(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -434,6 +440,40 @@ export default function FilesPage({ settings, auth }: {settings: ApiSettings;aut
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [settings.apiBaseUrl, settings.apiKey, settings.accessToken]);
|
}, [settings.apiBaseUrl, settings.apiKey, settings.accessToken]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const parameters = new URLSearchParams(location.search);
|
||||||
|
if (parameters.get("quickAction") !== "upload" || !spacesLoaded) return;
|
||||||
|
|
||||||
|
const uploadSpace =
|
||||||
|
activeSpace && !isConnectorSpace(activeSpace)
|
||||||
|
? activeSpace
|
||||||
|
: spaces.find((space) => !isConnectorSpace(space)) ?? null;
|
||||||
|
if (canUpload && uploadSpace) {
|
||||||
|
setActiveSpaceId(uploadSpace.id);
|
||||||
|
openDialog("upload", { spaceId: uploadSpace.id, folderPath: "" });
|
||||||
|
} else if (!canUpload) {
|
||||||
|
setError("File upload permission is required.");
|
||||||
|
} else {
|
||||||
|
setError("No authorized managed file space is available for upload.");
|
||||||
|
}
|
||||||
|
|
||||||
|
parameters.delete("quickAction");
|
||||||
|
const search = parameters.toString();
|
||||||
|
navigate(
|
||||||
|
{ pathname: location.pathname, search: search ? `?${search}` : "" },
|
||||||
|
{ replace: true, state: location.state }
|
||||||
|
);
|
||||||
|
}, [
|
||||||
|
activeSpace,
|
||||||
|
canUpload,
|
||||||
|
location.pathname,
|
||||||
|
location.search,
|
||||||
|
location.state,
|
||||||
|
navigate,
|
||||||
|
spaces,
|
||||||
|
spacesLoaded
|
||||||
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!contextMenu) return undefined;
|
if (!contextMenu) return undefined;
|
||||||
const close = () => setContextMenu(null);
|
const close = () => setContextMenu(null);
|
||||||
|
|||||||
Reference in New Issue
Block a user