fix(web): visual QA front-end items U4–U6, U13–U16 and copy list

- U4: form save failures toast the first problem and scroll/focus the first
  invalid field (policy draft, settings cards, resource drawers, subscription
  form) via lib/form-errors.
- U5: FormField keeps 8px between a hint and the next label.
- U6: file manager status bar keeps free space left-aligned, clear of the
  Agent button.
- U13: move/copy dialog stops "target is the source folder / inside the
  source" before requesting a preview (transferTargetProblem + tests).
- U14: account popover uses the solid float material.
- U15/U16: inbox tile hint shortened; TMDB assign dialog uses the shared
  Select and labelled fields (type, TMDB ID, note).
- Copy: "TMDB 已保存" spacing, API Key block no longer repeats the section
  title, whole-day intervals read "每 7 天", "最高级别:提示",
  "延后:已有计划在途", failed records show "未生成目标路径", upstream model
  column "默认模型".

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-28 16:58:38 +08:00
1 parent 2ae780d2a1
commit 082c556a67
18 files changed
+130 -38

No files matched your search

@@ -55,6 +55,7 @@ import {
newIdempotencyKey,
splitCommon,
stemRange,
transferTargetProblem,
} from "./model.ts";
// ─── 共用 ───────────────────────────────────────────────────────
@@ -485,6 +486,11 @@ export function TransferDialog({
const runPreview = (t: FileRef, m: "move" | "copy") => {
setPlan(null);
setErr(null);
const problem = transferTargetProblem(items, t);
if (problem) {
setErr(problem);
return;
}
dry.mutate(
{ idempotencyKey: `${key}-dry-${m}-${t.root}-${t.path}`, items, target: t, mode: m, dryRun: true },
{ onSuccess: setPlan, onError: (e) => setErr(filesErrorText(e, "预览失败")) },
@@ -23,6 +23,7 @@ import {
shiftEpisodes,
splitCommon,
stemRange,
transferTargetProblem,
} from "./model.ts";
const order = ["a", "b", "c", "d", "e"];
@@ -257,3 +258,20 @@ describe("演示开关", () => {
expect(resolveFilesMock({ dev: true, search: "?mock=0", storage })).toBe(false);
});
});
describe("transferTargetProblem(U13)", () => {
const items = [
{ root: "dl", path: "a/b.mkv" },
{ root: "dl", path: "pack" },
];
it("目标就是源所在目录 / 在源目录之内时拦下", () => {
expect(transferTargetProblem(items, { root: "dl", path: "a" })).toMatch(/已在这个目录里/);
expect(transferTargetProblem(items, { root: "dl", path: "pack/sub" })).toMatch(/目标在源目录之内/);
expect(transferTargetProblem([{ root: "dl", path: "x.mkv" }], { root: "dl", path: "" })).toMatch(/已在/);
});
it("其他目录、其他根放行", () => {
expect(transferTargetProblem(items, { root: "dl", path: "c" })).toBeNull();
expect(transferTargetProblem(items, { root: "lib", path: "a" })).toBeNull();
expect(transferTargetProblem(items, { root: "dl", path: "packs" })).toBeNull();
});
});
+17
View File
@@ -178,6 +178,23 @@ export function parentRel(path: string): string {
return i < 0 ? "" : path.slice(0, i);
}
/**
* 移动 / 复制目标的前端预检(与服务端 transfer 校验同口径,QA U13):目标就是源所在目录,
* 或目标在源目录之内时直接提示,不发预览请求。
*/
export function transferTargetProblem(
items: { root: string; path: string }[],
target: { root: string; path: string },
): string | null {
for (const it of items) {
if (it.root !== target.root) continue;
if (target.path === it.path || target.path.startsWith(`${it.path}/`))
return `目标在源目录之内(${baseName(it.path)}),换一个目录`;
if (parentRel(it.path) === target.path) return `${baseName(it.path)} 已在这个目录里,换一个目录`;
}
return null;
}
export function baseName(path: string): string {
const i = path.lastIndexOf("/");
return i < 0 ? path : path.slice(i + 1);