Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 38 additions & 16 deletions docs/features/grid-selector.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,12 @@ more isolated follow-up per the owner's Phase-2 direction.

## Select Version section (issue #167, Proposal H §4.4′)

The unified display page's (`/display`) rail "Choose Image" accordion
body no longer renders the flat `GridSelectorResults`/`CardResultSet`
grid — it mounts `SelectVersionResults.tsx`, which groups the same
The unified display page's (`/display`) rail's Select Version surface
(promoted + always open since the editor-completion package's left-panel
fidelity rebuild - see that section's own note below; was the "Choose
Image" accordion before that round) no longer renders the flat
`GridSelectorResults`/`CardResultSet` grid — it mounts
`SelectVersionResults.tsx`, which groups the same
candidate identifier list into the three ordered groups
`docs/proposals/proposal-h-unified-display-page.md`'s §4.4′ specifies,
and weaves in its three verification moments. **Scope: this replaces the
Expand Down Expand Up @@ -149,11 +152,30 @@ still back that modal.
`altered-frame > custom-art > ai-art`
(`SELECT_VERSION_REASON_TAG_PRIORITY`), an arbitrary but documented
choice, since the spec doesn't pin `custom-art` vs. `ai-art`'s relative
order. (2) The "Choose Image" accordion title/testids were kept
as-is (not renamed to "Select Version") to avoid churning every
existing passing test that references that exact heading text — a
pure-copy rename is a small, separate follow-up if the owner wants the
label itself to match the spec's own name for this section.
order.
- **Resolved by the editor-completion package's left-panel fidelity
rebuild (E2/E3/L4)**: the rail heading is now genuinely "Select
Version" (the once-deferred pure-copy rename above), promoted to an
always-visible, always-open surface with no `AutofillCollapse` wrapper
at all - it's no longer one accordion among several (D3: art selection
is the primary surface, not something a user has to expand). The same
round also fixed the rail-specific fidelity breakages this section used
to carry: `initialSettingsVisible={false}` on `useGridSelectorSearch`
(Filters starts collapsed in the rail regardless of viewport width -
previously auto-opened cramped on any desktop-width rail) and
`layout="stacked"` on `SelectVersionResults` (the Filters disclosure
renders full-width, stacked, in the rail's own scroll container instead
of the modal's `Col lg={3}` sidebar split - "Jump to Version" no longer
wraps vertically, bottom controls no longer clip at the rail edge). The
always-on `FilterChipBar` wall moved INTO the Filters disclosure for
the stacked/rail caller (still reachable, no longer a permanent
multi-row height sink atop every result); `GridSelectorFilters` gained
an additive `hiddenSections` prop the rail uses to drop "View"
(Group-by/Compressed - both redundant in the rail: it groups results
itself, and the 380px rail already forces compact tiles). Every one of
these is additive/optional and defaults to today's modal behavior -
`GridSelectorModal.tsx`'s own caller passes none of them and is
unaffected.

## Key files

Expand Down Expand Up @@ -184,11 +206,11 @@ still back that modal.
a 150-synthetic-card grid in a mocked sandbox; it doesn't reproduce
real backend/ES latency or real image-CDN load timing at that scale.
- Issue #167's Select Version section: see its own "Open items" bullet
above for the two owner-decidable gaps (group 2's ai-art/custom-art
order, and the un-renamed "Choose Image" label). The Filters column
(`GridSelectorFilters`) reused inside the rail still uses the same
viewport-keyed Bootstrap column breakpoints as the classic modal's
`FiltersColumn` (pre-existing, not introduced by this build) — cramped
at the rail's ~380px width, a known layout gap `CardRow`'s own
"embedded" variant comment already flags for the results grid and
wasn't in this issue's scope to fix for the filters column too.
above for the one still-owner-decidable gap (group 2's ai-art/custom-art
order). The rail's own Filters-column cramping (the previous bullet
here) was fixed by the editor-completion package's left-panel fidelity
rebuild - see this section's own "Resolved by..." paragraph above
(`layout="stacked"` drops the `Col lg={3}` sidebar split for the rail
caller specifically; `GridSelectorModal.tsx`'s own sidebar layout is
unchanged, so this is a rail-only fix, not a change to the shared
column-breakpoint default itself).
31 changes: 31 additions & 0 deletions docs/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -1131,6 +1131,37 @@ the flex-column wrapper (testing the single SVG alone) fixed it, which is
what pointed at the flex cross-axis stretch specifically rather than the
SVG/viewBox mechanics themselves.

