Clicking a summary badge shows only shots with that status; several can be combined. Selection drops shots the filter hides so bulk actions never reach them. Filter resets on project change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1130,6 +1130,8 @@ export function ShotStatusClient({
|
||||
const [expandedGroups, setExpandedGroups] = useState<Set<string>>(new Set());
|
||||
const [groupMode, setGroupMode] = useState<"flat" | "scene" | "group">("flat");
|
||||
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
|
||||
// Statuses picked from the summary badges; empty shows every shot.
|
||||
const [statusFilter, setStatusFilter] = useState<Set<string>>(new Set());
|
||||
|
||||
// Which sections were open is restored before the scroll position: until
|
||||
// they are, the page is shorter and the saved offset would not fit.
|
||||
@@ -1141,6 +1143,7 @@ export function ShotStatusClient({
|
||||
useEffect(() => {
|
||||
if (!selectedProjectId) return;
|
||||
setExpandRestored(false);
|
||||
setStatusFilter(new Set());
|
||||
const mode = (readStored(`shotStatus:${selectedProjectId}:groupMode`) ?? "flat") as "flat" | "scene" | "group";
|
||||
setGroupMode(mode);
|
||||
setExpandedEpisodes(new Set(readStoredList(`shotStatus:${selectedProjectId}:episodeExpand`) ?? []));
|
||||
@@ -1235,6 +1238,22 @@ export function ShotStatusClient({
|
||||
|
||||
const clearSelection = () => setSelectedIds(new Set());
|
||||
|
||||
const toggleStatusFilter = (status: string) => {
|
||||
const next = new Set(statusFilter);
|
||||
next.has(status) ? next.delete(status) : next.add(status);
|
||||
setStatusFilter(next);
|
||||
// Bulk actions must never reach shots the filter has hidden.
|
||||
if (next.size > 0) {
|
||||
setSelectedIds((prev) => {
|
||||
const kept = new Set<string>();
|
||||
for (const s of shots) if (prev.has(s.id) && next.has(s.status)) kept.add(s.id);
|
||||
return kept;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const visibleShots = statusFilter.size > 0 ? shots.filter((s) => statusFilter.has(s.status)) : shots;
|
||||
|
||||
const handleBulkDueDate = async (date: string, includeTasks: boolean) => {
|
||||
try {
|
||||
const result = await bulkUpdateDueDates([...selectedIds], date, includeTasks);
|
||||
@@ -1320,7 +1339,7 @@ export function ShotStatusClient({
|
||||
const episodeGroups: [string, ShotRow[]][] = isEpisodic
|
||||
? (() => {
|
||||
const map = new Map<string, ShotRow[]>();
|
||||
for (const shot of shots) {
|
||||
for (const shot of visibleShots) {
|
||||
const key = shot.episode ?? "(No Episode)";
|
||||
if (!map.has(key)) map.set(key, []);
|
||||
map.get(key)!.push(shot);
|
||||
@@ -1332,7 +1351,7 @@ export function ShotStatusClient({
|
||||
const sceneGroups: [string, ShotRow[]][] = !isEpisodic && groupMode === "scene"
|
||||
? (() => {
|
||||
const map = new Map<string, ShotRow[]>();
|
||||
for (const shot of shots) {
|
||||
for (const shot of visibleShots) {
|
||||
const key = shot.scene || "(No Scene)";
|
||||
if (!map.has(key)) map.set(key, []);
|
||||
map.get(key)!.push(shot);
|
||||
@@ -1344,7 +1363,7 @@ export function ShotStatusClient({
|
||||
const shotGroupGroups: [string, string, ShotRow[]][] = !isEpisodic && groupMode === "group"
|
||||
? (() => {
|
||||
const map = new Map<string, [string, ShotRow[]]>();
|
||||
for (const shot of shots) {
|
||||
for (const shot of visibleShots) {
|
||||
const id = shot.shotGroup?.id ?? "";
|
||||
const name = shot.shotGroup?.name ?? "(No Group)";
|
||||
if (!map.has(id)) map.set(id, [name, []]);
|
||||
@@ -1437,16 +1456,45 @@ export function ShotStatusClient({
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{selectionCount === 0 ? (
|
||||
<>
|
||||
<span className="text-xs text-zinc-500 self-center">{shots.length} shots total</span>
|
||||
<span className="text-xs text-zinc-500 self-center">
|
||||
{statusFilter.size > 0 ? `${visibleShots.length} of ${shots.length} shots` : `${shots.length} shots total`}
|
||||
</span>
|
||||
{Object.entries(STATUS_CONFIG).map(([key, cfg]) => {
|
||||
const count = statusCounts[key] ?? 0;
|
||||
if (!count) return null;
|
||||
const active = statusFilter.has(key);
|
||||
return (
|
||||
<Badge key={key} className={cn("gap-1 text-xs", cfg.color)}>
|
||||
{count} {cfg.label}
|
||||
</Badge>
|
||||
<button
|
||||
key={key}
|
||||
type="button"
|
||||
onClick={() => toggleStatusFilter(key)}
|
||||
aria-pressed={active}
|
||||
title={active ? `Stop filtering by ${cfg.label}` : `Show only ${cfg.label}`}
|
||||
className="rounded-full"
|
||||
>
|
||||
<Badge
|
||||
className={cn(
|
||||
"gap-1 text-xs cursor-pointer",
|
||||
cfg.color,
|
||||
statusFilter.size > 0 && !active && "opacity-40 hover:opacity-70",
|
||||
active && "ring-1 ring-current"
|
||||
)}
|
||||
>
|
||||
{count} {cfg.label}
|
||||
</Badge>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
{statusFilter.size > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setStatusFilter(new Set())}
|
||||
className="flex items-center gap-1 text-xs text-zinc-500 hover:text-zinc-300 transition-colors"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
Clear filter
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="flex items-center gap-3 rounded-lg border border-blue-500/30 bg-blue-500/10 px-4 py-2">
|
||||
@@ -1669,7 +1717,7 @@ export function ShotStatusClient({
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<ShotTable
|
||||
shots={shots}
|
||||
shots={visibleShots}
|
||||
canManage={canManage}
|
||||
projectId={selectedProjectId!}
|
||||
selectedIds={selectedIds}
|
||||
|
||||
Reference in New Issue
Block a user