Skip to content

Commit

Permalink
Unify sense refs and protection override
Browse files Browse the repository at this point in the history
  • Loading branch information
imnasnainaec committed Dec 16, 2024
1 parent c5e9cc6 commit 72aad50
Show file tree
Hide file tree
Showing 9 changed files with 111 additions and 112 deletions.
33 changes: 13 additions & 20 deletions src/goals/MergeDuplicates/MergeDupsStep/MergeDragDrop/DragSense.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,20 @@ import { Draggable } from "react-beautiful-dnd";

import { trashId } from "goals/MergeDuplicates/MergeDupsStep/MergeDragDrop";
import SenseCardContent from "goals/MergeDuplicates/MergeDupsStep/SenseCardContent";
import { MergeTreeSense } from "goals/MergeDuplicates/MergeDupsTreeTypes";
import {
MergeTreeReference,
MergeTreeSense,
} from "goals/MergeDuplicates/MergeDupsTreeTypes";
import { setSidebar } from "goals/MergeDuplicates/Redux/MergeDupsActions";
import { useAppDispatch, useAppSelector } from "rootRedux/hooks";
import { type StoreState } from "rootRedux/types";
import theme from "types/theme";

interface DragSenseProps {
index: number;
wordId: string;
mergeSenseId: string;
mergeSenses: MergeTreeSense[];
isOnlySenseInProtectedWord: boolean;
isProtectedSense: boolean;
mergeSenses: MergeTreeSense[];
senseRef: MergeTreeReference;
}

function arraysEqual<T>(arr1: T[], arr2: T[]): boolean {
Expand Down Expand Up @@ -48,17 +49,13 @@ export default function DragSense(props: DragSenseProps): ReactElement {
(state: StoreState) => state.mergeDuplicateGoal.tree.sidebar
);
const isInSidebar =
sidebar.wordId === props.wordId &&
sidebar.mergeSenseId === props.mergeSenseId &&
sidebar.senseRef.wordId === props.senseRef.wordId &&
sidebar.senseRef.mergeSenseId === props.senseRef.mergeSenseId &&
sidebar.mergeSenses.length > 1;

const updateSidebar = useCallback(() => {
dispatch(
setSidebar({
mergeSenses: props.mergeSenses,
wordId: props.wordId,
mergeSenseId: props.mergeSenseId,
})
setSidebar({ mergeSenses: props.mergeSenses, senseRef: props.senseRef })
);
}, [dispatch, props]);

Expand Down Expand Up @@ -91,12 +88,8 @@ export default function DragSense(props: DragSenseProps): ReactElement {

return (
<Draggable
key={props.mergeSenseId}
draggableId={JSON.stringify({
wordId: props.wordId,
mergeSenseId: props.mergeSenseId,
isSenseProtected: props.isProtectedSense,
})}
key={props.senseRef.mergeSenseId}
draggableId={JSON.stringify(props.senseRef)}
index={props.index}
isDragDisabled={props.isOnlySenseInProtectedWord && !overrideProtection}
>
Expand All @@ -112,13 +105,13 @@ export default function DragSense(props: DragSenseProps): ReactElement {
minWidth: 150,
maxWidth: 300,
opacity:
!props.isProtectedSense &&
!props.senseRef.isSenseProtected &&
(snapshot.draggingOver === trashId || snapshot.combineWith)
? 0.7
: 1,
background: isInSidebar
? "lightblue"
: props.isProtectedSense
: props.senseRef.isSenseProtected
? "lightyellow"
: snapshot.draggingOver === trashId
? "red"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,14 @@ export default function DropWord(props: DropWordProps): ReactElement {
<DragSense
key={id}
index={index}
wordId={props.wordId}
mergeSenseId={id}
mergeSenses={senses}
senseRef={{
isSenseProtected: senses[0].protected,
mergeSenseId: id,
protectReasons: senses[0].protectReasons,
wordId: props.wordId,
}}
isOnlySenseInProtectedWord={protectedWithOneChild}
isProtectedSense={senses[0].protected}
/>
);
})}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,7 @@ import { Draggable } from "react-beautiful-dnd";

import { trashId } from "goals/MergeDuplicates/MergeDupsStep/MergeDragDrop";
import SenseCardContent from "goals/MergeDuplicates/MergeDupsStep/SenseCardContent";
import {
MergeTreeReference,
MergeTreeSense,
} from "goals/MergeDuplicates/MergeDupsTreeTypes";
import { MergeTreeSense } from "goals/MergeDuplicates/MergeDupsTreeTypes";
import { useAppSelector } from "rootRedux/hooks";
import { type StoreState } from "rootRedux/types";
import theme from "types/theme";
Expand All @@ -21,10 +18,8 @@ export default function SidebarDragSense(
props: SidebarDragSenseProps
): ReactElement {
const draggableId = useAppSelector((state: StoreState) => {
const { mergeSenseId, wordId } = state.mergeDuplicateGoal.tree.sidebar;
const order = props.index;
const ref: MergeTreeReference = { wordId, mergeSenseId, order };
return JSON.stringify(ref);
const ref = state.mergeDuplicateGoal.tree.sidebar.senseRef;
return JSON.stringify({ ...ref, order: props.index });
});
const overrideProtection = useAppSelector(
(state: StoreState) => state.mergeDuplicateGoal.overrideProtection
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { ArrowForwardIos, HelpOutline } from "@mui/icons-material";
import { Grid, IconButton, Typography } from "@mui/material";
import { ReactElement } from "react";
import { type ReactElement } from "react";
import { Droppable } from "react-beautiful-dnd";

import SidebarDragSense from "goals/MergeDuplicates/MergeDupsStep/MergeDragDrop/SidebarDragSense";
import { MergeTreeSense } from "goals/MergeDuplicates/MergeDupsTreeTypes";
import { type MergeTreeSense } from "goals/MergeDuplicates/MergeDupsTreeTypes";
import { setSidebar } from "goals/MergeDuplicates/Redux/MergeDupsActions";
import { useAppDispatch, useAppSelector } from "rootRedux/hooks";
import { type StoreState } from "rootRedux/types";
Expand All @@ -15,16 +15,14 @@ export default function SidebarDrop(): ReactElement {
const sidebar = useAppSelector(
(state: StoreState) => state.mergeDuplicateGoal.tree.sidebar
);
const { mergeSenseId, wordId } = sidebar.senseRef;
const vernacular = useAppSelector((state: StoreState) => {
const tree = state.mergeDuplicateGoal.tree;
return tree.words[tree.sidebar.wordId]?.vern;
return tree.words[tree.sidebar.senseRef.wordId]?.vern;
});

return (
<Droppable
droppableId={`${sidebar.wordId} ${sidebar.mergeSenseId}`}
key={sidebar.mergeSenseId}
>
<Droppable droppableId={`${wordId} ${mergeSenseId}`} key={mergeSenseId}>
{(providedDroppable): ReactElement => (
<div
ref={providedDroppable.innerRef}
Expand Down
125 changes: 64 additions & 61 deletions src/goals/MergeDuplicates/MergeDupsStep/MergeDragDrop/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { DragDropContext, Droppable, DropResult } from "react-beautiful-dnd";
import { useTranslation } from "react-i18next";
import { v4 } from "uuid";

import { type ProtectReason } from "api/models";
import { appBarHeight } from "components/AppBar/AppBarTypes";
import { CancelConfirmDialog } from "components/Dialogs";
import DropWord from "goals/MergeDuplicates/MergeDupsStep/MergeDragDrop/DropWord";
Expand All @@ -18,6 +17,11 @@ import {
moveSense,
orderSense,
} from "goals/MergeDuplicates/Redux/MergeDupsActions";
import {
type CombineSenseMergePayload,
type MoveSensePayload,
type OrderSensePayload,
} from "goals/MergeDuplicates/Redux/MergeDupsReduxTypes";
import { useAppDispatch, useAppSelector } from "rootRedux/hooks";
import { type StoreState } from "rootRedux/types";
import theme from "types/theme";
Expand All @@ -29,9 +33,6 @@ export default function MergeDragDrop(): ReactElement {
const overrideProtection = useAppSelector(
(state: StoreState) => state.mergeDuplicateGoal.overrideProtection
);
const senses = useAppSelector(
(state: StoreState) => state.mergeDuplicateGoal.data.senses
);
const sidebarOpen = useAppSelector(
(state: StoreState) =>
state.mergeDuplicateGoal.tree.sidebar.mergeSenses.length > 1
Expand All @@ -45,69 +46,65 @@ export default function MergeDragDrop(): ReactElement {
(state: StoreState) => state.mergeDuplicateGoal.tree.words
);

const [protectedDataText, setProtectedDataText] = useState("");
const [protectedDest, setProtectedDest] = useState<
MergeTreeReference | undefined
>();
const [protectedSrc, setProtectedSrc] = useState<
MergeTreeReference | undefined
>();
interface ProtectedOverride {
combinePayload?: CombineSenseMergePayload;
deletePayload?: MergeTreeReference;
movePayload?: MoveSensePayload;
orderPayload?: OrderSensePayload;
protectReason: string;
}
const [override, setOverride] = useState<ProtectedOverride | undefined>();
const [srcToDelete, setSrcToDelete] = useState<
MergeTreeReference | undefined
>();

const { t } = useTranslation();

function startOverrideProtectedData(
protectedData: string,
src: MergeTreeReference,
dest?: MergeTreeReference
): void {
setProtectedDest(dest);
setProtectedSrc(src);
setProtectedDataText(
t("mergeDups.helpText.protectedOverrideWarning", {
val: protectedData,
})
);
}

function handleDrop(res: DropResult): void {
const src: MergeTreeReference = JSON.parse(res.draggableId);
const srcWordId = res.source.droppableId;
const srcWord = words[srcWordId];
const senseReasons = senses[src.mergeSenseId].sense.protectReasons ?? [];
let wordReasons: ProtectReason[] = [];
if (srcWord?.protected && Object.keys(srcWord.sensesGuids).length === 1) {

// Generate text for protected data that will be lost if user overrides.
const isOnlySenseInProtectedWord =
srcWord?.protected && Object.keys(srcWord.sensesGuids).length === 1;
let protectReason = "";
if (overrideProtection) {
const wordReasons = isOnlySenseInProtectedWord
? (srcWord.protectReasons ?? [])
: undefined;
const senseReasons = src.protectReasons;
protectReason = t("mergeDups.helpText.protectedOverrideWarning", {
val: protectReasonsText(t, wordReasons, senseReasons, false),
});
}

if (isOnlySenseInProtectedWord && !overrideProtection) {
// Case 0: The final sense of a protected word cannot be moved.
if (overrideProtection) {
wordReasons = srcWord.protectReasons ?? [];
} else {
return;
}
return;
}
const reasonsText = protectReasonsText(t, wordReasons, senseReasons);
if (res.destination?.droppableId === trashId) {
// Case 1: The sense was dropped on the trash icon.
if (src.isSenseProtected) {
if (src.isSenseProtected || isOnlySenseInProtectedWord) {
// Case 1a: Cannot delete a protected sense.
if (overrideProtection) {
// ... unless protection override is active and user confirms.
startOverrideProtectedData(reasonsText, src);
setOverride({ deletePayload: src, protectReason });
}
return;
}
setSrcToDelete(src);
} else if (res.combine) {
const combineRef: MergeTreeReference = JSON.parse(
res.combine.draggableId
);
const combinePayload: CombineSenseMergePayload = {
dest: JSON.parse(res.combine.draggableId),
src,
};
// Case 2: the sense was dropped on another sense.
if (src.isSenseProtected) {
if (src.isSenseProtected || isOnlySenseInProtectedWord) {
// Case 2a: Cannot merge a protected sense into another sense.
if (overrideProtection) {
// ... unless protection override is active and user confirms.
startOverrideProtectedData(reasonsText, src, combineRef);
setOverride({ combinePayload, protectReason });
} else if (srcWordId !== res.combine.droppableId) {
// Otherwise, if target sense is in different word, move instead of combine.
dispatch(
Expand All @@ -120,13 +117,14 @@ export default function MergeDragDrop(): ReactElement {
}
return;
}
if (combineRef.order !== undefined) {
if (combinePayload.dest.order !== undefined) {
// Case 2b: If the target is a sidebar sub-sense, it cannot receive a combine.
return;
}
// TODO: handle override case when sense is last in protected word
dispatch(combineSense({ src, dest: combineRef }));
dispatch(combineSense(combinePayload));
} else if (res.destination) {
const destOrder = res.destination.index;
const destWordId = res.destination.droppableId;
// Case 3: The sense was dropped in a droppable.
if (srcWordId !== destWordId) {
Expand All @@ -136,21 +134,26 @@ export default function MergeDragDrop(): ReactElement {
return;
}
// Move the sense to the dest MergeWord.
dispatch(
moveSense({ src, destWordId, destOrder: res.destination.index })
);
const movePayload: MoveSensePayload = { destOrder, destWordId, src };
if (isOnlySenseInProtectedWord) {
setOverride({ movePayload, protectReason });
return;
}
dispatch(moveSense(movePayload));
} else {
// Case 3b: The source & dest droppables are the same, so we reorder, not move.
const destOrder = res.destination.index;
const orderPayload: OrderSensePayload = { destOrder, src };
if (
src.order === destOrder ||
(destOrder === 0 && src.order !== undefined && sidebarProtected)
) {
// If the sense wasn't moved or was moved within the sidebar above a protected sense, do nothing.
// TODO: Handle override case when moving above protected sense in sidebar
if (overrideProtection) {
setOverride({ orderPayload, protectReason });
}
return;
}
dispatch(orderSense({ src, destOrder }));
dispatch(orderSense(orderPayload));
}
}
}
Expand All @@ -163,16 +166,16 @@ export default function MergeDragDrop(): ReactElement {
}

function onConfirmOverride(): void {
if (protectedSrc) {
if (protectedDest) {
dispatch(combineSense({ src: protectedSrc, dest: protectedDest }));
} else {
dispatch(deleteSense(protectedSrc));
}
setProtectedSrc(undefined);
if (override?.combinePayload) {
dispatch(combineSense(override.combinePayload));
} else if (override?.deletePayload) {
dispatch(deleteSense(override.deletePayload));
} else if (override?.movePayload) {
dispatch(moveSense(override.movePayload));
} else if (override?.orderPayload) {
dispatch(orderSense(override.orderPayload));
}
setProtectedDest(undefined);
setProtectedDataText("");
setOverride(undefined);
}

function renderSidebar(): ReactElement {
Expand Down Expand Up @@ -235,9 +238,9 @@ export default function MergeDragDrop(): ReactElement {
</ImageListItem>
{renderSidebar()}
<CancelConfirmDialog
open={!!protectedDataText}
text={protectedDataText}
handleCancel={() => setProtectedDataText("")}
open={!!override}
text={override?.protectReason ?? ""}
handleCancel={() => setOverride(undefined)}
handleConfirm={onConfirmOverride}
/>
<CancelConfirmDialog
Expand Down
10 changes: 5 additions & 5 deletions src/goals/MergeDuplicates/MergeDupsStep/protectReasonUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ const sep = "; ";

export function protectReasonsText(
t: TFunction<"translation", undefined>,
wordReasons: ProtectReason[],
senseReasons: ProtectReason[]
wordReasons: ProtectReason[] = [],
senseReasons: ProtectReason[] = [],
defaultPreface = true
): string {
const wordTexts = wordReasons.map((r) => wordReasonText(t, r));
const senseTexts = senseReasons.map((r) => senseReasonText(t, r));
return t("mergeDups.helpText.protectedData", {
val: [...wordTexts, ...senseTexts].join(sep),
});
const val = [...wordTexts, ...senseTexts].join(sep);
return defaultPreface ? t("mergeDups.helpText.protectedData", { val }) : val;
}

/** Cases match Backend/Helper/LiftHelper.cs > GetProtectedReasons(LiftSense sense) */
Expand Down
Loading

0 comments on commit 72aad50

Please sign in to comment.