## `DisplayPage.spec.ts`'s "floating sheet-position pill updates live while scrolling at phone width (D17)" fails intermittently with "2/3" instead of "3/3"

**Symptom**: `tests/DisplayPage.spec.ts`'s D17 sheet-position-pill test
(phone viewport, 18 cards / 3 sheets) intermittently reports the
`display-sheet-position-indicator` still reading `2/3` after
`scrollIntoView({block:"center"})` on the last sheet, where `3/3` is
expected - reproduces even running that ONE test alone,
`--repeat-each=5`+, on both a freshly-modified branch and on plain
`origin/master` with zero changes.

**Cause**: a genuine, pre-existing flake in this test's own
IntersectionObserver-based timing (its `rootMargin: "-45% 0px -45% 0px"`
thin center-band check races the browser's `scrollIntoView` completing),
not a regression from any particular change - confirmed by reproducing
the same ~1-in-6 failure rate on unmodified `origin/master` in this same
sandboxed VM. A change that happens to add or remove incidental
rendering cost nearby (e.g. widening a `useMemo`'s dependency array on
the sheet-content builder) can shift the failure rate up or down without
being the actual cause - don't chase a "regression" here without first
checking the identical test against a clean `origin/master` checkout in
the same environment, `--repeat-each=5` or more (a single passing/failing
run either way is not enough evidence).

**Fix/mitigation**: none applied - this is a test-timing flake to
tolerate (retry) rather than a product bug to fix; if it starts failing
CI at a rate that matters, the real fix is loosening the test's own
`scrollIntoView`-then-assert race (e.g. an explicit
`waitForFunction`/poll on the indicator text before asserting, instead of
relying on `expect(...).toContainText`'s own retry window alone), not
touching the sheet-content pipeline it happens to render.

## `run_image_evidence_cohort` (Stage C) parent process's RSS climbs unboundedly and OOMs the whole box on a long run

**Symptom**: a long (tens-of-thousands-of-cards) `run_image_evidence_cohort`
Expand Down
288 changes: 288 additions & 0 deletions frontend/src/features/cardDetailedView/CardDetailedViewBody.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,288 @@
/**
* The editor-completion package's §7.5/R3 extraction (docs/proposals - the "editor completion"
* left-panel-fidelity round, item E6/X5). Pulls CardDetailedViewModal.tsx's body content out into
* region-level, independently-mountable sub-blocks - CardMetaTable, CardIdentifierCopy,
* CardDownloadFavorite, PrintingTagsBlock, ReportBlock - plus CardDetailedViewBody, which
* recomposes them in the modal's own exact order. This is deliberately NOT one blob: the /display
* left rail's new demoted "Card Details"/"Printing Tags"/"Report" sections (DisplayPage.tsx) mount
* CardMetaTable+CardDownloadFavorite, PrintingTagsBlock, and ReportBlock individually, each as its
* own collapsed AutofillCollapse per the D3 hierarchy - never the whole modal body, since the rail
* already has its own art-selection surface (Select Version) and doesn't want a second full-size
* card image render.
*
* Acceptance test for this extraction (per the task's own directive) is
* tests/visual/CardDetailedViewModal.visual.spec.ts staying green UNMODIFIED - its aria snapshot
* asserts the modal's exact DOM shape, so CardDetailedViewModal.tsx's own render below must stay
* byte-for-byte equivalent to what CardDetailedViewModal.tsx used to render inline. Every
* sub-block is fully self-contained (fetches its own hook data - useGetLanguagesQuery,
* useDoImageDownload, useTagDisplayName, selectRemoteBackendURL - rather than threading it down
* from a parent), so the rail can mount any one of them standalone with no shared parent state.
*
* `showAddToProjectForm` (default true, matching every existing behavior) is the one prop this
* extraction adds: AddCardToProjectForm is dropped from the rail's own Card Details mount (the
* slot is already in the project there - see DisplayPage.tsx's Rail composition) but stays in the
* modal, unconditionally, exactly as before.
*/
import React, { useState } from "react";
import Badge from "react-bootstrap/Badge";
import Button from "react-bootstrap/Button";

import { PrintingConsensusResponse } from "@/common/schema_types";
import { useTagDisplayName } from "@/common/tagDisplayNames";
import { CardDocument, useAppDispatch, useAppSelector } from "@/common/types";
import { imageSizeToMBString, toTitleCase } from "@/common/utils";
import { ArtistSupportLink } from "@/components/ArtistSupportLink";
import { AutofillTable } from "@/components/AutofillTable";
import { ClickToCopy } from "@/components/ClickToCopy";
import { RightPaddedIcon } from "@/components/icon";
import { SetIcon } from "@/components/SetIcon";
import { AttributeVotingPanel } from "@/features/attributeVoting/AttributeVotingPanel";
import { AddCardToFavorites } from "@/features/card/AddCardToFavorites";
import { AddCardToProjectForm } from "@/features/card/AddCardToProjectForm";
import { useDoImageDownload } from "@/features/download/downloadImages";
import { PrintingTagPicker } from "@/features/printingTags/PrintingTagPicker";
import { ReportCardPanel } from "@/features/reporting/ReportCardPanel";
import { useGetLanguagesQuery } from "@/store/api";
import { selectRemoteBackendURL } from "@/store/slices/backendSlice";
import { setNotification } from "@/store/slices/toastsSlice";

//# region CardIdentifierCopy

interface CardIdentifierCopyProps {
identifier: string;
}

/** Thin, named wrapper over ClickToCopy for the metadata table's "Identifier" row - exported per
* the extraction's own sub-block list (E6) so a future standalone "copy identifier" affordance
* doesn't need to re-derive this from CardMetaTable. */
export function CardIdentifierCopy({ identifier }: CardIdentifierCopyProps) {
return <ClickToCopy text={identifier} />;
}

//# endregion

//# region CardMetaTable

interface CardMetaTableProps {
cardDocument: CardDocument;
}

export function CardMetaTable({ cardDocument }: CardMetaTableProps) {
const getLanguagesQuery = useGetLanguagesQuery();
const getTagDisplayName = useTagDisplayName();
const languageNameByCode = Object.fromEntries(
(getLanguagesQuery.data ?? []).map((row) => [row.code, row.name])
);

return (
<AutofillTable
headers={[]}
data={[
[
"Source Name",
cardDocument.sourceExternalLink != null &&
cardDocument.sourceExternalLink.length > 0 ? (
<a href={cardDocument.sourceExternalLink} target="_blank">
{cardDocument.sourceVerbose}
</a>
) : (
cardDocument.sourceVerbose
),
],
["Source Type", cardDocument.sourceType],
["Class", toTitleCase(cardDocument.cardType)],
[
"Identifier",
<CardIdentifierCopy
key={`${cardDocument.identifier}-click-to-copy`}
identifier={cardDocument.identifier}
/>,
],
["Language", languageNameByCode[cardDocument.language]],
[
"Tags",
cardDocument.tags.length > 0 ? (
<>
{cardDocument.tags.map((tag) => (
<Badge key={tag} pill>
{getTagDisplayName(tag)}
</Badge>
))}
</>
) : (
"Untagged"
),
],
["Resolution", `${cardDocument.dpi} DPI`],
["Date Created", cardDocument.dateCreated],
["Date Modified", cardDocument.dateModified],
["File Size", imageSizeToMBString(cardDocument.size, 2)],
[
"Canonical Card",
cardDocument.canonicalCard ? (
<>
<SetIcon
expansionCode={cardDocument.canonicalCard.expansionCode}
/>{" "}
{cardDocument.canonicalCard.expansionCode.toUpperCase()}{" "}
{cardDocument.canonicalCard.collectorNumber}
</>
) : (
"Unknown"
),
],
[
"Canonical Aritst",
cardDocument.canonicalArtist != null ? (
<ArtistSupportLink artistName={cardDocument.canonicalArtist.name}>
{cardDocument.canonicalArtist.name}
</ArtistSupportLink>
) : (
"Unknown"
),
],
]}
hover={true}
alignment={"left"}
uniformWidth={false}
columnLabels={true}
/>
);
}

//# endregion

//# region CardDownloadFavorite

interface CardDownloadFavoriteProps {
cardDocument: CardDocument;
}

export function CardDownloadFavorite({
cardDocument,
}: CardDownloadFavoriteProps) {
const dispatch = useAppDispatch();
const queueImageDownload = useDoImageDownload();

return (
<>
{cardDocument.sourceType === "Google Drive" && (
<div className="d-grid gap-0">
<Button
variant="primary"
onClick={async () => {
queueImageDownload(cardDocument);
dispatch(
setNotification([
Math.random().toString(),
{
name: "Enqueued Downloads",
message: `Enqueued 1 image download!`,
level: "info",
},
])
);
}}
>
<RightPaddedIcon bootstrapIconName="cloud-arrow-down" /> Download
Image
</Button>
</div>
)}
<AddCardToFavorites cardDocument={cardDocument} />
</>
);
}

//# endregion

//# region PrintingTagsBlock

interface PrintingTagsBlockProps {
cardDocument: CardDocument;
}

export function PrintingTagsBlock({ cardDocument }: PrintingTagsBlockProps) {
const backendURL = useAppSelector(selectRemoteBackendURL);
const [printingConsensus, setPrintingConsensus] =
useState<PrintingConsensusResponse | null>(null);

return (
<>
<hr />
<h5>What&apos;s That Card?</h5>
<p className="text-muted small mb-2">
Help us figure out which real-world printing this card is!
</p>
<PrintingTagPicker
cardIdentifier={cardDocument.identifier}
cardName={cardDocument.name}
onConsensusChange={setPrintingConsensus}
/>
{printingConsensus != null &&
printingConsensus.resolvedPrinting == null &&
backendURL != null && (
<>
<hr />
<AttributeVotingPanel
backendURL={backendURL}
cardIdentifier={cardDocument.identifier}
confidentlyKnownArtistName={
cardDocument.canonicalArtist != null &&
!cardDocument.canonicalArtistIsFromVoteOnly
? cardDocument.canonicalArtist.name
: null
}
/>
</>
)}
</>
);
}

//# endregion

//# region ReportBlock

interface ReportBlockProps {
cardDocument: CardDocument;
}

export function ReportBlock({ cardDocument }: ReportBlockProps) {
return <ReportCardPanel cardDocument={cardDocument} />;
}

//# endregion

//# region CardDetailedViewBody

interface CardDetailedViewBodyProps {
cardDocument: CardDocument;
/** Modal-only (default true, matching every existing behavior): the rail's own "Card Details"
* mount passes false, since the slot is already in the project there - see this file's own
* module comment. */
showAddToProjectForm?: boolean;
}

/** The modal's own right-column content (heading + metadata + download/favorite + add-to-project
* + report + printing tags), recomposed from the sub-blocks above in their original order - this
* is what makes CardDetailedViewModal.tsx's own render byte-for-byte unchanged. */
export function CardDetailedViewBody({
cardDocument,
showAddToProjectForm = true,
}: CardDetailedViewBodyProps) {
return (
<>
<h4>{cardDocument.name}</h4>
<CardMetaTable cardDocument={cardDocument} />
<CardDownloadFavorite cardDocument={cardDocument} />
{showAddToProjectForm && (
<AddCardToProjectForm cardDocument={cardDocument} />
)}
<ReportBlock cardDocument={cardDocument} />
<PrintingTagsBlock cardDocument={cardDocument} />
</>
);
}

//# endregion
Loading
Loading