feat(breadcrumb): build breadcrumb in 1 API call
add /api/folders/{id}/ancestors
this API to iterate parent up to the drive root or the shared folder
this will help UI to build the breadcrumb in 1 API call
and to identify the root element (is it a drive users has access to or
a shared folder ?)
ui: now only 1 API call is now required to build the breadcrumb
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
/** Folder endpoints — ported from filesModel.js + fileOperations.js. */
|
||||
import { apiFetch, apiJson } from '$lib/api/client';
|
||||
import { getCsrfHeaders } from '$lib/api/csrf';
|
||||
import type { FileItem, FolderItem, ItemType } from '$lib/api/types';
|
||||
import type { FileItem, FolderAncestorsResponse, FolderItem, ItemType } from '$lib/api/types';
|
||||
|
||||
const JSON_HEADERS = { 'Content-Type': 'application/json' };
|
||||
const NO_CACHE: RequestInit = {
|
||||
@@ -109,6 +109,35 @@ export function getFolder(id: string): Promise<FolderItem> {
|
||||
return request;
|
||||
}
|
||||
|
||||
// ── Ancestor chain (breadcrumb) ──────────────────────────────────────────
|
||||
// Backing store + inflight dedup for `GET /api/folders/{id}/ancestors` —
|
||||
// mirrors the folderInflight pattern for `getFolder`. Rapid navigation
|
||||
// (files → sub → sub-sub in <1s) folds concurrent requests for the same
|
||||
// leaf into one round-trip. Response also seeds `folderNames` for every
|
||||
// ancestor, so subsequent `getFolderName(id)` lookups are cache-free.
|
||||
const ancestorsInflight = new Map<string, Promise<FolderAncestorsResponse>>();
|
||||
|
||||
export function getFolderAncestors(id: string): Promise<FolderAncestorsResponse> {
|
||||
const inflight = ancestorsInflight.get(id);
|
||||
if (inflight) return inflight;
|
||||
const request = (async () => {
|
||||
try {
|
||||
const chain = await apiJson<FolderAncestorsResponse>(
|
||||
`/api/folders/${id}/ancestors`,
|
||||
NO_CACHE
|
||||
);
|
||||
// Prime the shared folder-name cache — the breadcrumb walk
|
||||
// happens to be the exact input that populates it.
|
||||
for (const a of chain.ancestors) rememberFolderName(a.id, a.name);
|
||||
return chain;
|
||||
} finally {
|
||||
ancestorsInflight.delete(id);
|
||||
}
|
||||
})();
|
||||
ancestorsInflight.set(id, request);
|
||||
return request;
|
||||
}
|
||||
|
||||
/** One page of `/api/folders/{id}/resources`. */
|
||||
export interface FolderPage {
|
||||
/**
|
||||
|
||||
@@ -413,3 +413,68 @@ export interface DriveMember {
|
||||
granted_at: string;
|
||||
expires_at?: string | null;
|
||||
}
|
||||
|
||||
// ─── Folder ancestors (breadcrumb endpoint) ──────────────────────────────
|
||||
// Wire shape of `GET /api/folders/{id}/ancestors`. Mirrors the backend
|
||||
// `FolderAncestorsDto` — see `src/application/dtos/folder_dto.rs`. One
|
||||
// round-trip returns the whole caller-visible parent chain plus an
|
||||
// `access_source` telling the breadcrumb component which root icon /
|
||||
// tooltip to render.
|
||||
|
||||
export interface FolderAncestor {
|
||||
id: string;
|
||||
name: string;
|
||||
/** `null` on the drive-root ancestor. */
|
||||
parent_id: string | null;
|
||||
/**
|
||||
* Drive the folder belongs to (always populated — every folder has a
|
||||
* drive_id post-D0). Lets `/files` derive `currentFolderDriveId` from
|
||||
* the ancestors response instead of firing an extra
|
||||
* `GET /api/folders/{id}` on load. Same value across every entry in
|
||||
* `ancestors` (all folders in a chain live in one drive).
|
||||
*/
|
||||
drive_id: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* How the caller reached the topmost accessible ancestor.
|
||||
* - `drive` — via drive membership (own personal, secondary personal, or
|
||||
* shared drive). `drive` field carries the drive's id/name/kind for
|
||||
* the root icon.
|
||||
* - `direct_share` — via a folder-level `role_grants` row (share).
|
||||
* `subject` may name the grantee (self or a group) once subject
|
||||
* enrichment lands; MVP leaves it null.
|
||||
* - `token` — reserved for public-link callers. Not emitted today.
|
||||
*/
|
||||
export type AccessSourceKind = 'drive' | 'direct_share' | 'token';
|
||||
|
||||
export interface AccessSourceDrive {
|
||||
id: string;
|
||||
name: string;
|
||||
kind: DriveKind;
|
||||
}
|
||||
|
||||
export interface AccessSourceSubject {
|
||||
kind: 'user' | 'group';
|
||||
id: string;
|
||||
/** Nullable in MVP (subject enrichment deferred). */
|
||||
name?: string | null;
|
||||
}
|
||||
|
||||
export interface AccessSource {
|
||||
kind: AccessSourceKind;
|
||||
/** Populated when `kind === 'drive'`. */
|
||||
drive?: AccessSourceDrive;
|
||||
/** Optional grantee info for shares / group grants. */
|
||||
subject?: AccessSourceSubject;
|
||||
}
|
||||
|
||||
/**
|
||||
* Response envelope of `GET /api/folders/{id}/ancestors`. `ancestors`
|
||||
* is root-first, leaf-last (length ≥ 1). `access_source` describes
|
||||
* the boundary at element 0 (drive root or share boundary).
|
||||
*/
|
||||
export interface FolderAncestorsResponse {
|
||||
ancestors: FolderAncestor[];
|
||||
access_source: AccessSource;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user