diff --git a/apps/google-docs/src/locations/Page/components/review/mapping/MappingEntryCards.tsx b/apps/google-docs/src/locations/Page/components/review/mapping/MappingEntryCards.tsx index 1626714453..82e2ecadb2 100644 --- a/apps/google-docs/src/locations/Page/components/review/mapping/MappingEntryCards.tsx +++ b/apps/google-docs/src/locations/Page/components/review/mapping/MappingEntryCards.tsx @@ -7,6 +7,7 @@ interface MappingEntryCardsProps { groupId: string; mappingCards: RenderedMappingCard[]; cardOffsetsByGroup: Record>; + cardHeightsByGroup: Record>; hoveredMappingKeys: string[]; onSetHoveredMappingKeys: (keys: string[]) => void; setCardWrapperRef: (cardKey: string) => RefCallback; @@ -22,13 +23,21 @@ export const MappingEntryCards = ({ groupId, mappingCards, cardOffsetsByGroup, + cardHeightsByGroup, hoveredMappingKeys, onSetHoveredMappingKeys, setCardWrapperRef, }: MappingEntryCardsProps): JSX.Element => { + const offsets = cardOffsetsByGroup[groupId] ?? {}; + const heights = cardHeightsByGroup[groupId] ?? {}; + const minHeight = Math.max( + 0, + ...mappingCards.map((card) => (offsets[card.key] ?? 0) + (heights[card.key] ?? 28)) + ); + return ( - + {mappingCards.length > 0 ? mappingCards.map((mappingCard) => ( > >({}); + const [cardHeightsByGroup, setCardHeightsByGroup] = useState< + Record> + >({}); const [editModalState, setEditModalState] = useState(EMPTY_EDIT_MODAL); const [pendingTextExclusionRanges, setPendingTextExclusionRanges] = useState< TextExclusionRange[] | null @@ -492,6 +495,7 @@ export const MappingView = ({ useLayoutEffect(() => { const measureOffsets = () => { const nextOffsets: Record> = {}; + const nextHeights: Record> = {}; allGroups.forEach((group) => { const groupNode = groupLayoutRefs.current[group.id]; @@ -516,12 +520,21 @@ export const MappingView = ({ }); nextOffsets[group.id] = resolveMarkerOffsets(cards); + nextHeights[group.id] = Object.fromEntries(cards.map((c) => [c.key, c.height])); }); setCardOffsetsByGroup(nextOffsets); + setCardHeightsByGroup(nextHeights); }; measureOffsets(); + + const observer = new ResizeObserver(measureOffsets); + Object.values(cardWrapperRefs.current).forEach((node) => { + if (node) observer.observe(node); + }); + + return () => observer.disconnect(); }, [allGroups]); const openAssignModal = ( @@ -1069,6 +1082,7 @@ export const MappingView = ({ groupId={group.id} mappingCards={group.mappingCards} cardOffsetsByGroup={cardOffsetsByGroup} + cardHeightsByGroup={cardHeightsByGroup} hoveredMappingKeys={hoveredMappingKeys} onSetHoveredMappingKeys={setHoveredMappingKeys} setCardWrapperRef={setCardWrapperRef} diff --git a/apps/google-docs/src/locations/Page/components/review/mapping/edit-modals/EditModal.tsx b/apps/google-docs/src/locations/Page/components/review/mapping/edit-modals/EditModal.tsx index 26ce14375e..7a80fe0a64 100644 --- a/apps/google-docs/src/locations/Page/components/review/mapping/edit-modals/EditModal.tsx +++ b/apps/google-docs/src/locations/Page/components/review/mapping/edit-modals/EditModal.tsx @@ -47,7 +47,9 @@ export const EditModal = ({ const hasCurrentLocations = viewModel.currentLocations.length > 0; const hasNewLocation = viewModel.newLocation.id !== ''; - const [selectedLocationIds, setSelectedLocationIds] = useState([]); + const [selectedLocationIds, setSelectedLocationIds] = useState(() => + viewModel.currentLocations.map((l) => l.id) + ); const [selectedFieldIds, setSelectedFieldIds] = useState( () => viewModel.newLocation.selectedFieldIds ?? [] @@ -64,7 +66,7 @@ export const EditModal = ({ // Do not depend on `viewModel.newLocation` reference (it can churn without semantic change). useEffect(() => { if (!isOpen) return; - setSelectedLocationIds([]); + setSelectedLocationIds(viewModel.currentLocations.map((l) => l.id)); setSelectedFieldIds(viewModel.newLocation.selectedFieldIds ?? []); setDestinationFieldState(null); // eslint-disable-next-line react-hooks/exhaustive-deps -- sync from props only on open / row-id set change, not array identity diff --git a/apps/google-docs/src/setupTests.ts b/apps/google-docs/src/setupTests.ts index 6befadb41a..331799b726 100644 --- a/apps/google-docs/src/setupTests.ts +++ b/apps/google-docs/src/setupTests.ts @@ -5,6 +5,14 @@ // Only configure if we're in a DOM environment (not Node.js) // This file is for frontend tests only - function tests use functions/vitest.config.mts +if (typeof window !== 'undefined' && typeof ResizeObserver === 'undefined') { + window.ResizeObserver = class ResizeObserver { + observe() {} + unobserve() {} + disconnect() {} + }; +} + if (typeof window !== 'undefined') { try { // Import jest-dom and matchers only in browser/DOM environment diff --git a/apps/google-docs/test/locations/Page/components/modals/EditModal.spec.tsx b/apps/google-docs/test/locations/Page/components/modals/EditModal.spec.tsx index dd399c9968..4e089fb714 100644 --- a/apps/google-docs/test/locations/Page/components/modals/EditModal.spec.tsx +++ b/apps/google-docs/test/locations/Page/components/modals/EditModal.spec.tsx @@ -113,28 +113,29 @@ describe('EditModal', () => { .filter((element) => element.getAttribute('aria-pressed') !== null); const [summaryCard, descriptionCard] = locationCards; - expect(summaryCard).toHaveAttribute('aria-pressed', 'false'); - expect(descriptionCard).toHaveAttribute('aria-pressed', 'false'); + // All current locations start pre-selected + expect(summaryCard).toHaveAttribute('aria-pressed', 'true'); + expect(descriptionCard).toHaveAttribute('aria-pressed', 'true'); fireEvent.click(summaryCard); await waitFor(() => { - expect(summaryCard).toHaveAttribute('aria-pressed', 'true'); - expect(descriptionCard).toHaveAttribute('aria-pressed', 'false'); + expect(summaryCard).toHaveAttribute('aria-pressed', 'false'); + expect(descriptionCard).toHaveAttribute('aria-pressed', 'true'); }); - fireEvent.click(descriptionCard); + fireEvent.click(summaryCard); await waitFor(() => { expect(summaryCard).toHaveAttribute('aria-pressed', 'true'); expect(descriptionCard).toHaveAttribute('aria-pressed', 'true'); }); - fireEvent.click(summaryCard); + fireEvent.click(descriptionCard); await waitFor(() => { - expect(summaryCard).toHaveAttribute('aria-pressed', 'false'); - expect(descriptionCard).toHaveAttribute('aria-pressed', 'true'); + expect(summaryCard).toHaveAttribute('aria-pressed', 'true'); + expect(descriptionCard).toHaveAttribute('aria-pressed', 'false'); }); }); @@ -190,15 +191,18 @@ describe('EditModal', () => { expect( screen.queryByText('No destination entry is available for the entry currently in view.') ).toBeNull(); - expect(screen.getByRole('button', { name: 'Exclude content' })).toBeDisabled(); + // Button starts enabled because all current locations are pre-selected + expect(screen.getByRole('button', { name: 'Exclude content' })).not.toBeDisabled(); const locationCards = screen .getAllByRole('button') .filter((el) => el.getAttribute('aria-pressed') !== null); - fireEvent.click(locationCards[0]); + + // Deselect all locations — button should disable + locationCards.forEach((card) => fireEvent.click(card)); await waitFor(() => { - expect(screen.getByRole('button', { name: 'Exclude content' })).not.toBeDisabled(); + expect(screen.getByRole('button', { name: 'Exclude content' })).toBeDisabled(); }); }); diff --git a/apps/google-docs/test/locations/Page/components/review/mapping/MappingView.spec.tsx b/apps/google-docs/test/locations/Page/components/review/mapping/MappingView.spec.tsx index e5272dfbc9..3d515c5733 100644 --- a/apps/google-docs/test/locations/Page/components/review/mapping/MappingView.spec.tsx +++ b/apps/google-docs/test/locations/Page/components/review/mapping/MappingView.spec.tsx @@ -307,7 +307,12 @@ describe('MappingView', () => { }); const { container } = render( - + ); expect(container.querySelectorAll('[data-testid^="mapping-card-"]')).toHaveLength(1); @@ -335,7 +340,12 @@ describe('MappingView', () => { }); const { container } = render( - + ); expect(container.querySelectorAll('[data-testid^="mapping-card-"]')).toHaveLength(1); @@ -368,7 +378,12 @@ describe('MappingView', () => { }); render( - + ); expect(screen.getByText('Body copy (1/2)')).toBeTruthy(); @@ -433,7 +448,12 @@ describe('MappingView', () => { }); const { container } = render( - + ); expect(screen.getByText('Body copy (1/3)')).toBeTruthy(); @@ -549,7 +569,12 @@ describe('MappingView', () => { }); const { container } = render( - + ); expect(screen.getAllByText('Body copy').length).toBeGreaterThan(0); @@ -650,7 +675,12 @@ describe('MappingView', () => { }); const { container } = render( - + ); expect(container.querySelectorAll('[data-testid^="mapping-group-surface-"]')).toHaveLength(0); @@ -683,7 +713,12 @@ describe('MappingView', () => { }); const { container } = render( - + ); const textSegments = Array.from( @@ -731,7 +766,12 @@ describe('MappingView', () => { }); const { container, rerender } = render( - + ); const groupedTextSegments = container.querySelectorAll('[data-review-text-segment="true"]'); const selectedRange = createDomRange(groupedTextSegments[1].firstChild as Text, 0, 6); @@ -744,7 +784,12 @@ describe('MappingView', () => { }); rerender( - + ); fireEvent.click(screen.getByRole('button', { name: 'Reassign' })); @@ -775,7 +820,12 @@ describe('MappingView', () => { const payload = createPayload(); render( - + ); fireEvent.click(screen.getByRole('button', { name: 'Assign' })); @@ -803,6 +853,7 @@ describe('MappingView', () => { payload={payload} {...mappingViewGraphProps(payload)} selectedEntryIndex={null} + mode="edit" /> ); @@ -821,7 +872,12 @@ describe('MappingView', () => { const payload = createPayload(); const { container, rerender } = render( - + ); const selectedRange = createDomRange( container.querySelector('[data-review-text-segment="true"]')?.firstChild as Text, @@ -835,7 +891,12 @@ describe('MappingView', () => { clearSelection: mockClearSelection, }); rerender( - + ); fireEvent.click(screen.getByRole('button', { name: 'Exclude' })); @@ -886,6 +947,7 @@ describe('MappingView', () => { entryBlockGraph={currentGraph} onEntryBlockGraphChange={onEntryBlockGraphChange} selectedEntryIndex={0} + mode="edit" /> ); @@ -910,6 +972,7 @@ describe('MappingView', () => { entryBlockGraph={currentGraph} onEntryBlockGraphChange={onEntryBlockGraphChange} selectedEntryIndex={0} + mode="edit" /> ); @@ -923,9 +986,9 @@ describe('MappingView', () => { ) ).toBeNull(); - const locationCard = document.querySelector('button[aria-pressed]') as HTMLElement; + // Location is pre-selected — confirm button is already enabled + const locationCard = document.querySelector('button[aria-pressed="true"]') as HTMLElement; expect(locationCard).toBeTruthy(); - fireEvent.click(locationCard); const confirmButton = screen.getAllByRole('button', { name: 'Exclude content' }).at(-1); expect(confirmButton).toBeTruthy(); @@ -939,6 +1002,7 @@ describe('MappingView', () => { entryBlockGraph={currentGraph} onEntryBlockGraphChange={onEntryBlockGraphChange} selectedEntryIndex={0} + mode="edit" /> ); @@ -958,6 +1022,7 @@ describe('MappingView', () => { entryBlockGraph={payload.entryBlockGraph} onEntryBlockGraphChange={onEntryBlockGraphChange} selectedEntryIndex={1} + mode="edit" /> );