From b3c5ca6d5f83924cca9b1240be1cd5f93ba3645f Mon Sep 17 00:00:00 2001 From: Philipinho <16838612+Philipinho@users.noreply.github.com> Date: Sun, 1 Feb 2026 17:39:55 +0000 Subject: [PATCH] WIP --- .../public/locales/en-US/translation.json | 4 + .../components/history-mobile.module.css | 69 ++++ .../components/history-modal-mobile.tsx | 307 ++++++++++++++++++ .../page-history/components/history-modal.tsx | 50 ++- 4 files changed, 422 insertions(+), 8 deletions(-) create mode 100644 apps/client/src/features/page-history/components/history-mobile.module.css create mode 100644 apps/client/src/features/page-history/components/history-modal-mobile.tsx diff --git a/apps/client/public/locales/en-US/translation.json b/apps/client/public/locales/en-US/translation.json index f36203166..a10f6762e 100644 --- a/apps/client/public/locales/en-US/translation.json +++ b/apps/client/public/locales/en-US/translation.json @@ -123,6 +123,10 @@ "page": "page", "Page deleted successfully": "Page deleted successfully", "Page history": "Page history", + "Version history for": "Version history for", + "document": "document", + "Select version": "Select version", + "Close": "Close", "Highlight changes": "Highlight changes", "Page import is in progress. Please do not close this tab.": "Page import is in progress. Please do not close this tab.", "Pages": "Pages", diff --git a/apps/client/src/features/page-history/components/history-mobile.module.css b/apps/client/src/features/page-history/components/history-mobile.module.css new file mode 100644 index 000000000..2db6d10c0 --- /dev/null +++ b/apps/client/src/features/page-history/components/history-mobile.module.css @@ -0,0 +1,69 @@ +.container { + display: flex; + flex-direction: column; + height: calc(100vh - 60px); + position: relative; + overflow: hidden; +} + +.selectorWrapper { + padding: var(--mantine-spacing-sm); + border-bottom: rem(1px) solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4)); + flex-shrink: 0; +} + +.selector { + width: 100%; + text-align: left; + background-color: light-dark(var(--mantine-color-white), var(--mantine-color-dark-6)); + padding: var(--mantine-spacing-xs) var(--mantine-spacing-sm); + cursor: pointer; + + &:hover { + background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-5)); + } +} + +.dropdown { + max-height: rem(300px); +} + +.option { + padding: var(--mantine-spacing-xs) var(--mantine-spacing-sm); + + &[data-combobox-selected] { + background-color: light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-5)); + } + + &:hover { + background-color: light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-5)); + } +} + +.editorArea { + flex: 1; + min-height: 0; +} + +.editorContent { + padding: var(--mantine-spacing-md); + padding-bottom: rem(60px); +} + +.actionButtons { + padding: var(--mantine-spacing-sm) var(--mantine-spacing-md); + padding-bottom: rem(70px); + border-top: rem(1px) solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4)); + background-color: light-dark(var(--mantine-color-white), var(--mantine-color-dark-7)); + flex-shrink: 0; +} + +.floatingBar { + position: fixed; + bottom: var(--mantine-spacing-md); + left: 50%; + transform: translateX(-50%); + z-index: 100; + background-color: light-dark(var(--mantine-color-white), var(--mantine-color-dark-6)); + white-space: nowrap; +} diff --git a/apps/client/src/features/page-history/components/history-modal-mobile.tsx b/apps/client/src/features/page-history/components/history-modal-mobile.tsx new file mode 100644 index 000000000..64e3bdfc9 --- /dev/null +++ b/apps/client/src/features/page-history/components/history-modal-mobile.tsx @@ -0,0 +1,307 @@ +import { + ActionIcon, + Box, + Button, + Combobox, + Group, + InputBase, + Paper, + ScrollArea, + Switch, + Text, + useCombobox, +} from "@mantine/core"; +import { useAtom } from "jotai"; +import { + activeHistoryIdAtom, + activeHistoryPrevIdAtom, + diffCountsAtom, + highlightChangesAtom, + historyAtoms, +} from "@/features/page-history/atoms/history-atoms"; +import HistoryView from "@/features/page-history/components/history-view"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { + IconChevronDown, + IconChevronUp, + IconSelector, +} from "@tabler/icons-react"; +import { useTranslation } from "react-i18next"; +import { + usePageHistoryListQuery, + usePageHistoryQuery, +} from "@/features/page-history/queries/page-history-query"; +import { formattedDate } from "@/lib/time"; +import { + pageEditorAtom, + titleEditorAtom, +} from "@/features/editor/atoms/editor-atoms"; +import { modals } from "@mantine/modals"; +import { notifications } from "@mantine/notifications"; +import { useSpaceAbility } from "@/features/space/permissions/use-space-ability"; +import { useSpaceQuery } from "@/features/space/queries/space-query"; +import { useParams } from "react-router-dom"; +import { + SpaceCaslAction, + SpaceCaslSubject, +} from "@/features/space/permissions/permissions.type"; +import classes from "./history-mobile.module.css"; + +interface Props { + pageId: string; + pageTitle?: string; +} + +export default function HistoryModalMobile({ pageId, pageTitle }: Props) { + const { t } = useTranslation(); + const combobox = useCombobox({ + onDropdownClose: () => combobox.resetSelectedOption(), + }); + + const [activeHistoryId, setActiveHistoryId] = useAtom(activeHistoryIdAtom); + const [, setActiveHistoryPrevId] = useAtom(activeHistoryPrevIdAtom); + const [highlightChanges, setHighlightChanges] = useAtom(highlightChangesAtom); + const [diffCounts, setDiffCounts] = useAtom(diffCountsAtom); + const [, setHistoryModalOpen] = useAtom(historyAtoms); + + const [currentChangeIndex, setCurrentChangeIndex] = useState(0); + const scrollViewportRef = useRef(null); + + const { data: pageHistoryData, isLoading } = usePageHistoryListQuery(pageId); + const { data: activeHistoryData } = usePageHistoryQuery(activeHistoryId); + + const historyItems = useMemo( + () => pageHistoryData?.pages.flatMap((page) => page.items) ?? [], + [pageHistoryData], + ); + + const [mainEditor] = useAtom(pageEditorAtom); + const [mainEditorTitle] = useAtom(titleEditorAtom); + + const { spaceSlug } = useParams(); + const { data: space } = useSpaceQuery(spaceSlug); + const spaceRules = space?.membership?.permissions; + const spaceAbility = useSpaceAbility(spaceRules); + + const canRestore = spaceAbility.can( + SpaceCaslAction.Manage, + SpaceCaslSubject.Page, + ); + + useEffect(() => { + setActiveHistoryId(""); + setActiveHistoryPrevId(""); + // @ts-ignore + setDiffCounts(null); + }, [pageId]); + + useEffect(() => { + if (historyItems.length > 0 && !activeHistoryId) { + setActiveHistoryId(historyItems[0].id); + setActiveHistoryPrevId(historyItems[1]?.id ?? ""); + } + }, [historyItems, activeHistoryId]); + + useEffect(() => { + if (diffCounts && diffCounts.total > 0) { + setCurrentChangeIndex(1); + requestAnimationFrame(() => scrollToChangeIndex(1)); + } else { + setCurrentChangeIndex(0); + } + }, [diffCounts]); + + const scrollToChangeIndex = (index: number) => { + const viewport = scrollViewportRef.current; + if (!viewport || index < 1) return; + const element = viewport.querySelector(`[data-diff-index="${index}"]`); + if (element instanceof HTMLElement) { + const elementTop = element.offsetTop; + const viewportHeight = viewport.clientHeight; + const scrollTarget = + elementTop - viewportHeight / 2 + element.offsetHeight / 2; + viewport.scrollTo({ top: scrollTarget, behavior: "smooth" }); + } + }; + + const handlePrevChange = () => { + if (!diffCounts || diffCounts.total === 0) return; + const newIndex = + currentChangeIndex <= 1 ? diffCounts.total : currentChangeIndex - 1; + setCurrentChangeIndex(newIndex); + scrollToChangeIndex(newIndex); + }; + + const handleNextChange = () => { + if (!diffCounts || diffCounts.total === 0) return; + const newIndex = + currentChangeIndex >= diffCounts.total ? 1 : currentChangeIndex + 1; + setCurrentChangeIndex(newIndex); + scrollToChangeIndex(newIndex); + }; + + const handleSelectVersion = useCallback( + (id: string) => { + const index = historyItems.findIndex((item) => item.id === id); + if (index >= 0) { + setActiveHistoryId(id); + setActiveHistoryPrevId(historyItems[index + 1]?.id ?? ""); + } + combobox.closeDropdown(); + }, + [historyItems, combobox], + ); + + const confirmRestore = () => + modals.openConfirmModal({ + title: t("Please confirm your action"), + children: ( + + {t( + "Are you sure you want to restore this version? Any changes not versioned will be lost.", + )} + + ), + labels: { confirm: t("Confirm"), cancel: t("Cancel") }, + onConfirm: handleRestore, + }); + + const handleRestore = useCallback(() => { + if (activeHistoryData) { + mainEditorTitle + .chain() + .clearContent() + .setContent(activeHistoryData.title, { emitUpdate: true }) + .run(); + mainEditor + .chain() + .clearContent() + .setContent(activeHistoryData.content) + .run(); + setHistoryModalOpen(false); + notifications.show({ message: t("Successfully restored") }); + } + }, [activeHistoryData, mainEditor, mainEditorTitle, setHistoryModalOpen, t]); + + const selectedItem = historyItems.find((item) => item.id === activeHistoryId); + + const options = historyItems.map((item) => ( + +
+ {formattedDate(new Date(item.createdAt))} + + {item.lastUpdatedBy?.name} + +
+
+ )); + + if (isLoading) { + return null; + } + + return ( + + + + + } + rightSectionPointerEvents="none" + onClick={() => combobox.toggleDropdown()} + className={classes.selector} + > + {selectedItem ? ( + + {formattedDate(new Date(selectedItem.createdAt))} + + ) : ( + + {t("Select version")} + + )} + + + + + + {options} + + + + + + + + {activeHistoryId && } + + + + {canRestore && ( + + + + + )} + + {activeHistoryId && ( + + + setHighlightChanges(e.currentTarget.checked)} + size="sm" + styles={{ label: { userSelect: "none", whiteSpace: "nowrap" } }} + /> + {highlightChanges && diffCounts && diffCounts.total > 0 && ( + + + {currentChangeIndex} of {diffCounts.total} + + + + + + + + + )} + + + )} + + ); +} diff --git a/apps/client/src/features/page-history/components/history-modal.tsx b/apps/client/src/features/page-history/components/history-modal.tsx index 2596ff68d..bcc4d20f5 100644 --- a/apps/client/src/features/page-history/components/history-modal.tsx +++ b/apps/client/src/features/page-history/components/history-modal.tsx @@ -2,37 +2,71 @@ import { Modal, Text } from "@mantine/core"; import { useAtom } from "jotai"; import { historyAtoms } from "@/features/page-history/atoms/history-atoms"; import HistoryModalBody from "@/features/page-history/components/history-modal-body"; +import HistoryModalMobile from "@/features/page-history/components/history-modal-mobile"; import { useTranslation } from "react-i18next"; +import { useMediaQuery } from "@mantine/hooks"; interface Props { pageId: string; + pageTitle?: string; } -export default function HistoryModal({ pageId }: Props) { + +export default function HistoryModal({ pageId, pageTitle }: Props) { const { t } = useTranslation(); const [isModalOpen, setModalOpen] = useAtom(historyAtoms); + const isMobile = useMediaQuery("(max-width: 800px)"); - return ( - <> + if (isMobile) { + return ( setModalOpen(false)} + fullScreen > - {t("Page history")} + {pageTitle ? ( + <> + {t("Version history for")} {pageTitle} {t("document")} + + ) : ( + t("Page history") + )} - - + + - + ); + } + + return ( + setModalOpen(false)} + > + + + + + + {t("Page history")} + + + + + + + + + ); }