feat(organize,web): sidecars follow selected videos; /console/organize?ids= prefill (FE-4 follow-ups)
- planImport (sourcePaths mode): when a selected source is a video file, same-directory subtitles / external audio whose stem (language tags stripped) matches it join the source list, so the organize wizard, which expands a directory into individual videos, no longer drops them. Unrelated sidecars stay out; pathGuard applies to the added files. - Organize records page honours ?ids= (wizard multi-group redirect): shows just those records via /files/records, refreshes while running, clear chip. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
363f85b1a1
commit
2ae780d2a1
5 files changed
+310
-7
No files matched your search
@@ -4,6 +4,7 @@ import { EventType } from "@umt/contracts";
|
||||
import { FileStack, FolderInput, X } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useNavigate, useSearchParams } from "react-router";
|
||||
import { useFileRecords } from "~/api/files.ts";
|
||||
import { RECORD_STATUS_LABEL, RECORD_STATUS_TONE, TRIGGER_LABEL, useRecords } from "~/api/records.ts";
|
||||
import { type DataColumn, DataTable } from "~/components/console/data-table.tsx";
|
||||
import { FilterBar, FilterSelect } from "~/components/console/filter-bar.tsx";
|
||||
@@ -89,6 +90,12 @@ export default function OrganizePage() {
|
||||
const [qInput, setQInput] = useState("");
|
||||
const [q, setQ] = useState("");
|
||||
const entityId = params.get("mediaEntityId") ?? undefined;
|
||||
// 整理向导执行多组后跳来:?ids=<记录 id,…> 只看这几条(执行中每 3 秒刷新)
|
||||
const ids = (params.get("ids") ?? "")
|
||||
.split(",")
|
||||
.map((x) => x.trim())
|
||||
.filter(Boolean);
|
||||
const byIds = useFileRecords({ ids }, ids.length > 0);
|
||||
const navigate = useNavigate();
|
||||
const qc = useQueryClient();
|
||||
useEffect(() => {
|
||||
@@ -101,9 +108,13 @@ export default function OrganizePage() {
|
||||
mediaEntityId: entityId,
|
||||
q: q || undefined,
|
||||
});
|
||||
useSse([EventType.organizationChanged], () => void qc.invalidateQueries({ queryKey: ["records"] }));
|
||||
const rows = records.data?.pages.flatMap((p) => p.items);
|
||||
const filtered = !!(status || trigger || q || entityId);
|
||||
useSse([EventType.organizationChanged], () => {
|
||||
void qc.invalidateQueries({ queryKey: ["records"] });
|
||||
if (ids.length) void qc.invalidateQueries({ queryKey: ["files", "records"] });
|
||||
});
|
||||
const pinned = ids.length > 0;
|
||||
const rows = pinned ? byIds.data?.items : records.data?.pages.flatMap((p) => p.items);
|
||||
const filtered = !!(status || trigger || q || entityId || pinned);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -148,16 +159,22 @@ export default function OrganizePage() {
|
||||
<X />
|
||||
</Button>
|
||||
) : null}
|
||||
{pinned ? (
|
||||
<Button variant="outline" size="sm" onClick={() => setParams({})}>
|
||||
本次整理的 {ids.length} 条
|
||||
<X />
|
||||
</Button>
|
||||
) : null}
|
||||
</FilterBar>
|
||||
<DataTable
|
||||
columns={COLUMNS}
|
||||
rows={rows}
|
||||
rowKey={(r) => r.id}
|
||||
loading={records.isPending}
|
||||
error={records.error}
|
||||
onRetry={() => void records.refetch()}
|
||||
loading={pinned ? byIds.isPending : records.isPending}
|
||||
error={pinned ? byIds.error : records.error}
|
||||
onRetry={() => void (pinned ? byIds.refetch() : records.refetch())}
|
||||
onRowClick={(r) => navigate(`/console/organize/${r.id}`)}
|
||||
hasMore={records.hasNextPage}
|
||||
hasMore={!pinned && records.hasNextPage}
|
||||
onLoadMore={() => void records.fetchNextPage()}
|
||||
loadingMore={records.isFetchingNextPage}
|
||||
empty={{
|
||||
|
||||
Reference in new issue
Block a user