fix(files): an upload is said once in any order, copy buttons speak, a link list that failed says so

- The queue holds an upload it sees land done while this tab's upload still waits for its answer, so
  the dialog's notice is the only one whichever comes first; another device's upload follows after.
- One `useCopyFeedback` for every Copy beside a value (token, share link, set-password link): Copied
  for a moment, a notice a screen reader hears, a warning when the clipboard refuses; the token
  dialog's two Copy buttons are named by what they copy.
- The share dialog's link list shows loading, and a load error with Retry, never "no links" — and no
  New link beside a list it has not seen.
- The settings texts say that signing out elsewhere or a password change revokes the API tokens.
- DESIGN.md logs the folder-link decisions; the design lab keeps only the chosen renderings of the
  Links, Tokens and Files rounds.
This commit is contained in:
2026-10-15 04:58:17 +02:00
parent 827df7494c
commit 2efa7d5a75
23 changed files with 505 additions and 773 deletions
+5 -13
View File
@@ -14,11 +14,9 @@ import {
type TokenRight,
} from "../lib/api";
import { qk } from "../lib/queryKeys";
import { copyText } from "../lib/clipboard";
import { compareText } from "../lib/collate";
import { formatDate } from "../lib/format";
import { navItemAttrs } from "../lib/listNav";
import { notify } from "../lib/notifications";
import { useConfirm } from "./confirmContext";
import { folderIcon, folderLabel, takesUploads } from "./fileFolders";
import { type ActionMenuItem } from "./ui/ActionMenu";
@@ -37,6 +35,7 @@ import { RelativeTime } from "./ui/RelativeTime";
import SegmentedToggle from "./ui/SegmentedToggle";
import { SURFACE } from "./ui/surfaces";
import { useContextMenu } from "./ui/useContextMenu";
import { useCopyFeedback } from "./useCopyFeedback";
// Settings → API tokens: a machine's key to the user's Files library (docs/DESIGN.md decision log,
// "API tokens"). A row per token in three lines — what it is, what it opens, when it was used and
@@ -477,16 +476,7 @@ function CopyField({
autoFocus?: boolean;
}) {
const { t } = useTranslation();
const [copied, setCopied] = useState(false);
const copy = async () => {
if (await copyText(value)) {
setCopied(true);
window.setTimeout(() => setCopied(false), 2000);
return;
}
// The value is on screen and selectable: a failed copy is a convenience that failed.
notify({ level: "warning", message: t("common.copy.failed") });
};
const { copied, copy } = useCopyFeedback();
return (
<div className="flex flex-col gap-1">
<span className="text-xs text-muted">{label}</span>
@@ -499,7 +489,9 @@ function CopyField({
size="sm"
autoFocus={autoFocus}
icon={copied ? <Check className="w-4 h-4" /> : <Copy className="w-4 h-4" />}
onClick={() => void copy()}
// Two of these sit in one dialog: each is named by the value it copies.
aria-label={t("settings.tokens.copyField", { field: label })}
onClick={() => void copy(value)}
>
{copied ? t("settings.tokens.copied") : t("settings.tokens.copy")}
</Button>