Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Box, Card, Flex, Paragraph, Text } from '@contentful/f36-components';
import { Box, Card, Checkbox, Flex, Paragraph, Text } from '@contentful/f36-components';
import tokens from '@contentful/f36-tokens';
import { cx } from '@emotion/css';
import type { EntryListRow as OverviewEntryListRow } from '../../../../utils/overviewEntryList';
Expand All @@ -13,56 +13,85 @@ import { truncateLabel } from '../../../../utils/utils';
export interface OverviewEntryListProps {
rows: OverviewEntryListRow[];
selectedEntryIndex: number | null;
selectedEntryKeys: ReadonlySet<string>;
onSelect: (entryIndex: number) => void;
onToggleEntrySelection: (entryKey: string, isSelected: boolean) => void;
areEntrySelectionsDisabled?: boolean;
}

interface OverviewEntryRowCardProps {
row: OverviewEntryListRow;
selectedEntryIndex: number | null;
selectedEntryKeys: ReadonlySet<string>;
onSelect: (entryIndex: number) => void;
onToggleEntrySelection: (entryKey: string, isSelected: boolean) => void;
areEntrySelectionsDisabled: boolean;
showTreeLines: boolean;
isLastRow?: boolean;
}

function OverviewEntryRowCard({
row,
selectedEntryIndex,
selectedEntryKeys,
onSelect,
onToggleEntrySelection,
areEntrySelectionsDisabled,
showTreeLines,
isLastRow = true,
}: OverviewEntryRowCardProps) {
const isSelected = row.entryIndex === selectedEntryIndex;
const isEntrySelectedForCreation = selectedEntryKeys.has(row.id);

const treeLineClass =
showTreeLines && cx(treeChildRowBase, isLastRow ? treeChildRowLast : treeChildRowNotLast);

const rowContent = (
<>
<Card
as="button"
type="button"
onClick={() => {
if (!isSelected) onSelect(row.entryIndex);
}}
style={{
border: `2px solid ${isSelected ? tokens.blue500 : tokens.gray300}`,
backgroundColor: tokens.colorWhite,
cursor: isSelected ? 'default' : 'pointer',
textAlign: 'left',
width: '100%',
padding: `${tokens.spacingXs} ${tokens.spacingS}`,
}}>
<Paragraph marginBottom="none">
<Text as="span" fontWeight="fontWeightDemiBold">
{row.contentTypeName || 'Untitled'}
</Text>
{row.contentTypeName && row.entryTitle ? (
<Text as="span" fontColor="gray600">
{' '}
({truncateLabel(row.entryTitle, 150)})
</Text>
) : null}
</Paragraph>
<Flex alignItems="center" gap="spacingXs">
<Checkbox
aria-label={`Create entry ${row.contentTypeName || 'Untitled'}${
row.entryTitle ? ` (${truncateLabel(row.entryTitle, 150)})` : ''
}`}
isChecked={isEntrySelectedForCreation}
isDisabled={areEntrySelectionsDisabled}
onChange={(event) => onToggleEntrySelection(row.id, event.target.checked)}
/>
Comment on lines +59 to +66
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 There seem to be an issue with the checkbox (saw this on the video). Instead of showing the hand pointer (default from forma) is showing the arrow pointer. Perhaps it has to do with the missing props in Card component that were setting up the cursor

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's weird, but that only happens when I record the video, when I use the app without recording, it shows the hand pointer. I cannot prove it as taking a screenshot also removes the cursor, but I checked and it works as expected

<button
type="button"
onClick={() => {
if (!isSelected) onSelect(row.entryIndex);
}}
style={{
appearance: 'none',
background: 'transparent',
border: 0,
cursor: isSelected ? 'default' : 'pointer',
flex: 1,
font: 'inherit',
padding: 0,
textAlign: 'left',
}}>
<Paragraph marginBottom="none">
<Text as="span" fontWeight="fontWeightDemiBold">
{row.contentTypeName || 'Untitled'}
</Text>
{row.contentTypeName && row.entryTitle ? (
<Text as="span" fontColor="gray600">
{' '}
({truncateLabel(row.entryTitle, 150)})
</Text>
) : null}
</Paragraph>
</button>
</Flex>
</Card>
{row.children.length > 0 ? (
<Box className={treeChildrenList}>
Expand All @@ -71,7 +100,10 @@ function OverviewEntryRowCard({
key={child.id}
row={child}
selectedEntryIndex={selectedEntryIndex}
selectedEntryKeys={selectedEntryKeys}
onSelect={onSelect}
onToggleEntrySelection={onToggleEntrySelection}
areEntrySelectionsDisabled={areEntrySelectionsDisabled}
showTreeLines
isLastRow={index === row.children.length - 1}
/>
Expand All @@ -92,15 +124,25 @@ function OverviewEntryRowCard({
);
}

export function OverviewEntryList({ rows, selectedEntryIndex, onSelect }: OverviewEntryListProps) {
export function OverviewEntryList({
rows,
selectedEntryIndex,
selectedEntryKeys,
onSelect,
onToggleEntrySelection,
areEntrySelectionsDisabled = false,
}: OverviewEntryListProps) {
return (
<Flex flexDirection="column" gap="spacingS">
{rows.map((row) => (
<OverviewEntryRowCard
key={row.id}
row={row}
selectedEntryIndex={selectedEntryIndex}
selectedEntryKeys={selectedEntryKeys}
onSelect={onSelect}
onToggleEntrySelection={onToggleEntrySelection}
areEntrySelectionsDisabled={areEntrySelectionsDisabled}
showTreeLines={false}
/>
))}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,27 @@ import Splitter from '../mainpage/Splitter';
interface OverviewProps {
payload: MappingReviewSuspendPayload;
selectedEntryIndex: number | null;
selectedEntryKeys: ReadonlySet<string>;
onSelectEntryIndex: (index: number) => void;
onToggleEntrySelection: (entryKey: string, isSelected: boolean) => void;
ctaLabel: string;
onCtaClick: () => void;
isCtaLoading?: boolean;
isCtaDisabled?: boolean;
areEntrySelectionsDisabled?: boolean;
}

const OverviewSection = ({
payload,
selectedEntryIndex,
selectedEntryKeys,
onSelectEntryIndex,
onToggleEntrySelection,
ctaLabel,
onCtaClick,
isCtaLoading = false,
isCtaDisabled = false,
areEntrySelectionsDisabled = false,
}: OverviewProps) => {
const entryRows = useMemo(
() =>
Expand All @@ -45,7 +53,7 @@ const OverviewSection = ({
</Flex>
<Paragraph marginBottom="none">
Review your content and associated entries below. Highlight text to make adjustments.
Create entries when you are complete.
Select which entries you’d like to create.
</Paragraph>
</Flex>

Expand All @@ -56,15 +64,14 @@ const OverviewSection = ({
<Text fontWeight="fontWeightDemiBold" fontSize="fontSizeL">
Entries
</Text>
<Text fontSize="fontSizeM">Click row to view content by entry below.</Text>
</Flex>

<Flex alignItems="center" gap="spacingS">
<Button
variant="primary"
onClick={onCtaClick}
isLoading={isCtaLoading}
isDisabled={isCtaLoading}>
isDisabled={isCtaLoading || isCtaDisabled}>
{ctaLabel}
</Button>
</Flex>
Expand All @@ -80,7 +87,10 @@ const OverviewSection = ({
<OverviewEntryList
rows={entryRows}
selectedEntryIndex={selectedEntryIndex}
selectedEntryKeys={selectedEntryKeys}
onSelect={onSelectEntryIndex}
onToggleEntrySelection={onToggleEntrySelection}
areEntrySelectionsDisabled={areEntrySelectionsDisabled}
/>
</Box>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ import { RunStatus } from '@types';
import { useWorkflowAgent } from '@hooks/useWorkflowAgent';
import { createEntriesFromPreviewPayload } from '../../../../services/entryService';
import type { ContentTypeDisplayInfoMap } from '../../../../utils/overviewEntryList';
import {
countSelectedEntries,
filterEntryBlockGraphBySelection,
getAllEntrySelectionKeys,
} from '../../../../utils/selectEntryBlockGraph';
import Splitter from '../mainpage/Splitter';
import { ConfirmCancelModal } from '../modals/ConfirmCancelModal';
import { ErrorModal } from '../modals/ErrorModal';
Expand Down Expand Up @@ -50,11 +55,16 @@ export const ReviewPage = ({
const [entryBlockGraph, setEntryBlockGraph] = useState<EntryBlockGraph>(() =>
structuredClone(payload.entryBlockGraph)
);
const [selectedEntryKeys, setSelectedEntryKeys] = useState<Set<string>>(() =>
getAllEntrySelectionKeys(payload.entryBlockGraph.entries)
);

// Reset local graph when starting a different run; do not depend on payload.entryBlockGraph
// alone or user edits would be wiped when the parent re-renders with a new object reference.
useEffect(() => {
setEntryBlockGraph(structuredClone(payload.entryBlockGraph));
const nextEntryBlockGraph = structuredClone(payload.entryBlockGraph);
setEntryBlockGraph(nextEntryBlockGraph);
setSelectedEntryKeys(getAllEntrySelectionKeys(nextEntryBlockGraph.entries));
// eslint-disable-next-line react-hooks/exhaustive-deps -- only re-init on run identity
}, [runId, payload.documentId]);

Expand All @@ -74,20 +84,45 @@ export const ReviewPage = ({
}, [payload.contentTypes]);
const hasCreatedEntries = createdEntries !== null;
const isMappingDisabled = isCreatePending || hasCreatedEntries;
const selectedEntryCount = useMemo(
() => countSelectedEntries(entryBlockGraph.entries, selectedEntryKeys),
[entryBlockGraph.entries, selectedEntryKeys]
);
const hasSelectedEntries = selectedEntryCount > 0;

const { resumeWorkflow } = useWorkflowAgent({ sdk, documentId: '', oauthToken: '' });

const handleToggleEntrySelection = (entryKey: string, isSelected: boolean) => {
setSelectedEntryKeys((previous) => {
const next = new Set(previous);
if (isSelected) {
next.add(entryKey);
} else {
next.delete(entryKey);
}
return next;
});
};

const handleCreateEntries = useCallback(async (): Promise<void> => {
if (!runId) {
onExitReview();
return;
}

if (!hasSelectedEntries) {
return;
}

setIsCreatePending(true);

try {
const result = await resumeWorkflow(runId, {
const selectedEntryBlockGraph = filterEntryBlockGraphBySelection(
entryBlockGraph,
selectedEntryKeys
);
const result = await resumeWorkflow(runId, {
entryBlockGraph: selectedEntryBlockGraph,
});

if (result.status === RunStatus.COMPLETED && 'googleDocPayload' in result) {
Expand Down Expand Up @@ -122,7 +157,15 @@ export const ReviewPage = ({
} finally {
setIsCreatePending(false);
}
}, [runId, resumeWorkflow, entryBlockGraph, sdk, onExitReview]);
}, [
runId,
hasSelectedEntries,
entryBlockGraph,
selectedEntryKeys,
resumeWorkflow,
sdk,
onExitReview,
]);

const handleConfirmCancel = useCallback(async () => {
setIsCancelling(true);
Expand Down Expand Up @@ -211,13 +254,17 @@ export const ReviewPage = ({
<OverviewSection
payload={reviewPayload}
selectedEntryIndex={selectedEntryIndex}
selectedEntryKeys={selectedEntryKeys}
onSelectEntryIndex={(index) => {
setSelectedEntryIndex(index);
setReviewMode('edit');
}}
ctaLabel={hasCreatedEntries ? 'View entries' : 'Create entries'}
onToggleEntrySelection={handleToggleEntrySelection}
ctaLabel={hasCreatedEntries ? 'View entries' : 'Create selected entries'}
onCtaClick={handleCreateOrViewEntries}
isCtaLoading={isCreatePending}
isCtaDisabled={!hasCreatedEntries && !hasSelectedEntries}
areEntrySelectionsDisabled={isMappingDisabled}
/>
<MappingView
payload={reviewPayload}
Expand Down
51 changes: 51 additions & 0 deletions apps/google-docs/src/utils/selectEntryBlockGraph.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import type { EntryBlockGraph, EntryBlockGraphEntry } from '../types/entryBlockGraph';

export function getEntrySelectionKey(entry: EntryBlockGraphEntry, index: number): string {
return entry.tempId ?? String(index);
}

export function getAllEntrySelectionKeys(entries: EntryBlockGraphEntry[]): Set<string> {
return new Set(entries.map((entry, index) => getEntrySelectionKey(entry, index)));
}

export function countSelectedEntries(
entries: EntryBlockGraphEntry[],
selectedEntryKeys: ReadonlySet<string>
): number {
return entries.reduce(
(count, entry, index) =>
selectedEntryKeys.has(getEntrySelectionKey(entry, index)) ? count + 1 : count,
0
);
}

export function filterEntryBlockGraphBySelection(
graph: EntryBlockGraph,
selectedEntryKeys: ReadonlySet<string>
): EntryBlockGraph {
const selectedEntries = graph.entries.filter((entry, index) =>
selectedEntryKeys.has(getEntrySelectionKey(entry, index))
);
const selectedTempIds = new Set(
selectedEntries
.map((entry) => entry.tempId)
.filter((tempId): tempId is string => typeof tempId === 'string')
);

return {
...graph,
entries: selectedEntries.map((entry) => ({
...entry,
fieldMappings: entry.fieldMappings.map((fieldMapping) => {
if (!fieldMapping.sourceEntryIds) {
return fieldMapping;
}

return {
...fieldMapping,
sourceEntryIds: fieldMapping.sourceEntryIds.filter((id) => selectedTempIds.has(id)),
};
}),
})),
};
}
Loading
Loading