fix(files): review round 1, the page — honest states, the keyboard, the tray's words

- A library past the list's cap is searched and sorted on the server; the count says "the newest".
- Each folder and each search opens on its first page (DataTable pageScope, useCardPager scope).
- A file dropped where the page takes none never reaches the browser (which would unload the app).
- The viewer's arrows wait while a layer opened from it is on top, and scroll a wide text line;
  a text shows its first 5000 lines and says where the rest is.
- Confirms name the file (no more "your downloads"); the asking menu rows end in "…".
- The tray names a running step beside its bar, shows a pause mark for paused work, polls slowly
  while only paused work waits, and forgets a finished file that was deleted.
- An unchanged rename sends nothing; a paste opens the download dialog only with nothing on top;
  the link box never replaces what was typed.
- Honest states: folders that failed to load, a file no longer there, the storage figure; the rail
  says "nothing downloaded" only when there is nothing.
- Focus after deleting the open folder and when the format form opens; the format name's label.
- ui/Meter (one danger line for both quota bars) and ui/RenameField in the platform layer.
- Copy fixes, stale comments and the speculative hub key; a contract test for the list's
  parameters; e2e: the Files page and the paste intake.
This commit is contained in:
2026-10-13 00:14:09 +02:00
parent 815083bcc4
commit 8b2260c9a8
41 changed files with 920 additions and 218 deletions
+12 -11
View File
@@ -1,4 +1,5 @@
import { useEffect, useRef, useState } from "react";
import { Meter } from "./ui/Meter";
import { useQuery } from "@tanstack/react-query";
import { useTranslation } from "react-i18next";
import clsx from "clsx";
@@ -12,7 +13,7 @@ import { useLiveQuery } from "../lib/useLiveQuery";
import FilterRail from "./FilterRail";
import { folderIcon, folderLabel } from "./fileFolders";
import { sameView, useFiles, useFilesActions, type FilesView } from "./filesContext";
import { RenameField } from "./RenameField";
import { RenameField } from "./ui/RenameField";
import { useFolderActions } from "./useFolderActions";
import { ContextMenu } from "./ui/ContextMenu";
import { Pressable } from "./ui/Pressable";
@@ -77,9 +78,13 @@ function StorageMeter() {
intervalMs: (u) => (u?.running_jobs ? 2000 : 15000),
});
const u = usage.data;
if (!u) return null;
const pct =
u.unlimited || !u.max_bytes ? 0 : Math.min(100, (u.footprint_bytes / u.max_bytes) * 100);
// A failed read says so where the figures would be, rather than the meter quietly not being there.
if (!u)
return usage.isError ? (
<p className="mt-3 px-2 text-xs text-muted" data-testid="storage-meter">
{t("files.usage.failed")}
</p>
) : null;
return (
<div className="mt-3 flex flex-col gap-1 px-2 text-xs text-muted" data-testid="storage-meter">
<span className="flex justify-between gap-2">
@@ -90,12 +95,7 @@ function StorageMeter() {
</span>
</span>
{!u.unlimited && (
<span className="h-1 overflow-hidden rounded-full bg-fg/10">
<span
className={clsx("block h-full rounded-full", pct > 90 ? "bg-danger" : "bg-accent")}
style={{ width: `${pct}%` }}
/>
</span>
<Meter value={u.footprint_bytes} max={u.max_bytes} label={t("files.usage.title")} />
)}
</div>
);
@@ -202,7 +202,8 @@ export default function FilesRail({
<div className="flex flex-col gap-0.5">
{failed}
{sources.map(entry)}
{foldersQ.data && sources.length === 0 && (
{/* Only when there is nothing at all: files moved into own folders were still downloaded. */}
{foldersQ.data && sources.length === 0 && total === 0 && (
<span className="px-2 py-1 text-xs text-muted">{t("files.rail.noSources")}</span>
)}
</div>