Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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,49 +1,73 @@
import { forwardRef } from 'react';
import { forwardRef, type FocusEvent } from 'react';
import { Box, Button } from '@contentful/f36-components';
import { PencilSimpleIcon } from '@contentful/f36-icons';
import tokens from '@contentful/f36-tokens';
import { PencilSimpleIcon, TrashSimpleIcon } from '@contentful/f36-icons';
import type { SelectionViewportRectangle } from './selectionViewportRectangle';
import tokens from '@contentful/f36-tokens';

interface EditMappingButtonProps {
anchorRectangle: SelectionViewportRectangle;
onEdit: () => void;
onRemove?: () => void;
onBlur?: () => void;
}

const BUTTON_ESTIMATE_WIDTH_PX = 160;
export const BUTTON_ESTIMATE_WIDTH_PX = 280;

export const EditMappingButton = forwardRef<HTMLDivElement, EditMappingButtonProps>(
({ anchorRectangle, onEdit, onBlur }, ref) => {
({ anchorRectangle, onEdit, onRemove, onBlur }, ref) => {
const centerX = (anchorRectangle.left + anchorRectangle.right) / 2;
const half = BUTTON_ESTIMATE_WIDTH_PX / 2;
const clampedCenterX = Math.min(Math.max(centerX, 8 + half), window.innerWidth - 8 - half);

const handleBlur = (event: FocusEvent<HTMLDivElement>) => {
if (event.currentTarget.contains(event.relatedTarget as Node | null)) {
return;
}
onBlur?.();
};

return (
<Box
ref={ref}
aria-label="Edit content mapping"
data-testid="review-selection-menu"
onBlur={handleBlur}
style={{
position: 'fixed',
top: Math.max(anchorRectangle.top - 36, 8),
left: clampedCenterX,
transform: 'translateX(-50%)',
zIndex: 3,
display: 'inline-flex',
gap: 0,
borderRadius: tokens.borderRadiusMedium,
border: `1px solid ${tokens.gray300}`,
backgroundColor: tokens.colorWhite,
width: 'fit-content',
padding: 0,
}}>
<Button
variant="secondary"
size="small"
startIcon={<PencilSimpleIcon />}
onClick={onEdit}
onBlur={onBlur}
startIcon={<PencilSimpleIcon size="small" />}
style={{ paddingTop: '2px', paddingBottom: '2px' }}>
style={{
borderRight: `1px solid ${tokens.gray400}`,
borderBottomRightRadius: 0,
borderTopRightRadius: 0,
}}>
Edit content mapping
</Button>
{onRemove ? (
<>
<Button
variant="negative"
size="small"
startIcon={<TrashSimpleIcon />}
onClick={onRemove}
style={{ borderBottomLeftRadius: 0, borderTopLeftRadius: 0, borderLeft: 0 }}>
Remove
</Button>
</>
) : null}
</Box>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,13 +71,28 @@ import {
collectTextExclusionRangesFromSelection,
type TextExclusionRange,
} from './entryBlockGraphExclusion';
import { RemoveContentModal } from './edit-modals/RemoveContentModal';

interface EditModalState {
viewModel: EditModalContent;
title: string;
primaryButtonLabel: string;
}

interface RemoveModalState {
isOpen: boolean;
locations: EditLocationOption[];
textRanges: TextExclusionRange[];
imageRefs: ImageSourceRef[];
}

const EMPTY_REMOVE_MODAL: RemoveModalState = {
isOpen: false,
locations: [],
textRanges: [],
imageRefs: [],
};

interface MappingViewProps {
payload: MappingReviewSuspendPayload;
entryBlockGraph: EntryBlockGraph;
Expand Down Expand Up @@ -170,6 +185,7 @@ export const MappingView = ({
Record<string, Record<string, number>>
>({});
const [editModalState, setEditModalState] = useState<EditModalState>(EMPTY_EDIT_MODAL);
const [removeModalState, setRemoveModalState] = useState<RemoveModalState>(EMPTY_REMOVE_MODAL);
const [pendingTextExclusionRanges, setPendingTextExclusionRanges] = useState<
TextExclusionRange[] | null
>(null);
Expand Down Expand Up @@ -439,16 +455,16 @@ export const MappingView = ({
return matches;
};

const getLocationsForSelectedText = (): EditLocationOption[] => {
const collectMappingKeysFromSelection = (): Set<string> => {
const root = textSelectionRootRef.current;
if (!root || !selectedRange) {
return [];
return new Set();
}

const mappingKeys = new Set<string>();
const selectedMappedSegments = root.querySelectorAll<HTMLElement>(
'[data-review-text-segment="true"][data-is-mapped="true"]'
'[data-review-text-segment="true"][data-is-mapped="true"], [data-review-image-segment="true"][data-is-mapped="true"]'
Comment thread
FBanfi marked this conversation as resolved.
);
const mappingKeys = new Set<string>();

for (const segment of selectedMappedSegments) {
if (!rangeIntersectsNode(selectedRange, segment)) {
Expand All @@ -463,6 +479,15 @@ export const MappingView = ({
.forEach((key) => mappingKeys.add(key));
}

return mappingKeys;
};

const getLocationsForSelectedText = (): EditLocationOption[] => {
const mappingKeys = collectMappingKeysFromSelection();
if (!mappingKeys.size) {
return [];
}

const locations = allGroups
.flatMap((group) => group.mappingCards)
.filter((card) => card.mappingKeys.some((key) => mappingKeys.has(key)))
Expand Down Expand Up @@ -586,6 +611,71 @@ export const MappingView = ({
clearSelection();
};

const handleRemoveFromSelection = (locations: EditLocationOption[]) => {
if (isDisabled || !selectedText.trim()) return;
const selectionRange = selectedRange ? selectedRange.cloneRange() : null;
const textRanges = collectTextExclusionRangesFromSelection(
textSelectionRootRef.current,
selectionRange
);
const imageRefs = collectRichTextSourceRefsFromSelection(
textSelectionRootRef.current,
selectionRange,
document,
{ mappedState: 'mapped' }
).filter(
(ref): ref is ImageSourceRef => isBlockImageSourceRef(ref) || isTableImageSourceRef(ref)
);

if (!locations.length || (!textRanges.length && !imageRefs.length)) {
clearSelection();
return;
}

setRemoveModalState({ isOpen: true, locations, textRanges, imageRefs });
clearSelection();
};

const handleRemoveImage = (sourceRef: ImageSourceRef) => {
if (isDisabled) return;
const locations = getLocationsForSourceRef(sourceRef);
if (!locations.length) return;

setRemoveModalState({ isOpen: true, locations, textRanges: [], imageRefs: [sourceRef] });
setHoveredMappingKeys([]);
};

const closeRemoveModal = () => {
setRemoveModalState(EMPTY_REMOVE_MODAL);
};

const handleConfirmRemove = () => {
const { locations, textRanges, imageRefs } = removeModalState;
let next = entryBlockGraph;

for (const location of locations) {
const locationSourceRefKeys = new Set(
(location.sourceRefs?.length ? location.sourceRefs : [location.sourceRef]).map(
buildSourceRefKey
)
);

if (textRanges.length) {
next = applyTextExclusionToEntryBlockGraph(next, location, textRanges);
}

const matchingImages = imageRefs.filter((ref) =>
locationSourceRefKeys.has(buildSourceRefKey(ref))
);
for (const imageRef of matchingImages) {
next = applyImageExclusionToEntryBlockGraph(next, location, imageRef);
}
}

if (next !== entryBlockGraph) onEntryBlockGraphChange(next);
closeRemoveModal();
};

const handleEditImage = (sourceRef: ImageSourceRef, label: string) => {
if (isDisabled) return;
const currentLocations = getLocationsForSourceRef(sourceRef);
Expand Down Expand Up @@ -777,6 +867,9 @@ export const MappingView = ({
return result;
}, [allGroups, locationsByCardKey, entryBlockGraph.entries, payload.contentTypes]);

const selectionLocations =
selectionRectangle && !isDisabled && !isViewMode ? getLocationsForSelectedText() : [];

return (
<>
<Flex
Expand Down Expand Up @@ -893,7 +986,8 @@ export const MappingView = ({
hoveredMappingKeys={hoveredMappingKeys}
isViewMode={isViewMode}
onSetHoveredMappingKeys={setHoveredMappingKeys}
onEditImage={isViewMode ? undefined : handleEditImage}
onEditImage={handleEditImage}
onRemoveImage={handleRemoveImage}
/>
))}
</Flex>
Expand All @@ -912,7 +1006,8 @@ export const MappingView = ({
hoveredMappingKeys={hoveredMappingKeys}
isViewMode={isViewMode}
onSetHoveredMappingKeys={setHoveredMappingKeys}
onEditImage={isViewMode ? undefined : handleEditImage}
onEditImage={handleEditImage}
onRemoveImage={handleRemoveImage}
/>
))}
</Flex>
Expand Down Expand Up @@ -951,6 +1046,11 @@ export const MappingView = ({
ref={editButtonRef}
anchorRectangle={selectionRectangle}
onEdit={handleEditFromSelection}
onRemove={
selectionLocations.length > 0
? () => handleRemoveFromSelection(selectionLocations)
: undefined
}
onBlur={clearSelection}
/>
) : null}
Expand Down Expand Up @@ -1002,6 +1102,12 @@ export const MappingView = ({
})()}
onConfirmPrimary={handleEditModalConfirmPrimary}
/>

<RemoveContentModal
isOpen={removeModalState.isOpen}
onConfirm={handleConfirmRemove}
onCancel={closeRemoveModal}
/>
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ interface ReviewDocumentBodyProps {
isViewMode: boolean;
onSetHoveredMappingKeys: (keys: string[]) => void;
onEditImage?: (sourceRef: ImageSourceRef, label: string) => void;
onRemoveImage?: (sourceRef: ImageSourceRef) => void;
}

export const NormalizedDocumentSection = ({
Expand All @@ -33,6 +34,7 @@ export const NormalizedDocumentSection = ({
isViewMode,
onSetHoveredMappingKeys,
onEditImage,
onRemoveImage,
}: ReviewDocumentBodyProps): JSX.Element => {
return (
<Box style={{ flex: 2 }}>
Expand All @@ -56,6 +58,7 @@ export const NormalizedDocumentSection = ({
isViewMode={isViewMode}
onSetHoveredMappingKeys={onSetHoveredMappingKeys}
onEditImage={onEditImage}
onRemoveImage={onRemoveImage}
/>
) : (
<BlockRenderer
Expand All @@ -70,6 +73,7 @@ export const NormalizedDocumentSection = ({
isViewMode={isViewMode}
onSetHoveredMappingKeys={onSetHoveredMappingKeys}
onEditImage={onEditImage}
onRemoveImage={onRemoveImage}
/>
)}
</Box>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Box, Card, Flex, Image, MenuItem, Text } from '@contentful/f36-components';
import { PencilSimpleIcon } from '@contentful/f36-icons';
import { PencilSimpleIcon, TrashSimpleIcon } from '@contentful/f36-icons';
import tokens from '@contentful/f36-tokens';
import type { ImageSourceRef, NormalizedDocumentImage } from '@types';
import Splitter from '../../mainpage/Splitter';
Expand All @@ -18,6 +18,7 @@ export interface ReviewImageAssetCardProps {
onMouseEnter?: () => void;
onMouseLeave?: () => void;
onEdit?: () => void;
onRemove?: () => void;
}

export function getNormalizedImageDisplayName(image: NormalizedDocumentImage): string {
Expand All @@ -35,6 +36,7 @@ export function ReviewImageAssetCard({
onMouseEnter,
onMouseLeave,
onEdit,
onRemove,
}: ReviewImageAssetCardProps): JSX.Element {
const title = getNormalizedImageDisplayName(image);

Expand All @@ -56,6 +58,25 @@ export function ReviewImageAssetCard({
: `1px solid ${tokens.gray300}`
: `1px solid ${isHighlighted ? 'transparent' : tokens.gray300}`;

const cardActions = [
onEdit ? (
<MenuItem key="edit" onClick={onEdit}>
<Flex alignItems="center" gap="spacing2Xs">
<PencilSimpleIcon size="tiny" />
<Text>Edit content mapping</Text>
</Flex>
</MenuItem>
) : null,
onRemove ? (
<MenuItem key="remove" onClick={onRemove}>
<Flex alignItems="center" gap="spacing2Xs">
<TrashSimpleIcon size="tiny" color={tokens.red600} />
<Text fontColor="red600">Remove</Text>
</Flex>
</MenuItem>
) : null,
].filter((action): action is JSX.Element => action !== null);

return (
<Box
data-testid={`review-image-asset-${buildSourceRefKey(sourceRef)}`}
Expand All @@ -74,20 +95,7 @@ export function ReviewImageAssetCard({
boxSizing: 'border-box',
padding: tokens.spacingXs,
}}>
<Card
ariaLabel={title}
actions={
onEdit
? [
<MenuItem key="edit" onClick={onEdit}>
<Flex alignItems="center" gap="spacing2Xs">
<PencilSimpleIcon size="tiny" />
<Text>Edit content mapping</Text>
</Flex>
</MenuItem>,
]
: undefined
}>
<Card ariaLabel={title} actions={cardActions.length ? cardActions : undefined}>
<Splitter />
<Box padding="spacingS">
<Image
Expand Down
Loading
Loading