feat(files): folders managed in place, a grid per folder, drop to upload, open on one file

- Own folders are made, renamed and deleted from the rail (right click, long press,
  Shift+F10) or the "…" beside an open folder's name; any folder's retention is a
  submenu (7/30/90 days, until deleted). Deleting confirms where its files go.
- List or grid per folder, sharing one sort; the grid is the one media card.
- A file dropped anywhere on the page opens Upload with it, bound for the folder
  on screen.
- A link (?item=) or the upload notice opens Files on the file: its page turned
  to, the keyboard on it, a ring round it.
- An in-place name field left by Enter or Escape hands the focus back to what
  replaced it (the row, the entry, the "…") instead of dropping it on <body>.
- The table's saved page size is per account.
This commit is contained in:
2026-10-11 15:59:12 +02:00
parent 85aa917b38
commit d22c64f4dd
23 changed files with 1569 additions and 175 deletions
+13
View File
@@ -72,8 +72,21 @@ function listQuery(p: FileListParams): string {
return s ? `?${s}` : "";
}
// What a folder PATCH may change. Presence decides: `retention_days: null` = keep its files forever.
export interface FolderPatch {
name?: string; // an own folder's only
retention_days?: number | null;
}
export const filesApi = {
fileFolders: (): Promise<FileFolder[]> => req("/api/files/folders"),
createFolder: (name: string): Promise<FileFolder> =>
req("/api/files/folders", { method: "POST", body: JSON.stringify({ name }) }),
updateFolder: (id: number, patch: FolderPatch): Promise<FileFolder> =>
req(`/api/files/folders/${id}`, { method: "PATCH", body: JSON.stringify(patch) }),
// An own folder only; its files go back to the folders they came from.
deleteFolder: (id: number): Promise<{ ok: true; deleted: number; files_moved: number }> =>
req(`/api/files/folders/${id}`, { method: "DELETE" }),
files: (p: FileListParams = {}): Promise<LibraryFile[]> => req(`/api/files${listQuery(p)}`),
filesUsage: (): Promise<StorageUsage> => req("/api/files/usage"),
updateFile: (id: number, patch: FilePatch): Promise<DownloadJob> =>