Skip to content

Commit 2b06b3d

Browse files
committed
Merge origin/dev (post-#721): infra/README conflict resolved (dev's /metrics removal + this branch's WAF and access-logs rows); graph.py references updated to graph_loader.py
2 parents 80af4a2 + 9388fba commit 2b06b3d

77 files changed

Lines changed: 2392 additions & 826 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

agent-skills/performance-memory/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ user-invocable: false
1313
block/VTD-level graph for the relevant state. Anything that changes how many copies
1414
of a graph exist per process, or how many processes exist, multiplies memory straight
1515
through. Backend task sizing (`backendMemory` in `infra/config.ts`) and the graph LRU
16-
cache (`_GRAPH_CACHE_MAX_SIZE`, `backend/app/evaluation/graph.py`) are coupled —
16+
cache (`_GRAPH_CACHE_MAX_SIZE`, `backend/app/evaluation/graph_loader.py`) are coupled —
1717
check both before resizing either.
1818

1919
## Where the rest lives
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import Image from '@tiptap/extension-image';
2+
3+
/** Tiptap Image with an editable max-width (px), persisted in the img style so
4+
* it round-trips through the stored HTML/JSON and the public renderer. */
5+
export const CmsImage = Image.extend({
6+
addAttributes() {
7+
return {
8+
...this.parent?.(),
9+
maxWidth: {
10+
default: null,
11+
parseHTML: element => {
12+
const value = (element as HTMLElement).style?.maxWidth;
13+
return value ? parseInt(value, 10) || null : null;
14+
},
15+
renderHTML: attributes =>
16+
attributes.maxWidth
17+
? {style: `max-width: ${attributes.maxWidth}px; height: auto; width: 100%;`}
18+
: {},
19+
},
20+
};
21+
},
22+
});
23+
24+
export default CmsImage;

app/src/app/components/Cms/RichTextEditor/extensions/MapCreateButtons/MapCreateButtons.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@ import Image from 'next/image';
77
export interface MapCreateButtonsProps {
88
views: Array<Pick<DistrictrMap, 'name' | 'districtr_map_slug'>>;
99
type: 'simple' | 'megaphone' | 'cards';
10+
/** Stored in created maps' metadata so tag-filtered galleries pick them up. */
11+
createTag?: string | null;
1012
}
11-
export const MapCreateButtons = ({views, type}: MapCreateButtonsProps) => {
13+
export const MapCreateButtons = ({views, type, createTag}: MapCreateButtonsProps) => {
1214
switch (type) {
1315
// Same start cards the place pages render, for visual consistency.
1416
case 'cards':
@@ -20,6 +22,7 @@ export const MapCreateButtons = ({views, type}: MapCreateButtonsProps) => {
2022
view={view}
2123
isCommunity={false}
2224
showOutcome={false}
25+
createTag={createTag}
2326
/>
2427
))}
2528
</CardGrid>
@@ -28,7 +31,7 @@ export const MapCreateButtons = ({views, type}: MapCreateButtonsProps) => {
2831
return (
2932
<Flex direction="row" gap="2">
3033
{views.map(view => (
31-
<CreateButton key={view.districtr_map_slug} view={view} />
34+
<CreateButton key={view.districtr_map_slug} view={view} createTag={createTag} />
3235
))}
3336
</Flex>
3437
);
@@ -54,6 +57,7 @@ export const MapCreateButtons = ({views, type}: MapCreateButtonsProps) => {
5457
<CreateButton
5558
key={view.districtr_map_slug}
5659
view={view}
60+
createTag={createTag}
5761
extraClasses="bg-districtrBlue text-white text-xl px-8 py-3 rounded-md font-bold hover:bg-blue-700 transition-colors cursor-pointer m-2"
5862
/>
5963
))}

app/src/app/components/Cms/RichTextEditor/extensions/MapCreateButtons/MapCreateButtonsNodeView.tsx

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use client';
22
import {NodeViewProps, NodeViewWrapper} from '@tiptap/react';
33
import React, {useRef} from 'react';
4-
import {Box, Button, Dialog, Flex, Heading, Select, Text} from '@radix-ui/themes';
4+
import {Box, Button, Dialog, Flex, Heading, Select, Text, TextField} from '@radix-ui/themes';
55
import {MapCreateButtons, MapCreateButtonsProps} from './MapCreateButtons';
66
import {GearIcon, TrashIcon} from '@radix-ui/react-icons';
77
import {NoFocusBoundary} from '../NoFocusBoundary';
@@ -18,6 +18,7 @@ const MapCreateButtonsNodeView: React.FC<NodeViewProps> = ({
1818
// Use a nested editor for the custom content
1919
const views: Array<Pick<DistrictrMap, 'name' | 'districtr_map_slug'>> = node.attrs.views || [];
2020
const type: MapCreateButtonsProps['type'] = node.attrs.type || 'simple';
21+
const createTag: string | null = node.attrs.createTag || null;
2122

2223
const [dialogOpen, setDialogOpen] = React.useState(false);
2324
const handleUpdate = (updates: Partial<MapCreateButtonsProps>) => {
@@ -33,7 +34,7 @@ const MapCreateButtonsNodeView: React.FC<NodeViewProps> = ({
3334
return (
3435
<NodeViewWrapper className="relative" ref={parentRef} contentEditable={false}>
3536
<NoFocusBoundary parentRef={parentRef}>
36-
<MapCreateButtons views={views} type={type} />
37+
<MapCreateButtons views={views} type={type} createTag={createTag} />
3738
</NoFocusBoundary>
3839
<Box position="absolute" top="2" right="2">
3940
<Flex direction="column" gap="2">
@@ -96,6 +97,18 @@ const MapCreateButtonsNodeView: React.FC<NodeViewProps> = ({
9697
</Select.Content>
9798
</Select.Root>
9899
</Flex>
100+
<Flex direction="column" gap="2">
101+
<Text>Creation Tag</Text>
102+
<Text size="1" color="gray">
103+
Stored in the metadata of maps created here, so tag-filtered galleries pick them
104+
up once they move past scratch.
105+
</Text>
106+
<TextField.Root
107+
placeholder="e.g. tn-workshop"
108+
value={createTag ?? ''}
109+
onChange={e => handleUpdate({createTag: e.target.value || null})}
110+
/>
111+
</Flex>
99112
<Flex direction="row" gap="2">
100113
<Button onClick={() => setDialogOpen(false)}>Close</Button>
101114
</Flex>

app/src/app/components/Cms/RichTextEditor/extensions/PlanGallery/PlanGallery.tsx

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ import {Table} from '@radix-ui/themes';
44
import {Gallery} from '@/app/components/Static/Gallery';
55
import {getPlans} from '@/app/utils/api/apiHandlers/getPlans';
66
import {MinPublicDocument} from '@utils/api/apiHandlers/types';
7+
import {
8+
DRAFT_STATUSES,
9+
SUBMITTED_STATUSES,
10+
type DraftStatus,
11+
} from '@constants/document/draftStatus';
712
import {PlanCard, PlanFlags, PlanTableRow} from './PlanGalleryRenderers';
813

914
export type PlanGalleryProps = {
@@ -14,6 +19,9 @@ export type PlanGalleryProps = {
1419
paginate?: boolean;
1520
limit?: number;
1621
showListView?: boolean;
22+
/** Tag-based galleries show ready-to-share maps only; opt in to
23+
* in-progress maps as well. */
24+
includeInProgress?: boolean;
1725
} & PlanFlags;
1826

1927
export const PlanGallery: React.FC<PlanGalleryProps> = ({
@@ -24,24 +32,40 @@ export const PlanGallery: React.FC<PlanGalleryProps> = ({
2432
paginate,
2533
limit = 12,
2634
showListView = false,
35+
includeInProgress = false,
2736
...flags
2837
}: PlanGalleryProps) => {
38+
const isTagBased = Boolean(tags?.length && !ids?.length);
39+
const draftStatuses = includeInProgress ? SUBMITTED_STATUSES : [DRAFT_STATUSES.READY_TO_SHARE];
40+
// Mixed-status lists annotate each plan with its status; ready-only lists
41+
// are uniform, so a badge would be noise.
42+
const showStatus = isTagBased && includeInProgress;
2943
return (
30-
<Gallery<MinPublicDocument, {ids?: number[]; tags?: string[]}, MinPublicDocument[] | null>
44+
<Gallery<
45+
MinPublicDocument,
46+
{ids?: number[]; tags?: string[]; draftStatuses?: DraftStatus[]},
47+
MinPublicDocument[] | null
48+
>
3149
title={title}
3250
description={description}
3351
paginate={paginate}
3452
limit={limit}
3553
showListView={showListView}
36-
filters={{ids, tags}}
54+
filters={{ids, tags, draftStatuses: isTagBased ? draftStatuses : undefined}}
3755
queryKey={['plans']}
3856
queryFunction={({filters, limit, offset}) =>
39-
getPlans({ids: filters.ids, tags: filters.tags, limit, offset}).then(result =>
40-
result?.ok ? result.response : null
41-
)
57+
getPlans({
58+
ids: filters.ids,
59+
tags: filters.tags,
60+
draftStatuses: filters.draftStatuses,
61+
limit,
62+
offset,
63+
}).then(result => (result?.ok ? result.response : null))
4264
}
4365
selectItems={data => (data || []) as MinPublicDocument[]}
44-
gridRenderer={(plan, i) => <PlanCard key={i} plan={plan} {...flags} />}
66+
gridRenderer={(plan, i) => (
67+
<PlanCard key={i} plan={plan} {...flags} showStatus={showStatus} />
68+
)}
4569
tableHeader={
4670
<>
4771
<Table.ColumnHeaderCell>ID</Table.ColumnHeaderCell>
@@ -51,9 +75,12 @@ export const PlanGallery: React.FC<PlanGalleryProps> = ({
5175
{flags.showDescriptions && <Table.ColumnHeaderCell>Description</Table.ColumnHeaderCell>}
5276
{flags.showTags && <Table.ColumnHeaderCell>Tags</Table.ColumnHeaderCell>}
5377
{flags.showUpdatedAt && <Table.ColumnHeaderCell>Updated At</Table.ColumnHeaderCell>}
78+
{showStatus && <Table.ColumnHeaderCell>Status</Table.ColumnHeaderCell>}
5479
</>
5580
}
56-
tableRowRenderer={(plan, i) => <PlanTableRow key={i} plan={plan} {...flags} />}
81+
tableRowRenderer={(plan, i) => (
82+
<PlanTableRow key={i} plan={plan} {...flags} showStatus={showStatus} />
83+
)}
5784
/>
5885
);
5986
};

app/src/app/components/Cms/RichTextEditor/extensions/PlanGallery/PlanGalleryNodeView.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ const PlanGalleryNodeView: React.FC<NodeViewProps> = ({node, updateAttributes, d
3636
const showUpdatedAt: boolean | undefined = node.attrs.showUpdatedAt || undefined;
3737
const showTags: boolean | undefined = node.attrs.showTags || undefined;
3838
const showModule: boolean | undefined = node.attrs.showModule || undefined;
39+
const includeInProgress: boolean = node.attrs.includeInProgress || false;
3940

4041
const [dialogOpen, setDialogOpen] = React.useState(false);
4142
const handleUpdate = (updates: Partial<PlanGalleryProps>) => {
@@ -67,6 +68,7 @@ const PlanGalleryNodeView: React.FC<NodeViewProps> = ({node, updateAttributes, d
6768
showUpdatedAt={showUpdatedAt}
6869
showTags={showTags}
6970
showModule={showModule}
71+
includeInProgress={includeInProgress}
7072
/>
7173
</NoFocusBoundary>
7274
<Box position="absolute" top="2" right="2">
@@ -141,6 +143,19 @@ const PlanGalleryNodeView: React.FC<NodeViewProps> = ({node, updateAttributes, d
141143
</CheckboxCards.Root>
142144
</Flex>
143145

146+
<Flex direction="column" gap="2">
147+
<Text>Map Status (tag-based galleries)</Text>
148+
<Text as="label" size="2">
149+
<Flex gap="2" align="center">
150+
<Switch
151+
checked={includeInProgress}
152+
onCheckedChange={value => handleUpdate({includeInProgress: value})}
153+
/>
154+
Also include in-progress maps (off: ready-to-share only)
155+
</Flex>
156+
</Text>
157+
</Flex>
158+
144159
<Tabs.Root
145160
defaultValue={!!ids ? 'ids' : 'tags'}
146161
onValueChange={value =>

app/src/app/components/Cms/RichTextEditor/extensions/PlanGallery/PlanGalleryRenderers.tsx

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
'use client';
2-
import {Box, Flex, Table, Text} from '@radix-ui/themes';
2+
import {Badge, Box, Flex, Table, Text} from '@radix-ui/themes';
33
import {thumbnailUrl} from '@/app/utils/api/thumbnailUrl';
44
import {MinPublicDocument} from '@utils/api/apiHandlers/types';
55
import {useRouter} from 'next/navigation';
6+
import {DRAFT_STATUS_COLORS, DRAFT_STATUS_TEXT} from '@constants/document/draftStatus';
67

78
export type PlanFlags = {
89
showThumbnails?: boolean;
@@ -11,6 +12,18 @@ export type PlanFlags = {
1112
showUpdatedAt?: boolean;
1213
showTags?: boolean;
1314
showModule?: boolean;
15+
/** Quiet per-plan completion-status badge, for mixed-status galleries. */
16+
showStatus?: boolean;
17+
};
18+
19+
const StatusBadge = ({plan}: {plan: MinPublicDocument}) => {
20+
const status = plan.map_metadata?.draft_status;
21+
if (!status) return null;
22+
return (
23+
<Badge color={DRAFT_STATUS_COLORS[status]} variant="soft" size="1" className="w-fit">
24+
{DRAFT_STATUS_TEXT[status]}
25+
</Badge>
26+
);
1427
};
1528

1629
const FALLBACK_IMAGE = '/home-megaphone-square.png';
@@ -40,6 +53,7 @@ export const PlanCard = ({plan, ...flags}: {plan: MinPublicDocument} & PlanFlags
4053
<Box px="4" py="2">
4154
<Flex direction="column" gap="2">
4255
{plan.public_id && <Text size="1">Map ID:{plan.public_id}</Text>}
56+
{!!flags.showStatus && <StatusBadge plan={plan} />}
4357
{!!flags.showTitles && plan.map_metadata?.name && (
4458
<Text size="5">{plan.map_metadata?.name}</Text>
4559
)}
@@ -53,9 +67,9 @@ export const PlanCard = ({plan, ...flags}: {plan: MinPublicDocument} & PlanFlags
5367
{plan.map_metadata?.description}
5468
</Text>
5569
)}
56-
{!!flags.showTags && plan.map_metadata?.tags && (
70+
{!!flags.showTags && !!plan.map_metadata?.tags?.length && (
5771
<Text size="2" color="gray">
58-
{plan.map_metadata?.tags}
72+
{plan.map_metadata.tags.join(', ')}
5973
</Text>
6074
)}
6175
{!!flags.showUpdatedAt && plan.updated_at && (
@@ -73,7 +87,12 @@ export const PlanCard = ({plan, ...flags}: {plan: MinPublicDocument} & PlanFlags
7387
export const PlanTableRow = ({plan, ...flags}: {plan: MinPublicDocument} & PlanFlags) => {
7488
const router = useRouter();
7589
return (
76-
<Table.Row onClick={() => router.push(`/map/${plan.public_id}`)}>
90+
// align-middle: the thumbnail cell is taller than one text line, and
91+
// top-aligned text reads as misaligned beside it.
92+
<Table.Row
93+
onClick={() => router.push(`/map/${plan.public_id}`)}
94+
className="[&>td]:align-middle"
95+
>
7796
<Table.Cell>{plan.public_id}</Table.Cell>
7897
{!!flags.showThumbnails && (
7998
<Table.Cell>
@@ -91,10 +110,15 @@ export const PlanTableRow = ({plan, ...flags}: {plan: MinPublicDocument} & PlanF
91110
{!!flags.showTitles && <Table.Cell>{plan.map_metadata?.name ?? ''}</Table.Cell>}
92111
{!!flags.showModule && <Table.Cell>{plan.map_module ?? ''}</Table.Cell>}
93112
{!!flags.showDescriptions && <Table.Cell>{plan.map_metadata?.description ?? ''}</Table.Cell>}
94-
{!!flags.showTags && <Table.Cell>{plan.map_metadata?.tags ?? ''}</Table.Cell>}
113+
{!!flags.showTags && <Table.Cell>{plan.map_metadata?.tags?.join(', ') ?? ''}</Table.Cell>}
95114
{!!flags.showUpdatedAt && (
96115
<Table.Cell>{new Date(plan.updated_at).toLocaleDateString() ?? ''}</Table.Cell>
97116
)}
117+
{!!flags.showStatus && (
118+
<Table.Cell>
119+
<StatusBadge plan={plan} />
120+
</Table.Cell>
121+
)}
98122
</Table.Row>
99123
);
100124
};

app/src/app/components/EvalPanel/CountySplitsSection.tsx

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {DocumentEvaluation} from '@utils/api/apiHandlers/getEvaluation';
77
import {useMapStore} from '@store/mapStore';
88
import {useMapControlsStore} from '@/app/store/mapControlsStore';
99
import {type GeoUnit, GEO_UNITS, GEO_UNIT_LABELS} from '@constants/document/geoUnits';
10+
import {useIsSingleCounty} from '@/app/hooks/useIsSingleCounty';
1011

1112
const GEO_UNIT_DESCRIPTIONS: Record<GeoUnit, string> = {
1213
[GEO_UNITS.VTD]:
@@ -26,6 +27,7 @@ export const CountySplitsSection: React.FC<CountySplitsSectionProps> = ({evaluat
2627
const mapOptions = useMapControlsStore(state => state.mapOptions);
2728
const setMapOptions = useMapControlsStore(state => state.setMapOptions);
2829
const setHoveredCountyGeoid = useMapControlsStore(state => state.setHoveredCountyGeoid);
30+
const isSingleCounty = useIsSingleCounty();
2931
const [showMode, setShowMode] = useState<'overly-split-only' | 'all'>('overly-split-only');
3032

3133
const countyPieces = evaluation.county_pieces;
@@ -154,18 +156,20 @@ export const CountySplitsSection: React.FC<CountySplitsSectionProps> = ({evaluat
154156
districting plan) if its population is smaller than the ideal size of a district.
155157
</Text>
156158

157-
<Flex align="center" gap="2" mb="3" justify="end">
158-
<Text size="1" color="gray">
159-
County boundaries
160-
</Text>
161-
<Switch
162-
size="1"
163-
checked={mapOptions.showCountyBoundaries ?? false}
164-
onCheckedChange={checked =>
165-
setMapOptions({showCountyBoundaries: checked, prominentCountyNames: checked})
166-
}
167-
/>
168-
</Flex>
159+
{!isSingleCounty && (
160+
<Flex align="center" gap="2" mb="3" justify="end">
161+
<Text size="1" color="gray">
162+
County boundaries
163+
</Text>
164+
<Switch
165+
size="1"
166+
checked={mapOptions.showCountyBoundaries ?? false}
167+
onCheckedChange={checked =>
168+
setMapOptions({showCountyBoundaries: checked, prominentCountyNames: checked})
169+
}
170+
/>
171+
</Flex>
172+
)}
169173

170174
<Text size="2" weight="bold" mb="2" mt="4" as="p">
171175
Summary

app/src/app/components/Map/PolygonLayers/CoiBlockLayers.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,18 +63,20 @@ export const CoiBlockLayers: React.FC<{
6363
/>
6464
)}
6565

66+
{/* Anchored above the boundary/overlay layers so the unassigned
67+
highlight isn't hidden under county lines. */}
6668
<ZoneHighlightLayer
6769
id={CANONICAL_LAYER_IDS.BLOCK[scope].HIGHLIGHT}
6870
sourceLayerId={sourceLayerId}
6971
filter={layerFilter}
70-
beforeId={DEFAULT_BLOCK_LAYER_ORDER.zoneBeforeId}
72+
beforeId={DEFAULT_BLOCK_LAYER_ORDER.highlightBeforeId}
7173
/>
7274
<Layer
7375
id={CANONICAL_LAYER_IDS.BLOCK[scope].HOVER}
7476
source={BLOCK_SOURCE_ID}
7577
source-layer={sourceLayerId}
7678
filter={layerFilter}
77-
beforeId={CANONICAL_LAYER_IDS.BLOCK[scope].HIGHLIGHT}
79+
beforeId={DEFAULT_BLOCK_LAYER_ORDER.zoneBeforeId}
7880
type="fill"
7981
layout={{visibility: 'visible'}}
8082
paint={{

0 commit comments

Comments
 (0)