feat(webui): complete files quick access

This commit is contained in:
2026-08-19 19:48:56 +02:00
parent ad55d47645
commit e1361427b8
9 changed files with 228 additions and 17 deletions
+1 -1
View File
@@ -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 }) });
}
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();
for (const [key, value] of Object.entries(params)) {
if (value !== undefined && value !== null && value !== "") search.set(key, String(value));
+21 -5
View File
@@ -1,5 +1,6 @@
import { FileText, X } from "lucide-react";
import { FileText, UploadCloud, X } from "lucide-react";
import { useCallback } from "react";
import { Link } from "react-router";
import {
Button,
DismissibleAlert,
@@ -7,6 +8,8 @@ import {
SelectionList,
SelectionListItem,
SelectionListItemContent,
hasScope,
quickAccessLaunchState,
useDashboardWidgetData,
type QuickAccessToolRenderContext
} from "@govoplan/core-webui";
@@ -14,7 +17,7 @@ import { listFiles } from "../../api/files";
type Props = Pick<
QuickAccessToolRenderContext,
"settings" | "launchContext" | "complete" | "cancel"
"settings" | "auth" | "launchContext" | "complete" | "cancel" | "close"
>;
/**
@@ -23,15 +26,18 @@ type Props = Pick<
*/
export default function FileQuickAccess({
settings,
auth,
launchContext,
complete,
cancel
cancel,
close
}: Props) {
const load = useCallback(
async () => (await listFiles(settings, { page_size: 7 })).files,
async () => (await listFiles(settings, { sort: "recent", page_size: 7 })).files,
[settings]
);
const { data: files, loading, error } = useDashboardWidgetData(load, 0);
const canUpload = hasScope(auth, "files:upload");
return (
<LoadingFrame loading={loading} label="Loading recent files">
@@ -74,7 +80,17 @@ export default function FileQuickAccess({
<p className="muted">No authorized files are available.</p>
) : null}
<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>
</LoadingFrame>
);
+40
View File
@@ -16,6 +16,7 @@ import { FormGrid, ActionToolbar,
type ApiSettings,
type AuthInfo, i18nMessage } from
"@govoplan/core-webui";
import { useLocation, useNavigate } from "react-router";
import {
bulkDeleteFiles,
bulkRenameFiles,
@@ -114,12 +115,15 @@ const FILES_WORKFLOW_DOCUMENTATION = {
} as const;
export default function FilesPage({ settings, auth }: {settings: ApiSettings;auth: AuthInfo;}) {
const location = useLocation();
const navigate = useNavigate();
const canDownload = hasScope(auth, "files:download");
const canUpload = hasScope(auth, "files:upload");
const canOrganize = hasScope(auth, "files:organize");
const canDelete = hasScope(auth, "files:delete");
const canShare = hasScope(auth, "files:file:share");
const [spaces, setSpaces] = useState<FileSpace[]>(EMPTY_SPACES);
const [spacesLoaded, setSpacesLoaded] = useState(false);
const [activeSpaceId, setActiveSpaceId] = useState("");
const activeSpace = spaces.find((space) => space.id === activeSpaceId) ?? spaces[0] ?? null;
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]);
async function loadSpaces() {
setSpacesLoaded(false);
setBusy(true);
setError("");
try {
@@ -341,6 +346,7 @@ export default function FilesPage({ settings, auth }: {settings: ApiSettings;aut
} catch (err) {
setError(err instanceof Error ? err.message : String(err));
} finally {
setSpacesLoaded(true);
setBusy(false);
}
}
@@ -434,6 +440,40 @@ export default function FilesPage({ settings, auth }: {settings: ApiSettings;aut
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [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(() => {
if (!contextMenu) return undefined;
const close = () => setContextMenu(null);