Skip to content

Commit ac5b4bc

Browse files
committed
feat: add index based highlighting
1 parent eb9aa39 commit ac5b4bc

7 files changed

Lines changed: 187 additions & 56 deletions

File tree

apps/google-docs/src/locations/Page/components/mainpage/ModalOrchestrator.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ export const ModalOrchestrator = forwardRef<ModalOrchestratorHandle, ModalOrches
172172
return;
173173
}
174174

175+
setFlowStep(null);
175176
onPreviewReady(workflowRun.googleDocPayload);
176177
};
177178

apps/google-docs/src/locations/Page/components/review/DocumentOutline.tsx

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,12 @@ import type {
2828
NormalizedDocumentTable,
2929
NormalizedDocumentTablePart,
3030
} from '@types';
31+
import {
32+
isBlockImageSourceRef,
33+
isBlockSourceRef,
34+
isTableImageSourceRef,
35+
isTextSourceRef,
36+
} from '@types';
3137
import { FileTextIcon } from '@contentful/f36-icons';
3238
import { MappingCard, type MappingCardData } from './MappingCard';
3339
import { getAnchorIdForSourceRef, resolveMarkerOffsets } from './mappingCardPositioning';
@@ -95,11 +101,22 @@ function buildUsageIndexes(entryBlockGraph: EntryBlockGraph): {
95101
sourceRef,
96102
};
97103

