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
@@ -0,0 +1,60 @@
import { css } from '@emotion/css';
import tokens from '@contentful/f36-tokens';

export const BUTTON_ESTIMATE_WIDTH_PX = 280;

const action = css({
display: 'inline-flex',
alignItems: 'center',
gap: tokens.spacing2Xs,
border: 'none',
cursor: 'pointer',
fontSize: tokens.fontSizeS,
fontWeight: tokens.fontWeightMedium,
padding: `${tokens.spacing2Xs} ${tokens.spacingS}`,
whiteSpace: 'nowrap',
});

export const editAction = css([
action,
{
color: tokens.gray900,
backgroundColor: tokens.colorWhite,
'&:hover': {
backgroundColor: tokens.gray200,
},
},
]);

export const removeAction = css([
action,
{
color: tokens.red600,
backgroundColor: tokens.gray100,
'&:hover': {
backgroundColor: tokens.gray200,
},
},
]);

export const divider = css({
width: 1,
alignSelf: 'stretch',
backgroundColor: tokens.gray300,
});

export const getMenuPosition = (top: number, left: number) =>
css({
display: 'inline-flex',
alignItems: 'stretch',
borderRadius: tokens.borderRadiusMedium,
border: `1px solid ${tokens.gray300}`,
backgroundColor: tokens.colorWhite,
boxShadow: '0 2px 6px rgba(0, 0, 0, 0.08)',
overflow: 'hidden',
position: 'fixed',
top,
left,
transform: 'translateX(-50%)',
zIndex: 3,
});
Original file line number Diff line number Diff line change
@@ -1,49 +1,55 @@
import { forwardRef } from 'react';
import { Box, Button } from '@contentful/f36-components';
import { PencilSimpleIcon } from '@contentful/f36-icons';
import { forwardRef, type FocusEvent } from 'react';
import { Box } from '@contentful/f36-components';
import { PencilSimpleIcon, TrashSimpleIcon } from '@contentful/f36-icons';
import tokens from '@contentful/f36-tokens';
import type { SelectionViewportRectangle } from './selectionViewportRectangle';
import {
BUTTON_ESTIMATE_WIDTH_PX,
divider,
editAction,
getMenuPosition,
removeAction,
} from './EditMappingButton.styles';

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

const BUTTON_ESTIMATE_WIDTH_PX = 160;

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"
style={{
position: 'fixed',
top: Math.max(anchorRectangle.top - 36, 8),
left: clampedCenterX,
transform: 'translateX(-50%)',
zIndex: 3,
display: 'inline-flex',
borderRadius: tokens.borderRadiusMedium,
border: `1px solid ${tokens.gray300}`,
backgroundColor: tokens.colorWhite,
width: 'fit-content',
}}>
<Button
variant="secondary"
size="small"
onClick={onEdit}
onBlur={onBlur}
startIcon={<PencilSimpleIcon size="small" />}
style={{ paddingTop: '2px', paddingBottom: '2px' }}>
onBlur={handleBlur}
className={getMenuPosition(Math.max(anchorRectangle.top - 36, 8), clampedCenterX)}>
<button type="button" className={editAction} onClick={onEdit}>
<PencilSimpleIcon size="small" color={tokens.gray700} />
Edit content mapping
</Button>
</button>
{onRemove ? (
<>
<span aria-hidden="true" className={divider} />
<button type="button" className={removeAction} onClick={onRemove}>
<TrashSimpleIcon size="small" color={tokens.red600} />
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"]'
);
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,72 @@ export const MappingView = ({
clearSelection();
};

const handleRemoveFromSelection = () => {
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)
);
const locations = getLocationsForSelectedText();

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 @@ -894,6 +985,7 @@ export const MappingView = ({
isViewMode={isViewMode}
onSetHoveredMappingKeys={setHoveredMappingKeys}
onEditImage={isViewMode ? undefined : handleEditImage}
onRemoveImage={isViewMode ? undefined : handleRemoveImage}
/>
))}
</Flex>
Expand All @@ -913,6 +1005,7 @@ export const MappingView = ({
isViewMode={isViewMode}
onSetHoveredMappingKeys={setHoveredMappingKeys}
onEditImage={isViewMode ? undefined : handleEditImage}
onRemoveImage={isViewMode ? undefined : handleRemoveImage}
/>
))}
</Flex>
Expand Down Expand Up @@ -951,6 +1044,9 @@ export const MappingView = ({
ref={editButtonRef}
anchorRectangle={selectionRectangle}
onEdit={handleEditFromSelection}
onRemove={
getLocationsForSelectedText().length > 0 ? handleRemoveFromSelection : undefined
}
onBlur={clearSelection}
/>
) : null}
Expand Down Expand Up @@ -1002,6 +1098,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
Loading
Loading