Add tracker pattern detail page with properties and detected trackers sections

Signed-off-by: Émile Ré <emile@probo.com>
This commit is contained in:
Émile Ré
2026-05-22 11:54:44 +02:00
parent f8debf5406
commit b46f2656f5
20 changed files with 1077 additions and 3 deletions

View File

@@ -264,6 +264,51 @@ pages/organizations/third-parties/ThirdPartiesPage.tsx
pages/organizations/third-parties/ThirdPartiesPageSkeleton.tsx
```
## Page sections
When a detail page has visually distinct sections (e.g. a properties card and a paginated list), extract each section into its own component in `_components/`. Each section owns a colocated Relay fragment (or pagination fragment) so that field additions never modify the parent page's query.
The page query spreads the section fragments and passes the fragment key to each section component:
```tsx
// TrackerPatternDetailPage.tsx (page — spreads section fragments)
export const trackerPatternDetailPageQuery = graphql`
query TrackerPatternDetailPageQuery($trackerPatternId: ID!) {
node(id: $trackerPatternId) {
... on TrackerPattern {
id
displayName
...TrackerPatternPropertiesSection_trackerPattern
...TrackerPatternDetectedTrackersSection_trackerPattern
}
}
}
`;
// In JSX:
<TrackerPatternPropertiesSection trackerPatternKey={pattern} />
<TrackerPatternDetectedTrackersSection trackerPatternKey={pattern} />
```
```tsx
// _components/TrackerPatternPropertiesSection.tsx — owns its fragment
const fragment = graphql`
fragment TrackerPatternPropertiesSection_trackerPattern on TrackerPattern {
pattern
matchType
trackerType
// ...
}
`;
export function TrackerPatternPropertiesSection({ trackerPatternKey }: Props) {
const pattern = useFragment(fragment, trackerPatternKey);
return <Card padded>{/* PropertyRows */}</Card>;
}
```
Section components follow the same naming and fragment conventions as connection item components (see below), but represent a **logical section** of a page rather than a single list item.
## `_components` folder
Sub-components that are used **only** by a single page live in a `_components/` folder next to that page. The underscore prefix visually distinguishes them from route-segment folders.

View File

@@ -400,3 +400,68 @@ export function MoveToCategoryMenu({ queryRef, onMove }: Props) {
See also the "Interaction-triggered queries" section in [`contrib/claude/relay.md`](relay.md).
(Snippet names and GraphQL types are illustrative; align with real schema and fragment names in the app.)
## Page sections are components
When a detail page has multiple distinct sections (e.g. a properties card and a paginated list), extract each section into its own component in `_components/`. Each section owns a colocated Relay fragment so that field additions never modify the parent page's query.
The page spreads the section fragments on the shared node and passes the fragment key:
```tsx
// Page query — spreads section fragments
export const detailPageQuery = graphql`
query DetailPageQuery($nodeId: ID!) {
node(id: $nodeId) {
... on MyType {
id
displayName
...MyTypePropertiesSection_myType
...MyTypeListSection_myType
}
}
}
`;
// Page JSX:
<MyTypePropertiesSection myTypeKey={node} />
<MyTypeListSection myTypeKey={node} />
```
```tsx
// _components/MyTypePropertiesSection.tsx
const fragment = graphql`
fragment MyTypePropertiesSection_myType on MyType {
field1
field2
}
`;
interface MyTypePropertiesSectionProps {
myTypeKey: MyTypePropertiesSection_myType$key;
}
export function MyTypePropertiesSection({ myTypeKey }: MyTypePropertiesSectionProps) {
const data = useFragment(fragment, myTypeKey);
return <Card padded>{/* PropertyRows */}</Card>;
}
```
For sections that own a paginated connection, use `usePaginationFragment` with a `@refetchable` fragment — the same pattern as a standalone page, but scoped to a section component.
## Connection items are components
When rendering items from a Relay connection (e.g. table rows via `edges.map(…)`), each item **must** be a dedicated component with its own colocated fragment — never inline the rendering of node fields directly in the parent's `.map()` body.
Place the item component in `_components/` adjacent to the page. Name it after the GraphQL type it renders (e.g. `DetectedTrackerRow.tsx`, `ThirdPartyCard.tsx`). The component receives a single fragment key prop (e.g. `detectedTrackerKey: DetectedTrackerRow_detectedTracker$key`) and calls `useFragment` internally.
```tsx
// Parent (page) — spreads the child fragment in the connection:
edges { node { id ...DetectedTrackerRow_detectedTracker } }
// Parent JSX:
{trackers.map(tracker => (
<DetectedTrackerRow key={tracker.id} detectedTrackerKey={tracker} />
))}
```
This ensures field additions/removals in the row never modify the parent's fragment, and keeps the item independently testable.