diff --git a/web/src/components/MemoContent/EmbeddedContent/EmbeddedMemo.tsx b/web/src/components/MemoContent/EmbeddedContent/EmbeddedMemo.tsx index 9f520ad9..335ca4a8 100644 --- a/web/src/components/MemoContent/EmbeddedContent/EmbeddedMemo.tsx +++ b/web/src/components/MemoContent/EmbeddedContent/EmbeddedMemo.tsx @@ -75,7 +75,7 @@ const EmbeddedMemo = ({ resourceId: uid, params: paramsStr }: Props) => { copyMemoUid(memo.uid)}> {memo.uid.slice(0, 6)} - + diff --git a/web/src/components/MemoContent/ReferencedContent/ReferencedMemo.tsx b/web/src/components/MemoContent/ReferencedContent/ReferencedMemo.tsx index 8534ebdb..4bd6ba6c 100644 --- a/web/src/components/MemoContent/ReferencedContent/ReferencedMemo.tsx +++ b/web/src/components/MemoContent/ReferencedContent/ReferencedMemo.tsx @@ -1,7 +1,8 @@ -import { useEffect } from "react"; +import { useContext, useEffect } from "react"; import useLoading from "@/hooks/useLoading"; import useNavigateTo from "@/hooks/useNavigateTo"; import { useMemoStore } from "@/store/v1"; +import { RendererContext } from "../types"; import Error from "./Error"; interface Props { @@ -15,6 +16,7 @@ const ReferencedMemo = ({ resourceId: uid, params: paramsStr }: Props) => { const memoStore = useMemoStore(); const memo = memoStore.getMemoByUid(uid); const params = new URLSearchParams(paramsStr); + const context = useContext(RendererContext); useEffect(() => { memoStore.fetchMemoByUid(uid).finally(() => loadingState.setFinish()); @@ -31,7 +33,11 @@ const ReferencedMemo = ({ resourceId: uid, params: paramsStr }: Props) => { const displayContent = paramsText || (memo.snippet.length > 12 ? `${memo.snippet.slice(0, 12)}...` : memo.snippet); const handleGotoMemoDetailPage = () => { - navigateTo(`/m/${memo.uid}`); + navigateTo(`/m/${memo.uid}`, { + state: { + from: context.parentPage, + }, + }); }; return ( diff --git a/web/src/components/MemoContent/Tag.tsx b/web/src/components/MemoContent/Tag.tsx index ace025ca..9ba8b952 100644 --- a/web/src/components/MemoContent/Tag.tsx +++ b/web/src/components/MemoContent/Tag.tsx @@ -1,6 +1,9 @@ import clsx from "clsx"; import { useContext } from "react"; -import { useMemoFilterStore } from "@/store/v1"; +import { useLocation } from "react-router-dom"; +import useNavigateTo from "@/hooks/useNavigateTo"; +import { Routes } from "@/router"; +import { stringifyFilters, useMemoFilterStore } from "@/store/v1"; import { RendererContext } from "./types"; interface Props { @@ -10,12 +13,24 @@ interface Props { const Tag: React.FC = ({ content }: Props) => { const context = useContext(RendererContext); const memoFilterStore = useMemoFilterStore(); + const location = useLocation(); + const navigateTo = useNavigateTo(); const handleTagClick = () => { if (context.disableFilter) { return; } + // If the tag is clicked in a memo detail page, we should navigate to the memo list page. + if (location.pathname.startsWith("/m")) { + const pathname = context.parentPage || Routes.ROOT; + const searchParams = new URLSearchParams(); + + searchParams.set("filter", stringifyFilters([{ factor: "tagSearch", value: content }])); + navigateTo(`${pathname}?${searchParams.toString()}`); + return; + } + const isActive = memoFilterStore.getFiltersByFactor("tagSearch").some((filter) => filter.value === content); if (isActive) { memoFilterStore.removeFilter((f) => f.factor === "tagSearch" && f.value === content); diff --git a/web/src/components/MemoContent/index.tsx b/web/src/components/MemoContent/index.tsx index a8c93bf2..bc893be8 100644 --- a/web/src/components/MemoContent/index.tsx +++ b/web/src/components/MemoContent/index.tsx @@ -24,6 +24,7 @@ interface Props { contentClassName?: string; onClick?: (e: React.MouseEvent) => void; onDoubleClick?: (e: React.MouseEvent) => void; + parentPage?: string; } type ContentCompactView = "ALL" | "SNIPPET"; @@ -79,6 +80,7 @@ const MemoContent: React.FC = (props: Props) => { readonly: !allowEdit, disableFilter: props.disableFilter, embeddedMemos: embeddedMemos || new Set(), + parentPage: props.parentPage, }} >
diff --git a/web/src/components/MemoContent/types/context.ts b/web/src/components/MemoContent/types/context.ts index e54d5e6a..13e59b46 100644 --- a/web/src/components/MemoContent/types/context.ts +++ b/web/src/components/MemoContent/types/context.ts @@ -9,6 +9,7 @@ interface Context { memoName?: string; readonly?: boolean; disableFilter?: boolean; + parentPage?: string; } export const RendererContext = createContext({ diff --git a/web/src/components/MemoDetailSidebar/MemoDetailSidebar.tsx b/web/src/components/MemoDetailSidebar/MemoDetailSidebar.tsx index 62b9bea9..4377eeae 100644 --- a/web/src/components/MemoDetailSidebar/MemoDetailSidebar.tsx +++ b/web/src/components/MemoDetailSidebar/MemoDetailSidebar.tsx @@ -9,9 +9,10 @@ import MemoRelationForceGraph from "../MemoRelationForceGraph"; interface Props { memo: Memo; className?: string; + parentPage?: string; } -const MemoDetailSidebar = ({ memo, className }: Props) => { +const MemoDetailSidebar = ({ memo, className, parentPage }: Props) => { const t = useTranslate(); const property = MemoProperty.fromPartial(memo.property || {}); const hasSpecialProperty = property.hasLink || property.hasTaskList || property.hasCode || property.hasIncompleteTasks; @@ -27,7 +28,7 @@ const MemoDetailSidebar = ({ memo, className }: Props) => {
{shouldShowRelationGraph && (
- +
Relations (Beta) diff --git a/web/src/components/MemoDetailSidebar/MemoDetailSidebarDrawer.tsx b/web/src/components/MemoDetailSidebar/MemoDetailSidebarDrawer.tsx index 22fb9cbc..d9712b7e 100644 --- a/web/src/components/MemoDetailSidebar/MemoDetailSidebarDrawer.tsx +++ b/web/src/components/MemoDetailSidebar/MemoDetailSidebarDrawer.tsx @@ -8,9 +8,10 @@ import MemoDetailSidebar from "./MemoDetailSidebar"; interface Props { memo: Memo; + parentPage?: string; } -const MemoDetailSidebarDrawer = ({ memo }: Props) => { +const MemoDetailSidebarDrawer = ({ memo, parentPage }: Props) => { const location = useLocation(); const [open, setOpen] = useState(false); @@ -32,7 +33,7 @@ const MemoDetailSidebarDrawer = ({ memo }: Props) => {
- +
diff --git a/web/src/components/MemoFilters.tsx b/web/src/components/MemoFilters.tsx index 7c028705..4527599f 100644 --- a/web/src/components/MemoFilters.tsx +++ b/web/src/components/MemoFilters.tsx @@ -1,47 +1,61 @@ import { isEqual } from "lodash-es"; import { CalendarIcon, CheckCircleIcon, CodeIcon, EyeIcon, FilterIcon, LinkIcon, SearchIcon, TagIcon, XIcon } from "lucide-react"; -import { useEffect } from "react"; +import { useEffect, useRef } from "react"; import { useSearchParams } from "react-router-dom"; -import usePrevious from "react-use/lib/usePrevious"; import { FilterFactor, getMemoFilterKey, MemoFilter, parseFilterQuery, stringifyFilters, useMemoFilterStore } from "@/store/v1"; const MemoFilters = () => { const [searchParams, setSearchParams] = useSearchParams(); const memoFilterStore = useMemoFilterStore(); const filters = memoFilterStore.filters; - const prevFilters = usePrevious(filters); const orderByTimeAsc = memoFilterStore.orderByTimeAsc; - const prevOrderByTimeAsc = usePrevious(orderByTimeAsc); + const lastUpdateRef = useRef<"url" | "store">("url"); - // Sync the filters and orderByTimeAsc to the search params. + // set lastUpdateRef to store when filters or orderByTimeAsc changes useEffect(() => { - const newSearchParams = new URLSearchParams(searchParams); + lastUpdateRef.current = "store"; + }, [filters, orderByTimeAsc]); - if (prevOrderByTimeAsc !== orderByTimeAsc) { - if (orderByTimeAsc) { - newSearchParams.set("orderBy", "asc"); - } else { - newSearchParams.delete("orderBy"); - } - } + // set lastUpdateRef to url when searchParams changes + useEffect(() => { + lastUpdateRef.current = "url"; + }, [searchParams]); + + const checkAndSync = () => { + const filtersInURL = searchParams.get("filter") || ""; + const orderByTimeAscInURL = searchParams.get("orderBy") === "asc"; + const storeMatchesURL = filtersInURL === stringifyFilters(filters) && orderByTimeAscInURL === orderByTimeAsc; + + if (!storeMatchesURL) { + if (lastUpdateRef.current === "url") { + // Sync URL -> Store + memoFilterStore.setState({ + filters: parseFilterQuery(filtersInURL), + orderByTimeAsc: orderByTimeAscInURL, + }); + } else if (lastUpdateRef.current === "store") { + // Sync Store -> URL + const newSearchParams = new URLSearchParams(searchParams); - if (prevFilters && stringifyFilters(prevFilters) !== stringifyFilters(filters)) { - if (filters.length > 0) { - newSearchParams.set("filter", stringifyFilters(filters)); - } else { - newSearchParams.delete("filter"); + if (orderByTimeAsc) { + newSearchParams.set("orderBy", "asc"); + } else { + newSearchParams.delete("orderBy"); + } + + if (filters.length > 0) { + newSearchParams.set("filter", stringifyFilters(filters)); + } else { + newSearchParams.delete("filter"); + } + + setSearchParams(newSearchParams); } } + }; - setSearchParams(newSearchParams); - }, [prevOrderByTimeAsc, orderByTimeAsc, prevFilters, filters, searchParams]); - - // Sync the search params to the filters and orderByTimeAsc when the component is mounted. - useEffect(() => { - const newFilters = parseFilterQuery(searchParams.get("filter")); - const newOrderByTimeAsc = searchParams.get("orderBy") === "asc"; - memoFilterStore.setState({ filters: newFilters, orderByTimeAsc: newOrderByTimeAsc }); - }, []); + // Watch both URL and store changes + useEffect(checkAndSync, [searchParams, filters, orderByTimeAsc]); const getFilterDisplayText = (filter: MemoFilter) => { if (filter.value) { diff --git a/web/src/components/MemoRelationForceGraph/MemoRelationForceGraph.tsx b/web/src/components/MemoRelationForceGraph/MemoRelationForceGraph.tsx index 695cd622..5a73824a 100644 --- a/web/src/components/MemoRelationForceGraph/MemoRelationForceGraph.tsx +++ b/web/src/components/MemoRelationForceGraph/MemoRelationForceGraph.tsx @@ -11,12 +11,13 @@ import { convertMemoRelationsToGraphData } from "./utils"; interface Props { memo: Memo; className?: string; + parentPage?: string; } const MAIN_NODE_COLOR = "#14b8a6"; const DEFAULT_NODE_COLOR = "#a1a1aa"; -const MemoRelationForceGraph = ({ className, memo }: Props) => { +const MemoRelationForceGraph = ({ className, memo, parentPage }: Props) => { const navigateTo = useNavigateTo(); const { mode } = useColorScheme(); const containerRef = useRef(null); @@ -30,7 +31,11 @@ const MemoRelationForceGraph = ({ className, memo }: Props) => { const onNodeClick = (node: NodeObject) => { if (node.memo.uid === memo.uid) return; - navigateTo(`/m/${node.memo.uid}`); + navigateTo(`/m/${node.memo.uid}`, { + state: { + from: parentPage, + }, + }); }; return ( diff --git a/web/src/components/MemoRelationListView.tsx b/web/src/components/MemoRelationListView.tsx index 2d1d3ba4..2b56c6c7 100644 --- a/web/src/components/MemoRelationListView.tsx +++ b/web/src/components/MemoRelationListView.tsx @@ -8,10 +8,11 @@ import { Memo } from "@/types/proto/api/v1/memo_service"; interface Props { memo: Memo; relations: MemoRelation[]; + parentPage?: string; } const MemoRelationListView = (props: Props) => { - const { memo, relations: relationList } = props; + const { memo, relations: relationList, parentPage } = props; const referencingMemoList = relationList .filter((relation) => relation.memo?.name === memo.name && relation.relatedMemo?.name !== memo.name) .map((relation) => relation.relatedMemo!); @@ -65,6 +66,9 @@ const MemoRelationListView = (props: Props) => { className="w-auto max-w-full flex flex-row justify-start items-center text-sm leading-5 text-gray-600 dark:text-gray-400 dark:border-zinc-700 dark:bg-zinc-900 hover:underline" to={`/m/${memo.uid}`} viewTransition + state={{ + from: parentPage, + }} > {memo.uid.slice(0, 6)} @@ -84,6 +88,9 @@ const MemoRelationListView = (props: Props) => { className="w-auto max-w-full flex flex-row justify-start items-center text-sm leading-5 text-gray-600 dark:text-gray-400 dark:border-zinc-700 dark:bg-zinc-900 hover:underline" to={`/m/${memo.uid}`} viewTransition + state={{ + from: parentPage, + }} > {memo.uid.slice(0, 6)} diff --git a/web/src/components/MemoView.tsx b/web/src/components/MemoView.tsx index 840e097b..88d23759 100644 --- a/web/src/components/MemoView.tsx +++ b/web/src/components/MemoView.tsx @@ -35,6 +35,7 @@ interface Props { showVisibility?: boolean; showPinned?: boolean; className?: string; + parentPage?: string; } const MemoView: React.FC = (props: Props) => { @@ -60,6 +61,7 @@ const MemoView: React.FC = (props: Props) => { const relativeTimeFormat = Date.now() - memo.displayTime!.getTime() > 1000 * 60 * 60 * 24 ? "datetime" : "auto"; const readonly = memo.creator !== user?.name && !isSuperUser(user); const isInMemoDetailPage = location.pathname.startsWith(`/m/${memo.uid}`); + const parentPage = props.parentPage || location.pathname; // Initial related data: creator. useAsyncEffect(async () => { @@ -68,8 +70,12 @@ const MemoView: React.FC = (props: Props) => { }, []); const handleGotoMemoDetailPage = useCallback(() => { - navigateTo(`/m/${memo.uid}`); - }, [memo.uid]); + navigateTo(`/m/${memo.uid}`, { + state: { + from: parentPage, + }, + }); + }, [memo.uid, parentPage]); const handleMemoContentClick = useCallback(async (e: React.MouseEvent) => { const targetEl = e.target as HTMLElement; @@ -217,6 +223,9 @@ const MemoView: React.FC = (props: Props) => { )} to={`/m/${memo.uid}#comments`} viewTransition + state={{ + from: parentPage, + }} > {commentAmount > 0 && {commentAmount}} @@ -242,10 +251,11 @@ const MemoView: React.FC = (props: Props) => { onClick={handleMemoContentClick} onDoubleClick={handleMemoContentDoubleClick} compact={props.compact && workspaceMemoRelatedSetting.enableAutoCompact} + parentPage={parentPage} /> {memo.location && } - + )} diff --git a/web/src/hooks/useNavigateTo.ts b/web/src/hooks/useNavigateTo.ts index 62e844fd..ad743cd6 100644 --- a/web/src/hooks/useNavigateTo.ts +++ b/web/src/hooks/useNavigateTo.ts @@ -1,15 +1,15 @@ -import { useNavigate } from "react-router-dom"; +import { NavigateOptions, useNavigate } from "react-router-dom"; const useNavigateTo = () => { const navigateTo = useNavigate(); - const navigateToWithViewTransition = (to: string) => { + const navigateToWithViewTransition = (to: string, options?: NavigateOptions) => { const document = window.document as any; if (!document.startViewTransition) { - navigateTo(to); + navigateTo(to, options); } else { document.startViewTransition(() => { - navigateTo(to); + navigateTo(to, options); }); } }; diff --git a/web/src/pages/MemoDetail.tsx b/web/src/pages/MemoDetail.tsx index 6766e2f6..3b006ec9 100644 --- a/web/src/pages/MemoDetail.tsx +++ b/web/src/pages/MemoDetail.tsx @@ -4,7 +4,7 @@ import { ArrowUpLeftFromCircleIcon, MessageCircleIcon } from "lucide-react"; import { ClientError } from "nice-grpc-web"; import { useEffect, useState } from "react"; import { toast } from "react-hot-toast"; -import { Link, useParams } from "react-router-dom"; +import { Link, useLocation, useParams } from "react-router-dom"; import { MemoDetailSidebar, MemoDetailSidebarDrawer } from "@/components/MemoDetailSidebar"; import MemoEditor from "@/components/MemoEditor"; import MemoView from "@/components/MemoView"; @@ -23,6 +23,7 @@ const MemoDetail = () => { const { md } = useResponsiveWidth(); const params = useParams(); const navigateTo = useNavigateTo(); + const { state: locationState } = useLocation(); const workspaceSettingStore = useWorkspaceSettingStore(); const currentUser = useCurrentUser(); const memoStore = useMemoStore(); @@ -86,7 +87,7 @@ const MemoDetail = () => {
{!md && ( - + )}
@@ -96,6 +97,7 @@ const MemoDetail = () => { @@ -108,6 +110,7 @@ const MemoDetail = () => { className="shadow hover:shadow-md transition-all" memo={memo} compact={false} + parentPage={locationState?.from} showCreator showVisibility showPinned @@ -141,7 +144,13 @@ const MemoDetail = () => { )}
{comments.map((comment) => ( - + ))} )} @@ -162,7 +171,7 @@ const MemoDetail = () => {
{md && (
- +
)}