98-
if (sourceRef.kind === 'blockText' || sourceRef.kind === 'blockImage') {
104+
if (isBlockSourceRef(sourceRef)) {
99105
blockUsage[sourceRef.blockId] = [...(blockUsage[sourceRef.blockId] ?? []), usage];
100106
return;
101107
}
102108

109+
if (
110+
!(
111+
'tableId' in sourceRef &&
112+
'rowId' in sourceRef &&
113+
'cellId' in sourceRef &&
114+
'partId' in sourceRef
115+
)
116+
) {
117+
return;
118+
}
119+
103120
const tablePartKey = [
104121
sourceRef.tableId,
105122
sourceRef.rowId,
@@ -134,20 +151,18 @@ function buildTextSegments(
134151
flattenedRuns: NormalizedDocumentFlattenedRun[],
135152
usage: Array<{ sourceRef: EntryBlockGraphSourceRef; mappingKey: string }>
136153
): TextSegment[] {
137-
console.log('flattenedRuns', flattenedRuns);
138154
if (!flattenedRuns.length) return [];
139155

140156
const textUsage = usage.filter(
141157
(
142158
usageItem
143159
): usageItem is {
144-
sourceRef: Extract<EntryBlockGraphSourceRef, { kind: 'blockText' | 'tableText' }>;
160+
sourceRef: isTextSourceRef;
145161
mappingKey: string;
146-
} => usageItem.sourceRef.kind === 'blockText' || usageItem.sourceRef.kind === 'tableText'
162+
} => isTextSourceRef(usageItem.sourceRef)
147163
);
148164

149-
const fullText = flattenedRuns.map((run) => run.text).join('');
150-
const boundaries = new Set<number>([0, fullText.length]);
165+
const boundaries = new Set<number>();
151166
flattenedRuns.forEach((run) => {
152167
boundaries.add(run.start);
153168
boundaries.add(run.end);
@@ -165,12 +180,16 @@ function buildTextSegments(
165180
return [];
166181
}
167182

168-
const text = fullText.slice(start, end);
183+
const run = flattenedRuns.find((candidate) => start >= candidate.start && end <= candidate.end);
184+
if (!run) {
185+
return [];
186+
}
187+
188+
const text = run.text.slice(start - run.start, end - run.start);
169189
if (!text) {
170190
return [];
171191
}
172192

173-
const run = flattenedRuns.find((r) => start >= r.start && end <= r.end);
174193
const mappingKeys = textUsage
175194
.filter(({ sourceRef }) => start >= sourceRef.start && end <= sourceRef.end)
176195
.map(({ mappingKey }) => mappingKey);
@@ -441,7 +460,6 @@ export const DocumentOutline = ({ payload, showChrome = true, onBack }: Document
441460
sourceRef: usage.sourceRef,
442461
mappingKey: getMappingCardKey(segmentId, usage),
443462
}));
444-
console.log(block);
445463
const textSegments = buildTextSegments(block.flattenedTextRuns, textUsage);
446464
const listItemPresentation = block.type === 'listItem' ? listItemPresentations[block.id] : null;
447465
const setHoveredMappings = (mappingKeys: string[]) => setHoveredMappingKeys(mappingKeys);
@@ -495,12 +513,12 @@ export const DocumentOutline = ({ payload, showChrome = true, onBack }: Document
495513
const image = imageById[imageId];
496514
if (!image) return null;
497515
const highlighted = visibleRefs.some(
498-
(ref) => ref.kind === 'blockImage' && ref.imageId === imageId
516+
(ref) => isBlockImageSourceRef(ref) && ref.imageId === imageId
499517
);
500518
const mappingKeys = visibleUsage
501519
.filter(
502520
(usage) =>
503-
usage.sourceRef.kind === 'blockImage' && usage.sourceRef.imageId === imageId
521+
isBlockImageSourceRef(usage.sourceRef) && usage.sourceRef.imageId === imageId
504522
)
505523
.map((usage) => getMappingCardKey(segmentId, usage));
506524
const hovered = isMappingHovered(mappingKeys);
@@ -548,9 +566,9 @@ export const DocumentOutline = ({ payload, showChrome = true, onBack }: Document
548566

549567
if (part.type === 'image') {
550568
const image = imageById[part.imageId];
551-
const highlighted = visibleRefs.some((ref) => ref.kind === 'tableImage');
569+
const highlighted = visibleRefs.some((ref) => isTableImageSourceRef(ref));
552570
const mappingKeys = visibleUsage
553-
.filter((usage) => usage.sourceRef.kind === 'tableImage')
571+
.filter((usage) => isTableImageSourceRef(usage.sourceRef))
554572
.map((usage) => getMappingCardKey(segmentId, usage));
555573
const hovered = isMappingHovered(mappingKeys);
556574

apps/google-docs/src/locations/Page/components/review/mappingCardPositioning.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import type { EntryBlockGraphSourceRef } from '@types';
1+
import { isBlockSourceRef, type EntryBlockGraphSourceRef } from '@types';
22

33
const DEFAULT_CARD_GAP = 8;
44

55
export const getAnchorIdForSourceRef = (sourceRef: EntryBlockGraphSourceRef): string =>
6-
sourceRef.kind === 'blockText' || sourceRef.kind === 'blockImage'
6+
isBlockSourceRef(sourceRef)
77
? `block:${sourceRef.blockId}`
88
: `row:${sourceRef.tableId}:${sourceRef.rowId}`;
99

apps/google-docs/src/types/entryBlockGraph.ts

Lines changed: 88 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,92 @@
11
import type { NormalizedDocumentFlattenedRun } from './normalizedDocument';
22

3-
export type EntryBlockGraphTextSourceRef =
4-
| {
5-
kind: 'blockText';
6-
blockId: string;
7-
start: number;
8-
end: number;
9-
flattenedRuns: NormalizedDocumentFlattenedRun[];
10-
}
11-
| {
12-
kind: 'tableText';
13-
tableId: string;
14-
rowId: string;
15-
cellId: string;
16-
partId: string;
17-
start: number;
18-
end: number;
19-
flattenedRuns: NormalizedDocumentFlattenedRun[];
20-
};
21-
22-
export type EntryBlockGraphImageSourceRef =
23-
| {
24-
kind: 'blockImage';
25-
blockId: string;
26-
imageId: string;
27-
}
28-
| {
29-
kind: 'tableImage';
30-
tableId: string;
31-
rowId: string;
32-
cellId: string;
33-
partId: string;
34-
imageId: string;
35-
};
36-
37-
export type EntryBlockGraphSourceRef = EntryBlockGraphTextSourceRef | EntryBlockGraphImageSourceRef;
38-
39-
export interface EntryBlockGraphFieldMapping {
3+
interface TextSourceRefBase {
4+
start: number;
5+
end: number;
6+
flattenedRuns: NormalizedDocumentFlattenedRun[];
7+
kind?: 'blockText' | 'tableText';
8+
type?: string;
9+
}
10+
11+
interface ImageSourceRefBase {
12+
imageId: string;
13+
kind?: 'blockImage' | 'tableImage';
14+
type?: string;
15+
}
16+
17+
export type BlockTextSourceRef = TextSourceRefBase & {
18+
blockId: string;
19+
};
20+
21+
export type TableTextSourceRef = TextSourceRefBase & {
22+
tableId: string;
23+
rowId: string;
24+
cellId: string;
25+
partId: string;
26+
};
27+
28+
export type isTextSourceRef = BlockTextSourceRef | TableTextSourceRef;
29+
30+
export type BlockImageSourceRef = ImageSourceRefBase & {
31+
blockId: string;
32+
};
33+
34+
export type TableImageSourceRef = ImageSourceRefBase & {
35+
tableId: string;
36+
rowId: string;
37+
cellId: string;
38+
partId: string;
39+
};
40+
41+
export type ImageSourceRef = BlockImageSourceRef | TableImageSourceRef;
42+
43+
export type EntryBlockGraphSourceRef = isTextSourceRef | ImageSourceRef;
44+
45+
export const isTextSourceRef = (
46+
sourceRef: EntryBlockGraphSourceRef
47+
): sourceRef is isTextSourceRef => {
48+
return 'start' in sourceRef && 'end' in sourceRef && 'flattenedRuns' in sourceRef;
49+
};
50+
51+
export const isBlockSourceRef = (
52+
sourceRef: EntryBlockGraphSourceRef
53+
): sourceRef is BlockTextSourceRef | BlockImageSourceRef => {
54+
return 'blockId' in sourceRef;
55+
};
56+
57+
export const isTableSourceRef = (
58+
sourceRef: EntryBlockGraphSourceRef
59+
): sourceRef is TableTextSourceRef | TableImageSourceRef => {
60+
return (
61+
'tableId' in sourceRef && 'rowId' in sourceRef && 'cellId' in sourceRef && 'partId' in sourceRef
62+
);
63+
};
64+
65+
export const isEntryBlockGraphBlockTextSourceRef = (
66+
sourceRef: EntryBlockGraphSourceRef
67+
): sourceRef is BlockTextSourceRef => {
68+
return isBlockSourceRef(sourceRef) && isTextSourceRef(sourceRef);
69+
};
70+
71+
export const isTableTextSourceRef = (
72+
sourceRef: EntryBlockGraphSourceRef
73+
): sourceRef is TableTextSourceRef => {
74+
return isTableSourceRef(sourceRef) && isTextSourceRef(sourceRef);
75+
};
76+
77+
export const isBlockImageSourceRef = (
78+
sourceRef: EntryBlockGraphSourceRef
79+
): sourceRef is BlockImageSourceRef => {
80+
return isBlockSourceRef(sourceRef) && 'imageId' in sourceRef;
81+
};
82+
83+
export const isTableImageSourceRef = (
84+
sourceRef: EntryBlockGraphSourceRef
85+
): sourceRef is TableImageSourceRef => {
86+
return isTableSourceRef(sourceRef) && 'imageId' in sourceRef;
87+
};
88+
89+
export interface FieldMapping {
4090
fieldId: string;
4191
fieldType: string;
4292
sourceRefs: EntryBlockGraphSourceRef[];
@@ -48,7 +98,7 @@ export interface EntryBlockGraphFieldMapping {
4898
export interface EntryBlockGraphEntry {
4999
contentTypeId: string;
50100
tempId?: string;
51-
fieldMappings: EntryBlockGraphFieldMapping[];
101+
fieldMappings: FieldMapping[];
52102
}
53103

54104
export interface EntryBlockGraph {

apps/google-docs/test/locations/Page/Page.spec.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,11 @@ describe('Page component', () => {
199199
fireEvent.click(screen.getByRole('button', { name: 'Mock from fixture' }));
200200

201201
await waitFor(() => {
202-
expect(screen.getByText('Review document "NRF Event Playbook"')).toBeTruthy();
202+
expect(
203+
screen.getByText(
204+
'Review document "Pinterest Business Site Brief_Pinterest Top of Search ads"'
205+
)
206+
).toBeTruthy();
203207
expect(screen.getByText('Mock fixture review')).toBeTruthy();
204208
expect(screen.queryByRole('heading', { name: 'Drive Integration' })).toBeNull();
205209
});

apps/google-docs/test/locations/Page/components/review/DocumentOutline.spec.tsx

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,28 @@ describe('DocumentOutline', () => {
318318
expect(screen.getByText('paragraph')).toBeTruthy();
319319
});
320320

321+
it('renders mapping cards and highlights for block refs from the API shape', () => {
322+
const fixture = buildFixture();
323+
324+
fixture.entryBlockGraph.entries[0].fieldMappings[0].sourceRefs = [
325+
{
326+
type: 'paragraph',
327+
blockId: 'block-1',
328+
start: 0,
329+
end: 5,
330+
flattenedRuns: [{ text: 'Body ', start: 0, end: 5, styles: {} }],
331+
} as (typeof fixture.entryBlockGraph.entries)[number]['fieldMappings'][number]['sourceRefs'][number],
332+
];
333+
334+
render(<DocumentOutline payload={fixture} onBack={vi.fn()} />);
335+
336+
expect(screen.getByTestId('mapping-card-block-1-0-body')).toBeTruthy();
337+
expect(screen.getByTestId('block-segment-block-1-0')).toHaveAttribute(
338+
'data-highlighted',
339+
'true'
340+
);
341+
});
342+
321343
it('renders mixed table cell text and image highlights independently', () => {
322344
render(<DocumentOutline payload={buildFixture()} onBack={vi.fn()} />);
323345

@@ -340,6 +362,42 @@ describe('DocumentOutline', () => {
340362
);
341363
});
342364

365+
it('highlights mapped offsets using flattened run coordinates', () => {
366+
const fixture = buildFixture();
367+
368+
fixture.normalizedDocument.tables[0].rows[0].cells[1].parts[2] = {
369+
id: 'table-0-row-0-cell-1-part-2',
370+
type: 'text',
371+
textRuns: [{ text: 'NoYes', styles: {} }],
372+
flattenedTextRuns: [
373+
{ text: 'No', start: 0, end: 2, styles: {} },
374+
{ text: 'Yes', start: 6, end: 9, styles: {} },
375+
],
376+
};
377+
fixture.entryBlockGraph.entries[0].fieldMappings[2].sourceRefs = [
378+
{
379+
type: 'tableText',
380+
tableId: 'table-0',
381+
rowId: 'table-0-row-0',
382+
cellId: 'table-0-row-0-cell-1',
383+
partId: 'table-0-row-0-cell-1-part-2',
384+
start: 6,
385+
end: 9,
386+
flattenedRuns: [{ text: 'Yes', start: 6, end: 9, styles: {} }],
387+
} as (typeof fixture.entryBlockGraph.entries)[number]['fieldMappings'][number]['sourceRefs'][number],
388+
];
389+
390+
render(<DocumentOutline payload={fixture} onBack={vi.fn()} />);
391+
392+
expect(
393+
screen.getByTestId('table-text-segment-table-0-row-0-cell-1-part-2-1')
394+
).toHaveTextContent('Yes');
395+
expect(screen.getByTestId('table-text-segment-table-0-row-0-cell-1-part-2-1')).toHaveAttribute(
396+
'data-highlighted',
397+
'true'
398+
);
399+
});
400+
343401
it('syncs hover styling between mapping cards and their highlights', () => {
344402
render(<DocumentOutline payload={buildFixture()} onBack={vi.fn()} />);
345403

apps/google-docs/test/locations/Page/components/review/mappingCardPositioning.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@ describe('mappingCardPositioning', () => {
99
it('derives block and table anchors from source refs', () => {
1010
expect(
1111
getAnchorIdForSourceRef({
12-
kind: 'blockText',
12+
type: 'paragraph',
1313
blockId: 'block-1',
1414
start: 0,
1515
end: 5,
1616
flattenedRuns: [],
17-
})
17+
} as Parameters<typeof getAnchorIdForSourceRef>[0])
1818
).toBe('block:block-1');
1919

2020
expect(

0 commit comments

Comments
 (0)