refactor(web): drop the memo origin scope
The "all" / "preserve" origin scope existed to decide whether returning
from a memo detail should clear the remembered Space. With the Space now
part of the origin URL itself, nothing branches on it any more; it was only
computed, serialized into router state as fromScope, and threaded through
props as parentScope.
Router state is now just { from }, createMemoNavigationState takes one
argument, and resolveMemoDetailOrigin / resolveMemoParentPage return the
origin page as a string. The archived-memo fallback to /archived stays.
This commit is contained in:
parent
af008b2153
commit
9da3765b19
29 changed files with 63 additions and 226 deletions
|
|
@ -270,7 +270,6 @@ const MemoDetailSidebarContent = () => {
|
|||
memo={memoDetail.memo}
|
||||
parentMemo={memoDetail.parentMemo}
|
||||
parentPage={memoDetail.from}
|
||||
parentScope={memoDetail.fromScope}
|
||||
hasExplicitOrigin={memoDetail.hasExplicitOrigin}
|
||||
commentCount={memoDetail.commentCount}
|
||||
forceReadonly={memoDetail.readonly}
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ export const AttachmentSourceChip = ({
|
|||
return (
|
||||
<Link
|
||||
to={`/${memoName}`}
|
||||
state={createMemoNavigationState(`${location.pathname}${location.search}`, "preserve")}
|
||||
state={createMemoNavigationState(`${location.pathname}${location.search}`)}
|
||||
className="inline-flex max-w-full items-center truncate rounded-full border border-border/60 bg-muted/30 px-1.5 py-0.5 text-[11px] text-muted-foreground hover:bg-muted/50"
|
||||
>
|
||||
<span className="truncate">{t("attachment-library.labels.memo")}</span>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import { type ComponentType, forwardRef, useCallback, useImperativeHandle, useRe
|
|||
import { loadMemoEditor } from "@/components/MemoEditor/loader";
|
||||
import type { MemoEditorProps } from "@/components/MemoEditor/types";
|
||||
import MemoView from "@/components/MemoView";
|
||||
import type { MemoOriginScope } from "@/components/MemoView/navigation";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import useCurrentUser from "@/hooks/useCurrentUser";
|
||||
import { MEMO_COMMENTS_ANCHOR_ID } from "@/lib/memo-comments";
|
||||
|
|
@ -17,7 +16,6 @@ interface Props {
|
|||
comments: Memo[];
|
||||
commentCount?: number;
|
||||
parentPage?: string;
|
||||
parentScope?: MemoOriginScope;
|
||||
hasMoreComments?: boolean;
|
||||
isFetchingMoreComments?: boolean;
|
||||
onLoadMoreComments?: () => void;
|
||||
|
|
@ -29,19 +27,7 @@ export interface MemoCommentSectionHandle {
|
|||
}
|
||||
|
||||
const MemoCommentSection = forwardRef<MemoCommentSectionHandle, Props>(
|
||||
(
|
||||
{
|
||||
memo,
|
||||
comments,
|
||||
commentCount = comments.length,
|
||||
parentPage,
|
||||
parentScope,
|
||||
hasMoreComments,
|
||||
isFetchingMoreComments,
|
||||
onLoadMoreComments,
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
({ memo, comments, commentCount = comments.length, parentPage, hasMoreComments, isFetchingMoreComments, onLoadMoreComments }, ref) => {
|
||||
const t = useTranslate();
|
||||
const currentUser = useCurrentUser();
|
||||
const [showEditor, setShowEditor] = useState(false);
|
||||
|
|
@ -154,7 +140,7 @@ const MemoCommentSection = forwardRef<MemoCommentSectionHandle, Props>(
|
|||
)}
|
||||
{comments.map((comment) => (
|
||||
<div className="w-full" key={comment.name} id={extractMemoIdFromName(comment.name)}>
|
||||
<MemoView memo={comment} parentPage={parentPage} parentScope={parentScope} showCreator showSpace compact />
|
||||
<MemoView memo={comment} parentPage={parentPage} showCreator showSpace compact />
|
||||
</div>
|
||||
))}
|
||||
{hasMoreComments && (
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import { isMentionElement, isTagElement, isTaskListItemElement } from "@/types/m
|
|||
import type { Attachment } from "@/types/proto/api/v1/attachment_service_pb";
|
||||
import { lazyWithReload } from "@/utils/lazy";
|
||||
import { resolveManagedAttachmentImageSource } from "@/utils/managed-attachment";
|
||||
import type { MemoOriginScope } from "../MemoView/navigation";
|
||||
import { CodeBlock } from "./CodeBlock";
|
||||
import { MarkdownRenderContext, rootMarkdownRenderContext } from "./MarkdownRenderContext";
|
||||
import { Mention } from "./Mention";
|
||||
|
|
@ -26,7 +25,6 @@ export interface MemoMarkdownRendererProps {
|
|||
memoName?: string;
|
||||
/** Collection page that opened the memo detail. */
|
||||
parentPage?: string;
|
||||
parentScope?: MemoOriginScope;
|
||||
/** Whether the memo is rendered as a collapsed feed card. */
|
||||
compact?: boolean;
|
||||
}
|
||||
|
|
@ -67,7 +65,6 @@ export const MemoMarkdownRendererCore = ({
|
|||
resolvedMentionUsernames,
|
||||
memoName,
|
||||
parentPage,
|
||||
parentScope,
|
||||
compact,
|
||||
mathRemarkPlugins = [],
|
||||
mathRehypePlugins = [],
|
||||
|
|
@ -134,7 +131,7 @@ export const MemoMarkdownRendererCore = ({
|
|||
// than opening a new tab; everything else is treated as an external link.
|
||||
if (typeof href === "string" && href.startsWith("#")) {
|
||||
return (
|
||||
<AnchorLink href={href} memoName={memoName} parentPage={parentPage} parentScope={parentScope} compact={compact} {...props}>
|
||||
<AnchorLink href={href} memoName={memoName} parentPage={parentPage} compact={compact} {...props}>
|
||||
{children}
|
||||
</AnchorLink>
|
||||
);
|
||||
|
|
@ -195,7 +192,6 @@ export const MemoMarkdownRenderer = memo(
|
|||
previous.attachments === next.attachments &&
|
||||
previous.memoName === next.memoName &&
|
||||
previous.parentPage === next.parentPage &&
|
||||
previous.parentScope === next.parentScope &&
|
||||
previous.compact === next.compact &&
|
||||
haveEqualResolvedMentions(previous.resolvedMentionUsernames, next.resolvedMentionUsernames),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -40,7 +40,6 @@ const MemoContent = (props: MemoContentProps) => {
|
|||
resolvedMentionUsernames={resolvedMentionUsernames}
|
||||
memoName={props.memoName}
|
||||
parentPage={props.parentPage}
|
||||
parentScope={props.parentScope}
|
||||
compact={Boolean(props.compact)}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { Link } from "react-router-dom";
|
|||
import { markdownStyles } from "@/lib/markdownStyles";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { findAnchorTarget } from "@/utils/markdown-manipulation";
|
||||
import { createMemoNavigationState, type MemoOriginScope } from "../../MemoView/navigation";
|
||||
import { createMemoNavigationState } from "../../MemoView/navigation";
|
||||
import type { ReactMarkdownProps } from "./types";
|
||||
|
||||
interface AnchorLinkProps extends React.AnchorHTMLAttributes<HTMLAnchorElement>, ReactMarkdownProps {
|
||||
|
|
@ -11,7 +11,6 @@ interface AnchorLinkProps extends React.AnchorHTMLAttributes<HTMLAnchorElement>,
|
|||
memoName?: string;
|
||||
/** Collection page that rendered the enclosing memo. */
|
||||
parentPage?: string;
|
||||
parentScope?: MemoOriginScope;
|
||||
/** Whether the memo is rendered as a collapsed feed card. */
|
||||
compact?: boolean;
|
||||
children: React.ReactNode;
|
||||
|
|
@ -26,17 +25,7 @@ interface AnchorLinkProps extends React.AnchorHTMLAttributes<HTMLAnchorElement>,
|
|||
* below the fold, so we fall back to navigating to the memo detail page (with the hash), where
|
||||
* MemoDetail scrolls the target into view.
|
||||
*/
|
||||
export const AnchorLink = ({
|
||||
href,
|
||||
memoName,
|
||||
parentPage,
|
||||
parentScope,
|
||||
compact,
|
||||
children,
|
||||
className,
|
||||
node: _node,
|
||||
...props
|
||||
}: AnchorLinkProps) => {
|
||||
export const AnchorLink = ({ href, memoName, parentPage, compact, children, className, node: _node, ...props }: AnchorLinkProps) => {
|
||||
const handleClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
|
||||
if (compact) return; // Let the link navigate to the detail page.
|
||||
const id = decodeURIComponent(href.slice(1));
|
||||
|
|
@ -57,7 +46,7 @@ export const AnchorLink = ({
|
|||
return (
|
||||
<Link
|
||||
to={`/${memoName}${href}`}
|
||||
state={parentPage && parentScope ? createMemoNavigationState(parentPage, parentScope) : undefined}
|
||||
state={parentPage ? createMemoNavigationState(parentPage) : undefined}
|
||||
onClick={handleClick}
|
||||
className={classes}
|
||||
{...props}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import type React from "react";
|
||||
import type { Attachment } from "@/types/proto/api/v1/attachment_service_pb";
|
||||
import type { MemoOriginScope } from "../MemoView/navigation";
|
||||
|
||||
export interface MemoContentProps {
|
||||
content: string;
|
||||
|
|
@ -10,7 +9,6 @@ export interface MemoContentProps {
|
|||
memoName?: string;
|
||||
/** Collection page that rendered the memo, preserved when an inline link opens its detail page. */
|
||||
parentPage?: string;
|
||||
parentScope?: MemoOriginScope;
|
||||
/** The card renders collapsed (ClampedSection), so footnote links navigate instead of scrolling. */
|
||||
compact?: boolean;
|
||||
className?: string;
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ import SidebarSection, { SIDEBAR_SECTION_STACK_CLASSES } from "@/components/AppS
|
|||
import { extractHeadings } from "@/components/MemoContent/pipeline";
|
||||
import { getRelationBuckets, getRelationMemo } from "@/components/MemoMetadata/Relation/relationHelpers";
|
||||
import { useResolvedRelationMemos } from "@/components/MemoMetadata/Relation/useResolvedRelationMemos";
|
||||
import { createMemoNavigationState, type MemoOriginScope } from "@/components/MemoView/navigation";
|
||||
import { createMemoNavigationState } from "@/components/MemoView/navigation";
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
|
||||
import { useInstance } from "@/contexts/InstanceContext";
|
||||
import { useOverflowTitle } from "@/hooks";
|
||||
|
|
@ -37,7 +37,6 @@ interface Props {
|
|||
memo: Memo;
|
||||
parentMemo?: Memo;
|
||||
parentPage?: string;
|
||||
parentScope?: MemoOriginScope;
|
||||
hasExplicitOrigin?: boolean;
|
||||
commentCount?: number;
|
||||
className?: string;
|
||||
|
|
@ -54,13 +53,11 @@ const BacklinkRow = ({
|
|||
relation,
|
||||
snippet,
|
||||
parentPage,
|
||||
parentScope,
|
||||
referencedByLabel,
|
||||
}: {
|
||||
relation: MemoRelation;
|
||||
snippet: string;
|
||||
parentPage?: string;
|
||||
parentScope?: MemoOriginScope;
|
||||
referencedByLabel: string;
|
||||
}) => {
|
||||
const { ref, title } = useOverflowTitle<HTMLSpanElement>(snippet);
|
||||
|
|
@ -74,7 +71,7 @@ const BacklinkRow = ({
|
|||
aria-label={`${referencedByLabel}: ${snippet}`}
|
||||
className={cn(SIDEBAR_ROW_CLASSES, "text-muted-foreground hover:bg-sidebar-accent/65 hover:text-foreground")}
|
||||
to={`/${relatedMemo.name}`}
|
||||
state={parentPage && parentScope ? createMemoNavigationState(parentPage, parentScope) : undefined}
|
||||
state={parentPage ? createMemoNavigationState(parentPage) : undefined}
|
||||
title={title}
|
||||
viewTransition
|
||||
>
|
||||
|
|
@ -90,7 +87,6 @@ const MemoDetailSidebar = ({
|
|||
memo,
|
||||
parentMemo,
|
||||
parentPage,
|
||||
parentScope,
|
||||
hasExplicitOrigin = false,
|
||||
commentCount,
|
||||
className,
|
||||
|
|
@ -219,7 +215,7 @@ const MemoDetailSidebar = ({
|
|||
aria-label={`${t("memo.parent-memo")}: ${parentSnippet}`}
|
||||
className={cn(SIDEBAR_ROW_CLASSES, "text-muted-foreground hover:bg-sidebar-accent/65 hover:text-foreground")}
|
||||
to={`/${parentMemo.name}`}
|
||||
state={parentPage && parentScope ? createMemoNavigationState(parentPage, parentScope) : undefined}
|
||||
state={parentPage ? createMemoNavigationState(parentPage) : undefined}
|
||||
title={parentSnippet}
|
||||
viewTransition
|
||||
>
|
||||
|
|
@ -235,7 +231,6 @@ const MemoDetailSidebar = ({
|
|||
relation={relation}
|
||||
snippet={backlinkSnippet(relation)}
|
||||
parentPage={parentPage}
|
||||
parentScope={parentScope}
|
||||
referencedByLabel={t("common.referenced-by")}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,21 +1,20 @@
|
|||
import MemoSnippetLink from "@/components/MemoView/components/MemoSnippetLink";
|
||||
import { createMemoNavigationState, type MemoOriginScope } from "@/components/MemoView/navigation";
|
||||
import { createMemoNavigationState } from "@/components/MemoView/navigation";
|
||||
import type { MemoRelation_Memo } from "@/types/proto/api/v1/memo_service_pb";
|
||||
|
||||
interface RelationCardProps {
|
||||
memo: MemoRelation_Memo;
|
||||
parentPage?: string;
|
||||
parentScope?: MemoOriginScope;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const RelationCard = ({ memo, parentPage, parentScope, className }: RelationCardProps) => {
|
||||
const RelationCard = ({ memo, parentPage, className }: RelationCardProps) => {
|
||||
return (
|
||||
<MemoSnippetLink
|
||||
name={memo.name}
|
||||
snippet={memo.snippet}
|
||||
to={`/${memo.name}`}
|
||||
state={parentPage && parentScope ? createMemoNavigationState(parentPage, parentScope) : undefined}
|
||||
state={parentPage ? createMemoNavigationState(parentPage) : undefined}
|
||||
className={className}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { LinkIcon, MilestoneIcon } from "lucide-react";
|
||||
import { useMemo, useState } from "react";
|
||||
import MetadataSection from "@/components/MemoMetadata/MetadataSection";
|
||||
import type { MemoOriginScope } from "@/components/MemoView/navigation";
|
||||
import { useNearViewport } from "@/hooks/useNearViewport";
|
||||
import type { MemoRelation } from "@/types/proto/api/v1/memo_service_pb";
|
||||
import { useTranslate } from "@/utils/i18n";
|
||||
|
|
@ -13,11 +12,10 @@ interface RelationListViewProps {
|
|||
relations: MemoRelation[];
|
||||
currentMemoName?: string;
|
||||
parentPage?: string;
|
||||
parentScope?: MemoOriginScope;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function RelationListView({ relations, currentMemoName, parentPage, parentScope, className }: RelationListViewProps) {
|
||||
function RelationListView({ relations, currentMemoName, parentPage, className }: RelationListViewProps) {
|
||||
const t = useTranslate();
|
||||
const [activeTab, setActiveTab] = useState<"referencing" | "referenced">("referencing");
|
||||
const { ref: viewportRef, isNearViewport } = useNearViewport<HTMLDivElement>();
|
||||
|
|
@ -81,12 +79,7 @@ function RelationListView({ relations, currentMemoName, parentPage, parentScope,
|
|||
return null;
|
||||
}
|
||||
return (
|
||||
<RelationCard
|
||||
key={getRelationMemoName(relation, direction)}
|
||||
memo={resolvedMemos[memo.name] ?? memo}
|
||||
parentPage={parentPage}
|
||||
parentScope={parentScope}
|
||||
/>
|
||||
<RelationCard key={getRelationMemoName(relation, direction)} memo={resolvedMemos[memo.name] ?? memo} parentPage={parentPage} />
|
||||
);
|
||||
})}
|
||||
</MetadataSection>
|
||||
|
|
|
|||
|
|
@ -29,7 +29,6 @@ const STUB_CONTEXT: MemoViewContextValue = {
|
|||
creator: undefined,
|
||||
currentUser: undefined,
|
||||
parentPage: "/",
|
||||
parentScope: "all",
|
||||
cardWidth: 0,
|
||||
isArchived: false,
|
||||
readonly: true,
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ import { MemoBody, MemoCommentListView, MemoHeader } from "./components";
|
|||
import { MEMO_CARD_BASE_CLASSES } from "./constants";
|
||||
import { useImagePreview } from "./hooks";
|
||||
import { computeCommentAmount, MemoViewContext } from "./MemoViewContext";
|
||||
import { isMemoDetailPath, resolveMemoOrigin } from "./navigation";
|
||||
import { isMemoDetailPath, resolveMemoParentPage } from "./navigation";
|
||||
import type { MemoViewHandle, MemoViewProps } from "./types";
|
||||
|
||||
const MemoShareImageDialog = lazyWithReload(() => import("../MemoActionMenu/MemoShareImageDialog"));
|
||||
|
|
@ -36,7 +36,6 @@ const MemoView = forwardRef<MemoViewHandle, MemoViewProps>((props, ref) => {
|
|||
memo: memoData,
|
||||
className,
|
||||
parentPage: parentPageProp,
|
||||
parentScope: parentScopeProp,
|
||||
compact,
|
||||
timeDisplay,
|
||||
showCreator,
|
||||
|
|
@ -55,9 +54,8 @@ const MemoView = forwardRef<MemoViewHandle, MemoViewProps>((props, ref) => {
|
|||
const isArchived = memoData.state === State.ARCHIVED;
|
||||
const readonly = memoData.creator !== currentUser?.name && !isSuperUser(currentUser);
|
||||
const location = useLocation();
|
||||
const { parentPage, parentScope } = resolveMemoOrigin({
|
||||
const parentPage = resolveMemoParentPage({
|
||||
explicitParentPage: parentPageProp,
|
||||
explicitParentScope: parentScopeProp,
|
||||
pathname: location.pathname,
|
||||
search: location.search,
|
||||
memoName: memoData.name,
|
||||
|
|
@ -135,7 +133,6 @@ const MemoView = forwardRef<MemoViewHandle, MemoViewProps>((props, ref) => {
|
|||
creator,
|
||||
currentUser,
|
||||
parentPage,
|
||||
parentScope,
|
||||
cardWidth,
|
||||
isArchived,
|
||||
readonly,
|
||||
|
|
@ -150,7 +147,6 @@ const MemoView = forwardRef<MemoViewHandle, MemoViewProps>((props, ref) => {
|
|||
creator,
|
||||
currentUser,
|
||||
parentPage,
|
||||
parentScope,
|
||||
cardWidth,
|
||||
isArchived,
|
||||
readonly,
|
||||
|
|
|
|||
|
|
@ -7,14 +7,13 @@ import { MemoRelation_Type } from "@/types/proto/api/v1/memo_service_pb";
|
|||
import type { User } from "@/types/proto/api/v1/user_service_pb";
|
||||
import type { PreviewMediaItem } from "@/utils/media-item";
|
||||
import { RELATIVE_TIME_THRESHOLD_MS } from "./constants";
|
||||
import { isMemoDetailPath, type MemoOriginScope } from "./navigation";
|
||||
import { isMemoDetailPath } from "./navigation";
|
||||
|
||||
export interface MemoViewContextValue {
|
||||
memo: Memo;
|
||||
creator: User | undefined;
|
||||
currentUser: User | undefined;
|
||||
parentPage: string;
|
||||
parentScope: MemoOriginScope;
|
||||
cardWidth: number;
|
||||
isArchived: boolean;
|
||||
readonly: boolean;
|
||||
|
|
|
|||
|
|
@ -32,8 +32,7 @@ const BlurOverlay: React.FC<{ onClick?: () => void }> = ({ onClick }) => {
|
|||
};
|
||||
|
||||
const MemoBody: React.FC<MemoBodyProps> = ({ compact }) => {
|
||||
const { memo, parentPage, parentScope, showBlurredContent, blurred, readonly, openEditor, openPreview, toggleBlurVisibility } =
|
||||
useMemoViewContext();
|
||||
const { memo, parentPage, showBlurredContent, blurred, readonly, openEditor, openPreview, toggleBlurVisibility } = useMemoViewContext();
|
||||
|
||||
const { handleMemoContentClick, handleMemoContentDoubleClick } = useMemoHandlers({ readonly, openEditor, openPreview });
|
||||
|
||||
|
|
@ -58,7 +57,6 @@ const MemoBody: React.FC<MemoBodyProps> = ({ compact }) => {
|
|||
<MemoContent
|
||||
memoName={memo.name}
|
||||
parentPage={parentPage}
|
||||
parentScope={parentScope}
|
||||
content={memo.content}
|
||||
attachments={memo.attachments}
|
||||
onClick={handleMemoContentClick}
|
||||
|
|
@ -66,7 +64,7 @@ const MemoBody: React.FC<MemoBodyProps> = ({ compact }) => {
|
|||
compact={Boolean(compact)}
|
||||
/>
|
||||
<AttachmentListView attachments={attachmentOnlyItems} onImagePreview={openPreview} />
|
||||
<RelationListView relations={referencedMemos} currentMemoName={memo.name} parentPage={parentPage} parentScope={parentScope} />
|
||||
<RelationListView relations={referencedMemos} currentMemoName={memo.name} parentPage={parentPage} />
|
||||
{memo.location && <LocationDisplayView location={memo.location} />}
|
||||
</ClampedSection>
|
||||
<MemoReactionListView memo={memo} reactions={memo.reactions} />
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import { useMemoViewContext, useMemoViewDerived } from "../MemoViewContext";
|
|||
import { createMemoNavigationState } from "../navigation";
|
||||
|
||||
const MemoCommentListView: React.FC = () => {
|
||||
const { memo, parentPage, parentScope } = useMemoViewContext();
|
||||
const { memo, parentPage } = useMemoViewContext();
|
||||
const { isInMemoDetailPage, commentAmount } = useMemoViewDerived();
|
||||
const { ref: viewportRef, isNearViewport } = useNearViewport<HTMLDivElement>();
|
||||
|
||||
|
|
@ -32,7 +32,7 @@ const MemoCommentListView: React.FC = () => {
|
|||
<span className="text-xs text-muted-foreground">Comments{commentAmount > 1 ? ` (${commentAmount})` : ""}</span>
|
||||
<Link
|
||||
to={`/${memo.name}#${MEMO_COMMENTS_ANCHOR_ID}`}
|
||||
state={createMemoNavigationState(parentPage, parentScope)}
|
||||
state={createMemoNavigationState(parentPage)}
|
||||
className="flex items-center gap-0.5 text-xs text-muted-foreground/80 hover:underline underline-offset-2 transition-colors"
|
||||
>
|
||||
View all
|
||||
|
|
@ -46,7 +46,7 @@ const MemoCommentListView: React.FC = () => {
|
|||
<Link
|
||||
key={comment.name}
|
||||
to={`/${memo.name}#${uid}`}
|
||||
state={createMemoNavigationState(parentPage, parentScope)}
|
||||
state={createMemoNavigationState(parentPage)}
|
||||
viewTransition
|
||||
className="rounded-md bg-muted/40 px-2 py-1 transition-colors hover:bg-muted/60"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -29,15 +29,15 @@ const MemoHeader: React.FC<MemoHeaderProps> = ({ timeDisplay = "relative", showC
|
|||
const t = useTranslate();
|
||||
const [reactionSelectorOpen, setReactionSelectorOpen] = useState(false);
|
||||
|
||||
const { memo, creator, currentUser, parentPage, parentScope, isArchived, readonly, openEditor } = useMemoViewContext();
|
||||
const { memo, creator, currentUser, parentPage, isArchived, readonly, openEditor } = useMemoViewContext();
|
||||
const { createTime, updateTime, displayTime: memoDisplayTime, isDisplayingUpdatedTime, relativeTimeFormat } = useMemoViewDerived();
|
||||
const { newMemoName } = useNewMemo();
|
||||
const visibilityOption = getVisibilityOption(memo.visibility);
|
||||
|
||||
const navigateTo = useNavigateTo();
|
||||
const handleGotoMemoDetailPage = useCallback(() => {
|
||||
navigateTo(`/${memo.name}`, { state: createMemoNavigationState(parentPage, parentScope) });
|
||||
}, [memo.name, parentPage, parentScope, navigateTo]);
|
||||
navigateTo(`/${memo.name}`, { state: createMemoNavigationState(parentPage) });
|
||||
}, [memo.name, parentPage, navigateTo]);
|
||||
|
||||
const { unpinMemo } = useMemoActions(memo);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,8 @@
|
|||
import { getProfileUsername, isMemoScopeRoute } from "@/lib/memo-views";
|
||||
import { ROUTES, resolveCollectionRoute } from "@/router/routes";
|
||||
|
||||
export type MemoOriginScope = "all" | "preserve";
|
||||
import { getProfileUsername } from "@/lib/memo-views";
|
||||
import { ROUTES } from "@/router/routes";
|
||||
|
||||
export interface MemoNavigationState {
|
||||
from: string;
|
||||
fromScope: MemoOriginScope;
|
||||
}
|
||||
|
||||
interface ResolveMemoDetailOriginOptions {
|
||||
|
|
@ -14,7 +11,6 @@ interface ResolveMemoDetailOriginOptions {
|
|||
|
||||
interface ResolveMemoParentPageOptions {
|
||||
explicitParentPage?: string;
|
||||
explicitParentScope?: MemoOriginScope;
|
||||
pathname: string;
|
||||
search: string;
|
||||
memoName: string;
|
||||
|
|
@ -40,32 +36,13 @@ export const isMemoResourcePath = (pathname: string): boolean => {
|
|||
return memoID.length > 0 && !memoID.includes("/");
|
||||
};
|
||||
|
||||
/** Whether this origin represents a collection whose complete URL should be preserved. */
|
||||
export const isMemoCollectionOrigin = (page: string): boolean => {
|
||||
const pathname = page.split(/[?#]/, 1)[0] || ROUTES.HOME;
|
||||
return isMemoScopeRoute(pathname) || resolveCollectionRoute(pathname).isCollection;
|
||||
};
|
||||
export const createMemoNavigationState = (from: string): MemoNavigationState => ({ from });
|
||||
|
||||
export const createMemoNavigationState = (from: string, fromScope: MemoOriginScope): MemoNavigationState => ({ from, fromScope });
|
||||
|
||||
/** Reads router state while remaining compatible with older `{ from }` links. */
|
||||
export const resolveMemoDetailOrigin = (
|
||||
state: unknown,
|
||||
options: ResolveMemoDetailOriginOptions = {},
|
||||
): { parentPage: string; parentScope: MemoOriginScope } => {
|
||||
const value = state && typeof state === "object" ? (state as { from?: unknown; fromScope?: unknown }) : undefined;
|
||||
/** Reads the origin page out of router state; without one, a detail returns to its collection. */
|
||||
export const resolveMemoDetailOrigin = (state: unknown, options: ResolveMemoDetailOriginOptions = {}): string => {
|
||||
const value = state && typeof state === "object" ? (state as { from?: unknown }) : undefined;
|
||||
const explicitParentPage = typeof value?.from === "string" ? value.from : undefined;
|
||||
const hasExplicitParent = explicitParentPage !== undefined;
|
||||
const parentPage = explicitParentPage || (options.memoArchived ? ROUTES.ARCHIVED : ROUTES.HOME);
|
||||
const parentScope =
|
||||
value?.fromScope === "all" || value?.fromScope === "preserve"
|
||||
? value.fromScope
|
||||
: hasExplicitParent && isMemoCollectionOrigin(parentPage)
|
||||
? "preserve"
|
||||
: options.memoArchived
|
||||
? "preserve"
|
||||
: "all";
|
||||
return { parentPage, parentScope };
|
||||
return explicitParentPage || (options.memoArchived ? ROUTES.ARCHIVED : ROUTES.HOME);
|
||||
};
|
||||
|
||||
/** Whether the current route is the canonical detail page for this memo. */
|
||||
|
|
@ -79,33 +56,12 @@ export const isMemoDetailPath = (pathname: string, memoName: string): boolean =>
|
|||
* Captures the list page that opened a memo so canonical detail routes can
|
||||
* return to the real collection lens instead of assuming Home.
|
||||
*/
|
||||
export const resolveMemoOrigin = ({
|
||||
explicitParentPage,
|
||||
explicitParentScope,
|
||||
pathname,
|
||||
search,
|
||||
memoName,
|
||||
}: ResolveMemoParentPageOptions): { parentPage: string; parentScope: MemoOriginScope } => {
|
||||
if (explicitParentPage !== undefined) {
|
||||
const parentPage = explicitParentPage || ROUTES.HOME;
|
||||
return {
|
||||
parentPage,
|
||||
parentScope: explicitParentScope ?? (isMemoCollectionOrigin(parentPage) ? "preserve" : "all"),
|
||||
};
|
||||
}
|
||||
|
||||
if (isMemoDetailPath(pathname, memoName)) {
|
||||
return { parentPage: ROUTES.HOME, parentScope: "all" };
|
||||
}
|
||||
|
||||
return {
|
||||
parentPage: `${pathname}${search}`,
|
||||
parentScope: isMemoCollectionOrigin(pathname) ? "preserve" : "all",
|
||||
};
|
||||
export const resolveMemoParentPage = ({ explicitParentPage, pathname, search, memoName }: ResolveMemoParentPageOptions): string => {
|
||||
if (explicitParentPage !== undefined) return explicitParentPage || ROUTES.HOME;
|
||||
if (isMemoDetailPath(pathname, memoName)) return ROUTES.HOME;
|
||||
return `${pathname}${search}`;
|
||||
};
|
||||
|
||||
export const resolveMemoParentPage = (options: ResolveMemoParentPageOptions): string => resolveMemoOrigin(options).parentPage;
|
||||
|
||||
/** Replaces only the memo filter while preserving the rest of the origin query. */
|
||||
export const withMemoFilter = (page: string, filter: string): string => {
|
||||
const [pathAndSearch] = page.split("#", 1);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import type { Memo } from "@/types/proto/api/v1/memo_service_pb";
|
||||
import type { MemoOriginScope } from "./navigation";
|
||||
|
||||
/** How the header names the memo's time: relative to now, or just the clock time for lists that already name the day. */
|
||||
export type MemoTimeDisplay = "relative" | "time";
|
||||
|
|
@ -14,7 +13,6 @@ export interface MemoViewProps {
|
|||
showSpace?: boolean;
|
||||
className?: string;
|
||||
parentPage?: string;
|
||||
parentScope?: MemoOriginScope;
|
||||
shareImageDialogOpen?: boolean;
|
||||
onShareImageDialogOpenChange?: (open: boolean) => void;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -157,7 +157,7 @@ const UserMemoMap = ({ creator, className }: Props) => {
|
|||
</div>
|
||||
<Link
|
||||
to={`/memos/${memo.name.split("/").pop()}`}
|
||||
state={createMemoNavigationState(parentPage, "all")}
|
||||
state={createMemoNavigationState(parentPage)}
|
||||
className="inline-flex items-center gap-1 rounded-full border border-border bg-background px-2.5 py-1 text-[11px] font-medium text-foreground transition-all hover:border-primary/40 hover:text-primary"
|
||||
>
|
||||
Open
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { createContext, type ReactNode, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import type { MemoOriginScope } from "@/components/MemoView/navigation";
|
||||
import type { PrimaryMemoScope } from "@/lib/memo-views";
|
||||
import type { Memo } from "@/types/proto/api/v1/memo_service_pb";
|
||||
|
||||
|
|
@ -11,7 +10,6 @@ export interface MemoDetailSidebarDescriptor {
|
|||
memo: Memo;
|
||||
parentMemo?: Memo;
|
||||
from?: string;
|
||||
fromScope?: MemoOriginScope;
|
||||
hasExplicitOrigin?: boolean;
|
||||
commentCount?: number;
|
||||
readonly?: boolean;
|
||||
|
|
|
|||
|
|
@ -41,8 +41,6 @@ export const isCalendarRoute = (pathname: string): boolean => {
|
|||
/**
|
||||
* Routes that render a memo collection the sidebar can narrow: the scope routes, a user
|
||||
* profile and the calendar. Views, calendar days and tags apply in place on all of them.
|
||||
* This is a different question from `isMemoCollectionOrigin` (MemoView/navigation.ts),
|
||||
* which asks whether a memo origin is a collection whose full URL is worth returning to.
|
||||
*/
|
||||
export const isMemoCollectionRoute = (pathname: string): boolean =>
|
||||
isMemoScopeRoute(pathname) || getProfileUsername(pathname) !== undefined || isCalendarRoute(pathname);
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import MemoCommentSection, { type MemoCommentSectionHandle } from "@/components/
|
|||
import { MentionResolutionProvider } from "@/components/MemoContent/MentionResolutionContext";
|
||||
import MemoView, { type MemoViewHandle } from "@/components/MemoView";
|
||||
import { computeCommentAmount } from "@/components/MemoView/MemoViewContext";
|
||||
import { createMemoNavigationState, type MemoOriginScope, resolveMemoDetailOrigin } from "@/components/MemoView/navigation";
|
||||
import { createMemoNavigationState, resolveMemoDetailOrigin } from "@/components/MemoView/navigation";
|
||||
import { useAppSidebar } from "@/contexts/AppSidebarContext";
|
||||
import { useAuth } from "@/contexts/AuthContext";
|
||||
import { useInstance } from "@/contexts/InstanceContext";
|
||||
|
|
@ -25,7 +25,6 @@ const MemoSidebarRegistration = ({
|
|||
memo,
|
||||
parentMemo,
|
||||
from,
|
||||
fromScope,
|
||||
hasExplicitOrigin,
|
||||
commentCount,
|
||||
readonly,
|
||||
|
|
@ -37,7 +36,6 @@ const MemoSidebarRegistration = ({
|
|||
memo: Memo;
|
||||
parentMemo?: Memo;
|
||||
from: string;
|
||||
fromScope: MemoOriginScope;
|
||||
hasExplicitOrigin: boolean;
|
||||
commentCount?: number;
|
||||
readonly: boolean;
|
||||
|
|
@ -53,7 +51,6 @@ const MemoSidebarRegistration = ({
|
|||
memo,
|
||||
parentMemo,
|
||||
from,
|
||||
fromScope,
|
||||
hasExplicitOrigin,
|
||||
commentCount,
|
||||
readonly,
|
||||
|
|
@ -65,7 +62,6 @@ const MemoSidebarRegistration = ({
|
|||
}, [
|
||||
commentCount,
|
||||
from,
|
||||
fromScope,
|
||||
hasExplicitOrigin,
|
||||
memo,
|
||||
onCommentCreate,
|
||||
|
|
@ -113,8 +109,7 @@ const MemoDetail = () => {
|
|||
const hasExplicitOrigin =
|
||||
!!locationState && typeof locationState === "object" && typeof (locationState as { from?: unknown }).from === "string";
|
||||
const resolvedOrigin = resolveMemoDetailOrigin(locationState, { memoArchived: memo?.state === State.ARCHIVED });
|
||||
const parentPage = !hasExplicitOrigin && !currentUser && memo?.state !== State.ARCHIVED ? ROUTES.EXPLORE : resolvedOrigin.parentPage;
|
||||
const parentScope = resolvedOrigin.parentScope;
|
||||
const parentPage = !hasExplicitOrigin && !currentUser && memo?.state !== State.ARCHIVED ? ROUTES.EXPLORE : resolvedOrigin;
|
||||
const memoName = memo?.name ?? memoNameFromParams;
|
||||
const displayMemo = useReactMemo(() => {
|
||||
if (!memo) return undefined;
|
||||
|
|
@ -197,7 +192,6 @@ const MemoDetail = () => {
|
|||
memo={displayMemo}
|
||||
parentMemo={parentMemo}
|
||||
from={parentPage}
|
||||
fromScope={parentScope}
|
||||
hasExplicitOrigin={hasExplicitOrigin}
|
||||
commentCount={isShareMode ? undefined : commentCount}
|
||||
readonly={isShareMode}
|
||||
|
|
@ -213,7 +207,7 @@ const MemoDetail = () => {
|
|||
<Link
|
||||
className="px-3 py-1 border border-border rounded-lg max-w-xs w-auto text-sm flex flex-row justify-start items-center flex-nowrap text-muted-foreground hover:shadow hover:opacity-80"
|
||||
to={`/${parentMemo.name}`}
|
||||
state={createMemoNavigationState(parentPage, parentScope)}
|
||||
state={createMemoNavigationState(parentPage)}
|
||||
viewTransition
|
||||
>
|
||||
<ArrowUpLeftFromCircleIcon className="w-4 h-auto shrink-0 opacity-60 mr-2" />
|
||||
|
|
@ -227,7 +221,6 @@ const MemoDetail = () => {
|
|||
memo={displayMemo}
|
||||
compact={false}
|
||||
parentPage={parentPage}
|
||||
parentScope={parentScope}
|
||||
shareImageDialogOpen={shareImageDialogOpen}
|
||||
showCreator
|
||||
showVisibility
|
||||
|
|
@ -242,7 +235,6 @@ const MemoDetail = () => {
|
|||
comments={comments}
|
||||
commentCount={commentCount}
|
||||
parentPage={parentPage}
|
||||
parentScope={parentScope}
|
||||
hasMoreComments={hasNextComments}
|
||||
isFetchingMoreComments={isFetchingNextComments}
|
||||
onLoadMoreComments={fetchNextComments}
|
||||
|
|
|
|||
|
|
@ -67,9 +67,7 @@ describe("<AttachmentMediaGrid>", () => {
|
|||
|
||||
fireEvent.click(memoLink);
|
||||
expect(onPreview).not.toHaveBeenCalled();
|
||||
expect(screen.getByTestId("location-state")).toHaveTextContent(
|
||||
'{"from":"/spaces/product/attachments?filter=tagSearch%3Awork","fromScope":"preserve"}',
|
||||
);
|
||||
expect(screen.getByTestId("location-state")).toHaveTextContent('{"from":"/spaces/product/attachments?filter=tagSearch%3Awork"}');
|
||||
});
|
||||
|
||||
it("keeps video duration inside the existing play badge", () => {
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ const renderFootnote = (compact = false, parentPage?: string) =>
|
|||
resolvedMentionUsernames={new Set()}
|
||||
memoName="memos/abc123"
|
||||
parentPage={parentPage}
|
||||
parentScope={parentPage ? "preserve" : undefined}
|
||||
compact={compact}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -83,7 +83,6 @@ describe("MemoDetailSidebar", () => {
|
|||
memo={memo}
|
||||
parentMemo={parentMemo}
|
||||
parentPage="/explore?filter=tagSearch%3Awork"
|
||||
parentScope="preserve"
|
||||
hasExplicitOrigin
|
||||
commentCount={3}
|
||||
onEdit={onEdit}
|
||||
|
|
@ -144,9 +143,7 @@ describe("MemoDetailSidebar", () => {
|
|||
relations: [createIncomingReference("memos/public")],
|
||||
});
|
||||
|
||||
renderSidebar(
|
||||
<MemoDetailSidebar memo={memo} parentPage="/explore" parentScope="all" commentCount={1} onEdit={vi.fn()} onCommentCreate={vi.fn()} />,
|
||||
);
|
||||
renderSidebar(<MemoDetailSidebar memo={memo} parentPage="/explore" commentCount={1} onEdit={vi.fn()} onCommentCreate={vi.fn()} />);
|
||||
|
||||
expect(screen.queryByRole("button", { name: "common.edit" })).not.toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "memo.comment.write-a-comment" })).toBeInTheDocument();
|
||||
|
|
@ -163,9 +160,7 @@ describe("MemoDetailSidebar", () => {
|
|||
content: "Body",
|
||||
});
|
||||
|
||||
renderSidebar(
|
||||
<MemoDetailSidebar memo={memo} parentPage="/" parentScope="all" commentCount={0} onEdit={vi.fn()} onCommentCreate={vi.fn()} />,
|
||||
);
|
||||
renderSidebar(<MemoDetailSidebar memo={memo} parentPage="/" commentCount={0} onEdit={vi.fn()} onCommentCreate={vi.fn()} />);
|
||||
|
||||
expect(screen.getByRole("link", { name: "memo.go-to:common.home" })).toHaveAttribute("href", "/");
|
||||
expect(screen.queryByText("memo.on-this-memo")).not.toBeInTheDocument();
|
||||
|
|
@ -187,7 +182,7 @@ describe("MemoDetailSidebar", () => {
|
|||
content: "Body",
|
||||
});
|
||||
|
||||
renderSidebar(<MemoDetailSidebar memo={memo} parentPage="/" parentScope="all" commentCount={0} />);
|
||||
renderSidebar(<MemoDetailSidebar memo={memo} parentPage="/" commentCount={0} />);
|
||||
fireEvent.click(screen.getByRole("button", { name: "common.share" }));
|
||||
|
||||
expect(await screen.findByRole("menuitem", { name: "memo.copy-link" })).toBeInTheDocument();
|
||||
|
|
@ -202,16 +197,7 @@ describe("MemoDetailSidebar", () => {
|
|||
content: "Archived body",
|
||||
});
|
||||
|
||||
renderSidebar(
|
||||
<MemoDetailSidebar
|
||||
memo={memo}
|
||||
parentPage="/archived"
|
||||
parentScope="preserve"
|
||||
commentCount={2}
|
||||
onEdit={vi.fn()}
|
||||
onCommentCreate={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
renderSidebar(<MemoDetailSidebar memo={memo} parentPage="/archived" commentCount={2} onEdit={vi.fn()} onCommentCreate={vi.fn()} />);
|
||||
|
||||
expect(screen.getByRole("link", { name: "memo.go-to:common.archived" })).toBeInTheDocument();
|
||||
expect(screen.getByRole("link", { name: /memo.comment.self/ })).toHaveTextContent("2");
|
||||
|
|
@ -237,7 +223,6 @@ describe("MemoDetailSidebar", () => {
|
|||
memo={memo}
|
||||
parentMemo={parentMemo}
|
||||
parentPage="/explore"
|
||||
parentScope="all"
|
||||
hasExplicitOrigin
|
||||
commentCount={4}
|
||||
forceReadonly
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ describe("MemoFilterProvider", () => {
|
|||
);
|
||||
render(<RouterProvider router={router} />);
|
||||
fireEvent.click(screen.getByRole("button", { name: "Select Tasks" }));
|
||||
const state = { from: origin, fromScope: "preserve" };
|
||||
const state = { from: origin };
|
||||
await act(() => router.navigate("/memos/1", { state }));
|
||||
await waitFor(() => expect(screen.getByTestId("filters")).toHaveTextContent("[]"));
|
||||
expect(router.state.location.search).toBe("");
|
||||
|
|
|
|||
|
|
@ -62,7 +62,6 @@ vi.mock("@/components/MemoView/MemoViewContext", () => ({
|
|||
creator: state.creator,
|
||||
currentUser: state.currentUser,
|
||||
parentPage: "/explore?filter=tagSearch%3Awork",
|
||||
parentScope: "preserve",
|
||||
isArchived: false,
|
||||
readonly: false,
|
||||
openEditor: vi.fn(),
|
||||
|
|
@ -142,9 +141,7 @@ describe("MemoHeader navigation", () => {
|
|||
expect(timestamp).toHaveFocus();
|
||||
|
||||
fireEvent.click(timestamp);
|
||||
expect(screen.getByTestId("location")).toHaveTextContent(
|
||||
'/memos/123|{"from":"/explore?filter=tagSearch%3Awork","fromScope":"preserve"}',
|
||||
);
|
||||
expect(screen.getByTestId("location")).toHaveTextContent('/memos/123|{"from":"/explore?filter=tagSearch%3Awork"}');
|
||||
});
|
||||
|
||||
it.each([false, true])("keeps the Space pill beside the timestamp when showCreator=%s", (showCreator) => {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,9 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
createMemoNavigationState,
|
||||
isMemoCollectionOrigin,
|
||||
isMemoDetailPath,
|
||||
isMemoResourcePath,
|
||||
resolveMemoDetailOrigin,
|
||||
resolveMemoOrigin,
|
||||
resolveMemoParentPage,
|
||||
withMemoFilter,
|
||||
} from "@/components/MemoView/navigation";
|
||||
|
|
@ -32,53 +30,25 @@ describe("memo view navigation", () => {
|
|||
).toBe("/explore?filter=contentSearch%3Aroadmap");
|
||||
});
|
||||
|
||||
it("marks collection cards as preserving the remembered scope", () => {
|
||||
expect(resolveMemoOrigin({ pathname: "/explore", search: "?filter=tagSearch%3Awork", memoName: "memos/123" })).toEqual({
|
||||
parentPage: "/explore?filter=tagSearch%3Awork",
|
||||
parentScope: "preserve",
|
||||
});
|
||||
it("keeps a Profile map origin intact", () => {
|
||||
expect(resolveMemoParentPage({ pathname: "/u/alice", search: "?view=map", memoName: "memos/123" })).toBe("/u/alice?view=map");
|
||||
});
|
||||
|
||||
it("marks Profile cards as global without losing their Profile origin", () => {
|
||||
expect(resolveMemoOrigin({ pathname: "/u/alice", search: "?view=map", memoName: "memos/123" })).toEqual({
|
||||
parentPage: "/u/alice?view=map",
|
||||
parentScope: "all",
|
||||
});
|
||||
it("uses Home for direct and shared resource entries", () => {
|
||||
expect(resolveMemoDetailOrigin(undefined)).toBe("/");
|
||||
expect(resolveMemoDetailOrigin({ unrelated: true })).toBe("/");
|
||||
});
|
||||
|
||||
it("uses an All Home origin for direct and shared resource entries", () => {
|
||||
expect(resolveMemoDetailOrigin(undefined)).toEqual({ parentPage: "/", parentScope: "all" });
|
||||
expect(resolveMemoDetailOrigin({ unrelated: true })).toEqual({ parentPage: "/", parentScope: "all" });
|
||||
});
|
||||
|
||||
it("uses a user-level Archived origin without changing the remembered Space", () => {
|
||||
expect(resolveMemoDetailOrigin(undefined, { memoArchived: true })).toEqual({ parentPage: "/archived", parentScope: "preserve" });
|
||||
it("uses the user archive for a direct entry to an archived memo", () => {
|
||||
expect(resolveMemoDetailOrigin(undefined, { memoArchived: true })).toBe("/archived");
|
||||
});
|
||||
|
||||
it("keeps an explicit origin ahead of the archived fallback", () => {
|
||||
expect(resolveMemoDetailOrigin(createMemoNavigationState("/u/alice", "all"), { memoArchived: true })).toEqual({
|
||||
parentPage: "/u/alice",
|
||||
parentScope: "all",
|
||||
});
|
||||
expect(resolveMemoDetailOrigin(createMemoNavigationState("/u/alice"), { memoArchived: true })).toBe("/u/alice");
|
||||
});
|
||||
|
||||
it("round-trips an explicit origin policy through router state", () => {
|
||||
const state = createMemoNavigationState("/", "preserve");
|
||||
expect(resolveMemoDetailOrigin(state)).toEqual({ parentPage: "/", parentScope: "preserve" });
|
||||
});
|
||||
|
||||
it("infers policy for legacy from-only router state", () => {
|
||||
expect(resolveMemoDetailOrigin({ from: "/archived" })).toEqual({ parentPage: "/archived", parentScope: "preserve" });
|
||||
expect(resolveMemoDetailOrigin({ from: "/u/alice" })).toEqual({ parentPage: "/u/alice", parentScope: "all" });
|
||||
});
|
||||
|
||||
it.each([
|
||||
"/",
|
||||
"/explore?filter=tagSearch%3Awork",
|
||||
"/archived",
|
||||
"/attachments",
|
||||
])("recognizes %s as a remembered collection origin", (page) => {
|
||||
expect(isMemoCollectionOrigin(page)).toBe(true);
|
||||
it("round-trips an explicit origin through router state", () => {
|
||||
expect(resolveMemoDetailOrigin(createMemoNavigationState("/explore?filter=tagSearch%3Awork"))).toBe("/explore?filter=tagSearch%3Awork");
|
||||
});
|
||||
|
||||
it.each([
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
|||
import { resolveQuickFindSubmission } from "@/components/AppSidebar/QuickFindDialog";
|
||||
import { getRouteActionPolicy, getSidebarRouteKind } from "@/components/AppSidebar/routes";
|
||||
import { CalendarHeader } from "@/components/CalendarView/CalendarHeader";
|
||||
import { resolveMemoDetailOrigin, resolveMemoOrigin } from "@/components/MemoView/navigation";
|
||||
import { resolveMemoDetailOrigin, resolveMemoParentPage } from "@/components/MemoView/navigation";
|
||||
import Calendar from "@/pages/Calendar";
|
||||
import { buildCollectionPath, getSpaceSwitchPath, resolveCollectionRoute } from "@/router/routes";
|
||||
import { SpaceRoute } from "@/router/SpaceRoute";
|
||||
|
|
@ -59,10 +59,10 @@ describe("Space route contract", () => {
|
|||
it("carries the full calendar origin through a permanent memo link", () => {
|
||||
const parentPage = "/spaces/a/calendar/2026/09/06?filter=tagSearch%3Awork";
|
||||
expect(
|
||||
resolveMemoOrigin({ pathname: "/spaces/a/calendar/2026/09/06", search: "?filter=tagSearch%3Awork", memoName: "memos/x" }),
|
||||
).toEqual({ parentPage, parentScope: "preserve" });
|
||||
expect(resolveMemoDetailOrigin({ from: parentPage })).toEqual({ parentPage, parentScope: "preserve" });
|
||||
expect(resolveMemoDetailOrigin(undefined)).toEqual({ parentPage: "/", parentScope: "all" });
|
||||
resolveMemoParentPage({ pathname: "/spaces/a/calendar/2026/09/06", search: "?filter=tagSearch%3Awork", memoName: "memos/x" }),
|
||||
).toBe(parentPage);
|
||||
expect(resolveMemoDetailOrigin({ from: parentPage })).toBe(parentPage);
|
||||
expect(resolveMemoDetailOrigin(undefined)).toBe("/");
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue