diff --git a/apps/client/public/locales/en-US/translation.json b/apps/client/public/locales/en-US/translation.json
index 18c981434..12d8bffed 100644
--- a/apps/client/public/locales/en-US/translation.json
+++ b/apps/client/public/locales/en-US/translation.json
@@ -1289,5 +1289,9 @@
"{{count}} rows deleted_one": "1 row deleted",
"{{count}} rows deleted_other": "{{count}} rows deleted",
"{{count}} selected_one": "1 selected",
- "{{count}} selected_other": "{{count}} selected"
+ "{{count}} selected_other": "{{count}} selected",
+ "Compare": "Compare",
+ "Compare versions": "Compare versions",
+ "Select version from {{date}}": "Select version from {{date}}",
+ "Version actions for {{date}}": "Version actions for {{date}}"
}
diff --git a/apps/client/src/features/page-history/components/css/history.module.css b/apps/client/src/features/page-history/components/css/history.module.css
index a4be38194..b69a643d3 100644
--- a/apps/client/src/features/page-history/components/css/history.module.css
+++ b/apps/client/src/features/page-history/components/css/history.module.css
@@ -1,7 +1,7 @@
.history {
- display: block;
+ display: flex;
+ align-items: center;
width: 100%;
- padding: var(--mantine-spacing-md);
color: light-dark(var(--mantine-color-black), var(--mantine-color-dark-0));
@mixin hover {
@@ -12,6 +12,28 @@
}
}
+.historyButton {
+ flex: 1;
+ min-width: 0;
+ color: inherit;
+}
+
+.compareCheckbox {
+ padding-left: var(--mantine-spacing-xs);
+}
+
+.itemMenu {
+ opacity: 0;
+ margin-right: var(--mantine-spacing-xs);
+}
+
+.history:hover .itemMenu,
+.history:focus-within .itemMenu,
+.history.active .itemMenu,
+.itemMenu[aria-expanded="true"] {
+ opacity: 1;
+}
+
.historyEditor {
:global(.ProseMirror) {
padding: 0 !important;
diff --git a/apps/client/src/features/page-history/components/history-item.tsx b/apps/client/src/features/page-history/components/history-item.tsx
index cc56b1911..2143fc305 100644
--- a/apps/client/src/features/page-history/components/history-item.tsx
+++ b/apps/client/src/features/page-history/components/history-item.tsx
@@ -1,10 +1,21 @@
-import { Text, Group, UnstyledButton, Avatar, Tooltip } from "@mantine/core";
+import {
+ Text,
+ Group,
+ UnstyledButton,
+ Avatar,
+ Tooltip,
+ ActionIcon,
+ Checkbox,
+ Menu,
+} from "@mantine/core";
+import { IconDots } from "@tabler/icons-react";
import { CustomAvatar } from "@/components/ui/custom-avatar.tsx";
import { formattedDate } from "@/lib/time";
import classes from "./css/history.module.css";
import clsx from "clsx";
import { IPageHistory } from "@/features/page-history/types/page.types";
import { memo, useCallback } from "react";
+import { useTranslation } from "react-i18next";
const MAX_VISIBLE_AVATARS = 5;
@@ -15,6 +26,13 @@ interface HistoryItemProps {
onHover?: (id: string, index: number) => void;
onHoverEnd?: () => void;
isActive: boolean;
+ compareMode: boolean;
+ isChecked: boolean;
+ isCheckboxDisabled: boolean;
+ canCompare: boolean;
+ onToggleCompare: (id: string) => void;
+ onStartCompare: (id: string) => void;
+ onRestore?: (id: string, index: number) => void;
}
const HistoryItem = memo(function HistoryItem({
@@ -24,10 +42,24 @@ const HistoryItem = memo(function HistoryItem({
onHover,
onHoverEnd,
isActive,
+ compareMode,
+ isChecked,
+ isCheckboxDisabled,
+ canCompare,
+ onToggleCompare,
+ onStartCompare,
+ onRestore,
}: HistoryItemProps) {
+ const { t } = useTranslation();
+ const date = formattedDate(new Date(historyItem.createdAt));
+
const handleClick = useCallback(() => {
- onSelect(historyItem.id, index);
- }, [onSelect, historyItem.id, index]);
+ if (compareMode) {
+ onToggleCompare(historyItem.id);
+ } else {
+ onSelect(historyItem.id, index);
+ }
+ }, [compareMode, onToggleCompare, onSelect, historyItem.id, index]);
const handleMouseEnter = useCallback(() => {
onHover?.(historyItem.id, index);
@@ -37,63 +69,115 @@ const HistoryItem = memo(function HistoryItem({
const hasContributors = contributors && contributors.length > 0;
return (
-
- {formattedDate(new Date(historyItem.createdAt))}
+ {compareMode && (
+ onToggleCompare(historyItem.id)}
+ aria-label={t("Select version from {{date}}", { date })}
+ />
+ )}
-
- {hasContributors ? (
- <>
-
-
- {contributors.slice(0, MAX_VISIBLE_AVATARS).map((contributor) => (
-
-
-
- ))}
- {contributors.length > MAX_VISIBLE_AVATARS && (
- (
- {c.name}
+
+ {date}
+
+
+ {hasContributors ? (
+ <>
+
+
+ {contributors
+ .slice(0, MAX_VISIBLE_AVATARS)
+ .map((contributor) => (
+
+
+
))}
- >
-
- +{contributors.length - MAX_VISIBLE_AVATARS}
-
-
- )}
-
-
- {contributors.length === 1 && (
+ {contributors.length > MAX_VISIBLE_AVATARS && (
+ (
+ {c.name}
+ ))}
+ >
+
+ +{contributors.length - MAX_VISIBLE_AVATARS}
+
+
+ )}
+
+
+ {contributors.length === 1 && (
+
+ {contributors[0].name}
+
+ )}
+ >
+ ) : (
+ <>
+
- {contributors[0].name}
+ {historyItem.lastUpdatedBy?.name}
+ >
+ )}
+
+
+
+ {!compareMode && (
+
+ )}
+
);
});
diff --git a/apps/client/src/features/page-history/components/history-list.tsx b/apps/client/src/features/page-history/components/history-list.tsx
index 3b6d45dd8..6720dd27a 100644
--- a/apps/client/src/features/page-history/components/history-list.tsx
+++ b/apps/client/src/features/page-history/components/history-list.tsx
@@ -6,8 +6,12 @@ import HistoryItem from "@/features/page-history/components/history-item";
import {
activeHistoryIdAtom,
activeHistoryPrevIdAtom,
+ compareModeAtom,
+ comparePairAtom,
+ compareSelectionAtom,
historyAtoms,
} from "@/features/page-history/atoms/history-atoms";
+import { resolveComparePair } from "@/features/page-history/utils/resolve-compare-pair";
import { useAtom, useSetAtom } from "jotai";
import { useCallback, useEffect, useMemo, useRef } from "react";
import {
@@ -32,6 +36,10 @@ function HistoryList({ pageId }: Props) {
const [activeHistoryId, setActiveHistoryId] = useAtom(activeHistoryIdAtom);
const setActiveHistoryPrevId = useSetAtom(activeHistoryPrevIdAtom);
const setHistoryModalOpen = useSetAtom(historyAtoms);
+ const [compareMode, setCompareMode] = useAtom(compareModeAtom);
+ const [compareSelection, setCompareSelection] = useAtom(compareSelectionAtom);
+ // @ts-ignore
+ const setComparePair = useSetAtom(comparePairAtom);
const {
data: pageHistoryData,
@@ -79,10 +87,60 @@ function HistoryList({ pageId }: Props) {
const handleSelect = useCallback(
(id: string, index: number) => {
+ // @ts-ignore
+ setComparePair(null);
setActiveHistoryId(id);
setActiveHistoryPrevId(historyItems[index + 1]?.id ?? "");
},
- [historyItems, setActiveHistoryId, setActiveHistoryPrevId],
+ [historyItems, setActiveHistoryId, setActiveHistoryPrevId, setComparePair],
+ );
+
+ const handleToggleCompare = useCallback(
+ (id: string) => {
+ setCompareSelection((prev) => {
+ if (prev.includes(id)) return prev.filter((item) => item !== id);
+ if (prev.length >= 2) return prev;
+ return [...prev, id];
+ });
+ },
+ [setCompareSelection],
+ );
+
+ const handleStartCompare = useCallback(
+ (id: string) => {
+ // @ts-ignore
+ setComparePair(null);
+ setCompareMode(true);
+ setCompareSelection([id]);
+ },
+ [setComparePair, setCompareMode, setCompareSelection],
+ );
+
+ const handleCancelCompare = useCallback(() => {
+ setCompareMode(false);
+ setCompareSelection([]);
+ }, [setCompareMode, setCompareSelection]);
+
+ const handleConfirmCompare = useCallback(() => {
+ const pair = resolveComparePair(historyItems, compareSelection);
+ if (!pair) return;
+ setComparePair(pair);
+ setCompareMode(false);
+ setCompareSelection([]);
+ }, [
+ historyItems,
+ compareSelection,
+ setComparePair,
+ setCompareMode,
+ setCompareSelection,
+ ]);
+
+ const handleRestoreItem = useCallback(
+ (id: string, index: number) => {
+ handleSelect(id, index);
+ confirmRestore(id);
+ },
+ [handleSelect, confirmRestore],
);
useEffect(() => {
@@ -138,6 +196,16 @@ function HistoryList({ pageId }: Props) {
onHover={handleHover}
onHoverEnd={clearPrefetchTimeout}
isActive={historyItem.id === activeHistoryId}
+ compareMode={compareMode}
+ isChecked={compareSelection.includes(historyItem.id)}
+ isCheckboxDisabled={
+ !compareSelection.includes(historyItem.id) &&
+ compareSelection.length >= 2
+ }
+ canCompare={historyItems.length >= 2}
+ onToggleCompare={handleToggleCompare}
+ onStartCompare={handleStartCompare}
+ onRestore={canRestore ? handleRestoreItem : undefined}
/>
))}
{hasNextPage && }
@@ -148,22 +216,44 @@ function HistoryList({ pageId }: Props) {
)}
- {canRestore && (
+ {compareMode ? (
<>
-
>
+ ) : (
+ canRestore && (
+ <>
+
+
+ setHistoryModalOpen(false)}
+ >
+ {t("Cancel")}
+
+ confirmRestore()}>
+ {t("Restore")}
+
+
+ >
+ )
)}
);